{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "reciprocating-pump",
  "type": "registry:component",
  "title": "Plunger pump (API 674 triplex)",
  "description": "Horizontal single-acting plunger pump package on a structural skid: ribbed power frame with inspection covers, crosshead extensions under mesh guards, packed stuffing boxes, forged fluid end with plunger-bore and valve covers, suction and discharge manifolds with a suction stabiliser and a bladder pulsation dampener, relief valve back to suction, parallel-shaft gear reducer, guarded coupling and IEC motor. `plungers` gives a triplex or quintuplex; kW sizes the frame.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "rotating/reciprocating-pump.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/rotating/reciprocating-pump.tsx",
      "content": "// API 674 horizontal plunger pump package (triplex / quintuplex): power frame, crosshead extensions, fluid end with valve covers, manifolds with pulsation dampeners, gear reducer, IEC motor, structural skid.\nimport { useEffect, useMemo } from 'react'\nimport { Kit, type MatKey } from '../lib/kit'\nimport { KitMesh, type GroupProps } from '../lib/mesh'\nimport { gauge, liftingLug, motor, pipeRun, skid, tagPlate } from '../lib/parts'\nimport { port, type Port } from '../lib/ports'\nimport { Rotors, cowlFan, guardedCoupling, type RotorSpec } from '../lib/rotors'\nimport { SECTIONS, pipeOD, type Rating } from '../lib/standards'\nimport { clamp, controlStation, flangeLen, flangedBranch, meshPanel, motorEnvelope, mountPad } from './_pumps'\n\nexport interface ReciprocatingPumpProps {\n  suctionNps?: number\n  dischargeNps?: number\n  /** suction flange class */\n  suctionRating?: Rating\n  /** discharge flange class */\n  rating?: Rating\n  /** motor rated power, kW */\n  kw?: number\n  /** 3 = triplex, 5 = quintuplex */\n  plungers?: number\n  frame?: MatKey\n  tag?: string\n  running?: boolean\n}\n\nexport const defaults = {\n  suctionNps: 4,\n  dischargeNps: 2,\n  suctionRating: 300 as Rating,\n  rating: 1500 as Rating,\n  kw: 250,\n  plungers: 3,\n  frame: 'machineryGreen' as MatKey,\n  running: true,\n}\n\ntype P = Omit<typeof defaults, 'running'> & { tag?: string }\n\nfunction layout(p: P) {\n  const n = p.plungers >= 5 ? 5 : 3\n  const kw = clamp(p.kw, 10, 2000)\n  const m = motorEnvelope(kw, 4)\n  const top = SECTIONS['HEB 200'].d + 0.008\n  const pitch = 0.17 + 0.00018 * kw // plunger centres, typical\n  const Lf = n * pitch + 0.25\n  const yc = top + clamp(0.38 + 0.0003 * kw, 0.4, 0.85) // crank / plunger centreline\n  const Hf = (yc - top) * 1.9\n  const zCase1 = 0.28\n  const zSB = zCase1 + clamp(0.3 + 0.0002 * kw, 0.32, 0.6) // crosshead extension\n  const zFE0 = zSB + 0.12\n  const dFE = clamp(0.36 + 0.0002 * kw, 0.38, 0.7)\n  const zFE = zFE0 + dFE / 2\n  const hFE = clamp(0.32 + 0.0002 * kw, 0.34, 0.6)\n  const ySm = yc - hFE / 2 - 0.22\n  const yDm = yc + hFE / 2 + 0.2\n  const xf0 = 0.2 // frame -X face\n  const xf1 = xf0 + Lf\n  const xEnd = xf1 + 0.45\n  const xGb = xf0 - 0.42 // gearbox centre\n  const xSE = xGb - 0.3 // gearbox input shaft end\n  const xMS = xSE - 0.32\n  const Rm = m.R\n  const stub = 0.1 + Rm * 0.2\n  const xm = xMS - stub + 0.02\n  const x0 = xm - m.L - Math.max(0.12, Rm * 0.6) - 0.15\n  const x1 = xEnd + 0.1\n  const cx = (x0 + x1) / 2\n  const zMin = Math.min(-0.6, -Rm - 0.35)\n  const zMax = zFE0 + dFE + 0.3\n  const W = zMax - zMin\n  const zc = -(zMax + zMin) / 2 // drive-train line z (crank axis), centres the package on the skid\n  const ym = top + m.H\n  return { n, kw, m, top, pitch, Lf, yc, Hf, zCase1, zSB, zFE0, dFE, zFE, hFE, ySm, yDm, xf0, xf1, xEnd, xGb, xSE, xMS, xm, x0, x1, cx, W, zc, ym }\n}\n\nexport function ports(props: ReciprocatingPumpProps = {}): Port[] {\n  const p = { ...defaults, ...props }\n  const L = layout(p)\n  const z = L.zc + L.zFE\n  const xs = L.xEnd + flangeLen(p.suctionNps, p.suctionRating)\n  const xd = L.xEnd + flangeLen(p.dischargeNps, p.rating)\n  return [\n    port('suction', 'Suction', [xs - L.cx, L.ySm, z], [1, 0, 0], p.suctionNps, p.suctionRating),\n    port('discharge', 'Discharge', [xd - L.cx, L.yDm, z], [1, 0, 0], p.dischargeNps, p.rating),\n  ]\n}\n\nexport function ReciprocatingPump({ tag, ...rest }: ReciprocatingPumpProps & GroupProps) {\n  const { suctionNps, dischargeNps, suctionRating, rating, kw, plungers, frame, running, ...group } = { ...defaults, ...rest }\n  const { built, rotors } = useMemo(\n    () => build({ suctionNps, dischargeNps, suctionRating, rating, kw, plungers, frame, tag }),\n    [suctionNps, dischargeNps, suctionRating, rating, kw, plungers, frame, tag],\n  )\n  useEffect(() => () => [built, ...rotors.map((r) => r.built)].forEach((b) => b.geometry.dispose()), [built, rotors])\n  return (\n    <group {...group}>\n      <KitMesh data={built} />\n      <Rotors rotors={rotors} running={running} />\n    </group>\n  )\n}\n\nexport default ReciprocatingPump\n\nfunction build(p: P) {\n  const k = new Kit()\n  const L = layout(p)\n  const rotors: RotorSpec[] = []\n  k.at([-L.cx, 0, 0], undefined, () => skid(k, L.x1 - L.x0, L.W, 'HEB 200'))\n  k.at([-L.cx, 0, L.zc], undefined, () => draw(k, p, L, rotors))\n  for (const r of rotors) r.pos = [r.pos[0] - L.cx, r.pos[1], r.pos[2] + L.zc]\n  if (p.tag) tagPlate(k, p.tag, [0, L.top * 0.5, L.W / 2 + 0.002], 'z', 0.24)\n  controlStation(k, [L.x0 + 0.25 - L.cx, L.top, L.W / 2 - 0.2])\n  return { built: k.build(), rotors }\n}\n\nfunction draw(k: Kit, p: P, L: ReturnType<typeof layout>, rotors: RotorSpec[]) {\n  const F = p.frame\n  const { n, pitch, top, yc, Hf, zCase1, zSB, zFE0, dFE, zFE, hFE, ySm, yDm, xf0, xf1, xEnd, xGb, xSE, xMS, xm, ym, Lf } = L\n  const xp = (i: number) => xf0 + 0.125 + pitch * (i + 0.5)\n\n  // power frame: crankcase with ribs, inspection covers, breather, sight glass, crank bearing caps\n  const zCase0 = -0.42\n  k.rbox(F, [Lf, Hf, zCase1 - zCase0], 0.03, [(xf0 + xf1) / 2, top + Hf / 2, (zCase0 + zCase1) / 2])\n  k.box(F, [Lf + 0.12, 0.03, zCase1 - zCase0 + 0.12], [(xf0 + xf1) / 2, top + 0.015, (zCase0 + zCase1) / 2])\n  for (let i = 0; i < n; i++) {\n    k.rbox(F, [pitch * 0.7, 0.02, (zCase1 - zCase0) * 0.6], 0.008, [xp(i), top + Hf + 0.01, (zCase0 + zCase1) / 2])\n    for (const dx of [-pitch * 0.28, pitch * 0.28]) for (const dz of [-0.12, 0.12]) k.hex('nut', 0.024, 0.014, [xp(i) + dx, top + Hf + 0.027, (zCase0 + zCase1) / 2 + dz])\n  }\n  for (const x of [xf0 + 0.06, xf1 - 0.06]) k.box(F, [0.03, Hf * 0.8, zCase1 - zCase0 + 0.06], [x, top + Hf * 0.45, (zCase0 + zCase1) / 2])\n  k.cyl(F, 0.13, 0.08, [xf1 + 0.04, yc, 0], 'x', 28)\n  k.boltCircle('boltXylan', 'nut', 6, 0.1, [xf1 + 0.02, yc, 0], 'x', 0.012, 0.08, [0.07])\n  k.cyl('brass', 0.012, 0.06, [xf0 + 0.2, top + Hf + 0.05, zCase0 + 0.1], 'y', 8)\n  k.sphere('brass', 0.025, [xf0 + 0.2, top + Hf + 0.09, zCase0 + 0.1], undefined, 10)\n  k.cyl('brass', 0.03, 0.012, [xf1 - 0.15, top + Hf * 0.3, zCase0 - 0.006], '-z', 16)\n  k.disc('glass', 0.024, [xf1 - 0.15, top + Hf * 0.3, zCase0 - 0.013], '-z', 16)\n  for (const sx of [0.15, 0.85]) liftingLug(k, [xf0 + Lf * sx, top + Hf, zCase0 + 0.06], 'x', 0.8, F)\n\n  // crosshead extensions with guarded windows, stuffing boxes, plungers, drip tray\n  const Hx = Math.min(Hf * 0.75, 2 * (yc - top) - 0.02)\n  const zxm = (zCase1 + zSB) / 2\n  const zxl = zSB - zCase1\n  k.box(F, [Lf, Hx * 0.35, zxl], [(xf0 + xf1) / 2, yc - Hx / 2 + Hx * 0.175, zxm])\n  for (let i = 0; i <= n; i++) k.box(F, [0.04, Hx, zxl], [i === 0 ? xf0 + 0.04 : i === n ? xf1 - 0.04 : xf0 + 0.125 + pitch * i, yc, zxm])\n  for (const z of [zCase1 + 0.02, zSB - 0.02]) k.box(F, [Lf, 0.03, 0.04], [(xf0 + xf1) / 2, yc + Hx / 2 - 0.015, z])\n  meshPanel(k, Lf - 0.08, zxl - 0.06, [(xf0 + xf1) / 2, yc + Hx / 2 - 0.004, zxm], [0, 1, 0])\n  k.box(F, [Lf, 0.03, zxl + 0.02], [(xf0 + xf1) / 2, top + 0.015, zxm])\n  for (let i = 0; i < n; i++) {\n    k.cyl('stainless', 0.032 + 0.00003 * L.kw, zSB - zCase1 + 0.05, [xp(i), yc, (zCase1 + zSB) / 2], 'z', 14)\n    k.cyl(F, pitch * 0.32, 0.12, [xp(i), yc, zSB + 0.06], 'z', 20)\n    k.cyl('stainless', pitch * 0.24, 0.03, [xp(i), yc, zSB + 0.015], 'z', 18)\n    k.boltCircle('boltXylan', 'nut', 4, pitch * 0.27, [xp(i), yc, zSB - 0.02], 'z', 0.01, 0.06, [0.01], Math.PI / 4)\n    k.tube('tubing', [[xp(i), yc + pitch * 0.32, zSB + 0.06], [xp(i), yc + pitch * 0.32 + 0.08, zSB + 0.06], [xf0 - 0.02, yc + pitch * 0.32 + 0.08, zSB]], 0.005)\n  }\n  k.box('stainless', [Lf, 0.01, zSB - zCase1 + 0.15], [(xf0 + xf1) / 2, top + 0.06, (zCase1 + zSB) / 2 + 0.05])\n  // plunger lubricator box\n  k.rbox('actuatorBlue', [0.22, 0.18, 0.14], 0.01, [xf0 - 0.05, yc + pitch * 0.32 + 0.12, zSB - 0.05])\n\n  // fluid end: forged block, plunger-bore covers (+Z), discharge valve covers on top, suction covers under\n  const xfe0 = xf0 + 0.08\n  const xfe1 = xf1 - 0.08\n  k.rbox('stainless', [xfe1 - xfe0, hFE, dFE], 0.02, [(xfe0 + xfe1) / 2, yc, zFE])\n  for (let i = 0; i < n; i++) {\n    const x = xp(i)\n    k.cyl('stainless', pitch * 0.3, 0.05, [x, yc, zFE0 + dFE + 0.025], 'z', 24)\n    k.cyl('nut', pitch * 0.22, 0.04, [x, yc, zFE0 + dFE + 0.07], 'z', 6)\n    for (const sy of [1, -1]) {\n      const yv = yc + sy * (hFE / 2 + 0.025)\n      k.cyl('stainless', pitch * 0.3, 0.05, [x, yv, zFE], 'y', 24)\n      k.boltCircle('boltXylan', 'nut', 6, pitch * 0.24, [x, yv - sy * 0.04, zFE], sy > 0 ? 'y' : '-y', 0.012, 0.09, [0.075])\n    }\n  }\n  // fluid-end tie studs into the frame\n  for (const sy of [-1, 1]) for (const xx of [xfe0 + 0.04, xfe1 - 0.04]) k.rod('boltXylan', [xx, yc + sy * hFE * 0.38, zSB], [xx, yc + sy * hFE * 0.38, zFE0 + 0.02], 0.018, 8)\n\n  // suction manifold below, discharge manifold above, risers to every valve\n  const sNps = p.suctionNps\n  const dNps = p.dischargeNps\n  const zM = zFE\n  const xm0 = xfe0 - 0.12\n  for (let i = 0; i < n; i++) {\n    k.pipe('equipmentPaint', [xp(i), ySm, zM], [xp(i), yc - hFE / 2, zM], pipeOD(sNps) * 0.8, 0.008, 20)\n    k.pipe('stainless', [xp(i), yc + hFE / 2 + 0.05, zM], [xp(i), yDm, zM], pipeOD(dNps) * 0.9, 0.006, 16)\n  }\n  k.pipe('equipmentPaint', [xm0, ySm, zM], [xEnd - 0.35, ySm, zM], pipeOD(sNps), 0.008, 28)\n  k.dome('equipmentPaint', pipeOD(sNps) / 2, [xm0, ySm, zM], '-x', Math.PI / 2, 0.5, 24)\n  flangedBranch(k, 'equipmentPaint', [xEnd - 0.36, ySm, zM], [xEnd + flangeLen(sNps, p.suctionRating), ySm, zM], sNps, p.suctionRating, 'STD')\n  k.pipe('stainless', [xm0, yDm, zM], [xEnd - 0.35, yDm, zM], pipeOD(dNps), 0.01, 24)\n  k.dome('stainless', pipeOD(dNps) / 2, [xm0, yDm, zM], '-x', Math.PI / 2, 0.5, 20)\n  flangedBranch(k, 'stainless', [xEnd - 0.36, yDm, zM], [xEnd + flangeLen(dNps, p.rating), yDm, zM], dNps, p.rating, '80')\n\n  // suction stabiliser (vertical vessel on the suction header) and discharge bladder dampener\n  const rSt = clamp(pipeOD(sNps) * 1.4, 0.12, 0.3)\n  const xSt = xm0 + 0.05\n  const ySt0 = top + 0.05\n  k.cyl('equipmentPaint', rSt, ySm - ySt0 + 0.25, [xSt - rSt - 0.05, (ySm + 0.25 + ySt0) / 2, zM], 'y', 32)\n  k.dome('equipmentPaint', rSt, [xSt - rSt - 0.05, ySm + 0.25, zM], 'y', Math.PI / 2, 0.5, 32)\n  k.pipe('equipmentPaint', [xSt - 0.06, ySm, zM], [xm0 + 0.02, ySm, zM], pipeOD(sNps) * 0.8, 0.008, 20)\n  gauge(k, [xSt - rSt - 0.05, ySm + 0.25 + rSt * 0.5 + 0.08, zM], 'z', 0.08, 'bar', 10)\n  const rDp = clamp(0.1 + 0.00012 * L.kw, 0.12, 0.3)\n  const xDp = (xfe0 + xfe1) / 2\n  k.pipe('stainless', [xDp, yDm, zM], [xDp, yDm + 0.18, zM], pipeOD(dNps) * 0.8, 0.008, 16)\n  k.sphere('stainless', rDp, [xDp, yDm + 0.18 + rDp * 0.9, zM], undefined, 28)\n  k.cyl('stainless', rDp * 0.45, 0.04, [xDp, yDm + 0.18 + rDp * 0.05, zM], 'y', 20)\n  k.cyl('brass', 0.02, 0.06, [xDp, yDm + 0.18 + rDp * 1.9, zM], 'y', 10)\n  gauge(k, [xDp + rDp + 0.06, yDm + 0.18 + rDp, zM], 'z', 0.08, 'bar', 400)\n  k.rod('stainless', [xDp + rDp * 0.7, yDm + 0.18 + rDp, zM], [xDp + rDp + 0.06, yDm + 0.18 + rDp, zM - 0.02], 0.006, 8)\n  // discharge relief valve back to suction\n  const xRv = xEnd - 0.55\n  k.pipe('stainless', [xRv, yDm, zM], [xRv, yDm + 0.15, zM], 0.06, 0.006, 14)\n  k.cyl('actuatorRed', 0.05, 0.12, [xRv, yDm + 0.2, zM], 'y', 16)\n  k.cyl('actuatorRed', 0.03, 0.2, [xRv, yDm + 0.36, zM], 'y', 14)\n  pipeRun(k, [[xRv, yDm + 0.2, zM], [xRv, yDm + 0.2, zM + dFE / 2 + 0.16], [xRv, ySm, zM + dFE / 2 + 0.16], [xRv, ySm, zM + pipeOD(sNps) / 2 - 0.004]], 1, { mat: 'stainless' })\n\n  // gear reducer (parallel shaft, input at motor height, output on the crank)\n  const gbH = Math.max(yc, ym) - top + 0.25\n  const gbW = 0.5\n  k.rbox(F, [0.5, gbH, gbW], 0.04, [xGb, top + gbH / 2, 0])\n  for (const dz of [-gbW / 2 - 0.01, gbW / 2 + 0.01]) for (let i = 0; i < 3; i++) k.box(F, [0.02, gbH * 0.8, 0.02], [xGb - 0.15 + i * 0.15, top + gbH * 0.45, dz])\n  k.box(F, [0.54, 0.03, gbW + 0.06], [xGb, top + gbH * 0.62, 0])\n  k.cyl(F, 0.08, 0.08, [xGb - 0.29, ym, 0], 'x', 20)\n  k.cyl('flangeSteel', Math.max(0.025, L.m.shaftDia * 0.55), 0.12, [xSE + 0.06, ym, 0], 'x', 14)\n  k.cyl(F, 0.11, 0.06, [xGb + 0.28, yc, 0], 'x', 24)\n  k.cyl('brass', 0.012, 0.05, [xGb, top + gbH + 0.025, 0.1], 'y', 8)\n  k.box('nameplate', [0.12, 0.07, 0.003], [xGb, top + gbH * 0.4, gbW / 2 + 0.022])\n  for (const s of [-1, 1]) k.box(F, [0.6, 0.03, 0.12], [xGb, top + 0.015, s * (gbW / 2 + 0.03)])\n\n  // motor + guarded coupling\n  const Rm = L.m.R\n  const Lm = L.m.L\n  k.at([xm, ym, 0], undefined, () => motor(k, { R: Rm, L: Lm, H: ym - top, tbox: 'side', cable: true }))\n  tagPlate(k, `${L.m.frame} ${p.kw} kW`, [xm - Lm * 0.62, ym + Rm * 0.62, Rm * 0.82], [0, 0.5, 0.87], Math.max(0.1, Rm * 0.45))\n  for (const fx of [xm - Lm * 0.82, xm - Lm * 0.18]) for (const s of [-1, 1]) mountPad(k, fx, top, s * Rm * 0.78, 0.22, 0.2, 1, 'structSteel')\n  const rsh = Math.max(0.025, L.m.shaftDia * 0.55)\n  rotors.push(guardedCoupling(k, xMS - 0.02, xSE + 0.04, ym, Math.max(0.15, rsh * 4.2), top, 'safetyYellow', 160, 0, rsh))\n  rotors.push(cowlFan(Rm, Lm, 160, [xm, ym, 0]))\n}\n"
    },
    {
      "path": "rotating/_pumps.ts",
      "type": "registry:file",
      "target": "components/deck-twin/rotating/_pumps.ts",
      "content": "// Shared pump, motor and fan geometry: plates, guards, vertical motor, drain-rim baseplate, flanged branches, control stations.\nimport * as THREE from 'three'\nimport { Kit, mat4, uvScale, type MatKey, type Orient, type V3 } from '../lib/kit'\nimport { flange, gauge, motorSize, tagPlate } from '../lib/parts'\nimport { fanGuardMaterial, meshGuardMaterial } from '../lib/textures'\nimport { flange as flangeDims, pipeOD, pipeWall, type Rating } from '../lib/standards'\n\nexport const TAU = Math.PI * 2\nexport const clamp = (v: number, a: number, b: number) => Math.min(b, Math.max(a, v))\n\n/** Quaternion turning local +Z onto n (plates facing n). */\nexport const faceQ = (n: V3) => new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 0, 1), new THREE.Vector3(...n).normalize())\n\n/** Flat ring (rIn = 0: disc) between y0 and y1 along `axis` from `pos`. */\nexport function annulus(k: Kit, mat: MatKey, rIn: number, rOut: number, y0: number, y1: number, pos: V3 = [0, 0, 0], axis: Orient = 'y', seg = 40) {\n  k.lathe(mat, [[rIn, y0], [rOut, y0], [rOut, y1], [rIn, y1], [rIn, y0]], pos, axis, seg)\n}\n\n/** Open (uncapped) cylinder between two points. */\nexport function openTube(k: Kit, mat: MatKey, a: V3, b: V3, r: number, seg = 24, r2?: number) {\n  const va = new THREE.Vector3(...a)\n  const d = new THREE.Vector3(...b).sub(va)\n  const L = d.length()\n  if (L < 1e-6) return\n  const mid = va.addScaledVector(d, 0.5)\n  k.add(mat, new THREE.CylinderGeometry(r2 ?? r, r, L, seg, 1, true), mat4([mid.x, mid.y, mid.z], d.toArray() as V3))\n}\n\n/** Expanded-metal guard tube between two points. */\nexport function meshTube(k: Kit, a: V3, b: V3, r: number, r2?: number, color?: string) {\n  const va = new THREE.Vector3(...a)\n  const d = new THREE.Vector3(...b).sub(va)\n  const L = d.length()\n  const g = new THREE.CylinderGeometry(r2 ?? r, r, L, 28, 1, true)\n  uvScale(g, TAU * Math.max(r, r2 ?? r) * 8, L * 8)\n  const mid = va.addScaledVector(d, 0.5)\n  k.add(meshGuardMaterial(color), g, mat4([mid.x, mid.y, mid.z], d.toArray() as V3))\n}\n\n/** Expanded-metal flat panel w × h facing +normal. */\nexport function meshPanel(k: Kit, w: number, h: number, pos: V3, normal: V3, color?: string) {\n  const g = new THREE.PlaneGeometry(w, h)\n  uvScale(g, w * 8, h * 8)\n  const m = new THREE.Matrix4().compose(new THREE.Vector3(...pos), faceQ(normal), new THREE.Vector3(1, 1, 1))\n  k.add(meshGuardMaterial(color), g, m)\n}\n\n/** Plate from a to b: width w along `side` (made perpendicular), thickness t. */\nexport function strut(k: Kit, mat: MatKey, a: V3, b: V3, w: number, t: number, side: V3) {\n  const A = new THREE.Vector3(...a)\n  const Y = new THREE.Vector3(...b).sub(A)\n  const L = Y.length()\n  if (L < 1e-6) return\n  Y.normalize()\n  const X = new THREE.Vector3(...side)\n  X.addScaledVector(Y, -X.dot(Y)).normalize()\n  const Z = new THREE.Vector3().crossVectors(X, Y)\n  const m = new THREE.Matrix4().makeBasis(X, Y, Z).setPosition(A.addScaledVector(Y, L / 2))\n  k.add(mat, new THREE.BoxGeometry(w, L, t), m)\n}\n\n/** Triangular gusset in the radial plane at angle `ang` about +Y, r0 → r1 at yPlate, height h (h < 0 hangs below). */\nexport function gusset(k: Kit, mat: MatKey, ang: number, r0: number, r1: number, yPlate: number, h: number, t: number) {\n  const s = new THREE.Shape()\n  s.moveTo(r0, yPlate)\n  s.lineTo(r1, yPlate)\n  s.lineTo(r0, yPlate + h)\n  s.closePath()\n  const m = new THREE.Matrix4().makeRotationY(ang).multiply(new THREE.Matrix4().makeTranslation(0, 0, -t / 2))\n  k.extrude(mat, s, t, m, 1)\n}\n\n/** Toothed (cooling-fin) TEFC frame section in the XY plane. */\nexport function finnedShape(R: number, finH: number, nFin: number) {\n  const sh = new THREE.Shape()\n  const step = TAU / nFin\n  const finW = Math.min(0.012, step * R * 0.28)\n  const da = finW / 2 / R\n  const pts: [number, number][] = []\n  for (let i = 0; i < nFin; i++) {\n    const a = i * step\n    for (let s = 0; s < 3; s++) {\n      const aa = a - step / 2 + (s / 3) * (step / 2 - da)\n      pts.push([Math.cos(aa) * R, Math.sin(aa) * R])\n    }\n    pts.push([Math.cos(a - da) * R, Math.sin(a - da) * R])\n    pts.push([Math.cos(a - da * 0.6) * (R + finH), Math.sin(a - da * 0.6) * (R + finH)])\n    pts.push([Math.cos(a + da * 0.6) * (R + finH), Math.sin(a + da * 0.6) * (R + finH)])\n    pts.push([Math.cos(a + da) * R, Math.sin(a + da) * R])\n  }\n  pts.forEach(([x, y], i) => (i ? sh.lineTo(x, y) : sh.moveTo(x, y)))\n  sh.closePath()\n  return sh\n}\n\n/** Weld-neck flange length (weld end to raised face). */\nexport const flangeLen = (nps: number, rating: Rating) => {\n  const f = flangeDims(nps, rating)\n  return f.hubLength + f.raisedFaceHeight\n}\n\n/** Branch pipe from `from` along `dir` to a WN flange (with studs) whose raised face is at `face`. */\nexport function flangedBranch(k: Kit, mat: MatKey, from: V3, face: V3, nps: number, rating: Rating, schedule: '40' | '80' | 'STD' = '80') {\n  const d = new THREE.Vector3(...face).sub(new THREE.Vector3(...from)).normalize()\n  const L = flange(k, nps, rating, face, d.toArray() as V3, true)\n  const end = new THREE.Vector3(...face).addScaledVector(d, -L).toArray() as V3\n  const ro = pipeOD(nps) / 2\n  k.pipe(mat, from, end, pipeOD(nps), pipeWall(nps, schedule), nps >= 6 ? 32 : 20)\n  // bore through the flange hub up to the face, and a dark end where the branch opens into the casing\n  openTube(k, 'pipeBore', end, new THREE.Vector3(...face).addScaledVector(d, -0.0005).toArray() as V3, ro * 0.88, nps >= 6 ? 32 : 20)\n  k.disc('pipeBore', ro - pipeWall(nps, schedule) + 0.001, from, d.toArray() as V3, 24)\n}\n\n/** IEC motor envelope from kW; above the IEC 60072 range (1 MW) the frame grows with the cube root of power. */\nexport function motorEnvelope(kw: number, poles: 2 | 4 | 6 = 4) {\n  const m = motorSize(kw, poles)\n  const s = kw > 1000 ? Math.cbrt(kw / 1000) : 1\n  return { ...m, R: m.R * s, L: m.L * s, H: m.H * s, frame: kw > 1000 ? 'HV' : m.frame }\n}\n\nexport interface VerticalMotorOpts {\n  /** frame radius (without fins), m */\n  R: number\n  /** frame length flange to fan end, m */\n  H: number\n  /** D-flange underside height, m */\n  yF: number\n  mat?: MatKey\n  /** text on the nameplate, e.g. \"IEC 315M 160 kW\" */\n  plate?: string\n  /** height of the deck the cables run down to */\n  deck?: number\n  canopy?: boolean\n}\n\n/** Vertical TEFC motor (IM V1): D-flange, finned frame, end shields, fan cowl, drip canopy, terminal + heater boxes. Returns the top height. */\nexport function verticalMotor(k: Kit, o: VerticalMotorOpts) {\n  const { R: Rm, H: Hm, yF } = o\n  const drv = o.mat ?? 'motorPaint'\n  const deck = o.deck ?? 0\n  const finH = Math.max(0.012, Rm * 0.1)\n  const shL = Math.max(0.05, Rm * 0.22)\n  annulus(k, drv, 0, Rm * 1.12, yF, yF + 0.05, [0, 0, 0], 'y', 48)\n  k.boltCircle('boltXylan', 'nut', 8, Rm * 1.04, [0, yF - 0.04, 0], 'y', Math.max(0.012, Rm * 0.04), 0.12, [0.105], Math.PI / 8)\n  const y0f = yF + 0.05 + shL\n  const yM1 = yF + 0.05 + Hm\n  const y1f = yM1 - shL\n  k.lathe(drv, [[Rm * 0.6, yF + 0.05], [Rm + finH * 0.6, yF + 0.05 + shL * 0.55], [Rm + finH * 0.6, y0f]], [0, 0, 0], 'y', 48)\n  const nFin = Math.max(28, Math.round(Rm * 100))\n  k.extrude(drv, finnedShape(Rm, finH, nFin), y1f - y0f, new THREE.Matrix4().makeTranslation(0, y0f, 0).multiply(new THREE.Matrix4().makeRotationX(-Math.PI / 2)), 2)\n  k.lathe(drv, [[Rm + finH * 0.6, y1f], [Rm + finH * 0.6, y1f + shL * 0.4], [Rm * 0.7, yM1]], [0, 0, 0], 'y', 48)\n  k.boltCircle('nut', 'nut', 8, Rm + finH * 0.3, [0, yF + 0.05 + shL * 0.7, 0], 'y', Math.max(0.008, Rm * 0.03), 0.03, [0.02])\n  const cR = Rm + finH + 0.015\n  const cowlH = Math.max(0.12, Rm * 0.45)\n  k.lathe(drv, [\n    [cR - 0.005, y1f - shL * 0.3],\n    [cR, y1f - shL * 0.3],\n    [cR, yM1 + cowlH * 0.8],\n    [cR * 0.94, yM1 + cowlH],\n    [cR * 0.94 - 0.004, yM1 + cowlH],\n  ], [0, 0, 0], 'y', 48)\n  k.disc('rubberBlack', cR * 0.9, [0, yM1 + cowlH - 0.02, 0], 'y', 32)\n  k.disc(fanGuardMaterial(), cR * 0.92, [0, yM1 + cowlH + 0.001, 0], 'y', 32)\n  let top = yM1 + cowlH\n  if (o.canopy !== false) {\n    const yCan = yM1 + cowlH + Math.max(0.06, Rm * 0.14)\n    for (let i = 0; i < 4; i++) {\n      const a = (i / 4) * TAU\n      k.rod(drv, [Math.cos(a) * cR * 0.88, yM1 + cowlH, Math.sin(a) * cR * 0.88], [Math.cos(a) * cR * 0.88, yCan, Math.sin(a) * cR * 0.88], 0.01, 6)\n    }\n    const hc = Math.max(0.04, Rm * 0.1)\n    k.lathe(drv, [[cR * 1.12, yCan - 0.04], [cR * 1.12, yCan], [cR * 0.4, yCan + hc], [0, yCan + hc]], [0, 0, 0], 'y', 48)\n    k.lathe(drv, [[0, yCan - 0.005], [cR * 1.1, yCan - 0.04]], [0, 0, 0], 'y', 48)\n    top = yCan + hc\n  }\n  // terminal box (+Z), anti-condensation heater / RTD box (-Z), cables to the deck\n  const tb = clamp(Rm * 0.75, 0.14, 0.5)\n  const yT = yF + Hm * 0.45\n  const zT = Rm + finH + tb * 0.28\n  k.box(drv, [tb * 0.5, tb * 0.5, finH + 0.03], [0, yT, Rm + finH * 0.5])\n  k.rbox(drv, [tb * 1.1, tb * 0.9, tb * 0.55], tb * 0.06, [0, yT, zT])\n  k.rbox(drv, [tb * 1.0, tb * 0.8, tb * 0.06], tb * 0.03, [0, yT, zT + tb * 0.3])\n  k.around(6, tb * 0.42, [0, yT, zT + tb * 0.335], 'z', (p) => k.hex('nut', 0.016, 0.01, p, 'z'), Math.PI / 6)\n  for (const dx of [-tb * 0.25, tb * 0.25]) {\n    k.cyl('brass', tb * 0.08, tb * 0.14, [dx, yT - tb * 0.5, zT], 'y', 12)\n    k.spline('cable', [[dx, yT - tb * 0.56, zT], [dx, yT - tb * 1.4, zT + 0.04], [dx * 1.4, (yT + deck) / 2, zT + 0.1], [dx * 1.6, deck + 0.03, zT + 0.12]], tb * 0.065, 8, 30)\n  }\n  const ta = tb * 0.5\n  const zA = -(Rm + finH + ta * 0.22)\n  k.rbox(drv, [ta, ta * 0.8, ta * 0.45], ta * 0.08, [0, yT + tb * 0.2, zA])\n  k.cyl('brass', ta * 0.1, ta * 0.18, [0, yT + tb * 0.2 - ta * 0.45, zA], 'y', 10)\n  k.spline('cable', [[0, yT + tb * 0.2 - ta * 0.5, zA], [0, yT - 0.4, zA - 0.08], [0.1, deck + 0.03, zA - 0.1]], 0.012, 6, 24)\n  // lifting lugs, nameplate, earth boss, bearing vibration probes\n  for (const s of [-1, 1]) {\n    const x = s * (Rm + finH + 0.03)\n    k.box(drv, [0.08, 0.16, 0.035], [x, y1f - 0.06, 0])\n    k.torus('nut', 0.035, 0.009, [x + s * 0.02, y1f + 0.03, 0], 'z', 6, 14)\n  }\n  const pn = new THREE.Vector3(0.707, 0, 0.707)\n  const pr = Rm + finH + 0.006\n  const pw = Math.max(0.1, Rm * 0.5)\n  k.box(drv, [pw * 0.6, pw * 0.4, finH + 0.01], [pn.x * (Rm + finH * 0.5), yF + Hm * 0.68, pn.z * (Rm + finH * 0.5)], faceQ([pn.x, 0, pn.z]))\n  if (o.plate) tagPlate(k, o.plate, [pn.x * pr, yF + Hm * 0.68, pn.z * pr], [pn.x, 0, pn.z], pw)\n  else k.box('nameplate', [pw, pw * 0.6, 0.003], [pn.x * pr, yF + Hm * 0.68, pn.z * pr], faceQ([pn.x, 0, pn.z]))\n  k.cyl('brass', 0.012, 0.03, [-(Rm + finH * 0.5) * 0.7, yF + 0.2, (Rm + finH * 0.5) * 0.7], [-0.7, 0, 0.7], 8)\n  for (const a of [Math.PI / 2 + 0.6, Math.PI / 2 - 0.6]) {\n    const u: V3 = [Math.cos(a), 0, Math.sin(a)]\n    k.rod('stainless', [u[0] * (Rm * 0.75), yF + 0.05 + shL * 0.5, u[2] * (Rm * 0.75)], [u[0] * (Rm * 0.75 + 0.08), yF + 0.05 + shL * 0.5 + 0.04, u[2] * (Rm * 0.75 + 0.08)], 0.012, 8)\n  }\n  return top\n}\n\n/** Drain-rim baseplate (API 610): channels, sloped deck, rim, grout holes, anchor bolts, lifting lugs, drain. Top at y = hb. */\nexport function drainRimBaseplate(k: Kit, L: number, W: number, hb: number, mat: MatKey = 'equipmentPaintDark', x0 = 0) {\n  const t = 0.012\n  k.at([x0, 0, 0], undefined, () => {\n    k.box(mat, [L, 0.016, W + 0.08], [0, 0.008, 0])\n    for (const s of [-1, 1]) {\n      k.box(mat, [L, hb - 0.016, t], [0, hb / 2 + 0.008, s * (W / 2 - t / 2)])\n      k.box(mat, [t, hb - 0.016, W], [s * (L / 2 - t / 2), hb / 2 + 0.008, 0])\n      const n = Math.round(L / 0.45)\n      for (let i = 1; i < n; i++) k.box(mat, [0.01, hb - 0.03, 0.04], [-L / 2 + (i * L) / n, hb / 2 + 0.008, s * (W / 2 + 0.02)])\n    }\n    k.box(mat, [L - 2 * t, 0.01, W - 2 * t], [0, hb - 0.03, 0])\n    for (const s of [-1, 1]) {\n      k.box(mat, [L, 0.045, 0.02], [0, hb - 0.005, s * (W / 2 - 0.01)])\n      k.box(mat, [0.02, 0.045, W], [s * (L / 2 - 0.01), hb - 0.005, 0])\n    }\n    for (const s of [-1, 1]) k.box(mat, [L - 0.1, hb - 0.05, 0.12], [0, (hb - 0.05) / 2 + 0.016, s * W * 0.28])\n    const nG = Math.max(2, Math.round(L / 0.9))\n    for (let i = 0; i < nG; i++) {\n      const gx = -L / 2 + (L * (i + 0.5)) / nG\n      k.torus(mat, 0.05, 0.008, [gx, hb - 0.022, 0], 'y', 6, 24)\n      k.disc('rubberBlack', 0.048, [gx, hb - 0.0245, 0], 'y', 24)\n    }\n    const nA = Math.max(3, Math.round(L / 0.7) + 1)\n    for (let i = 0; i < nA; i++) {\n      const ax = -L / 2 + 0.08 + ((L - 0.16) * i) / (nA - 1)\n      for (const s of [-1, 1]) anchorBolt(k, [ax, 0.016, s * (W / 2 + 0.035)])\n    }\n    for (const sx of [-1, 1])\n      for (const sz of [-1, 1]) {\n        k.box(mat, [0.1, 0.1, 0.02], [sx * (L / 2 - 0.12), hb * 0.55, sz * (W / 2 + 0.01)])\n        k.torus('nut', 0.025, 0.006, [sx * (L / 2 - 0.12), hb * 0.55 + 0.02, sz * (W / 2 + 0.022)], 'z', 6, 14)\n      }\n    k.cyl(mat, 0.022, 0.12, [L / 2 + 0.05, hb - 0.04, -W / 2 + 0.15], 'x', 12)\n    k.hex('nut', 0.05, 0.03, [L / 2 + 0.11, hb - 0.04, -W / 2 + 0.15], 'x')\n    k.cyl('brass', 0.015, 0.03, [-L / 2 + 0.3, hb * 0.5, W / 2 + 0.015], 'z', 10)\n  })\n}\n\n/** Anchor bolt with washer and nut on a plate whose top is at p[1]. */\nexport function anchorBolt(k: Kit, p: V3, d = 0.022) {\n  k.cyl('stainless', d * 0.9, 0.004, [p[0], p[1] + 0.002, p[2]], 'y', 16)\n  k.hex('nut', d * 1.9, d * 1.1, [p[0], p[1] + 0.004 + d * 0.55, p[2]])\n  k.cyl('boltXylan', d / 2, d * 3, [p[0], p[1] + d * 1.5, p[2]], 'y', 8)\n}\n\n/** Machined mounting pad with jack-screw block, top at y. */\nexport function mountPad(k: Kit, x: number, y: number, z: number, sx = 0.22, sz = 0.2, jack = 1, mat: MatKey = 'equipmentPaintDark') {\n  k.box('flangeSteel', [sx, 0.025, sz], [x, y - 0.0125, z])\n  if (!jack) return\n  k.box(mat, [0.04, 0.06, 0.06], [x + jack * (sx / 2 + 0.05), y + 0.02, z])\n  k.cyl('boltXylan', 0.008, 0.08, [x + jack * (sx / 2 + 0.03), y + 0.03, z], 'x', 6)\n}\n\n/** Local control station on a post (start / stop, E-stop, ammeter), facing +Z; base at `foot`. */\nexport function controlStation(k: Kit, foot: V3, face: V3 = [0, 0, 1], h = 1.2) {\n  const [lx, y0, lz] = foot\n  const q = faceQ(face)\n  const n = new THREE.Vector3(...face).normalize()\n  const P = (u: number, v: number, w: number): V3 => new THREE.Vector3(u, v, w).applyQuaternion(q).add(new THREE.Vector3(lx, 0, lz)).toArray() as V3\n  k.rod('structSteel', [lx, y0, lz], [lx, h + 0.25, lz], 0.03, 12)\n  k.box('structSteel', [0.15, 0.012, 0.15], [lx, y0 + 0.006, lz])\n  k.box('structSteel', [0.04, 0.3, 0.02], P(0, h, 0.04), q)\n  k.rbox('stainless', [0.24, 0.32, 0.13], 0.012, P(0, h, 0.115), q)\n  const tilt = q.clone().multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), 0.15))\n  k.box('stainless', [0.3, 0.012, 0.2], P(0, h + 0.18, 0.12), tilt)\n  const f = 0.18\n  k.cyl('safetyGreen', 0.016, 0.02, P(-0.055, h + 0.05, f), n.toArray() as V3, 16)\n  k.cyl('actuatorRed', 0.016, 0.02, P(0.055, h + 0.05, f), n.toArray() as V3, 16)\n  k.cyl('actuatorYellow', 0.04, 0.004, P(0, h - 0.07, f - 0.002), n.toArray() as V3, 24)\n  k.cyl('actuatorRed', 0.012, 0.03, P(0, h - 0.07, f + 0.012), n.toArray() as V3, 12)\n  k.dome('actuatorRed', 0.028, P(0, h - 0.07, f + 0.025), n.toArray() as V3, Math.PI / 2, 0.6)\n  gauge(k, P(0, h + 0.11, f - 0.01), n.toArray() as V3, 0.06, 'bar', 600)\n  k.hex('brass', 0.03, 0.02, P(0, h - 0.17, 0.11))\n  k.spline('cable', [P(0, h - 0.18, 0.11), P(0, h - 0.35, 0.06), P(0, y0 + 0.2, 0.04), P(0.1, y0 + 0.02, 0)], 0.008, 6, 24)\n}\n\n/** Pressure gauge on a root valve + siphon above a pipe top at `p` (dial facing +normal). */\nexport function pipeGauge(k: Kit, p: V3, normal: V3 = [0, 0, 1], max = 25, d = 0.1) {\n  const [x, y, z] = p\n  k.cyl('stainless', 0.012, 0.1, [x, y + 0.05, z], 'y', 8)\n  k.hex('stainless', 0.04, 0.035, [x, y + 0.09, z])\n  k.torus('stainless', 0.025, 0.006, [x, y + 0.14, z], [normal[0], 0, normal[2]], 6, 18)\n  k.cyl('stainless', 0.01, 0.05, [x, y + 0.19, z], 'y', 8)\n  gauge(k, [x + normal[0] * 0.02, y + 0.25, z + normal[2] * 0.02], normal, d, 'bar', max)\n}\n\n/** Spiral volute outline (Archimedean, r0 at the tongue to r1) with a straight tangential outlet of length `out` along +Y at u ∈ [r0, r1]. */\nexport function voluteShape(r0: number, r1: number, out: number, n = 48) {\n  const s = new THREE.Shape()\n  s.moveTo(r0, 0)\n  for (let i = 1; i <= n; i++) {\n    const th = (i / n) * TAU\n    const r = r0 + ((r1 - r0) * i) / n\n    s.lineTo(Math.cos(th) * r, Math.sin(th) * r)\n  }\n  s.lineTo(r1, out)\n  s.lineTo(r0, out)\n  s.closePath()\n  return s\n}\n\n/** Extruded shape with rounded (bevelled) edges, depth along local +Z centred on z = 0, placed by m. */\nexport function castExtrude(k: Kit, mat: MatKey, shape: THREE.Shape, depth: number, bevel: number, m: THREE.Matrix4, seg = 12) {\n  const g = new THREE.ExtrudeGeometry(shape, { depth: Math.max(depth - 2 * bevel, 0.001), bevelEnabled: true, bevelThickness: bevel, bevelSize: bevel, bevelSegments: 3, curveSegments: seg })\n  g.translate(0, 0, -(depth - 2 * bevel) / 2)\n  k.add(mat, g, m)\n}\n\n/** Top-mounted air-to-air cooler (IC611) on a horizontal HV motor whose DE face is at xm, shaft height ys. */\nexport function topCooler(k: Kit, xm: number, ys: number, Rm: number, Lm: number, mat: MatKey = 'motorPaint') {\n  const ch = Rm * 0.7\n  const y = ys + Rm * 1.12 + ch / 2\n  k.box(mat, [Lm * 0.3, Rm * 0.25, Rm * 0.9], [xm - Lm * 0.5, ys + Rm * 1.05, 0])\n  k.rbox(mat, [Lm * 0.75, ch, Rm * 1.7], 0.03, [xm - Lm * 0.5, y, 0])\n  for (let i = 0; i < 6; i++) k.box('rubberBlack', [Lm * 0.7, 0.01, Rm * 1.71], [xm - Lm * 0.5, y - ch * 0.35 + i * ch * 0.14, 0])\n  k.cyl(mat, ch * 0.45, 0.1, [xm - Lm * 0.875 - 0.05, y, 0], 'x', 28)\n  k.disc(fanGuardMaterial(), ch * 0.42, [xm - Lm * 0.875 - 0.101, y, 0], '-x', 24)\n}\n\n/** Constant-level oiler: arm from the bearing-housing surface point p along n, glass reservoir above. */\nexport function oiler(k: Kit, p: V3, n: V3 = [0, 0, 1], s = 1) {\n  const o = new THREE.Vector3(...p).addScaledVector(new THREE.Vector3(...n).normalize(), 0.07 * s)\n  k.rod('stainless', p, o.toArray() as V3, 0.008 * s, 8)\n  k.cyl('stainless', 0.03 * s, 0.03 * s, [o.x, o.y + 0.015 * s, o.z], 'y', 16)\n  k.cyl('glass', 0.032 * s, 0.09 * s, [o.x, o.y + 0.075 * s, o.z], 'y', 16)\n  k.dome('glass', 0.032 * s, [o.x, o.y + 0.12 * s, o.z], 'y', Math.PI / 2, 1)\n}\n\n/** Small valve (forged body, bonnet, handwheel) on a vertical line at p, handwheel up. */\nexport function smallValve(k: Kit, p: V3, s = 1, dir: Orient = 'y') {\n  k.at(p, dir, () => {\n    k.box('valveBody', [0.06 * s, 0.07 * s, 0.05 * s], [0, 0, 0])\n    k.cyl('valveBody', 0.016 * s, 0.07 * s, [0, 0.07 * s, 0], 'y', 10)\n    k.handwheel('handwheel', 0.07 * s, [0, 0.11 * s, 0], 'y')\n  })\n}\n\n/** API 682 Plan 23 seal cooler on a post: shell with bolted heads and cooling-water nozzles. Returns the top and bottom process connection points. */\nexport function sealCooler(k: Kit, foot: V3, yTop: number, r = 0.055) {\n  const [x, y0, z] = foot\n  const h = Math.max(0.3, r * 6)\n  const yc = yTop - h / 2\n  k.rod('structSteel', [x, y0, z], [x, yc - h / 2 - 0.02, z], 0.022, 10)\n  k.box('structSteel', [0.12, 0.012, 0.12], [x, y0 + 0.006, z])\n  k.box('structSteel', [0.03, 0.12, 0.008], [x, yc - h / 2 + 0.02, z - r - 0.004])\n  k.cyl('stainless', r, h, [x, yc, z], 'y', 20)\n  for (const s of [-1, 1]) {\n    k.cyl('stainless', r * 1.3, 0.02, [x, yc + s * (h / 2 + 0.01), z], 'y', 20)\n    k.boltCircle('boltXylan', 'nut', 6, r * 1.15, [x, yc + s * (h / 2 + 0.01) - 0.015, z], 'y', 0.006, 0.03, [0.005, 0.025])\n  }\n  for (const s of [-1, 1]) {\n    k.rod('stainless', [x, yc + s * h * 0.35, z], [x + r + 0.05, yc + s * h * 0.35, z], 0.01, 8)\n    k.cyl('flangeSteel', 0.018, 0.012, [x + r + 0.05, yc + s * h * 0.35, z], 'x', 12)\n  }\n  return { top: [x, yc + h / 2 + 0.02, z] as V3, bottom: [x, yc - h / 2 - 0.02, z] as V3 }\n}\n"
    }
  ],
  "categories": [
    "rotating"
  ],
  "meta": {
    "export": "ReciprocatingPump",
    "anchor": "base",
    "standards": [
      "API 674",
      "API 618 (pulsation guidance)",
      "IEC 60034 / 60072",
      "ASME B16.5"
    ]
  }
}