{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "submersible-pump",
  "type": "registry:component",
  "title": "Submersible caisson pump",
  "description": "Electric submersible seawater lift pump hung in a caisson: top plate bolted to the caisson flange with a long-radius discharge bend, air release valve, gauge, hypochlorite injection point and power-cable junction box; flanged riser stack in 6 m sections with centralisers and the clamped power cable; pump-motor unit with the pump below the motor, water rising through the shell around the motor, and a suction strainer at the bottom. Geometry goes below y = 0.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "rotating/submersible-pump.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/rotating/submersible-pump.tsx",
      "content": "// Electric submersible caisson pump (seawater lift): pump-motor unit hung on a flanged riser stack with centralisers, top plate with discharge bend, junction box and air release.\nimport * as THREE from 'three'\nimport { useEffect, useMemo } from 'react'\nimport { Kit, type MatKey } from '../lib/kit'\nimport { KitMesh, type GroupProps } from '../lib/mesh'\nimport { paint } from '../lib/materials'\nimport { junctionBox, liftingLug, tagPlate } from '../lib/parts'\nimport { port, type Port } from '../lib/ports'\nimport { elbow90, flange as flangeDims, pipeOD, pipeWall, type Rating } from '../lib/standards'\nimport { TAU, annulus, clamp, flangeLen, flangedBranch, pipeGauge, smallValve } from './_pumps'\nimport { caisson, caissonLayout } from './_pumpsCaisson'\n\nexport interface SubmersiblePumpProps {\n  /** motor rated power, kW */\n  kw?: number\n  dischargeNps?: number\n  rating?: Rating\n  /** caisson length below the deck, m */\n  depth?: number\n  caissonDiameter?: number\n  cutaway?: boolean\n  color?: string\n  tag?: string\n}\n\nexport const defaults = {\n  kw: 300,\n  dischargeNps: 12,\n  rating: 150 as Rating,\n  depth: 25,\n  caissonDiameter: 0.9,\n  cutaway: false,\n  color: '#5f7487',\n}\n\ntype P = typeof defaults & { tag?: string }\n\nfunction layout(p: P) {\n  const C = caissonLayout({ caissonDiameter: p.caissonDiameter, depth: p.depth, dischargeNps: p.dischargeNps, rating: p.rating, stages: 1 })\n  const R = C.R\n  const dOD = pipeOD(p.dischargeNps)\n  const yTp = 0.13 // top plate top\n  const Re = elbow90(p.dischargeNps, 'LR')\n  const yB = yTp + 0.25 + flangeDims(p.dischargeNps, p.rating).od / 2 // bend centre height\n  const xFace = Re + 0.35 + flangeLen(p.dischargeNps, p.rating)\n  // pump-motor unit sized from kW (typical): motor shell, pump below, suction strainer at the bottom\n  const Ru = clamp(0.12 + 0.012 * Math.sqrt(p.kw), Math.max(dOD / 2 + 0.04, 0.15), R - 0.05)\n  const Lm = clamp(1.2 + 0.0035 * p.kw, 1.4, 5)\n  const Lp = clamp(0.5 + Ru * 1.8, 0.7, 1.8)\n  const unit = Lm + Lp + 0.5\n  const depth = Math.max(C.depth, unit + 2.5)\n  return { C: { ...C, depth }, R, dOD, yTp, Re, yB, xFace, Ru, Lm, Lp, depth }\n}\n\nexport function ports(props: SubmersiblePumpProps = {}): Port[] {\n  const p = { ...defaults, ...props }\n  const L = layout(p)\n  return [port('discharge', 'Discharge', [L.xFace, L.yB, 0], [1, 0, 0], p.dischargeNps, p.rating)]\n}\n\nexport function SubmersiblePump({ tag, ...rest }: SubmersiblePumpProps & GroupProps) {\n  const { kw, dischargeNps, rating, depth, caissonDiameter, cutaway, color, ...group } = { ...defaults, ...rest }\n  const built = useMemo(() => build({ kw, dischargeNps, rating, depth, caissonDiameter, cutaway, color, tag }), [kw, dischargeNps, rating, depth, caissonDiameter, cutaway, color, tag])\n  useEffect(() => () => built.geometry.dispose(), [built])\n  return (\n    <group {...group}>\n      <KitMesh data={built} />\n    </group>\n  )\n}\n\nexport default SubmersiblePump\n\nfunction build(p: P) {\n  const k = new Kit()\n  const L = layout(p)\n  const { R, dOD, yTp, Re, yB, xFace, Ru, Lm, Lp, depth } = L\n  const H: MatKey = paint(p.color, { metalness: 0.3, roughness: 0.45, clearcoat: 0.35, clearcoatRoughness: 0.4 })\n  const STEEL: MatKey = 'internalsSteel'\n  const nps = p.dischargeNps\n  caisson(k, L.C, p.cutaway)\n\n  // top plate on the caisson flange, studs, lifting lugs, riser hanger\n  const flR = R + 0.11\n  k.box('equipmentPaintDark', [2 * flR + 0.12, 0.03, 2 * flR + 0.12], [0, 0.065, 0])\n  annulus(k, H, 0, flR - 0.01, 0.08, yTp, [0, 0, 0], 'y', 48)\n  k.boltCircle('boltXylan', 'nut', Math.max(8, Math.round((TAU * (R + 0.06)) / 0.16)), R + 0.06, [0, 0, 0], 'y', 0.022, 0.17, [0.145])\n  for (const s of [-1, 1]) liftingLug(k, [0, yTp, s * (flR - 0.12)], 'x', 1, H)\n\n  // riser through the top plate, long-radius bend, spool and WN flange at the discharge port\n  const yRT = yB - Re\n  k.pipe(H, [0, 0.05, 0], [0, yRT, 0], dOD, pipeWall(nps, 'STD'), 32)\n  k.cyl(H, dOD / 2 + 0.04, 0.06, [0, yTp + 0.03, 0], 'y', 32)\n  k.bend(H, [0, yRT, 0], [0, 1, 0], [1, 0, 0], Re, dOD, Math.PI / 2, pipeWall(nps, 'STD'))\n  flangedBranch(k, H, [Re - 0.002, yB, 0], [xFace, yB, 0], nps, p.rating, 'STD')\n  // air release valve on the crown, gauge, hypochlorite injection quill\n  const yTopP = yB + dOD / 2\n  k.cyl(H, 0.03, 0.1, [Re + 0.08, yTopP + 0.03, 0], 'y', 12)\n  smallValve(k, [Re + 0.08, yTopP + 0.1, 0], 0.9)\n  k.rod('castIron', [Re + 0.08, yTopP + 0.1, 0], [Re + 0.08, yTopP + 0.1, -0.12], 0.02, 10)\n  k.cyl('castIron', 0.06, 0.15, [Re + 0.08, yTopP + 0.12, -0.17], 'y', 18)\n  k.dome('castIron', 0.06, [Re + 0.08, yTopP + 0.195, -0.17], 'y', Math.PI / 2, 0.6, 18)\n  pipeGauge(k, [Re + 0.25, yTopP, 0], [0, 0, 1], 16)\n  k.rod('stainless', [-dOD / 2 - 0.15, yTp + 0.2, 0], [-dOD / 2 + 0.005, yTp + 0.2, 0], 0.012, 8)\n  smallValve(k, [-dOD / 2 - 0.12, yTp + 0.2, 0], 0.7, '-x')\n  k.rod('stainless', [-dOD / 2 - 0.1, yTp + 0.2, 0], [-dOD / 2 - 0.1, yTp, 0], 0.01, 8)\n\n  // power cable junction box and gland on the top plate (-Z side)\n  const jz = -(R * 0.55)\n  k.box('equipmentPaintDark', [0.05, 0.6, 0.05], [-0.25, yTp + 0.3, jz])\n  junctionBox(k, [-0.25, yTp + 0.45, jz + 0.025], [0, 0, 1], [0.35, 0.3, 0.18])\n  k.cyl('brass', 0.035, 0.08, [-0.25, yTp + 0.04, jz + 0.12], 'y', 12)\n  k.spline('cable', [[-0.25, yTp + 0.27, jz + 0.12], [-0.25, yTp + 0.15, jz + 0.12], [-0.25, yTp + 0.06, jz + 0.12]], 0.022, 8, 12)\n  if (p.tag) tagPlate(k, p.tag, [0, yTp - 0.02, flR - 0.009], 'z', 0.16)\n  tagPlate(k, `${p.kw} kW SUBMERSIBLE`, [-0.25, yTp + 0.2, jz + 0.03], 'z', 0.18)\n\n  // riser stack: 6 m flanged sections with centralisers, cable clamped alongside\n  const yU = -depth + 0.6 + Lp + Lm + 0.25 // riser bottom = unit top\n  const joints: number[] = []\n  for (let y = -0.5; y > yU + 0.5; y -= 6) joints.push(y)\n  k.pipe(H, [0, yU, 0], [0, 0.05, 0], dOD, pipeWall(nps, 'STD'), 32)\n  const f = flangeDims(nps, p.rating)\n  for (const y of joints) {\n    annulus(k, 'flangeSteel', dOD / 2 - 0.002, f.od / 2, y - f.thickness, y + f.thickness, [0, 0, 0], 'y', 32)\n    k.boltCircle('boltXylan', 'nut', f.bolts, f.boltCircle / 2, [0, y - f.thickness - f.boltDia, 0], 'y', f.boltDia, f.thickness * 2 + f.boltDia * 2, [f.boltDia * 0.5, f.thickness * 2 + f.boltDia * 1.5])\n    // centraliser: three bow arms to the caisson wall\n    for (let i = 0; i < 3; i++) {\n      const a = (i / 3) * TAU + 0.3\n      const u = [Math.cos(a), Math.sin(a)]\n      k.box(STEEL, [0.012, 0.35, R - 0.02 - f.od / 2], [u[0] * (f.od / 2 + (R - 0.02 - f.od / 2) / 2), y - 0.3, u[1] * (f.od / 2 + (R - 0.02 - f.od / 2) / 2)], new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), -a + Math.PI / 2))\n    }\n  }\n  const zc = dOD / 2 + 0.04\n  k.rod('cable', [0, yU + 0.1, -zc], [0, 0.05, -zc], 0.02, 8)\n  for (let y = yU + 1.5; y < -0.2; y += 1.5) k.torus('stainless', zc, 0.006, [0, y, 0], 'y', 4, 24)\n\n  // pump-motor unit: outer shell (water rises around the motor), motor inside, pump and strainer below\n  const yUt = yU\n  const yM0 = yUt - 0.25 - Lm\n  k.lathe(H, [[dOD / 2, yUt + 0.01], [Ru * 0.8, yUt - 0.1], [Ru, yUt - 0.25], [Ru, yM0], [Ru * 0.9, yM0 - 0.08]], [0, 0, 0], 'y', 40)\n  for (let i = 1; i < 4; i++) annulus(k, H, Ru, Ru + 0.012, yM0 + (Lm * i) / 4 - 0.01, yM0 + (Lm * i) / 4 + 0.01, [0, 0, 0], 'y', 40)\n  k.boltCircle('boltXylan', 'nut', 12, Ru + 0.02, [0, yUt - 0.27, 0], 'y', 0.012, 0.06, [0.005, 0.055])\n  const yP1 = yM0 - 0.08\n  const yP0 = yP1 - Lp\n  k.lathe(H, [[Ru * 0.9, yP1], [Ru * 0.95, yP1 - Lp * 0.2], [Ru * 0.95, yP0 + Lp * 0.3], [Ru * 0.75, yP0]], [0, 0, 0], 'y', 36)\n  for (const y of [yP1 - Lp * 0.2, yP0 + Lp * 0.3]) annulus(k, H, Ru * 0.94, Ru * 1.02, y - 0.015, y + 0.015, [0, 0, 0], 'y', 36)\n  k.disc('pipeBore', Ru * 0.75, [0, yP0 + 0.002, 0], '-y', 32)\n  const yS1 = yP0\n  const yS0 = Math.max(yS1 - 0.45, -depth + 0.15)\n  for (let i = 0; i < 18; i++) {\n    const a = (i / 18) * TAU\n    k.rod(STEEL, [Math.cos(a) * Ru * 0.72, yS1, Math.sin(a) * Ru * 0.72], [Math.cos(a) * Ru * 0.72, yS0, Math.sin(a) * Ru * 0.72], 0.008, 5)\n  }\n  annulus(k, STEEL, 0, Ru * 0.75, yS0 - 0.01, yS0 + 0.01, [0, 0, 0], 'y', 32)\n  annulus(k, STEEL, Ru * 0.68, Ru * 0.76, (yS0 + yS1) / 2 - 0.01, (yS0 + yS1) / 2 + 0.01, [0, 0, 0], 'y', 32)\n  // unit centralisers\n  for (let i = 0; i < 3; i++) {\n    const a = (i / 3) * TAU\n    k.box(STEEL, [0.012, 0.3, R - 0.02 - Ru], [Math.cos(a) * (Ru + (R - 0.02 - Ru) / 2), yM0 + Lm * 0.5, Math.sin(a) * (Ru + (R - 0.02 - Ru) / 2)], new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), -a + Math.PI / 2))\n  }\n  return k.build()\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"
    },
    {
      "path": "rotating/_pumpsCaisson.ts",
      "type": "registry:file",
      "target": "components/deck-twin/rotating/_pumpsCaisson.ts",
      "content": "// Vertical line-shaft caisson pump below the driver (API 610 VS1 / NFPA 20): caisson, column and bowls, discharge head with side outlet.\nimport * as THREE from 'three'\nimport { Kit, type MatKey, type V3 } from '../lib/kit'\nimport { tagPlate } from '../lib/parts'\nimport { paint } from '../lib/materials'\nimport { NPS_SIZES, flange as flangeDims, pipeOD, type Rating } from '../lib/standards'\nimport { TAU, annulus, clamp, faceQ, flangeLen, flangedBranch, gusset, openTube, pipeGauge } from './_pumps'\n\nexport interface CaissonInput {\n  caissonDiameter: number\n  depth: number\n  dischargeNps: number\n  rating: Rating\n  stages: number\n}\n\n/** Shared layout for the caisson pump geometry and its ports. Origin: caisson top flange centre at deck level, discharge along +X. */\nexport function caissonLayout(p: CaissonInput) {\n  const R = Math.max(p.caissonDiameter, 0.4) / 2\n  const dNps = p.dischargeNps\n  const dOD = pipeOD(dNps)\n  const fits = NPS_SIZES.filter((s) => pipeOD(s) <= 2 * R * 0.6)\n  const colNps = fits.find((s) => s >= dNps + 2) ?? fits[fits.length - 1] ?? dNps\n  const colR = pipeOD(colNps) / 2\n  const rb = clamp(Math.max(colR + 0.05, (dOD / 2) * 1.3), 0.14, R + 0.05)\n  const fOD = flangeDims(dNps, p.rating).od\n  const yOut = Math.max(0.6 + dOD / 2, fOD / 2 + 0.3)\n  const yTop = yOut + Math.max(dOD / 2, rb * 0.8) + 0.14\n  const rs = clamp(0.018 + colR * 0.12, 0.025, 0.06)\n  const xFace = Math.max(rb + 0.3, R + 0.25) + flangeLen(dNps, p.rating)\n  const bowlR = clamp(Math.min(R - 0.06, colR * 1.9), colR * 1.15, R - 0.03)\n  const stages = clamp(Math.round(p.stages), 1, 10)\n  // never shorter than the bowl stack, bell, discharge case and one column joint\n  const depth = Math.max(p.depth, 1.1 + bowlR * (0.75 + stages * 0.95 + 0.8) + 0.8)\n  return { R, depth, dNps, rating: p.rating, dOD, colR, rb, yOut, yTop, rs, xFace, bowlR, stages }\n}\nexport type CaissonLayout = ReturnType<typeof caissonLayout>\n\nconst CAISSON = () => paint('#6f7774', { metalness: 0.3, roughness: 0.65, clearcoat: 0, weather: 1 })\nconst COLUMN = () => paint('#55697a', { metalness: 0.35, roughness: 0.5, clearcoat: 0 })\nconst BOWL = () => paint('#2f5f8a', { metalness: 0.3, roughness: 0.45 })\nconst ANODE = () => paint('#9a9d9b', { metalness: 0.55, roughness: 0.75, clearcoat: 0 })\nconst DARK: MatKey = 'equipmentPaintDark'\n\n// the quadrant facing the default camera (+X..+Z) is removed in cutaway mode\nconst CUT0 = Math.PI / 2\nconst CUT1 = TAU\nconst inCut = (a: number) => {\n  const phi = (Math.atan2(Math.cos(a), Math.sin(a)) + TAU) % TAU // lathe phi of a point at angle a (x = cos a, z = sin a)\n  return phi < CUT0\n}\n\n/** Caisson: deck support plate, top flange, shell, guide clamps every 6 m, anodes, barred open end. */\nexport function caisson(k: Kit, L: CaissonLayout, cutaway = false) {\n  const { R, depth: Lc } = L\n  const shell = CAISSON()\n  const flR = R + 0.11\n  const cageH = Math.min(1.6, Lc * 0.12)\n  const yCage = -Lc + cageH\n  const seg = 48\n  const p0 = cutaway ? CUT0 : 0\n  const pl = cutaway ? CUT1 - CUT0 : TAU\n  const ring = (mat: MatKey, r0: number, r1: number, y0: number, y1: number) =>\n    k.lathe(mat, [[r0, y0], [r1, y0], [r1, y1], [r0, y1], [r0, y0]], [0, 0, 0], 'y', seg, p0, pl)\n\n  const sp = 2 * flR + 0.4\n  k.box(DARK, [sp, 0.05, sp], [0, -0.025, 0])\n  for (let i = 0; i < 8; i++) gusset(k, DARK, (i / 8) * TAU + Math.PI / 8, R, sp / 2 - 0.04, -0.05, -0.45, 0.02)\n  annulus(k, DARK, R - 0.014, flR, 0, 0.05, [0, 0, 0], 'y', seg)\n  ring(shell, R - 0.014, R, yCage, -0.05)\n  for (let y = -12; y > yCage + 0.5; y -= 12) ring(shell, R, R + 0.004, y - 0.012, y + 0.012)\n\n  for (let y = -6; y > yCage + 1; y -= 6) {\n    ring(DARK, R, R + 0.035, y - 0.15, y + 0.15)\n    for (const s of [-1, 1]) ring(DARK, R + 0.035, R + 0.06, y + s * 0.15 - 0.012, y + s * 0.15 + 0.012)\n    for (const s of [-1, 1]) {\n      const z = s * (R + 0.035 + 0.07)\n      for (const dx of [-0.028, 0.028]) k.box(DARK, [0.022, 0.28, 0.14], [dx, y, z])\n      for (const dy of [-0.08, 0.08]) {\n        k.cyl('boltXylan', 0.011, 0.15, [0, y + dy, z + 0.02], 'x', 8)\n        for (const sx of [-1, 1]) k.hex('nut', 0.036, 0.018, [sx * 0.05, y + dy, z + 0.02], 'x')\n      }\n    }\n    // guide-frame stubs towards the jacket bracing\n    for (const s of [-1, 1]) {\n      if (cutaway && s > 0) continue\n      const x0 = s * (R + 0.035)\n      const x1 = s * (R + 0.75)\n      k.box(DARK, [Math.abs(x1 - x0), 0.012, 0.2], [(x0 + x1) / 2, y + 0.14, 0])\n      k.box(DARK, [Math.abs(x1 - x0), 0.012, 0.2], [(x0 + x1) / 2, y - 0.14, 0])\n      k.box(DARK, [Math.abs(x1 - x0), 0.28, 0.012], [(x0 + x1) / 2, y, 0])\n    }\n  }\n\n  const anodeRows = Math.min(3, Math.floor((Lc - cageH - 2) / 4))\n  for (let r = 0; r < anodeRows; r++) {\n    const ya = yCage + 1.6 + r * 4\n    for (let i = 0; i < 3; i++) {\n      const a = (i / 3) * TAU + r * 0.6 + 2.2\n      if (cutaway && inCut(a)) continue\n      const ux = Math.cos(a)\n      const uz = Math.sin(a)\n      for (const dy of [-0.5, 0.5]) k.rod(DARK, [ux * R, ya + dy, uz * R], [ux * (R + 0.13), ya + dy, uz * (R + 0.13)], 0.012, 6)\n      k.box(ANODE(), [0.13, 0.85, 0.12], [ux * (R + 0.15), ya, uz * (R + 0.15)], faceQ([ux, 0, uz]))\n    }\n  }\n\n  const nBars = Math.max(16, Math.round((TAU * R) / 0.11))\n  for (let i = 0; i < nBars; i++) {\n    const a = (i / nBars) * TAU\n    if (cutaway && inCut(a)) continue\n    k.rod(shell, [Math.cos(a) * (R - 0.012), yCage + 0.02, Math.sin(a) * (R - 0.012)], [Math.cos(a) * (R - 0.012), -Lc + 0.06, Math.sin(a) * (R - 0.012)], 0.016, 6)\n  }\n  ring(DARK, R - 0.03, R + 0.02, yCage - 0.03, yCage + 0.05)\n  ring(DARK, R - 0.03, R + 0.02, yCage - cageH / 2 - 0.02, yCage - cageH / 2 + 0.02)\n  ring(DARK, R - 0.04, R + 0.03, -Lc, -Lc + 0.08)\n}\n\n/** Column (flanged 3 m sections, line shaft, bearing spiders), bowl assembly (one bowl per stage), suction bell and basket strainer. */\nexport function pumpColumn(k: Kit, L: CaissonLayout) {\n  const { depth: Lc, colR, rs, bowlR, stages: nSt } = L\n  const bowl = BOWL()\n  const col = COLUMN()\n  const STEEL: MatKey = 'internalsSteel'\n  const hSt = bowlR * 0.95\n  const yBell = -Lc + Math.min(1.1, Lc * 0.12)\n  const hBell = bowlR * 0.75\n\n  const hBk = Math.min(0.45, yBell + Lc - 0.1)\n  const rBk = bowlR * 0.95\n  for (let i = 0; i < 14; i++) {\n    const a = (i / 14) * TAU\n    k.rod(STEEL, [Math.cos(a) * rBk, yBell - hBk, Math.sin(a) * rBk], [Math.cos(a) * rBk * 1.08, yBell, Math.sin(a) * rBk * 1.08], 0.008, 5)\n  }\n  annulus(k, STEEL, 0, rBk + 0.01, yBell - hBk - 0.012, yBell - hBk, [0, 0, 0], 'y', 32)\n  annulus(k, STEEL, rBk * 0.95, rBk * 1.1, yBell - hBk * 0.5 - 0.01, yBell - hBk * 0.5 + 0.01, [0, 0, 0], 'y', 32)\n  k.lathe(bowl, [\n    [bowlR * 1.0, yBell + 0.02],\n    [bowlR * 1.22, yBell],\n    [bowlR * 1.24, yBell + 0.03],\n    [bowlR * 1.05, yBell + hBell * 0.35],\n    [bowlR * 0.88, yBell + hBell],\n    [bowlR * 0.74, yBell + hBell],\n    [bowlR * 0.82, yBell + hBell * 0.5],\n    [bowlR * 1.0, yBell + 0.02],\n  ], [0, 0, 0], 'y', 36)\n  const yb0 = yBell + hBell\n  for (let i = 0; i < nSt; i++) {\n    const y0 = yb0 + i * hSt\n    k.lathe(bowl, [\n      [bowlR * 0.84, y0],\n      [bowlR * 0.97, y0 + hSt * 0.06],\n      [bowlR, y0 + hSt * 0.2],\n      [bowlR, y0 + hSt * 0.62],\n      [bowlR * 0.9, y0 + hSt * 0.9],\n      [bowlR * 0.84, y0 + hSt],\n    ], [0, 0, 0], 'y', 36)\n    annulus(k, bowl, bowlR * 0.8, bowlR * 1.05, y0 - 0.018, y0 + 0.018, [0, 0, 0], 'y', 36)\n    k.boltCircle('boltXylan', 'nut', 8, bowlR * 0.96, [0, y0 - 0.03, 0], 'y', 0.014, 0.07, [0.006, 0.06])\n  }\n  const yb1 = yb0 + nSt * hSt\n  annulus(k, bowl, bowlR * 0.8, bowlR * 1.05, yb1 - 0.018, yb1 + 0.018, [0, 0, 0], 'y', 36)\n  const yDc = yb1 + bowlR * 0.8\n  k.lathe(bowl, [\n    [bowlR * 0.86, yb1],\n    [bowlR * 0.82, yb1 + bowlR * 0.2],\n    [colR * 1.05, yDc - 0.04],\n    [colR * 1.05, yDc],\n  ], [0, 0, 0], 'y', 36)\n\n  const yTopCol = 0.08\n  openTube(k, col, [0, yDc, 0], [0, yTopCol, 0], colR, 32)\n  const joints: number[] = [yDc + 0.04]\n  for (let y = yTopCol - 3; y > yDc + 0.5; y -= 3) joints.push(y)\n  joints.push(yTopCol - 0.06)\n  const fR = colR + 0.065\n  for (const y of joints) {\n    annulus(k, col, colR - 0.004, fR, y - 0.035, y + 0.035, [0, 0, 0], 'y', 32)\n    k.boltCircle('boltXylan', 'nut', 8, colR + 0.035, [0, y - 0.06, 0], 'y', 0.016, 0.12, [0.012, 0.108])\n  }\n  k.cyl(STEEL, rs, L.yTop + 0.25 - (yb0 + 0.05), [0, (L.yTop + 0.25 + yb0 + 0.05) / 2, 0], 'y', 14)\n  for (const y of joints.slice(1, -1)) {\n    annulus(k, STEEL, colR * 0.86, colR * 0.94, y - 0.04, y + 0.04, [0, 0, 0], 'y', 24)\n    k.cyl('brass', rs * 1.7, 0.14, [0, y, 0], 'y', 14)\n    for (let i = 0; i < 3; i++) {\n      const a = (i / 3) * TAU\n      k.rod(STEEL, [Math.cos(a) * rs * 1.6, y, Math.sin(a) * rs * 1.6], [Math.cos(a) * colR * 0.88, y, Math.sin(a) * colR * 0.88], 0.012, 6)\n    }\n    k.cyl(STEEL, rs * 1.6, 0.16, [0, y + 0.35, 0], 'y', 14)\n  }\n}\n\n/** Fabricated discharge head on a soleplate: gusseted barrel, side outlet with WN flange at the port, stuffing box, ARV, gauge, tag. */\nexport function dischargeHead(k: Kit, L: CaissonLayout, drv: MatKey, tag?: string) {\n  const { R, rb, yOut, yTop, dOD, rs, xFace, dNps, rating } = L\n  const flR = R + 0.11\n  const yHb = 0.13\n\n  k.box(DARK, [2 * flR + 0.12, 0.03, 2 * flR + 0.12], [0, 0.065, 0])\n  annulus(k, drv, 0, flR - 0.01, 0.08, yHb, [0, 0, 0], 'y', 48)\n  k.boltCircle('boltXylan', 'nut', Math.max(8, Math.round((TAU * (R + 0.06)) / 0.16)), R + 0.06, [0, 0.0, 0], 'y', 0.022, 0.17, [0.145])\n  for (const sx of [-1, 1])\n    for (const sz of [-1, 1]) {\n      const p: V3 = [sx * (flR + 0.02), 0.08, sz * (flR + 0.02)]\n      k.hex('nut', 0.04, 0.025, [p[0], p[1] + 0.012, p[2]])\n      k.cyl('boltXylan', 0.01, 0.06, [p[0], p[1] + 0.03, p[2]], 'y', 6)\n    }\n\n  openTube(k, drv, [0, yHb, 0], [0, yTop, 0], rb, 40)\n  k.lathe(drv, [[rb * 1.5, yHb], [rb * 1.18, yHb + 0.06], [rb, yHb + 0.14]], [0, 0, 0], 'y', 40)\n  for (let i = 0; i < 6; i++) gusset(k, drv, (i / 6) * TAU + Math.PI / 6, rb * 0.98, flR - 0.05, yHb, Math.min(0.5, yOut - dOD / 2 - yHb - 0.04), 0.018)\n  annulus(k, drv, 0, rb + 0.05, yTop - 0.04, yTop, [0, 0, 0], 'y', 40)\n  k.boltCircle('boltXylan', 'nut', 8, rb + 0.025, [0, yTop - 0.06, 0], 'y', 0.014, 0.08, [0.07])\n\n  // side outlet: branch + reinforcing pad + WN flange at the discharge port\n  flangedBranch(k, drv, [0, yOut, 0], [xFace, yOut, 0], dNps, rating, 'STD')\n  k.cyl(drv, dOD / 2 + 0.03, 0.03, [rb + 0.012, yOut, 0], 'x', 32)\n  const xEnd = xFace - flangeLen(dNps, rating)\n  k.box(drv, [Math.max(0.05, xEnd - rb - 0.06), Math.max(0.05, yOut - dOD / 2 - yHb), 0.016], [rb + (xEnd - rb - 0.06) / 2, (yOut - dOD / 2 + yHb) / 2, 0])\n\n  // stuffing box, gland, leak-off\n  const ySb = yTop\n  k.cyl(drv, rs * 3.4, 0.16, [0, ySb + 0.08, 0], 'y', 28)\n  annulus(k, drv, rs * 1.2, rs * 3.9, ySb + 0.15, ySb + 0.18, [0, 0, 0], 'y', 28)\n  k.cyl('stainless', rs * 2.6, 0.05, [0, ySb + 0.205, 0], 'y', 24)\n  annulus(k, 'stainless', rs * 1.1, rs * 3.4, ySb + 0.23, ySb + 0.245, [0, 0, 0], 'y', 24)\n  for (const s of [-1, 1]) {\n    k.cyl('boltXylan', 0.008, 0.12, [0, ySb + 0.22, s * rs * 3.0], 'y', 6)\n    k.hex('nut', 0.026, 0.014, [0, ySb + 0.255, s * rs * 3.0])\n  }\n  k.tube('tubing', [[-rs * 3.4, ySb + 0.06, 0], [-rb - 0.06, ySb + 0.03, 0], [-rb - 0.06, yHb + 0.04, 0], [-rb - 0.06, yHb + 0.04, rb * 0.9]], 0.008)\n\n  // air release / vacuum breaker valve on the outlet\n  const xa = rb + (xEnd - rb) * 0.55\n  const yTopBr = yOut + dOD / 2\n  k.cyl(drv, 0.03, 0.08, [xa, yTopBr + 0.03, 0], 'y', 14)\n  annulus(k, 'flangeSteel', 0, 0.06, yTopBr + 0.07, yTopBr + 0.09, [xa, 0, 0], 'y', 18)\n  k.box('valveBody', [0.08, 0.1, 0.07], [xa, yTopBr + 0.14, 0])\n  k.cyl('handwheel', 0.006, 0.08, [xa, yTopBr + 0.23, 0], 'y', 6)\n  k.handwheel('handwheel', 0.1, [xa, yTopBr + 0.27, 0], 'y')\n  k.cyl('castIron', 0.065, 0.16, [xa + 0.13, yTopBr + 0.2, 0], 'y', 20)\n  k.dome('castIron', 0.065, [xa + 0.13, yTopBr + 0.28, 0], 'y', Math.PI / 2, 0.6, 20)\n  k.rod('castIron', [xa, yTopBr + 0.14, 0], [xa + 0.13, yTopBr + 0.14, 0], 0.022, 10)\n  const zDr = -dOD / 2 - 0.12\n  k.tube('tubing', [[xa + 0.19, yTopBr + 0.24, 0], [xa + 0.19, yTopBr + 0.24, zDr], [xa + 0.19, yHb + 0.02, zDr]], 0.012)\n  pipeGauge(k, [rb + (xEnd - rb) * 0.2, yTopBr, 0], [0, 0, 1], 25)\n\n  k.hex('nut', 0.04, 0.03, [-rb * 1.2, yHb + 0.02, -rb * 0.6])\n  for (const s of [-1, 1]) {\n    k.box(drv, [0.12, 0.1, 0.02], [0, yTop + 0.05, s * (rb + 0.02)])\n    k.torus('nut', 0.025, 0.007, [0, yTop + 0.08, s * (rb + 0.02)], 'z', 6, 14)\n  }\n  const ty = (yHb + yTop) / 2 + 0.05\n  const tq = new THREE.Vector3(0, 0, 1)\n  if (tag) tagPlate(k, tag, [0, ty, rb + 0.003], tq.toArray() as V3, 0.16)\n}\n"
    }
  ],
  "categories": [
    "rotating"
  ],
  "meta": {
    "export": "SubmersiblePump",
    "anchor": "base",
    "standards": [
      "API 610 (VS family, submersible)",
      "ASME B16.5",
      "ASME B16.9"
    ]
  }
}