{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "diesel-firewater-pump",
  "type": "registry:component",
  "title": "Diesel firewater pump (NFPA 20)",
  "description": "Diesel-driven fire pump set in firewater red: engine with crankcase doors, turbocharger, residential exhaust silencer, bunded day tank with flame arrester and sight glass, A/B starter batteries and a fire pump controller. Engine jacket water is cooled by a heat exchanger fed from the pump discharge through a duplicated NFPA 20 cooling loop (valves, strainers, regulator, solenoid) to an open waste cone, or by a radiator. `pumpType` selects a vertical turbine caisson pump driven through a fan-cooled right-angle gear and guarded cardan shaft, or a horizontal split-case (BB1 double suction) pump with packed boxes, grease-lubricated bearings, air release valve and gauges on a common skid.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "rotating/diesel-firewater-pump.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/rotating/diesel-firewater-pump.tsx",
      "content": "// NFPA 20 diesel firewater pump set: engine with radiator, silencer, day tank, batteries and controller, driving a right-angle gear on a caisson pump or a horizontal split-case pump.\nimport * as THREE from 'three'\nimport { useEffect, useMemo } from 'react'\nimport { Kit, type MatKey, type V3 } from '../lib/kit'\nimport { KitMesh, type GroupProps } from '../lib/mesh'\nimport { paint } from '../lib/materials'\nimport { anchorBolt, annulus, castExtrude, clamp, faceQ, flangeLen, flangedBranch, meshTube, openTube, pipeGauge, smallValve, voluteShape } from './_pumps'\nimport { caisson, caissonLayout, dischargeHead, pumpColumn } from './_pumpsCaisson'\nimport { port, type Port } from '../lib/ports'\nimport { Rotors, fanImpeller, guardedCoupling, type RotorSpec } from '../lib/rotors'\nimport { liftingLug, tagPlate } from '../lib/parts'\nimport { fanGuardMaterial, lcdMaterial } from '../lib/textures'\nimport { flange as flangeDims, pipeOD, type Rating } from '../lib/standards'\n\nexport type FirePumpType = 'vertical' | 'split-case'\n\nexport interface DieselFirewaterPumpProps {\n  /** vertical turbine in a caisson (right-angle gear) or horizontal split-case */\n  pumpType?: FirePumpType\n  /** engine rated power, kW */\n  kw?: number\n  dischargeNps?: number\n  /** split-case only */\n  suctionNps?: number\n  rating?: Rating\n  /** caisson length below the deck (vertical), m */\n  depth?: number\n  caissonDiameter?: number\n  stages?: number\n  cutaway?: boolean\n  /** engine jacket-water cooling: raw-water heat exchanger fed from the pump discharge, or radiator */\n  cooling?: 'heat-exchanger' | 'radiator'\n  tag?: string\n  running?: boolean\n}\n\nexport const defaults = {\n  pumpType: 'vertical' as FirePumpType,\n  kw: 750,\n  dischargeNps: 10,\n  suctionNps: 12,\n  rating: 150 as Rating,\n  depth: 25,\n  caissonDiameter: 1.0,\n  stages: 3,\n  cutaway: false,\n  cooling: 'heat-exchanger' as 'heat-exchanger' | 'radiator',\n  running: true,\n}\n\ntype P = Omit<typeof defaults, 'running'> & { tag?: string }\n\n// split-case layout: shaft along X at ys, skid top at yk, volute centred on x = 0\nfunction splitLayout(p: P) {\n  const dOD = pipeOD(p.dischargeNps)\n  const sOD = pipeOD(p.suctionNps)\n  const Rimp = 0.12 + 0.025 * p.dischargeNps\n  const r0 = Rimp * 1.05\n  const r1 = r0 + dOD\n  const wv = dOD * 1.15\n  const yk = 0.24\n  const fOD = Math.max(flangeDims(p.dischargeNps, p.rating).od, flangeDims(p.suctionNps, p.rating).od)\n  const ys = yk + Math.max(r1 + 0.15, (r0 + r1) / 2 + fOD / 2 + 0.15)\n  const yN = ys - (r0 + r1) / 2\n  const out = r1 + 0.05\n  const zD = out + 0.25 + flangeLen(p.dischargeNps, p.rating)\n  const zS = -(r1 + sOD * 0.6 + 0.3 + flangeLen(p.suctionNps, p.rating))\n  return { dOD, sOD, Rimp, r0, r1, wv, yk, ys, yN, out, zD, zS }\n}\n\nexport function ports(props: DieselFirewaterPumpProps = {}): Port[] {\n  const p = { ...defaults, ...props }\n  if (p.pumpType === 'split-case') {\n    const s = splitLayout(p)\n    return [\n      port('suction', 'Suction', [0, s.yN, s.zS], [0, 0, -1], p.suctionNps, p.rating),\n      port('discharge', 'Discharge', [0, s.yN, s.zD], [0, 0, 1], p.dischargeNps, p.rating),\n    ]\n  }\n  const L = caissonLayout(p)\n  return [port('discharge', 'Discharge', [L.xFace, L.yOut, 0], [1, 0, 0], p.dischargeNps, p.rating)]\n}\n\nexport function DieselFirewaterPump({ tag, ...rest }: DieselFirewaterPumpProps & GroupProps) {\n  const { pumpType, kw, dischargeNps, suctionNps, rating, depth, caissonDiameter, stages, cutaway, cooling, running, ...group } = { ...defaults, ...rest }\n  const { built, rotors } = useMemo(\n    () => build({ pumpType, kw, dischargeNps, suctionNps, rating, depth, caissonDiameter, stages, cutaway, cooling, tag }),\n    [pumpType, kw, dischargeNps, suctionNps, rating, depth, caissonDiameter, stages, cutaway, cooling, 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 DieselFirewaterPump\n\nconst RED = 'firewaterRed' as const\nconst DARK: MatKey = 'equipmentPaintDark'\nconst EXHAUST = () => paint('#a3a6a2', { metalness: 0.65, roughness: 0.38, clearcoat: 0 })\nconst CORE = () => paint('#2a2e31', { metalness: 0.5, roughness: 0.6, clearcoat: 0 })\n\nfunction build(p: P) {\n  const k = new Kit()\n  const rotors: RotorSpec[] = []\n  if (p.pumpType === 'split-case') splitCase(k, p, rotors)\n  else verticalGearDrive(k, p, rotors)\n  return { built: k.build(), rotors }\n}\n\n// ------------------------------------------------------------------ vertical: caisson pump + right-angle gear + cardan shaft\nfunction verticalGearDrive(k: Kit, p: P, rotors: RotorSpec[]) {\n  const L = caissonLayout(p)\n  caisson(k, L, p.cutaway)\n  pumpColumn(k, L)\n  dischargeHead(k, L, RED, p.tag)\n  const { yTop, rb, rs } = L\n  const Rg = clamp(0.2 + 0.00011 * p.kw, 0.24, 0.45)\n  const yG0 = yTop\n  const hGs = 0.45\n  const yG1 = yG0 + hGs\n  const rSt = Math.max(rb + 0.02, Rg * 0.85)\n  annulus(k, RED, rSt - 0.07, rSt + 0.05, yG0 - 0.01, yG0 + 0.04, [0, 0, 0], 'y', 40)\n  annulus(k, RED, rSt - 0.07, rSt + 0.05, yG1 - 0.04, yG1, [0, 0, 0], 'y', 40)\n  for (let i = 0; i < 4; i++) {\n    const a = (i / 4) * Math.PI * 2 + Math.PI / 4\n    k.box(RED, [0.08, hGs - 0.08, 0.025], [Math.cos(a) * rSt, (yG0 + yG1) / 2, Math.sin(a) * rSt], faceQ([Math.cos(a), 0, Math.sin(a)]))\n  }\n  meshTube(k, [0, yG0 + 0.04, 0], [0, yG1 - 0.04, 0], rSt + 0.01)\n  {\n    const r = new Kit()\n    r.cyl('flangeSteel', rs, yG1 - yTop - 0.2, [0, 0, 0], 'y', 14)\n    r.cyl('flangeSteel', rs * 3.2, 0.05, [0, yG0 + 0.36 - (yG1 + yTop + 0.2) / 2, 0], 'y', 20)\n    r.box('nut', [rs * 0.5, 0.07, rs * 0.5], [rs * 3.2, yG0 + 0.36 - (yG1 + yTop + 0.2) / 2, 0])\n    rotors.push({ built: r.build(), axis: 'y', pos: [0, (yG1 + yTop + 0.2) / 2, 0], rpm: 150 })\n  }\n  // gear drive: base flange, finned vertical housing, horizontal bevel-gear housing, input towards -X\n  const Hl = Rg\n  annulus(k, RED, 0, Rg * 1.18, yG1, yG1 + 0.05, [0, 0, 0], 'y', 40)\n  k.boltCircle('boltXylan', 'nut', 8, Rg * 1.08, [0, yG1 - 0.03, 0], 'y', 0.016, 0.1, [0.09], Math.PI / 8)\n  k.cyl(RED, Rg, Hl, [0, yG1 + 0.05 + Hl / 2, 0], 'y', 40)\n  for (let i = 1; i <= 5; i++) annulus(k, RED, Rg, Rg + 0.03, yG1 + 0.05 + (Hl * i) / 6 - 0.006, yG1 + 0.05 + (Hl * i) / 6 + 0.006, [0, 0, 0], 'y', 40)\n  const yIn = yG1 + 0.05 + Hl + Rg * 0.5\n  const rH = Rg * 0.82\n  k.cyl(RED, Rg * 0.9, yIn - (yG1 + 0.05 + Hl), [0, (yIn + yG1 + 0.05 + Hl) / 2, 0], 'y', 40)\n  k.cyl(RED, rH, Rg * 1.75, [-Rg * 0.25, yIn, 0], 'x', 40)\n  k.dome(RED, rH, [Rg * 0.62, yIn, 0], 'x', Math.PI / 2, 0.35, 32)\n  k.cyl(RED, Rg * 0.92, 0.06, [0, yIn + rH * 0.75, 0], 'y', 40)\n  k.boltCircle('boltXylan', 'nut', 8, Rg * 0.8, [0, yIn + rH * 0.75, 0], 'y', 0.012, 0.05, [0.04])\n  // shaft-driven cooling fan under a guard on top, cooling-water in / out low on the body\n  k.cyl(RED, Rg * 0.6, 0.1, [0, yIn + rH * 0.75 + 0.08, 0], 'y', 32, Rg * 0.55, true)\n  k.disc(fanGuardMaterial(), Rg * 0.56, [0, yIn + rH * 0.75 + 0.13, 0], 'y', 32)\n  for (const a of [0.5, -0.5]) {\n    const q: V3 = [Math.cos(a + Math.PI / 2) * Rg, yG1 + 0.05 + Hl * 0.2, Math.sin(a + Math.PI / 2) * Rg]\n    k.rod('stainless', q, [q[0] * 1.25, q[1], q[2] * 1.25], 0.014, 8)\n    k.hex('brass', 0.035, 0.02, [q[0] * 1.25, q[1], q[2] * 1.25], [q[0], 0, q[2]])\n  }\n  const xIn0 = -Rg * 1.12\n  k.cyl(RED, Rg * 0.42, 0.06, [xIn0 + 0.03, yIn, 0], 'x', 32)\n  k.cyl(RED, Rg * 0.34, 0.18, [xIn0 - 0.09, yIn, 0], 'x', 28)\n  const xGi = xIn0 - 0.22\n  k.cyl('stainless', 0.04, 0.06, [xGi + 0.03, yIn, 0], 'x', 14)\n  k.cyl('flangeSteel', 0.11, 0.035, [xGi, yIn, 0], 'x', 24)\n  k.cyl('brass', 0.04, 0.02, [0, yG1 + 0.05 + Hl * 0.3, Rg + 0.01], 'z', 18)\n  k.disc('glass', 0.03, [0, yG1 + 0.05 + Hl * 0.3, Rg + 0.021], 'z', 18)\n  k.cyl('brass', 0.012, 0.06, [-Rg * 0.4, yIn + rH * 0.75 + 0.06, Rg * 0.3], 'y', 8)\n  k.sphere('brass', 0.025, [-Rg * 0.4, yIn + rH * 0.75 + 0.1, Rg * 0.3], undefined, 10)\n  k.hex('nut', 0.05, 0.03, [Rg * 0.3, yIn + rH * 0.75 + 0.045, -Rg * 0.3])\n  k.box('nameplate', [0.16, 0.1, 0.003], [Rg * 0.5, yG1 + 0.05 + Hl * 0.6, Rg * 0.866 + 0.004], faceQ([0.5, 0, 0.866]))\n  for (const s of [-1, 1]) k.torus('nut', 0.035, 0.009, [0, yIn + rH * 0.75 + 0.06, s * Rg * 0.6], 'z', 6, 14)\n\n  // guarded cardan shaft, slightly inclined as installed\n  const Ls = 1.1\n  const drop = 0.09\n  const xF = xGi - 0.04 - Ls\n  const yCr = yIn - drop\n  const P1 = new THREE.Vector3(xGi - 0.02, yIn, 0)\n  const P2 = new THREE.Vector3(xF + 0.02, yCr, 0)\n  const dir = P1.clone().sub(P2).normalize()\n  const at = (t: number) => P2.clone().lerp(P1, t).toArray() as V3\n  {\n    const Lsh = P1.distanceTo(P2)\n    const r = new Kit()\n    const xt = (t: number) => -Lsh / 2 + t * Lsh\n    for (const [x0, s] of [[Lsh / 2, -1], [-Lsh / 2, 1]] as [number, number][]) {\n      r.cyl('flangeSteel', 0.11, 0.03, [x0 + s * 0.05, 0, 0], 'x', 24)\n      const j = x0 + s * 0.13\n      for (const zz of [-1, 1]) r.box('flangeSteel', [0.1, 0.05, 0.03], [j, 0, zz * 0.06])\n      r.box('nut', [0.04, 0.13, 0.04], [j, 0, 0])\n      r.box('nut', [0.04, 0.04, 0.13], [j, 0, 0])\n      for (const yy of [-1, 1]) r.box('flangeSteel', [0.1, 0.03, 0.05], [x0 + s * 0.2, yy * 0.06, 0])\n    }\n    r.cyl('flangeSteel', 0.06, (0.55 - 0.22) * Lsh, [xt(0.385), 0, 0], 'x', 18)\n    r.cyl('flangeSteel', 0.045, (0.82 - 0.18) * Lsh, [0, 0, 0], 'x', 18)\n    r.box('nut', [0.06, 0.02, 0.04], [xt(0.68), 0.05, 0])\n    const mid = P1.clone().add(P2).multiplyScalar(0.5)\n    rotors.push({ built: r.build(), axis: 'x', pos: mid.toArray() as V3, rot: [0, 0, Math.atan2(dir.y, dir.x)], rpm: 150 })\n  }\n  meshTube(k, at(0.06), at(0.94), 0.2)\n  for (const tt of [0.06, 0.94]) k.torus('safetyYellow', 0.2, 0.012, at(tt), dir.toArray() as V3, 6, 28)\n  for (const tt of [0.3, 0.7]) {\n    const q = at(tt)\n    k.rod('safetyYellow', [q[0], q[1] - 0.2, q[2]], [q[0], 0.02, q[2]], 0.02, 8)\n    k.box('safetyYellow', [0.14, 0.012, 0.14], [q[0], 0.006, q[2]])\n  }\n  const hc = clamp(0.5 + 0.00012 * p.kw, 0.55, 0.8)\n  const xTap = rb + (L.xFace - flangeLen(p.dischargeNps, p.rating) - rb) * 0.75\n  dieselEngine(k, rotors, { xF, yCr, yk: yCr - hc, xs1: xF + 0.12, kw: p.kw, tag: p.tag, cooling: p.cooling, tap: [xTap, L.yOut, -L.dOD / 2] })\n}\n\n// ------------------------------------------------------------------ horizontal split-case (BB1 double suction) on a common skid\nfunction splitCase(k: Kit, p: P, rotors: RotorSpec[]) {\n  const s = splitLayout(p)\n  const { r0, r1, wv, yk, ys, yN, out, zD, zS, dOD, sOD } = s\n  // volute in the YZ plane: shape u -> -Y, v -> +Z (tangential outlet heading +Z below the shaft)\n  const vm = new THREE.Matrix4().makeBasis(new THREE.Vector3(0, -1, 0), new THREE.Vector3(0, 0, 1), new THREE.Vector3(-1, 0, 0)).setPosition(0, ys, 0)\n  castExtrude(k, RED, voluteShape(r0, r1, out), wv, Math.min(0.04, wv * 0.15), vm, 16)\n  // discharge: outlet throat -> round neck -> WN flange\n  const zNeck = out + 0.05\n  k.lathe(RED, [[0, out - 0.06], [Math.max(wv, r1 - r0) * 0.56, out - 0.06], [dOD * 0.56, zNeck], [dOD * 0.45, zNeck]], [0, yN, 0], 'z', 32)\n  flangedBranch(k, RED, [0, yN, out - 0.1], [0, yN, zD], p.dischargeNps, p.rating, 'STD')\n  // double-suction chamber on -Z feeding both eyes, suction neck and flange\n  const sw = wv * 2.3\n  const yR = yN - ys\n  const zi = -r1 * 0.3\n  const zo = -(r1 + sOD * 0.5)\n  const yt = -0.03\n  const yb = yR - sOD * 0.7\n  const rr = sOD * 0.45\n  const sh = new THREE.Shape()\n  sh.moveTo(zi, yt)\n  sh.lineTo(zo + rr * 0.4, yt)\n  sh.quadraticCurveTo(zo, yt, zo, yt - rr * 0.4)\n  sh.lineTo(zo, yb + rr)\n  sh.quadraticCurveTo(zo, yb, zo + rr, yb)\n  sh.lineTo(zi, yb + rr * 0.5)\n  sh.closePath()\n  // shape x -> world Z, shape y -> world Y (relative to ys), extrude -> world X\n  castExtrude(k, RED, sh, sw, 0.04, new THREE.Matrix4().makeBasis(new THREE.Vector3(0, 0, 1), new THREE.Vector3(0, 1, 0), new THREE.Vector3(-1, 0, 0)).setPosition(0, ys, 0), 12)\n  for (const sx of [-1, 1]) k.lathe(RED, [[0, -wv * 0.1], [r0 * 0.75, -wv * 0.1], [r0 * 0.7, wv * 0.6], [Rshaft(p) * 2.2, wv * 0.75], [0, wv * 0.75]], [sx * wv * 0.45, ys, 0], sx > 0 ? 'x' : '-x', 36)\n  const zSn = zo + 0.03\n  flangedBranch(k, RED, [0, yN, zSn], [0, yN, zS], p.suctionNps, p.rating, 'STD')\n  // horizontal split flange with stud rows, upper-casing lifting lugs\n  const fx = sw * 0.5 + 0.05\n  const fz0 = zo\n  const fz1 = r0 + 0.06\n  k.box(RED, [fx * 2, 0.05, fz1 - fz0], [0, ys, (fz0 + fz1) / 2])\n  for (const z of [fz0 + 0.035, fz1 - 0.035]) {\n    const n = Math.max(4, Math.round((fx * 2) / 0.12))\n    for (let i = 0; i < n; i++) {\n      const x = -fx + 0.05 + ((fx * 2 - 0.1) * i) / (n - 1)\n      k.cyl('boltXylan', 0.011, 0.11, [x, ys + 0.03, z], 'y', 6)\n      k.hex('nut', 0.036, 0.02, [x, ys + 0.035, z])\n    }\n  }\n  for (const sx of [-1, 1]) liftingLug(k, [sx * wv * 0.25, ys + r1 - 0.02, -0.05], 'z', 1, RED)\n  // bearing brackets, housings, seals at both ends; shaft\n  const rsh = Rshaft(p)\n  const xb = fx + 0.32\n  for (const sx of [-1, 1]) {\n    const x = sx * xb\n    k.cyl(RED, rsh * 2.6, 0.12, [sx * (fx + 0.08), ys, 0], 'x', 24)\n    k.cyl('brass', rsh * 2.0, 0.04, [sx * (fx + 0.16), ys, 0], 'x', 20)\n    for (const sz of [-1, 1]) {\n      k.cyl('boltXylan', 0.007, 0.1, [sx * (fx + 0.13), ys, sz * rsh * 2.4], 'x', 6)\n      k.hex('nut', 0.022, 0.012, [sx * (fx + 0.185), ys, sz * rsh * 2.4], 'x')\n    }\n    k.tube('tubing', [[sx * (fx + 0.08), ys - rsh * 2.6, 0], [sx * (fx + 0.08), yk + 0.04, 0.05]], 0.007)\n    k.lathe(RED, [[rsh * 3.4, -0.11], [rsh * 3.8, -0.08], [rsh * 3.8, 0.08], [rsh * 3.4, 0.11]], [x, ys, 0], 'x', 32)\n    for (let i = 0; i < 4; i++) k.cyl(RED, rsh * 4.1, 0.008, [x - 0.06 + i * 0.04, ys, 0], 'x', 32)\n    k.box(RED, [0.18, ys - yk - rsh * 3.4, 0.06], [x, (ys - rsh * 3.4 + yk) / 2, 0])\n    k.box(RED, [0.06, ys - yk - rsh * 3.4, 0.3], [x, (ys - rsh * 3.4 + yk) / 2, 0])\n    // regreaseable ball bearings: grease nipple on top, relief plug below\n    k.cyl('brass', 0.008, 0.03, [x, ys + rsh * 3.8 + 0.015, 0], 'y', 8)\n    k.sphere('brass', 0.011, [x, ys + rsh * 3.8 + 0.035, 0], undefined, 8)\n    k.hex('nut', 0.022, 0.014, [x, ys - rsh * 3.6, rsh * 1.8])\n  }\n  k.cyl('flangeSteel', rsh, xb * 2 + 0.2, [0, ys, 0], 'x', 16)\n  // casing feet on pedestals\n  for (const sz of [-1, 1]) {\n    const z = sz > 0 ? fz1 - 0.08 : fz0 + 0.08\n    k.box(RED, [sw * 0.6, ys - 0.05 - yk, 0.1], [0, (ys - 0.05 + yk) / 2, z])\n    k.box('flangeSteel', [sw * 0.7, 0.02, 0.16], [0, yk + 0.01, z])\n  }\n  // NFPA 20 accessories: automatic air release valve, suction compound + discharge gauges\n  const yTopC = ys + r1\n  k.cyl(RED, 0.02, 0.06, [0, yTopC + 0.02, 0.02], 'y', 10)\n  k.cyl('brass', 0.035, 0.1, [0, yTopC + 0.09, 0.02], 'y', 16)\n  k.dome('brass', 0.035, [0, yTopC + 0.14, 0.02], 'y', Math.PI / 2, 0.7, 16)\n  pipeGauge(k, [-0.08, yN + dOD / 2, out + 0.12], [1, 0, 0], 25)\n  pipeGauge(k, [-0.08, yN + sOD / 2, zSn - 0.08], [1, 0, 0], 10)\n  tagPlate(k, p.tag ?? 'FIRE PUMP', [wv * 0.6, ys - r0 * 0.5, r1 * 0.2 + 0.01], [1, 0, 0], 0.16)\n\n  // coupling to the engine flywheel\n  const xShaft = -(xb + 0.1)\n  const cl = 0.32\n  const xF = xShaft - cl\n  rotors.push(guardedCoupling(k, xF, xShaft, ys, Math.max(0.18, rsh * 4.2), yk, 'safetyYellow', 150, 0, rsh))\n  const zMin = Math.max(-fz0, fz1) + 0.08\n  k.box(DARK, [xb * 2 + 0.6, 0.012, 2 * zMin + 0.16], [0, yk - 0.006, 0])\n  dieselEngine(k, rotors, { xF, yCr: ys, yk, xs1: xb + 0.35, kw: p.kw, tag: p.tag, zMin, cooling: p.cooling, tap: [0, yN - dOD / 2, zD - flangeLen(p.dischargeNps, p.rating) - 0.08] })\n}\n\nconst Rshaft = (p: P) => clamp(0.025 + 0.00004 * p.kw, 0.035, 0.07)\n\n// ------------------------------------------------------------------ diesel engine package, everything towards -X of the flywheel flange at xF\ninterface EngineOpts {\n  xF: number\n  yCr: number\n  /** skid top */\n  yk: number\n  /** skid end towards +X */\n  xs1: number\n  kw: number\n  tag?: string\n  /** minimum half-width of the skid */\n  zMin?: number\n  cooling: 'heat-exchanger' | 'radiator'\n  /** raw-water take-off on the pump discharge (bottom of the pipe) */\n  tap: V3\n}\n\nfunction dieselEngine(k: Kit, rotors: RotorSpec[], o: EngineOpts) {\n  const { xF, yCr, yk, kw } = o\n  const drv = RED\n  const ex = EXHAUST()\n  const Le = clamp(1.3 + 0.0011 * kw, 1.4, 3.2)\n  const We = clamp(0.75 + 0.00028 * kw, 0.8, 1.5)\n  const He = clamp(0.9 + 0.0004 * kw, 0.9, 1.8)\n  const vee = kw > 1100\n\n  // skid: rails, cross members, drip tray; posts down to the deck when raised\n  const zb = Math.max(We / 2 + 0.1, o.zMin ?? 0)\n  const xBack = xF - 0.3\n  const xFront = xBack - Le\n  const xRad = xFront - 0.45\n  const xs1 = o.xs1\n  const xs0 = xRad - 0.25\n  const Lk = xs1 - xs0\n  const xm = (xs0 + xs1) / 2\n  const hbm = Math.min(0.22, yk - 0.02)\n  for (const s of [-1, 1]) {\n    const z = s * zb\n    k.box(DARK, [Lk, 0.016, 0.16], [xm, yk - 0.008, z])\n    k.box(DARK, [Lk, 0.016, 0.16], [xm, yk - hbm + 0.008, z])\n    k.box(DARK, [Lk, hbm, 0.012], [xm, yk - hbm / 2, z])\n  }\n  const nX = Math.max(2, Math.ceil(Lk / 1.2) + 1)\n  for (let i = 0; i < nX; i++) k.box(DARK, [0.14, hbm * 0.8, 2 * zb], [xs0 + 0.08 + ((Lk - 0.16) * i) / (nX - 1), yk - hbm / 2, 0])\n  k.box(DARK, [Lk - 0.2, 0.008, 2 * zb - 0.1], [xm, yk - hbm + 0.03, 0])\n  const yPost = yk - hbm\n  const nP = Math.max(2, Math.ceil(Lk / 1.6) + 1)\n  for (let i = 0; i < nP; i++) {\n    const x = xs0 + 0.1 + ((Lk - 0.2) * i) / (nP - 1)\n    for (const s of [-1, 1]) {\n      if (yPost > 0.05) {\n        k.box(DARK, [0.16, yPost, 0.012], [x, yPost / 2, s * zb])\n        k.box(DARK, [0.012, yPost, 0.16], [x - 0.074, yPost / 2, s * zb])\n        k.box(DARK, [0.012, yPost, 0.16], [x + 0.074, yPost / 2, s * zb])\n        if (i < nP - 1) {\n          const x2 = xs0 + 0.1 + ((Lk - 0.2) * (i + 1)) / (nP - 1)\n          k.rod('structSteelDark', [x + 0.08, 0.05, s * zb], [x2 - 0.08, yPost - 0.04, s * zb], 0.03, 8)\n        }\n      } else k.box(DARK, [0.16, Math.max(yPost, 0.001), 0.16], [x, Math.max(yPost, 0.001) / 2, s * zb])\n      k.box(DARK, [0.26, 0.02, 0.26], [x, 0.01, s * zb])\n      for (const dx of [-0.09, 0.09]) anchorBolt(k, [x + dx, 0.02, s * (zb + 0.09)], 0.018)\n    }\n  }\n  for (const sx of [-1, 1]) for (const sz of [-1, 1]) k.torus('nut', 0.03, 0.008, [sx > 0 ? xs1 - 0.15 : xs0 + 0.15, yk - hbm * 0.5, sz * (zb + 0.09)], 'z', 6, 14)\n\n  // engine block, sump, crankcase doors, mounts\n  const xEm = (xBack + xFront) / 2\n  const yBlk0 = yCr - 0.12\n  const hBlk = He * 0.62\n  const yBT = yBlk0 + hBlk\n  k.rbox(drv, [Le, hBlk, We * 0.72], 0.04, [xEm, yBlk0 + hBlk / 2, 0])\n  k.rbox(drv, [Le * 0.88, yBlk0 - (yk + 0.06), We * 0.55], 0.03, [xEm, (yBlk0 + yk + 0.06) / 2, 0])\n  const nCyl = 6\n  const pitch = (Le * 0.9) / nCyl\n  for (let i = 0; i < nCyl; i++) {\n    const x = xFront + Le * 0.05 + pitch * (i + 0.5)\n    for (const s of [-1, 1]) k.rbox(drv, [pitch * 0.6, hBlk * 0.3, 0.02], 0.01, [x, yBlk0 + hBlk * 0.3, s * We * 0.36])\n  }\n  for (const x of [xFront + 0.2, xBack - 0.2])\n    for (const s of [-1, 1]) {\n      k.box(drv, [0.16, 0.05, zb - We * 0.36], [x, yBlk0 + 0.06, s * (We * 0.36 + (zb - We * 0.36) / 2)])\n      k.cyl('rubberBlack', 0.06, 0.06, [x, yk + 0.03, s * zb], 'y', 16)\n      k.box(drv, [0.14, Math.max(0.01, yBlk0 - yk - 0.06), 0.04], [x, (yBlk0 + 0.06 + yk + 0.06) / 2 - 0.03, s * zb])\n    }\n  // cylinder heads + rocker covers (inline 6, V12 above 1100 kW), injector lines\n  const banks = vee ? [{ z: -We * 0.18, tilt: 0.45 }, { z: We * 0.18, tilt: -0.45 }] : [{ z: 0, tilt: 0 }]\n  const wHead = vee ? We * 0.32 : We * 0.55\n  for (const b of banks) {\n    k.at([0, yBT, b.z], new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), b.tilt), () => {\n      for (let i = 0; i < nCyl; i++) {\n        const x = xFront + Le * 0.05 + pitch * (i + 0.5)\n        k.rbox(drv, [pitch * 0.94, 0.17, wHead], 0.012, [x, 0.085, 0])\n        k.rbox('castIron', [pitch * 0.82, 0.1, wHead * 0.78], 0.035, [x, 0.215, 0])\n        for (const dx of [-pitch * 0.25, pitch * 0.25]) k.hex('nut', 0.025, 0.02, [x + dx, 0.275, 0])\n        k.tube('tubing', [[x, 0.17, -wHead * 0.5], [x, 0.2, -wHead * 0.62], [xFront + Le * 0.55, 0.0, -wHead * 0.7]], 0.005)\n      }\n    })\n  }\n  const yHT = yBT + 0.3\n  // exhaust manifold to the turbocharger over the flywheel housing\n  const zEx = vee ? 0 : We * 0.38\n  const yEx = vee ? yHT + 0.05 : yBT + 0.12\n  k.rod(ex, [xFront + 0.1, yEx, zEx], [xBack - 0.35, yEx, zEx], 0.07, 16)\n  if (!vee)\n    for (let i = 0; i < nCyl; i++) {\n      const x = xFront + Le * 0.05 + pitch * (i + 0.5)\n      k.rod(ex, [x, yEx, zEx], [x, yEx, wHead * 0.45], 0.045, 10)\n    }\n  const tx = xBack - 0.38\n  const ty = yHT + 0.18\n  const tz = vee ? 0 : We * 0.25\n  k.rod(ex, [xBack - 0.35, yEx, zEx], [tx, ty - 0.12, tz], 0.07, 12)\n  k.cyl(ex, 0.15, 0.16, [tx, ty, tz], 'x', 24)\n  k.cyl('flangeSteel', 0.07, 0.12, [tx + 0.13, ty, tz], 'x', 16)\n  k.cyl('stainless', 0.14, 0.14, [tx + 0.26, ty, tz], 'x', 24)\n  k.spline('rubberBlack', [[tx + 0.34, ty, tz], [tx + 0.45, ty + 0.05, tz - 0.2], [tx + 0.4, ty + 0.05, -We * 0.3]], 0.065, 10, 16)\n  k.cyl('castIron', 0.18, 0.45, [tx + 0.35, ty + 0.05, -We * 0.3 - 0.2], 'z', 24)\n  k.cyl('rubberBlack', 0.19, 0.04, [tx + 0.35, ty + 0.05, -We * 0.3 - 0.44], 'z', 24)\n  k.rbox('castIron', [Le * 0.8, 0.16, 0.14], 0.03, [xEm, yBT - 0.05, -We * 0.42])\n  for (let i = 0; i < 3; i++) k.cyl(i < 2 ? 'castIron' : 'stainless', 0.06, 0.24, [xFront + Le * (0.3 + i * 0.1), yBlk0 + hBlk * 0.45, -We * 0.36 - 0.07], 'y', 16)\n  k.rbox('castIron', [0.25, 0.18, 0.12], 0.02, [xFront + Le * 0.6, yCr + 0.05, -We * 0.36 - 0.06])\n  k.cyl('castIron', 0.085, 0.36, [xBack - 0.05, yCr - 0.12, We * 0.36 + 0.09], 'x', 18)\n  k.cyl('castIron', 0.045, 0.14, [xBack - 0.1, yCr - 0.01, We * 0.36 + 0.09], 'x', 12)\n  k.cyl('castIron', 0.1, 0.18, [xFront + 0.1, yBT - 0.1, We * 0.3], 'x', 18)\n  // flywheel housing (SAE bell) + output flange\n  const bell = Math.min(We * 0.42, yCr - yk + 0.05)\n  k.cyl(drv, bell, 0.3, [xBack + 0.15, yCr, 0], 'x', 36, Math.min(We * 0.34, bell * 0.85))\n  k.cyl('flangeSteel', 0.12, 0.04, [xF, yCr, 0], 'x', 24)\n  const yFan = yBlk0 + hBlk * 0.75\n  {\n    const r = new Kit()\n    r.cyl('castIron', 0.13, 0.08, [0, 0, 0], 'x', 24)\n    r.box('nut', [0.085, 0.03, 0.02], [-0.002, 0.09, 0])\n    rotors.push({ built: r.build(), axis: 'x', pos: [xFront - 0.04, yCr, 0], rpm: 150 })\n  }\n  for (const z of [-0.02, 0.02]) k.rod('rubberBlack', [xFront - 0.05, yCr + 0.1, z], [xFront - 0.05, yFan - 0.07, z], 0.012, 6)\n\n  if (o.cooling === 'radiator') {\n    // radiator with engine-driven pusher fan, header tank, hoses\n    const Hr = clamp(yHT - yk, 1.0, 2.2)\n    const Wr = We + 0.3\n    const yR0 = yk + 0.05\n    k.box(CORE(), [0.14, Hr - 0.12, Wr - 0.12], [xRad, yR0 + Hr / 2, 0])\n    for (const s of [-1, 1]) {\n      k.box(drv, [0.18, Hr, 0.06], [xRad, yR0 + Hr / 2, s * (Wr / 2 - 0.03)])\n      k.box(drv, [0.2, 0.08, Wr], [xRad, s > 0 ? yR0 + Hr - 0.04 : yR0 + 0.04, 0])\n    }\n    const rFan = Math.min(Hr, Wr) * 0.42\n    openTube(k, drv, [xRad + 0.08, yFan, 0], [xFront - 0.08, yFan, 0], rFan, 32, rFan * 0.9)\n    k.box(drv, [0.02, Hr - 0.1, Wr - 0.1], [xRad + 0.085, yR0 + Hr / 2, 0])\n    k.disc(fanGuardMaterial(), rFan * 0.98, [xRad + 0.1, yFan, 0], 'x', 32)\n    k.cyl('castIron', 0.06, 0.12, [xFront - 0.14, yFan, 0], 'x', 14)\n    {\n      const xf = xRad + 0.2\n      const r = new Kit()\n      fanImpeller(r, 'castIron', rFan * 0.92, { axis: 'x', blades: 6, chord: rFan * 0.32, pitch: 0.36, hub: 0.07, hubLen: 0.08 })\n      const xp = xFront - 0.05 - xf\n      r.cyl('castIron', 0.09, 0.06, [xp, 0, 0], 'x', 20)\n      r.box('nut', [0.065, 0.025, 0.02], [xp, 0.06, 0])\n      rotors.push({ built: r.build(), axis: 'x', pos: [xf, yFan, 0], rpm: 195 })\n    }\n    k.cyl(drv, 0.1, Wr * 0.6, [xRad + 0.02, yR0 + Hr + 0.1, 0], 'z', 18)\n    k.cyl('brass', 0.035, 0.05, [xRad + 0.02, yR0 + Hr + 0.21, 0], 'y', 14)\n    k.spline('rubberBlack', [[xFront + 0.05, yBT + 0.06, 0], [xFront - 0.15, yBT + 0.15, We * 0.25], [xRad + 0.1, yR0 + Hr - 0.15, We * 0.3]], 0.045, 10, 20)\n    k.spline('rubberBlack', [[xFront + 0.05, yCr + 0.05, -We * 0.3], [xFront - 0.15, yCr - 0.05, -We * 0.35], [xRad + 0.1, yR0 + 0.2, -We * 0.3]], 0.045, 10, 20)\n    for (const s of [-1, 1]) k.box(drv, [0.3, 0.05, 0.12], [xRad, yk + 0.025, s * (Wr / 2 - 0.08)])\n  } else heatExchangerCooling(k, o, { xFront, xBack, xEm, yBT, yCr, zb, We })\n\n  // exhaust: bellows, residential silencer on supports, tail pipe with rain flap\n  const rSil = clamp(0.16 + 0.00008 * kw, 0.18, 0.35)\n  const LSil = clamp(Le * 0.85, 1.0, 2.6)\n  const ySil = yHT + 0.55 + rSil\n  const zSil = zb * 0.55\n  const xSil1 = xBack - 0.3\n  const xSil0 = xSil1 - LSil\n  k.cyl(ex, 0.09, 0.2, [tx, ty + 0.25, tz], 'y', 16)\n  for (let i = 0; i < 5; i++) k.torus('stainless', 0.1, 0.012, [tx, ty + 0.38 + i * 0.03, tz], 'y', 6, 20)\n  k.tube(ex, [[tx, ty + 0.36, tz], [tx, ty + 0.55, tz], [tx, ySil - rSil * 0.5, zSil]], 0.085, 14)\n  k.cyl(ex, rSil, LSil - 0.12, [(xSil0 + xSil1) / 2, ySil, zSil], 'x', 32)\n  for (const [x, s] of [[xSil0 + 0.06, -1], [xSil1 - 0.06, 1]] as [number, number][]) {\n    k.dome(ex, rSil, [x, ySil, zSil], s > 0 ? 'x' : '-x', Math.PI / 2, 0.4, 32)\n    annulus(k, ex, rSil, rSil + 0.012, -0.02, 0.02, [x, ySil, zSil], 'x', 32)\n  }\n  const yTail = ySil + rSil + 1.0\n  k.rod(ex, [xSil0 + 0.25, ySil, zSil], [xSil0 + 0.25, yTail, zSil], 0.09, 18)\n  k.box(ex, [0.26, 0.012, 0.22], [xSil0 + 0.28, yTail + 0.06, zSil], new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), -0.5))\n  for (const x of [xSil0 + 0.3, xSil1 - 0.3]) {\n    k.rod('structSteelDark', [x, yk, zb], [x, ySil - rSil, zb], 0.035, 8)\n    k.box('structSteelDark', [0.1, 0.03, zb - zSil + rSil], [x, ySil - rSil - 0.02, (zb + zSil) / 2])\n    k.torus('structSteelDark', rSil + 0.012, 0.012, [x, ySil, zSil], 'x', 6, 32, Math.PI)\n  }\n\n  // diesel day tank on a bunded stand (-Z): NFPA 20 sizing ~5 L/kW\n  const vol = clamp(kw * 0.005, 0.4, 4)\n  const Wt = 0.8\n  const Ht = 1.0\n  const Lt = clamp(vol / (Wt * Ht), 0.7, 2.6)\n  const xt = xEm\n  const zt = -(zb + 0.35 + Wt / 2)\n  const yt0 = 0.5\n  k.box(DARK, [Lt + 0.3, 0.02, Wt + 0.3], [xt, 0.01, zt])\n  for (const s of [-1, 1]) {\n    k.box(DARK, [Lt + 0.3, 0.15, 0.012], [xt, 0.085, zt + s * (Wt / 2 + 0.15)])\n    k.box(DARK, [0.012, 0.15, Wt + 0.3], [xt + s * (Lt / 2 + 0.15), 0.085, zt])\n  }\n  for (const sx of [-1, 1]) for (const sz of [-1, 1]) k.box(DARK, [0.08, yt0, 0.08], [xt + sx * (Lt / 2 - 0.06), yt0 / 2, zt + sz * (Wt / 2 - 0.06)])\n  k.rbox(drv, [Lt, Ht, Wt], 0.03, [xt, yt0 + Ht / 2, zt])\n  const nR = Math.round(Lt / 0.5)\n  for (let i = 1; i < nR; i++) k.box(drv, [0.04, Ht - 0.1, 0.012], [xt - Lt / 2 + (i * Lt) / nR, yt0 + Ht / 2, zt - Wt / 2 - 0.006])\n  const xsg = xt + Lt / 2 + 0.05\n  k.rod('glass', [xsg, yt0 + 0.1, zt], [xsg, yt0 + Ht - 0.1, zt], 0.016, 10)\n  for (const y of [yt0 + 0.1, yt0 + Ht - 0.1]) k.rod('brass', [xt + Lt / 2, y, zt], [xsg, y, zt], 0.014, 8)\n  k.tube('tubing', [[xt - Lt * 0.3, yt0 + Ht, zt], [xt - Lt * 0.3, yt0 + Ht + 0.9, zt], [xt - Lt * 0.3 + 0.12, yt0 + Ht + 0.9, zt], [xt - Lt * 0.3 + 0.12, yt0 + Ht + 0.75, zt]], 0.025, 10, 0.06)\n  k.cyl('castIron', 0.045, 0.1, [xt - Lt * 0.3 + 0.12, yt0 + Ht + 0.72, zt], 'y', 12)\n  k.cyl('castIron', 0.06, 0.06, [xt + Lt * 0.25, yt0 + Ht + 0.03, zt], 'y', 16)\n  k.box('valveBody', [0.08, 0.08, 0.08], [xt + Lt * 0.4, yt0 - 0.06, zt])\n  k.tube('tubing', [[xt + Lt * 0.3, yt0 + 0.08, zt + Wt / 2], [xt + Lt * 0.3, yt0 + 0.08, -zb + 0.05], [xFront + Le * 0.6, yCr + 0.05, -We * 0.42]], 0.012)\n  k.tube('tubing', [[xFront + Le * 0.5, yBT - 0.15, -We * 0.42], [xt - Lt * 0.2, yBT - 0.15, -zb], [xt - Lt * 0.2, yt0 + Ht + 0.05, zt + Wt * 0.3], [xt - Lt * 0.2, yt0 + Ht, zt + Wt * 0.3]], 0.01)\n  tagPlate(k, 'DIESEL', [xt, yt0 + Ht * 0.6, zt + Wt / 2 + 0.003], 'z', 0.2)\n\n  // starter batteries A + B on a rack (+Z, by the starter)\n  const zbat = zb + 0.45\n  const xbat = xBack - 0.3\n  for (const sx of [-1, 1]) for (const sz of [-1, 1]) k.box(DARK, [0.05, 0.42, 0.05], [xbat + sx * 0.6, 0.21, zbat + sz * 0.2])\n  k.box(DARK, [1.3, 0.03, 0.46], [xbat, 0.42, zbat])\n  for (const dx of [-0.31, 0.31]) {\n    k.rbox('stainless', [0.56, 0.34, 0.4], 0.015, [xbat + dx, 0.6, zbat])\n    k.box('stainless', [0.58, 0.02, 0.42], [xbat + dx, 0.78, zbat])\n    for (const vx of [-0.15, 0, 0.15]) k.cyl('rubberBlack', 0.015, 0.03, [xbat + dx + vx, 0.8, zbat], 'y', 8)\n    k.spline('cable', [[xbat + dx, 0.79, zbat - 0.15], [xbat + dx * 0.5, 0.95, zbat - 0.3], [xBack - 0.05, yCr - 0.05, We * 0.36 + 0.18]], 0.014, 6, 24)\n  }\n\n  // fire pump controller cabinet (+Z, radiator end)\n  const xc = xFront + 0.2\n  const zc = zb + 0.55\n  const Hc = 1.8\n  k.box(DARK, [0.86, 0.1, 0.46], [xc, 0.05, zc])\n  k.rbox(drv, [0.8, Hc, 0.42], 0.02, [xc, 0.1 + Hc / 2, zc])\n  k.box(drv, [0.86, 0.03, 0.5], [xc, 0.1 + Hc + 0.015, zc])\n  const fz = zc + 0.212\n  k.box('rubberBlack', [0.004, Hc - 0.1, 0.004], [xc, 0.1 + Hc / 2, fz])\n  k.box(lcdMaterial(), [0.22, 0.14, 0.006], [xc - 0.18, 0.1 + Hc * 0.72, fz])\n  const lamps: MatKey[] = ['safetyGreen', 'actuatorYellow', 'actuatorRed', 'actuatorRed']\n  lamps.forEach((m, i) => k.cyl(m, 0.018, 0.02, [xc + 0.08 + i * 0.07, 0.1 + Hc * 0.74, fz + 0.005], 'z', 14))\n  k.cyl('handwheel', 0.03, 0.03, [xc + 0.18, 0.1 + Hc * 0.6, fz + 0.01], 'z', 16)\n  k.cyl('actuatorRed', 0.035, 0.03, [xc - 0.18, 0.1 + Hc * 0.58, fz + 0.01], 'z', 16)\n  for (const dx of [-0.05, 0.05]) k.box('stainless', [0.02, 0.12, 0.03], [xc + dx, 0.1 + Hc * 0.48, fz + 0.012])\n  tagPlate(k, 'FIRE PUMP CONTROLLER', [xc, 0.1 + Hc * 0.86, fz + 0.002], 'z', 0.3)\n  if (o.tag) tagPlate(k, o.tag, [xc, 0.1 + Hc * 0.93, fz + 0.002], 'z', 0.2)\n  k.spline('cable', [[xc, 0.1 + Hc + 0.03, zc - 0.1], [xc + 0.3, 0.1 + Hc + 0.25, zc - 0.3], [xFront + Le * 0.3, yBT + 0.3, We * 0.2]], 0.02, 6, 24)\n}\n\n// NFPA 20 heat-exchanger cooling: engine-mounted shell-and-tube cooler with expansion tank, raw water from the pump\n// discharge through a duplicated cooling loop (strainer, valves, regulator), discharged to an open waste cone.\nfunction heatExchangerCooling(k: Kit, o: EngineOpts, e: { xFront: number; xBack: number; xEm: number; yBT: number; yCr: number; zb: number; We: number }) {\n  const { yk, kw, tap } = o\n  const { xFront, xEm, yBT, yCr, zb, We } = e\n  const hxR = clamp(0.08 + 0.00006 * kw, 0.09, 0.16)\n  const hxL = We * 0.85\n  const xh = xFront - hxR - 0.06\n  const yh = yBT - 0.1\n  k.cyl('castIron', hxR, hxL, [xh, yh, 0], 'z', 24)\n  for (const s of [-1, 1]) {\n    k.cyl('castIron', hxR * 1.15, 0.03, [xh, yh, s * (hxL / 2 + 0.015)], 'z', 24)\n    k.dome('castIron', hxR, [xh, yh, s * (hxL / 2 + 0.03)], s > 0 ? 'z' : '-z', Math.PI / 2, 0.4, 24)\n  }\n  for (const s of [-1, 1]) k.box(RED, [0.06, yh - yk - hxR, 0.06], [xh, (yh - hxR + yk) / 2, s * hxL * 0.35])\n  k.cyl(RED, 0.1, We * 0.5, [xh + 0.05, yh + hxR + 0.16, 0], 'z', 18)\n  k.cyl('brass', 0.03, 0.05, [xh + 0.05, yh + hxR + 0.28, 0], 'y', 14)\n  k.rod(RED, [xh + 0.05, yh + hxR, 0], [xh + 0.05, yh + hxR + 0.07, 0], 0.03, 10)\n  k.spline('rubberBlack', [[xFront + 0.02, yBT + 0.05, We * 0.2], [xh + 0.1, yh + hxR * 0.6, We * 0.25], [xh, yh + hxR * 0.8, hxL * 0.3]], 0.035, 10, 16)\n  k.spline('rubberBlack', [[xh, yh - hxR * 0.8, -hxL * 0.3], [xh + 0.1, yCr - 0.05, -We * 0.25], [xFront + 0.02, yCr - 0.05, -We * 0.2]], 0.035, 10, 16)\n  // raw-water connections on the end bonnets\n  const zi = -(hxL / 2 + 0.04)\n  const zo = hxL / 2 + 0.04\n  // cooling loop on the -Z side: two parallel lines (normal + bypass), each valve - strainer - regulator - valve\n  const zl = -(zb + 0.12)\n  const xl0 = xEm - 0.55\n  const xl1 = xEm + 0.55\n  const yl = [yk + 0.32, yk + 0.6]\n  const r = 0.02\n  for (const x of [xl0 + 0.05, xl1 - 0.05]) {\n    k.rod('structSteelDark', [x, 0, zl - 0.06], [x, yl[1] + 0.05, zl - 0.06], 0.018, 8)\n    k.box('structSteelDark', [0.1, 0.01, 0.1], [x, 0.005, zl - 0.06])\n    for (const y of yl) k.box('structSteelDark', [0.04, 0.02, 0.07], [x, y - r - 0.01, zl - 0.03])\n  }\n  for (const y of yl) {\n    k.rod('tubing', [xl0, y, zl], [xl1, y, zl], r, 10)\n    smallValve(k, [xl0 + 0.18, y, zl], 0.9)\n    k.rod('castIron', [xl0 + 0.36, y, zl], [xl0 + 0.42, y - 0.08, zl], 0.03, 12)\n    k.cyl('castIron', 0.035, 0.08, [xEm + 0.12, y, zl], 'x', 14)\n    k.dome('castIron', 0.04, [xEm + 0.12, y + 0.03, zl], 'y', Math.PI / 2, 1.2, 14)\n    k.cyl('brass', 0.01, 0.06, [xEm + 0.12, y + 0.09, zl], 'y', 8)\n    smallValve(k, [xl1 - 0.18, y, zl], 0.9)\n  }\n  for (const x of [xl0, xl1]) k.rod('tubing', [x, yl[0], zl], [x, yl[1], zl], r, 10)\n  k.box('actuatorRed', [0.06, 0.06, 0.06], [xEm - 0.12, yl[0] + 0.05, zl])\n  pipeGauge(k, [xl1 - 0.06, yl[1] + r, zl], [0, 0, -1], 10, 0.07)\n  // supply from the pump discharge, along the deck, up into the loop; loop out to the exchanger\n  const yd = 0.06\n  k.tube('tubing', [tap, [tap[0], tap[1], tap[2] - 0.1], [tap[0], yd, tap[2] - 0.1], [tap[0], yd, zl], [xl1, yd, zl], [xl1, yl[0], zl]], r, 10, 0.06)\n  k.tube('tubing', [[xl0, yl[1], zl], [xl0, yh, zl], [xh, yh, zl], [xh, yh, zi]], r, 10, 0.06)\n  // exchanger out to a waste cone (open, visible drain) beside the skid\n  const xw = xh + 0.05\n  const zw = zb + 0.3\n  k.tube('tubing', [[xh, yh, zo], [xh, yh, zw], [xw, yh, zw], [xw, 0.42, zw]], r, 10, 0.06)\n  k.lathe('stainless', [[0.03, 0.06], [0.11, 0.3], [0.112, 0.3], [0.032, 0.06], [0.03, 0.06]], [xw, 0, zw], 'y', 24)\n  k.cyl('stainless', 0.03, 0.06, [xw, 0.03, zw], 'y', 14)\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": "DieselFirewaterPump",
    "anchor": "base",
    "standards": [
      "NFPA 20",
      "API 610 (VS1 / BB1)",
      "ASME B16.5"
    ]
  }
}