{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "progressive-cavity-pump",
  "type": "registry:component",
  "title": "Progressive cavity pump",
  "description": "Horizontal progressive cavity pump in the bearing-housing layout: helical gear motor and guarded coupling on a drain-rim baseplate, finned bearing housing with seal, suction housing with top inlet flange, bolted inspection cover and drain, elastomer-lined stator clamped by four tie rods with a mid support, and a discharge housing with an axial outlet flange. Stages set the stator length; nozzle sizes set the housings.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "rotating/progressive-cavity-pump.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/rotating/progressive-cavity-pump.tsx",
      "content": "// Horizontal progressive cavity pump (bearing-housing design): suction housing with top inlet and inspection cover, stator on tie rods, axial discharge, helical gear motor on a drain-rim baseplate.\nimport { useEffect, useMemo } from 'react'\nimport { Kit, type MatKey } from '../lib/kit'\nimport { KitMesh, type GroupProps } from '../lib/mesh'\nimport { motor, tagPlate } from '../lib/parts'\nimport { port, type Port } from '../lib/ports'\nimport { Rotors, cowlFan, guardedCoupling, type RotorSpec } from '../lib/rotors'\nimport { pipeOD, type Rating } from '../lib/standards'\nimport { clamp, drainRimBaseplate, flangeLen, flangedBranch, motorEnvelope, mountPad, pipeGauge, smallValve } from './_pumps'\n\nexport interface ProgressiveCavityPumpProps {\n  suctionNps?: number\n  dischargeNps?: number\n  rating?: Rating\n  /** motor rated power, kW */\n  kw?: number\n  /** pressure stages (stator length) */\n  stages?: number\n  casing?: MatKey\n  tag?: string\n  running?: boolean\n}\n\nexport const defaults = {\n  suctionNps: 4,\n  dischargeNps: 3,\n  rating: 150 as Rating,\n  kw: 11,\n  stages: 2,\n  casing: 'actuatorBlue' as MatKey,\n  running: true,\n}\n\ntype P = Omit<typeof defaults, 'running'> & { tag?: string }\n\nfunction layout(p: P) {\n  const st = clamp(Math.round(p.stages), 1, 6)\n  const m = motorEnvelope(clamp(p.kw, 0.75, 250), 4)\n  const Ds = 0.12 + 0.03 * p.dischargeNps // stator OD, typical\n  const Ls = st * Ds * 2.2\n  const Dh = Math.max(Ds * 1.7, pipeOD(p.suctionNps) * 1.5) // suction housing\n  const Lh = Math.max(Dh * 1.3, pipeOD(p.suctionNps) * 1.8)\n  const hb = 0.22\n  const ys = hb + Math.max(m.H + 0.04, Dh / 2 + 0.12)\n  const gH = Math.max(m.R * 1.6, 0.22, ys - hb + 0.1) // gear unit height\n  // along +X: gear motor | coupling | bearing housing | suction housing | stator | discharge housing\n  const xBh0 = 0\n  const Lbh = Math.max(0.28, Ds * 1.6)\n  const xSh0 = xBh0 + Lbh\n  const xSt0 = xSh0 + Lh\n  const xSt1 = xSt0 + Ls\n  const Ld = Math.max(0.16, Ds * 0.9)\n  const xDe = xSt1 + Ld\n  const xFace = xDe + 0.1 + flangeLen(p.dischargeNps, p.rating)\n  const xSuc = xSh0 + Lh * 0.5\n  const ySuc = ys + Dh / 2 + 0.18 + flangeLen(p.suctionNps, p.rating)\n  const xSE = xBh0 - 0.1\n  const xMS = xSE - 0.24\n  const gL = Math.max(0.3, m.R * 2) // gear unit length\n  const xg1 = xMS - 0.02\n  const xg0 = xg1 - gL\n  const xm = xg0 - 0.02\n  const x0 = xm - m.L - Math.max(0.12, m.R * 0.6) - 0.12\n  const x1 = xDe + 0.1\n  const cx = (x0 + x1) / 2\n  const W = Math.max(0.7, Dh + 0.35, 2 * m.R + 0.4)\n  return { st, m, Ds, Ls, Dh, Lh, hb, gH, ys, xBh0, Lbh, xSh0, xSt0, xSt1, Ld, xDe, xFace, xSuc, ySuc, xSE, xMS, gL, xg0, xg1, xm, x0, x1, cx, W }\n}\n\nexport function ports(props: ProgressiveCavityPumpProps = {}): Port[] {\n  const p = { ...defaults, ...props }\n  const L = layout(p)\n  return [\n    port('suction', 'Suction', [L.xSuc - L.cx, L.ySuc, 0], [0, 1, 0], p.suctionNps, p.rating),\n    port('discharge', 'Discharge', [L.xFace - L.cx, L.ys, 0], [1, 0, 0], p.dischargeNps, p.rating),\n  ]\n}\n\nexport function ProgressiveCavityPump({ tag, ...rest }: ProgressiveCavityPumpProps & GroupProps) {\n  const { suctionNps, dischargeNps, rating, kw, stages, casing, running, ...group } = { ...defaults, ...rest }\n  const { built, rotors } = useMemo(() => build({ suctionNps, dischargeNps, rating, kw, stages, casing, tag }), [suctionNps, dischargeNps, rating, kw, stages, casing, tag])\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 ProgressiveCavityPump\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, () => draw(k, p, L, rotors))\n  for (const r of rotors) r.pos = [r.pos[0] - L.cx, r.pos[1], r.pos[2]]\n  return { built: k.build(), rotors }\n}\n\nfunction draw(k: Kit, p: P, L: ReturnType<typeof layout>, rotors: RotorSpec[]) {\n  const C = p.casing\n  const { m, Ds, Dh, Lh, hb, gH, ys, xBh0, Lbh, xSh0, xSt0, xSt1, Ld, xDe, xFace, xSuc, ySuc, xSE, xMS, xg0, xg1, xm, W } = L\n  const rsh = Math.max(0.025, m.shaftDia * 0.6)\n\n  // bearing housing (lantern with seal access windows), shaft seal at the suction-housing wall\n  k.lathe(C, [[Ds * 0.42, xBh0], [Ds * 0.5, xBh0 + 0.02], [Ds * 0.5, xBh0 + Lbh * 0.45], [Ds * 0.4, xBh0 + Lbh * 0.55], [Ds * 0.62, xSh0 - 0.03], [Ds * 0.66, xSh0]], [0, ys, 0], 'x', 32)\n  for (let i = 0; i < 4; i++) k.cyl(C, Ds * 0.55, 0.008, [xBh0 + 0.04 + i * Lbh * 0.08, ys, 0], 'x', 28)\n  k.cyl('stainless', Ds * 0.22, 0.05, [xSh0 - Lbh * 0.25, ys, 0], 'x', 20)\n  k.box(C, [Lbh * 0.7, ys - Ds * 0.45 - hb, Ds * 0.6], [xBh0 + Lbh * 0.35, (ys - Ds * 0.45 + hb) / 2, 0])\n  k.box('flangeSteel', [Lbh * 0.8, 0.02, Ds * 0.9], [xBh0 + Lbh * 0.35, hb + 0.01, 0])\n  k.cyl('brass', 0.008, 0.03, [xBh0 + Lbh * 0.2, ys + Ds * 0.5 + 0.015, 0], 'y', 8)\n  k.sphere('brass', 0.011, [xBh0 + Lbh * 0.2, ys + Ds * 0.5 + 0.035, 0], undefined, 8)\n  k.cyl('flangeSteel', rsh, xBh0 - xSE + 0.02, [(xBh0 + xSE) / 2, ys, 0], 'x', 14)\n\n  // suction housing: body, end flanges, top inlet branch, bolted inspection cover, drain\n  k.cyl(C, Dh / 2, Lh, [xSh0 + Lh / 2, ys, 0], 'x', 40)\n  for (const x of [xSh0 + 0.015, xSt0 - 0.015]) {\n    k.cyl(C, Dh / 2 + 0.03, 0.03, [x, ys, 0], 'x', 40)\n    k.boltCircle('boltXylan', 'nut', 8, Dh / 2 + 0.012, [x - 0.03, ys, 0], 'x', 0.012, 0.06, [0.006, 0.054])\n  }\n  k.lathe(C, [[0, 0], [pipeOD(p.suctionNps) * 0.62, 0], [pipeOD(p.suctionNps) * 0.56, Dh / 2 + 0.06], [pipeOD(p.suctionNps) * 0.44, Dh / 2 + 0.06]], [xSuc, ys, 0], 'y', 32)\n  flangedBranch(k, C, [xSuc, ys + Dh * 0.25, 0], [xSuc, ySuc, 0], p.suctionNps, p.rating, 'STD')\n  const cw = Lh * 0.55\n  k.rbox(C, [cw, Dh * 0.5, 0.03], 0.01, [xSuc, ys, Dh / 2 + 0.01])\n  for (const dx of [-cw * 0.4, 0, cw * 0.4]) for (const dy of [-Dh * 0.2, Dh * 0.2]) k.hex('nut', 0.026, 0.016, [xSuc + dx, ys + dy, Dh / 2 + 0.032], 'z')\n  k.box('handwheel', [cw * 0.35, 0.025, 0.03], [xSuc, ys, Dh / 2 + 0.045])\n  k.cyl(C, 0.02, 0.05, [xSuc, ys - Dh / 2 - 0.02, 0], 'y', 10)\n  k.hex('nut', 0.035, 0.02, [xSuc, ys - Dh / 2 - 0.05, 0])\n  for (const sz of [-1, 1]) k.box(C, [Lh * 0.6, ys - Dh * 0.35 - hb, 0.04], [xSuc, (ys - Dh * 0.35 + hb) / 2, sz * Dh * 0.3])\n  k.box('flangeSteel', [Lh * 0.7, 0.02, Dh * 0.8], [xSuc, hb + 0.01, 0])\n  pipeGauge(k, [xSuc + pipeOD(p.suctionNps) * 0.6 + 0.03, ys + Dh / 2, 0], [0, 0, 1], 10, 0.08)\n\n  // stator: rubber-lined tube with end collars, four tie rods with nuts, support saddle\n  k.cyl('castIron', Ds / 2, xSt1 - xSt0, [(xSt0 + xSt1) / 2, ys, 0], 'x', 32)\n  for (const x of [xSt0 + 0.02, xSt1 - 0.02]) k.cyl('castIron', Ds / 2 + 0.012, 0.04, [x, ys, 0], 'x', 32)\n  const rT = Ds / 2 + 0.035\n  k.around(4, rT, [0, ys, 0], 'x', (q) => {\n    k.rod('boltXylan', [xSt0 - 0.03, q[1], q[2]], [xDe - Ld * 0.3, q[1], q[2]], 0.009, 8)\n    for (const x of [xSt0 - 0.04, xDe - Ld * 0.3]) k.hex('nut', 0.03, 0.018, [x, q[1], q[2]], 'x')\n  }, Math.PI / 4)\n  const xSup = (xSt0 + xSt1) / 2\n  k.box(C, [0.06, ys - Ds / 2 - hb, Ds * 0.9], [xSup, (ys - Ds / 2 + hb) / 2, 0])\n  k.box(C, [0.08, 0.02, Ds * 1.1], [xSup, ys - Ds / 2 - 0.01, 0])\n  k.box('flangeSteel', [0.14, 0.02, Ds * 1.1], [xSup, hb + 0.01, 0])\n\n  // discharge housing with tie-rod flange, axial discharge branch, pressure gauge and drain\n  k.cyl(C, Ds / 2 + 0.05, 0.04, [xSt1 + 0.02, ys, 0], 'x', 32)\n  k.cyl(C, Ds * 0.42, Ld - 0.04, [xSt1 + 0.04 + (Ld - 0.04) / 2, ys, 0], 'x', 32, Ds * 0.32)\n  flangedBranch(k, C, [xDe - 0.02, ys, 0], [xFace, ys, 0], p.dischargeNps, p.rating, 'STD')\n  pipeGauge(k, [xDe + 0.05, ys + pipeOD(p.dischargeNps) / 2, 0], [0, 0, 1], 25, 0.08)\n  k.box(C, [Ld * 0.6, ys - Ds * 0.4 - hb, 0.05], [xSt1 + Ld * 0.5, (ys - Ds * 0.4 + hb) / 2, 0])\n  k.box('flangeSteel', [Ld * 0.8, 0.02, Ds * 0.8], [xSt1 + Ld * 0.5, hb + 0.01, 0])\n  k.rod(C, [xSt1 + Ld * 0.6, ys - Ds * 0.38, Ds * 0.2], [xSt1 + Ld * 0.6, ys - Ds * 0.38 - 0.05, Ds * 0.3], 0.012, 8)\n  smallValve(k, [xSt1 + Ld * 0.6, ys - Ds * 0.38 - 0.07, Ds * 0.3 + 0.03], 0.6, 'z')\n\n  // helical gear unit + motor (gear motor on the baseplate), guarded coupling to the pump shaft\n  const gW = Math.max(m.R * 1.9, 0.24)\n  const gC = (xg0 + xg1) / 2\n  k.rbox('motorPaint', [xg1 - xg0, gH, gW], 0.03, [gC, hb + gH / 2 + 0.03, 0])\n  k.box('motorPaint', [xg1 - xg0 + 0.08, 0.03, gW + 0.08], [gC, hb + 0.045, 0])\n  for (const s of [-1, 1]) k.box('motorPaint', [0.02, gH * 0.7, 0.012], [gC, hb + gH * 0.5, s * (gW / 2 + 0.006)])\n  k.cyl('motorPaint', rsh * 2.2, 0.06, [xg1 + 0.03, ys, 0], 'x', 20)\n  k.cyl('brass', 0.01, 0.04, [gC, hb + gH + 0.05, 0], 'y', 8)\n  k.cyl('brass', 0.016, 0.01, [gC + (xg1 - xg0) * 0.25, hb + gH * 0.35, gW / 2 + 0.005], 'z', 14)\n  k.box('nameplate', [0.08, 0.05, 0.002], [gC - (xg1 - xg0) * 0.2, hb + gH * 0.6, gW / 2 + 0.002])\n  const yM = Math.max(m.H + hb, ys - gH * 0.2)\n  k.at([xm, yM, 0], undefined, () => motor(k, { R: m.R, L: m.L, H: yM - hb, tbox: 'top' }))\n  for (const fx of [xm - m.L * 0.82, xm - m.L * 0.18]) for (const s of [-1, 1]) mountPad(k, fx, hb, s * m.R * 0.78, 0.18, 0.16, 0)\n  rotors.push(guardedCoupling(k, xMS - 0.02, xSE + 0.04, ys, Math.max(0.12, rsh * 4), hb, 'safetyYellow', 60, 0, rsh))\n  rotors.push(cowlFan(m.R, m.L, 160, [xm, yM, 0]))\n  tagPlate(k, `${m.frame} ${p.kw} kW`, [xm - m.L * 0.55, yM, m.R * 1.13], 'z', Math.max(0.07, m.R * 0.6))\n\n  drainRimBaseplate(k, L.x1 - L.x0, W, hb, 'equipmentPaintDark', (L.x0 + L.x1) / 2)\n  if (p.tag) tagPlate(k, p.tag, [(L.x0 + L.x1) / 2, hb * 0.5, W / 2 + 0.002], 'z', 0.18)\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": "ProgressiveCavityPump",
    "anchor": "base",
    "standards": [
      "API 676",
      "IEC 60034 / 60072",
      "ASME B16.5"
    ]
  }
}