{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "reciprocating-compressor",
  "type": "registry:component",
  "title": "Reciprocating compressor (API 618)",
  "description": "Balanced-opposed process gas compressor on a skid with walkways: crankcase frame with 2, 4 or 6 throws, crosshead guides, two-compartment distance pieces, jacketed cylinders with suction and discharge valve covers and a clearance pocket, suction and discharge pulsation bottles with headers, discharge PSV, orifice plates at the bottle flanges, a temporary suction strainer, distance-piece vent and drain headers with an atmospheric vent and flame arrester, jacket cooling-water headers, crankcase breather, frame-mounted lube system and cylinder lubricator. Driven through a guarded flywheel and coupling by a TEFC motor (up to 1 MW) or a large box-frame motor. Cylinder, bottle and frame sizes follow `bore`.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "rotating/reciprocating-compressor.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/rotating/reciprocating-compressor.tsx",
      "content": "// API 618 balanced-opposed reciprocating compressor: frame with 2/4/6 throws, crosshead guides, two-compartment distance pieces, cylinders with valve covers, pulsation bottles, flywheel, motor, skid.\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 { flange, gauge, grating, handrail, junctionBox, motor, motorSize, pipeRun, tagPlate } from '../lib/parts'\nimport { port, type Port } from '../lib/ports'\nimport { Rotors, couplingGuardShell, couplingRotor, cowlFan, type RotorSpec } from '../lib/rotors'\nimport { signMaterial } from '../lib/textures'\nimport { elbow90, flange as flangeDims, pipeOD, pipeWall, type Rating } from '../lib/standards'\nimport { OIL, SKID, annulus, baseFrame, clamp, joint, largeMotor, lineRun, neckTo, runToFlange, teePipe } from './_compressors-kit'\n\nexport interface ReciprocatingCompressorProps {\n  throws?: 2 | 4 | 6\n  /** cylinder bore, m */\n  bore?: number\n  /** motor rated power, kW */\n  kw?: number\n  suctionNps?: number\n  dischargeNps?: number\n  rating?: Rating\n  tag?: string\n  running?: boolean\n}\n\nexport const defaults = {\n  throws: 4 as 2 | 4 | 6,\n  bore: 0.32,\n  kw: 1500,\n  suctionNps: 8,\n  dischargeNps: 6,\n  rating: 600 as Rating,\n  tag: 'K-3100',\n  running: true,\n}\n\ntype P = typeof defaults\n\nconst HS = 0.45\nconst WALK = 0.8\n\nfunction layout(p: P) {\n  const n = p.throws === 2 || p.throws === 6 ? p.throws : 4\n  const bore = clamp(p.bore, 0.1, 0.8)\n  const stroke = clamp(0.1 + bore * 0.3, 0.12, 0.3) // typical: 4.5–11 in stroke\n  const Dc = bore + 0.36\n  const Db = Math.max(clamp(bore * 1.6 + 0.1, 0.4, 1.2), pipeOD(Math.max(p.suctionNps, p.dischargeNps)) * 1.6)\n  const yc = HS + Dc / 2 + 0.3 + Db + 0.15\n  const npos = n / 2\n  const pitch = Math.max(Dc + 0.4, 0.9)\n  const Lf = npos * pitch + 0.5\n  const Wf = 0.8 + stroke * 2\n  const yTop = yc + 0.4 + stroke\n  const Lg = 0.45 + stroke\n  const Ld = 0.6\n  const Lcyl = bore * 0.9 + stroke + 0.35\n  const zc0 = Wf / 2 + Lg + Ld\n  const zc1 = zc0 + Lcyl\n  const zcc = (zc0 + zc1) / 2\n  const yS = yc + Dc / 2 + 0.3 + Db / 2\n  const yD = yc - Dc / 2 - 0.3 - Db / 2\n  const Lb = npos * pitch + 0.3\n  const hd = Db / 4\n  // motor and flywheel at -X, discharge trunk at +X\n  const Rf = Math.min(clamp(0.45 + p.kw / 6000, 0.5, 1.0), yc - HS - 0.12)\n  const big = p.kw > 1000\n  const ms = motorSize(p.kw, 6)\n  const Lm = big ? clamp(1.0 + Math.pow(p.kw, 0.33) * 0.12, 1.8, 3.6) : ms.L\n  const mW = big ? clamp(1.0 + p.kw / 4000, 1.1, 2.2) : ms.R * 2\n  const xf0 = -Lf / 2\n  const xFw = xf0 - 0.45\n  const xCpl1 = xFw - 0.3\n  const xCpl0 = xCpl1 - 0.45\n  const xMotorEnd = xCpl0\n  const xM0 = xMotorEnd - (big ? 0.8 + Lm : Lm + 0.25 + Math.max(0.12, ms.R * 0.6))\n  const dR = elbow90(p.dischargeNps)\n  const xe = Lb / 2 + hd + 0.5 + dR\n  const xFace = xe + dR + 0.9\n  const x0 = xM0 - 0.4\n  const x1 = xFace + 0.15\n  const shift = -(x0 + x1) / 2\n  const SL = x1 - x0\n  const SW = 2 * (zc1 + 0.2 + WALK)\n  const yH = yS + Db / 2 + 0.55\n  return { n, bore, stroke, Dc, Db, yc, npos, pitch, Lf, Wf, yTop, Lg, Ld, Lcyl, zc0, zc1, zcc, yS, yD, Lb, hd, Rf, big, ms, Lm, mW, xFw, xCpl0, xCpl1, xMotorEnd, xM0, xe, xFace, shift, SL, SW, yH, ySuc: yH + 0.7 }\n}\n\nexport function ports(props: ReciprocatingCompressorProps = {}): Port[] {\n  const p = { ...defaults, ...props }\n  const L = layout(p)\n  return [\n    port('suction', 'Suction', [L.shift, L.ySuc, 0], [0, 1, 0], p.suctionNps, p.rating),\n    port('discharge', 'Discharge', [L.xFace + L.shift, L.yD, 0], [1, 0, 0], p.dischargeNps, p.rating),\n  ]\n}\n\nexport function ReciprocatingCompressor(props: ReciprocatingCompressorProps & GroupProps) {\n  const { throws, bore, kw, suctionNps, dischargeNps, rating, tag, running, ...group } = { ...defaults, ...props }\n  const { built, rotors } = useMemo(() => build({ throws, bore, kw, suctionNps, dischargeNps, rating, tag, running }), [throws, bore, kw, suctionNps, dischargeNps, rating, tag, running])\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 ReciprocatingCompressor\n\nconst FRAME: MatKey = 'machineryGreen'\nconst CYL: MatKey = 'equipmentPaint'\nconst BOTTLE: MatKey = 'equipmentPaint'\n\nfunction build(p: P) {\n  const k = new Kit()\n  const L = layout(p)\n  const rotors: RotorSpec[] = []\n  k.at([L.shift, 0, 0], undefined, () => draw(k, p, L, rotors))\n  for (const r of rotors) r.pos = [r.pos[0] + L.shift, r.pos[1], r.pos[2]]\n  return { built: k.build(), rotors }\n}\n\nfunction draw(k: Kit, p: P, L: ReturnType<typeof layout>, rotors: RotorSpec[]) {\n  const { yc, Dc, Db, Lf, Wf, yTop, Lg, Ld, Lcyl, zc0, zc1, zcc, yS, yD, Lb, hd, pitch, npos, stroke, SL, SW } = L\n  const hs = HS\n  const cx = -L.shift\n  k.at([cx, 0, 0], undefined, () => baseFrame(k, SL, SW, hs))\n\n  // walkways along both long edges\n  const wx0 = cx - SL / 2\n  for (const sz of [-1, 1]) {\n    const zw = sz * (SW / 2 - WALK / 2)\n    const nG = Math.max(2, Math.round(SL / 2))\n    for (let i = 0; i < nG; i++) grating(k, [wx0 + (SL * (i + 0.5)) / nG, hs + 0.03, zw], SL / nG - 0.01, WALK - 0.02)\n    handrail(k, [[wx0 + 0.03, hs + 0.03, sz * (SW / 2 - 0.03)], [cx - 0.6, hs + 0.03, sz * (SW / 2 - 0.03)]])\n    handrail(k, [[cx + 0.6, hs + 0.03, sz * (SW / 2 - 0.03)], [wx0 + SL - 0.03, hs + 0.03, sz * (SW / 2 - 0.03)]])\n    for (let j = 0; j < 2; j++) k.box('walkwayPaint', [1.0, 0.03, 0.25], [cx, hs - (j + 1) * (hs / 3), sz * (SW / 2 + 0.13 + j * 0.25)])\n    for (const sx of [-1, 1]) k.box('structSteel', [0.012, hs, 0.55], [cx + sx * 0.5, hs / 2, sz * (SW / 2 + 0.27)])\n  }\n\n  // crankcase frame: body, top covers per throw, crankshaft end, breather, oil filler, foundation feet\n  // fabricated pedestal under the crankcase (sole plates on top, stiffeners)\n  const yFb = Math.max(hs + 0.12, yc - 0.5 - stroke * 1.5)\n  const fH = yTop - yFb\n  k.box(SKID, [Lf + 0.3, 0.06, Wf + 0.3], [0, hs + 0.03, 0])\n  k.box(SKID, [Lf * 0.96, yFb - hs - 0.06, Wf * 0.86], [0, (yFb + hs + 0.06) / 2, 0])\n  k.box(SKID, [Lf + 0.1, 0.05, Wf + 0.1], [0, yFb - 0.025, 0])\n  for (let i = 0; i <= npos * 2; i++)\n    for (const s of [-1, 1]) k.box(SKID, [0.025, yFb - hs - 0.11, 0.1], [-Lf * 0.46 + (i * Lf * 0.92) / (npos * 2), (yFb + hs) / 2, s * (Wf * 0.43 + 0.05)])\n  k.rbox(FRAME, [Lf, fH, Wf], 0.05, [0, yFb + fH / 2, 0])\n  for (let i = 0; i < npos; i++) {\n    const x = -Lf / 2 + 0.25 + pitch * (i + 0.5)\n    k.rbox(FRAME, [pitch * 0.7, 0.05, Wf * 0.7], 0.02, [x, yTop + 0.025, 0])\n    k.around(10, Math.min(pitch, Wf) * 0.3, [x, yTop + 0.055, 0], 'y', (q) => k.hex('nut', 0.03, 0.015, q))\n    for (const s of [-1, 1]) k.box(FRAME, [0.06, fH - 0.1, 0.04], [x - pitch / 2, yFb + fH / 2, s * (Wf / 2 + 0.02)])\n  }\n  for (const s of [-1, 1]) k.box(FRAME, [0.08, fH + 0.04, Wf + 0.1], [s * (Lf / 2 - 0.04), yFb + fH / 2, 0])\n  k.cyl(FRAME, 0.3, 0.12, [-Lf / 2 - 0.06, yc, 0], 'x', 32)\n  k.cyl('stainless', 0.11, 0.4, [-Lf / 2 - 0.25, yc, 0], 'x', 20)\n  k.rod('flangeSteel', [Lf * 0.3, yTop, Wf * 0.3], [Lf * 0.3, yTop + 0.15, Wf * 0.3], 0.025, 10)\n  k.cyl(OIL, 0.08, 0.2, [Lf * 0.3, yTop + 0.25, Wf * 0.3], 'y', 18)\n  k.dome(OIL, 0.08, [Lf * 0.3, yTop + 0.35, Wf * 0.3], 'y', Math.PI / 2, 0.5, 14)\n  tagPlate(k, 'BREATHER', [Lf * 0.3, yTop + 0.25, Wf * 0.3 + 0.081], 'z', 0.1)\n  k.cyl('brass', 0.035, 0.1, [-Lf * 0.3, yTop + 0.05, -Wf * 0.3], 'y', 12)\n  k.box('nameplate', [0.4, 0.25, 0.01], [-Lf / 2 - 0.001, yTop - 0.3, 0], new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), -Math.PI / 2))\n  // auxiliary end (+X): frame-driven oil pump, duplex oil filter, oil cooler\n  const xa = Lf / 2 + 0.06\n  k.cyl(OIL, 0.18, 0.25, [xa + 0.12, yc - 0.15, 0], 'x', 24)\n  for (const s of [-1, 1]) {\n    k.cyl(OIL, 0.08, 0.5, [xa + 0.35, hs + 0.5, s * 0.22], 'y', 18)\n    k.dome(OIL, 0.08, [xa + 0.35, hs + 0.75, s * 0.22], 'y', Math.PI / 2, 0.5, 14)\n  }\n  k.cyl(OIL, 0.13, Wf * 0.8, [xa + 0.35, yc - 0.15, 0], 'z', 24)\n  for (const s of [-1, 1]) k.cyl(OIL, 0.16, 0.05, [xa + 0.35, yc - 0.15, s * Wf * 0.4], 'z', 24)\n  lineRun(k, [[xa + 0.25, yc - 0.15, 0], [xa + 0.35, yc - 0.15 - 0.13, 0]], 1.5, OIL)\n  gauge(k, [xa + 0.5, yc + 0.2, 0], 'x', 0.1, 'bar', 10)\n  // force-feed cylinder lubricator with tubing to each cylinder\n  k.rbox('actuatorRed', [0.35, 0.3, 0.25], 0.02, [-Lf / 2 + 0.3, yTop + 0.15, -Wf * 0.25])\n\n  // throws: crosshead guide, distance piece, cylinder with valve covers, nozzles to the bottles\n  const zDp = Wf / 2 + Lg\n  const rg = 0.24 + stroke * 0.3\n  const rd = Math.min(Dc * 0.4, rg)\n  const rv = clamp(p.bore * 0.22, 0.07, 0.15)\n  const nN = Math.min(p.suctionNps, Math.max(3, Math.round(p.bore * 20)))\n  for (let i = 0; i < npos; i++) {\n    const x = -Lf / 2 + 0.25 + pitch * (i + 0.5)\n    for (const s of [-1, 1]) {\n      const z = (v: number) => s * v\n      const ax: V3 = [0, 0, s]\n      // crosshead guide bolted to the frame\n      k.cyl(FRAME, rg + 0.06, 0.05, [x, yc, z(Wf / 2 + 0.025)], ax, 32)\n      k.cyl(FRAME, rg, Lg, [x, yc, z(Wf / 2 + Lg / 2)], ax, 32)\n      k.cyl(FRAME, rg + 0.06, 0.05, [x, yc, z(zDp - 0.025)], ax, 32)\n      k.boltCircle('boltXylan', 'nut', 12, rg + 0.03, [x, yc, z(zDp - 0.07)], ax, 0.016, 0.08, [0.06])\n      k.rbox(FRAME, [0.18, 0.18, Lg * 0.6], 0.02, [x, yc + rg, z(Wf / 2 + Lg / 2)])\n      // two-compartment distance piece with access covers and a vent\n      k.rbox(CYL, [rd * 2, rd * 2, Ld], 0.04, [x, yc, z(zDp + Ld / 2)])\n      k.box(CYL, [rd * 2 + 0.08, rd * 2 + 0.08, 0.04], [x, yc, z(zDp + Ld / 2)])\n      for (const f of [0.25, 0.75])\n        for (const sx of [-1, 1]) {\n          k.box(CYL, [0.02, rd * 1.3, Ld * 0.35], [x + sx * (rd + 0.01), yc, z(zDp + Ld * f)])\n          k.around(6, rd * 0.45, [x + sx * (rd + 0.025), yc, z(zDp + Ld * f)], 'x', (q) => k.hex('nut', 0.025, 0.012, q, 'x'))\n        }\n      // packing / distance-piece vents up to the side vent header, drains down to the drain header\n      const yVh = yc + rd + 0.3\n      for (const f of [0.25, 0.75]) {\n        k.tube('tubing', [[x + rd * 0.4, yc + rd, z(zDp + Ld * f)], [x + rd * 0.4, yVh, z(zDp + Ld * f)], [x + rd * 0.4, yVh, z(zDp - 0.08)]], 0.009, 6, 0.04)\n        k.tube('tubing', [[x - rd * 0.4, yc - rd, z(zDp + Ld * f)], [x - rd * 0.4, hs + 0.2, z(zDp + Ld * f)], [x - rd * 0.4, hs + 0.2, z(zDp - 0.12)]], 0.009, 6, 0.04)\n      }\n      // jacket cooling water in (low) and out (high) to the side headers\n      k.tube('tubing', [[x + Dc * 0.3, yc - Dc * 0.35, z(zc0 + 0.12)], [x + Dc * 0.3, hs + 0.32, z(zc0 + 0.12)], [x + Dc * 0.3, hs + 0.32, z(zc0 - 0.05)]], 0.014, 8, 0.05)\n      k.tube('tubing', [[x - Dc * 0.3, yc + Dc * 0.35, z(zc0 + 0.12)], [x - Dc * 0.3, yc + Dc * 0.35 + 0.1, z(zc0 + 0.12)], [x - Dc * 0.3, yc + Dc * 0.35 + 0.1, z(zc0 - 0.05)], [x - Dc * 0.3, hs + 0.46, z(zc0 - 0.05)]], 0.014, 8, 0.05)\n      // cylinder: crank-end flange, jacketed body with ribs, head-end cover with studs, clearance pocket\n      k.cyl(CYL, Dc / 2 + 0.05, 0.06, [x, yc, z(zc0 + 0.03)], ax, 40)\n      k.cyl(CYL, Dc / 2, Lcyl, [x, yc, z(zcc)], ax, 40)\n      for (const f of [0.33, 0.66]) k.cyl(CYL, Dc / 2 + 0.02, 0.04, [x, yc, z(zc0 + Lcyl * f)], ax, 40)\n      k.cyl(CYL, Dc / 2 + 0.04, 0.06, [x, yc, z(zc1 - 0.03)], ax, 40)\n      k.cyl(CYL, Dc / 2 - 0.02, 0.07, [x, yc, z(zc1 + 0.035)], ax, 40)\n      k.boltCircle('boltXylan', 'nut', 16, Dc / 2 - 0.01, [x, yc, z(zc1 - 0.04)], ax, 0.02, 0.15, [0.135])\n      k.cyl(CYL, Dc * 0.22, 0.2, [x, yc, z(zc1 + 0.17)], ax, 24)\n      if (p.bore >= 0.25) k.handwheel('handwheel', Dc * 0.45, [x, yc, z(zc1 + 0.3)], ax, 4)\n      // valve covers: suction valves on the upper quarter, discharge on the lower, at both ends\n      for (const ze of [zc0 + Lcyl * 0.2, zc1 - Lcyl * 0.2])\n        for (const a of [Math.PI * 0.25, Math.PI * 0.75, -Math.PI * 0.25, -Math.PI * 0.75]) {\n          const d: V3 = [Math.cos(a), Math.sin(a), 0]\n          const c: V3 = [x + d[0] * (Dc / 2 + 0.03), yc + d[1] * (Dc / 2 + 0.03), z(ze)]\n          k.cyl(CYL, rv + 0.02, 0.08, c, d, 20)\n          k.cyl(CYL, rv + 0.03, 0.03, [c[0] + d[0] * 0.055, c[1] + d[1] * 0.055, c[2]], d, 20)\n          k.boltCircle('boltXylan', 'nut', 6, rv * 0.95, [c[0] + d[0] * 0.03, c[1] + d[1] * 0.03, c[2]], d, 0.014, 0.08, [0.07])\n        }\n      // nozzles up to the suction bottle and down to the discharge bottle, with flanged joints\n      for (const [dir, nps] of [[1, p.suctionNps], [-1, p.dischargeNps]] as const) {\n        const n = Math.min(nps, nN)\n        const y0 = yc + dir * (Dc / 2 - 0.02)\n        const y1 = (dir > 0 ? yS : yD) - dir * (Db / 2 - 0.02)\n        const ym = (y0 + y1) / 2\n        annulus(k, CYL, pipeOD(n) / 2, pipeOD(n) * 0.7, 0.12, [x, yc + dir * (Dc / 2 + 0.03), z(zcc)], 'y', 20)\n        k.pipe(BOTTLE, [x, y0, z(zcc)], [x, y1, z(zcc)], pipeOD(n), pipeWall(n))\n        joint(k, n, p.rating, [x, ym, z(zcc)], 'y')\n        // pulsation orifice plate between the flanges: paddle handle with stamped bore\n        const fod = flangeDims(n, p.rating).od\n        k.box('stainless', [0.11, 0.005, 0.05], [x + fod / 2 + 0.045, ym, z(zcc)])\n      }\n      // discharge temperature element and pressure gauge\n      k.rod('stainless', [x + pipeOD(nN) / 2, yc - Dc / 2 - 0.12, z(zcc)], [x + pipeOD(nN) / 2 + 0.12, yc - Dc / 2 - 0.12, z(zcc)], 0.012, 8)\n      k.rbox('stainless', [0.06, 0.08, 0.06], 0.01, [x + pipeOD(nN) / 2 + 0.15, yc - Dc / 2 - 0.12, z(zcc)])\n      gauge(k, [x - pipeOD(nN) / 2 - 0.1, yS - Db / 2 - 0.12, z(zcc + 0.1)], [0, 0, s], 0.09, 'bar', 100)\n      // lubricator tubing\n      k.tube('tubing', [[-Lf / 2 + 0.3, yTop + 0.02, -Wf * 0.25], [-Lf / 2 + 0.3, yTop + 0.02, z(Wf / 2 - 0.05)], [x, yTop + 0.02, z(Wf / 2 - 0.05)], [x, yc + Dc / 2 + 0.02, z(zc0 + 0.1)]], 0.005, 6, 0.05)\n      // cylinder support (head end) and distance-piece support\n      k.box(SKID, [0.16, yD - Db / 2 - hs - 0.04, 0.16], [x, (yD - Db / 2 - 0.04 + hs) / 2, z(zc1 + 0.08)])\n      k.box(SKID, [0.3, yc - rd - hs, 0.25], [x, (yc - rd + hs) / 2, z(zDp + Ld / 2)])\n    }\n  }\n\n  // side headers: distance-piece vent (up to an atmospheric vent with flame arrester), drain, cooling water supply / return\n  const xh0 = -Lf / 2 + 0.1\n  const xh1 = Lf / 2 - 0.1\n  for (const s of [-1, 1]) {\n    const yVh = yc + rd + 0.3\n    k.rod('stainless', [xh0 - 0.2, yVh, s * (zDp - 0.08)], [xh1, yVh, s * (zDp - 0.08)], pipeOD(1) / 2, 10)\n    lineRun(k, [[xh0 - 0.2, yVh, s * (zDp - 0.08)], [xh0 - 0.3, yVh, s * (zDp - 0.08)], [xh0 - 0.3, yTop + 1.3, s * (zDp - 0.08)]], 1, 'stainless')\n    k.cyl('flangeSteel', 0.05, 0.14, [xh0 - 0.3, yTop + 1.37, s * (zDp - 0.08)], 'y', 14)\n    k.lathe('stainless', [[0.02, 0], [0.02, 0.1], [0.06, 0.12], [0.06, 0.14], [0.015, 0.14]], [xh0 - 0.3, yTop + 1.44, s * (zDp - 0.08)], 'y', 14)\n    k.rod('stainless', [xh0 - 0.2, hs + 0.2, s * (zDp - 0.12)], [xh1, hs + 0.2, s * (zDp - 0.12)], pipeOD(1) / 2, 10)\n    k.cyl(OIL, 0.08, 0.3, [xh0 - 0.25, hs + 0.2, s * (zDp - 0.12)], 'y', 16)\n    for (const [y, c] of [[hs + 0.32, 'pipePaint'], [hs + 0.46, 'pipePaint']] as const) k.rod(c, [xh0, y, s * (zc0 - 0.05)], [xh1, y, s * (zc0 - 0.05)], pipeOD(1.5) / 2, 10)\n    for (const xx of [xh0 + 0.1, xh1 - 0.1]) k.box(SKID, [0.06, 0.5, 0.06], [xx, hs + 0.25, s * (zc0 - 0.05)])\n  }\n\n  // pulsation bottles (suction above, discharge below), with 2:1 heads and saddles\n  for (const s of [-1, 1]) {\n    for (const [yb, top] of [[yS, true], [yD, false]] as const) {\n      const prof: [number, number][] = [[0, -Lb / 2 - hd]]\n      for (let i = 1; i <= 12; i++) {\n        const t = (i / 12) * (Math.PI / 2)\n        prof.push([(Db / 2) * Math.sin(t), -Lb / 2 - hd * Math.cos(t)])\n      }\n      for (let i = 0; i <= 12; i++) {\n        const t = (i / 12) * (Math.PI / 2)\n        prof.push([(Db / 2) * Math.cos(t), Lb / 2 + hd * Math.sin(t)])\n      }\n      k.lathe(BOTTLE, prof, [0, yb, s * zcc], 'x', 40)\n      for (const xx of [-Lb / 2, Lb / 2]) k.torus(BOTTLE, Db / 2 + 0.001, 0.006, [xx, yb, s * zcc], 'x', 6, 40)\n      tagPlate(k, top ? 'SUCTION BOTTLE' : 'DISCHARGE BOTTLE', [0, yb, s * (zcc + Db / 2 + 0.003)], s > 0 ? 'z' : '-z', 0.3)\n      // drain / vent bosses\n      k.cyl(BOTTLE, 0.03, 0.1, [-Lb * 0.4, yb + (top ? Db / 2 : -Db / 2), s * zcc], 'y', 10)\n    }\n    // discharge bottle supports\n    for (const xx of [-Lb * 0.35, Lb * 0.35]) {\n      k.box(SKID, [0.2, yD - hs - Db * 0.3, Db * 0.6], [xx, (yD - Db * 0.3 + hs) / 2, s * zcc])\n      k.box(SKID, [0.3, 0.02, Db * 0.8], [xx, hs + 0.01, s * zcc])\n    }\n    // suction bottle supports on the cylinders' frames: braces from the discharge bottle\n    for (const xx of [-Lb / 2 + 0.15, Lb / 2 - 0.15]) k.box(SKID, [0.12, yS - yD - Db, 0.12], [xx, (yS + yD) / 2, s * (zcc + Db * 0.35)])\n  }\n\n  // suction header: hollow risers from each bottle, equal tee over the frame, riser to the suction flange\n  const sN = p.suctionNps\n  const sOD = pipeOD(sN)\n  const yTopS = yS + Db / 2\n  const ht = sOD * 0.75\n  for (const s of [-1, 1]) {\n    annulus(k, BOTTLE, sOD / 2, sOD * 0.7, 0.08, [0, yTopS, s * zcc], 'y', 24)\n    pipeRun(k, [[0, yTopS - 0.04, s * zcc], [0, L.yH, s * zcc], [0, L.yH, s * ht]], sN, { mat: 'pipePaint' })\n  }\n  const tTop = teePipe(k, [0, L.yH, -ht], [0, L.yH, ht], [0, L.yH, 0], [0, 1, 0], sOD, pipeWall(sN), sOD, pipeWall(sN), sOD * 0.8, 'pipePaint')\n  neckTo(k, tTop, [0, L.ySuc, 0], [0, 1, 0], sN, p.rating, 'pipePaint', false)\n  const yJ = (tTop[1] + L.ySuc) / 2 - 0.05\n  joint(k, sN, p.rating, [0, yJ, 0], 'y')\n  k.box('actuatorRed', [0.11, 0.005, 0.05], [flangeDims(sN, p.rating).od / 2 + 0.045, yJ, 0])\n  for (const dy of [-0.15, 0.15]) k.rod('stainless', [0, yJ + dy, sOD / 2 - 0.004], [0, yJ + dy, sOD / 2 + 0.1], 0.008, 8)\n  k.rod('tubing', [0, yJ - 0.15, sOD / 2 + 0.1], [0, yJ + 0.15, sOD / 2 + 0.1], 0.006, 6)\n  gauge(k, [0, yJ, sOD / 2 + 0.16], 'z', 0.08, 'bar', 2)\n  for (const s of [-1, 1]) k.box(SKID, [0.12, L.yH - yTop - sOD / 2, 0.12], [0, (L.yH - sOD / 2 + yTop) / 2, s * Wf * 0.3])\n  for (const s of [-1, 1]) k.box(SKID, [0.25, 0.02, 0.25], [0, yTop + 0.06, s * Wf * 0.3])\n  // discharge: outlets from each bottle's +X head, equal tee, trunk to the discharge flange\n  const dN = p.dischargeNps\n  const dOD = pipeOD(dN)\n  const xh = Lb / 2 + hd\n  const dt = dOD * 0.75\n  for (const s of [-1, 1]) {\n    annulus(k, BOTTLE, dOD / 2, dOD * 0.7, 0.1, [xh - 0.02, yD, s * zcc], 'x', 24)\n    k.pipe(BOTTLE, [xh - 0.08, yD, s * zcc], [xh + 0.2, yD, s * zcc], dOD, pipeWall(dN))\n    joint(k, dN, p.rating, [xh + 0.2, yD, s * zcc], 'x')\n    pipeRun(k, [[xh + 0.2, yD, s * zcc], [L.xe, yD, s * zcc], [L.xe, yD, s * dt]], dN, { mat: 'pipePaint' })\n  }\n  const tOut = teePipe(k, [L.xe, yD, -dt], [L.xe, yD, dt], [L.xe, yD, 0], [1, 0, 0], dOD, pipeWall(dN), dOD, pipeWall(dN), dOD * 0.8, 'pipePaint')\n  runToFlange(k, [tOut], [L.xFace, yD, 0], [1, 0, 0], dN, p.rating, 'pipePaint', false)\n  for (const xx of [L.xe, L.xFace - 0.4]) k.box(SKID, [0.12, yD - dOD / 2 - hs, 0.12], [xx, (yD - dOD / 2 + hs) / 2, xx === L.xe ? zcc * 0.5 : 0])\n  // discharge PSV stub on the trunk\n  const xp = (L.xe + L.xFace) / 2 - 0.1\n  annulus(k, 'pipePaint', pipeOD(2) / 2, pipeOD(2) * 0.7, 0.03, [xp, yD + dOD / 2 + 0.015, 0], 'y', 16)\n  k.rod('pipePaint', [xp, yD + dOD / 2 + 0.001, 0], [xp, yD + pipeOD(dN) / 2 + 0.25, 0], pipeOD(2) / 2, 12)\n  k.disc('pipeBore', pipeOD(2) / 2, [xp, yD + dOD / 2 + 0.002, 0], 'y', 12)\n  flange(k, 2, p.rating, [xp, yD + pipeOD(dN) / 2 + 0.3, 0], [0, 1, 0], true)\n  k.cyl('actuatorRed', 0.06, 0.35, [xp, yD + pipeOD(dN) / 2 + 0.5, 0], 'y', 16)\n  k.cyl('actuatorRed', 0.035, 0.25, [xp, yD + pipeOD(dN) / 2 + 0.8, 0], 'y', 12)\n\n  // flywheel under a mesh guard, coupling, motor\n  const Rf = L.Rf\n  const fk = new Kit()\n  fk.cyl('castIron', Rf, 0.2, [0, 0, 0], 'x', 48)\n  fk.cyl('castIron', Rf * 0.88, 0.22, [0, 0, 0], 'x', 48)\n  fk.cyl('flangeSteel', 0.16, 0.32, [0, 0, 0], 'x', 24)\n  for (let i = 0; i < 6; i++) {\n    const a = (i / 6) * Math.PI * 2\n    fk.box('safetyYellow', [0.03, 0.05, 0.08], [0.11, Math.sin(a) * Rf * 0.94, Math.cos(a) * Rf * 0.94], new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), -a))\n  }\n  rotors.push({ built: fk.build(), axis: 'x', pos: [L.xFw, yc, 0], rpm: 120 })\n  couplingGuardShell(k, L.xFw - 0.25, L.xFw + 0.25, yc, Rf + 0.06, hs + 0.03)\n  rotors.push({ built: couplingRotor(L.xCpl1 - L.xCpl0, 0.25, 0.1), axis: 'x', pos: [(L.xCpl0 + L.xCpl1) / 2, yc, 0], rpm: 120 })\n  couplingGuardShell(k, L.xCpl0 - 0.02, L.xCpl1 + 0.02, yc, 0.28, hs + 0.03)\n  k.cyl('stainless', 0.1, L.xCpl1 - L.xFw, [(L.xCpl1 + L.xFw) / 2, yc, 0], 'x', 16)\n  if (L.big) {\n    const mB1 = L.xMotorEnd - 0.4\n    rotors.push(largeMotor(k, { mB0: mB1 - L.Lm, mB1, xEnd: L.xMotorEnd, ym: yc, hs, mW: L.mW, rpm: 120 }))\n  } else {\n    const ped = yc - L.ms.H - hs\n    const xm = L.xMotorEnd - 0.05\n    if (ped > 0.02) k.box(SKID, [L.Lm + 0.1, ped, L.ms.R * 2 + 0.3], [xm - L.Lm / 2, hs + ped / 2, 0])\n    k.at([xm, yc, 0], undefined, () => motor(k, { R: L.ms.R, L: L.Lm, H: L.ms.H, tbox: 'side', cable: false }))\n    rotors.push(cowlFan(L.ms.R, L.Lm, 120, [xm, yc, 0]))\n  }\n  k.plane(signMaterial('ler', `${p.tag}  RECIPROCATING COMPRESSOR`), 1.8, 0.3, [cx - SL * 0.25, hs * 0.5, SW / 2 + 0.003])\n  junctionBox(k, [-Lf / 2 + 0.2, hs + 0.2, Wf / 2 + 0.02], 'z')\n  tagPlate(k, p.tag, [Lf / 2 - 0.3, yTop - 0.3, Wf / 2 + 0.002], 'z', 0.3)\n}\n"
    },
    {
      "path": "rotating/_compressors-kit.ts",
      "type": "registry:file",
      "target": "components/deck-twin/rotating/_compressors-kit.ts",
      "content": "// Shared machinery parts for compressor, turbine and generator packages: base frames, louvres, doors, valves, gearboxes, lube consoles, fans.\nimport * as THREE from 'three'\nimport { Kit, eul, latheCrisp, mat4, orientQuat, uvScale, type MatKey, type V3 } from '../lib/kit'\nimport { flange, gauge, pipeRun } from '../lib/parts'\nimport { fanRotor, type RotorSpec } from '../lib/rotors'\nimport { paint } from '../lib/materials'\nimport { fanGuardMaterial, meshGuardMaterial, signMaterial } from '../lib/textures'\nimport { elbow90, flange as flangeDims, pipeOD, pipeWall, type Rating } from '../lib/standards'\n\nexport const clamp = (v: number, a: number, b: number) => Math.max(a, Math.min(b, v))\nexport const UP = new THREE.Vector3(0, 1, 0)\nexport type Face = 'x' | '-x' | 'z' | '-z'\n/** Quaternion taking a face frame (x along the face, y up, z outward) onto the outward normal. */\nexport const faceQ = (f: Face) => new THREE.Quaternion().setFromAxisAngle(UP, f === 'z' ? 0 : f === '-z' ? Math.PI : f === 'x' ? Math.PI / 2 : -Math.PI / 2)\n\nexport const SKID: MatKey = 'equipmentPaintDark'\nexport const OIL: MatKey = 'machineryGreen'\nexport const screen = () => paint('#2c3034', { metalness: 0.4, roughness: 0.6, clearcoat: 0 })\n\n/** Light solid pipe along an orthogonal polyline with swept bends (small-bore services, no flanges). */\nexport function lineRun(k: Kit, pts: V3[], nps: number, mat: MatKey = 'pipePaint') {\n  const r = pipeOD(nps) / 2\n  const radial = nps >= 3 ? 16 : 12\n  const P = pts.map((p) => new THREE.Vector3(...p))\n  let cur = P[0].clone()\n  for (let i = 1; i < P.length; i++) {\n    const a = P[i - 1]\n    const b = P[i]\n    if (i === P.length - 1) {\n      k.rod(mat, cur.toArray() as V3, b.toArray() as V3, r, radial)\n      break\n    }\n    const c = P[i + 1]\n    const rb = Math.min(elbow90(Math.max(nps, 0.5)), a.distanceTo(b) * 0.45, b.distanceTo(c) * 0.45)\n    const p1 = b.clone().add(a.clone().sub(b).normalize().multiplyScalar(rb))\n    const p2 = b.clone().add(c.clone().sub(b).normalize().multiplyScalar(rb))\n    if (cur.distanceTo(p1) > 1e-4) k.rod(mat, cur.toArray() as V3, p1.toArray() as V3, r, radial)\n    if (rb > 1e-4) k.add(mat, new THREE.TubeGeometry(new THREE.QuadraticBezierCurve3(p1, b.clone(), p2), 6, r, radial, false))\n    cur = p2\n  }\n}\n\n/** Hollow pipe run ending in a weld-neck flange at `face` (pointing `dir`), with studs. Returns the face. */\nexport function runToFlange(k: Kit, pts: V3[], face: V3, dir: V3, nps: number, rating: Rating, mat: MatKey = 'pipePaint', cap = true) {\n  const L = flange(k, nps, rating, face, dir, true)\n  const end: V3 = [face[0] - dir[0] * L, face[1] - dir[1] * L, face[2] - dir[2] * L]\n  if (pts.length) {\n    pipeRun(k, [...pts, end], nps, { mat })\n    const n = new THREE.Vector3(...pts[0]).sub(new THREE.Vector3(...(pts[1] ?? end))).normalize()\n    if (cap) k.disc('pipeBore', pipeOD(nps) / 2 - pipeWall(nps) + 0.001, pts[0], n.negate().toArray() as V3, 20)\n  }\n  return face\n}\n\n/** Flanged nozzle neck from a base point on a casing straight out to the face; the bore is closed deep inside by a dark cap. */\nexport function neckTo(k: Kit, base: V3, face: V3, dir: V3, nps: number, rating: Rating, mat: MatKey, cap = true) {\n  const L = flange(k, nps, rating, face, dir, true)\n  k.pipe(mat, base, [face[0] - dir[0] * L, face[1] - dir[1] * L, face[2] - dir[2] * L], pipeOD(nps), pipeWall(nps, '80'))\n  if (cap) k.disc('pipeBore', pipeOD(nps) / 2 - pipeWall(nps, '80') + 0.001, [base[0] + dir[0] * 0.002, base[1] + dir[1] * 0.002, base[2] + dir[2] * 0.002], dir, 24)\n}\n\n/** Ring (annulus) between radii rIn and rOut, length h centred on pos along axis: no faces cross the bore. */\nexport function annulus(k: Kit, mat: MatKey, rIn: number, rOut: number, h: number, pos: V3, axis: V3 | 'x' | 'y' | 'z', seg = 32) {\n  const a = typeof axis === 'string' ? ({ x: [1, 0, 0], y: [0, 1, 0], z: [0, 0, 1] } as Record<string, V3>)[axis] : axis\n  const p0: V3 = [pos[0] - (a[0] * h) / 2, pos[1] - (a[1] * h) / 2, pos[2] - (a[2] * h) / 2]\n  k.add(mat, latheCrisp([[rIn, 0], [rOut, 0], [rOut, h], [rIn, h], [rIn, 0]], seg), mat4(p0, a))\n}\n\n/** Bolted flange pair with a ring gasket (open bore) and studs, centred on pos. */\nexport function joint(k: Kit, nps: number, rating: Rating, pos: V3, axis: V3 | 'x' | 'y' | 'z') {\n  const a = typeof axis === 'string' ? ({ x: [1, 0, 0], y: [0, 1, 0], z: [0, 0, 1] } as Record<string, V3>)[axis] : axis\n  const f = flangeDims(nps, rating)\n  const g = 0.0045\n  const at = (d: number): V3 => [pos[0] + a[0] * d, pos[1] + a[1] * d, pos[2] + a[2] * d]\n  flange(k, nps, rating, at(-g / 2), a)\n  flange(k, nps, rating, at(g / 2), [-a[0], -a[1], -a[2]])\n  annulus(k, 'gasket', pipeOD(nps) / 2 - pipeWall(nps) * 0.5, f.raisedFace / 2 + 0.006, g, pos, a, 28)\n  const span = 2 * (f.thickness + f.raisedFaceHeight) + g\n  const studL = span + f.boltDia * 3\n  k.boltCircle('boltXylan', 'nut', f.bolts, f.boltCircle / 2, at(-studL / 2), a, f.boltDia, studL, [f.boltDia * 0.95, studL - f.boltDia * 0.95])\n}\n\n/**\n * Hollow run pipe a -> b with a set-on branch at c towards bDir. The run wall has a stepped-circle hole the size of the branch\n * bore, and the branch walls are saddle-cut to sit on the run's outer surface, so both bores read through with no gaps.\n */\nexport function teePipe(k: Kit, a: V3, b: V3, c: V3, bDir: V3, od: number, wall: number, bod: number, bwall: number, bLen: number, mat: MatKey) {\n  const A = new THREE.Vector3(...a)\n  const d = new THREE.Vector3(...b).sub(A)\n  const L = d.length()\n  d.normalize()\n  const ro = od / 2\n  const ri = ro - wall\n  const rbo = Math.min(bod / 2, ro)\n  const rbi = Math.max(rbo - bwall, rbo * 0.5)\n  const tc = new THREE.Vector3(...c).sub(A).dot(d)\n  const q = orientQuat(d.toArray() as V3)\n  const lb = new THREE.Vector3(...bDir).applyQuaternion(q.clone().invert())\n  const phiB = Math.atan2(lb.x, lb.z)\n  const m = mat4(a, d.toArray() as V3)\n  const seg = 40\n  const ringSeg = (y0: number, y1: number, gap: number) => {\n    if (y1 - y0 < 1e-5) return\n    const ph0 = phiB + gap\n    const phL = Math.PI * 2 - 2 * gap\n    k.add(mat, latheCrisp([[ro, y0], [ro, y1]], seg, ph0, phL), m.clone())\n    k.add('pipeBore', latheCrisp([[ri, y1], [ri, y0]], seg, ph0, phL), m.clone())\n  }\n  const t0 = Math.max(0.001, tc - rbi)\n  const t1 = Math.min(L - 0.001, tc + rbi)\n  k.add(mat, latheCrisp([[ri, 0], [ro, 0], [ro, t0]], seg), m.clone())\n  k.add('pipeBore', latheCrisp([[ri, t0], [ri, 0]], seg), m.clone())\n  k.add(mat, latheCrisp([[ro, t1], [ro, L], [ri, L]], seg), m.clone())\n  k.add('pipeBore', latheCrisp([[ri, L], [ri, t1]], seg), m.clone())\n  const N = 12\n  for (let j = 0; j < N; j++) {\n    const ya = t0 + ((t1 - t0) * j) / N\n    const yb = t0 + ((t1 - t0) * (j + 1)) / N\n    const w = Math.sqrt(Math.max(0, rbi * rbi - Math.max((ya - tc) ** 2, (yb - tc) ** 2)))\n    ringSeg(ya, yb, w > 0 ? Math.asin(Math.min(0.99, w / ro)) : 0)\n  }\n  // saddle-cut branch: outer and inner walls start on the run's outer surface\n  const e1 = d.clone()\n  const e2 = new THREE.Vector3(...bDir).normalize()\n  const e3 = new THREE.Vector3().crossVectors(e1, e2).normalize()\n  const C = A.clone().addScaledVector(d, tc)\n  const wallGeo = (r: number, inward: boolean) => {\n    const pos: number[] = []\n    const nor: number[] = []\n    const idx: number[] = []\n    for (let i = 0; i <= seg; i++) {\n      const ph = (i / seg) * Math.PI * 2\n      const cs = Math.cos(ph)\n      const sn = Math.sin(ph)\n      const n = e1.clone().multiplyScalar(cs).addScaledVector(e3, sn).multiplyScalar(inward ? -1 : 1)\n      const hb = Math.sqrt(Math.max(ro * ro - (r * sn) ** 2, 0)) - 0.0015\n      for (const h of [hb, bLen]) {\n        const p = C.clone().addScaledVector(e1, r * cs).addScaledVector(e3, r * sn).addScaledVector(e2, h)\n        pos.push(p.x, p.y, p.z)\n        nor.push(n.x, n.y, n.z)\n      }\n    }\n    for (let i = 0; i < seg; i++) {\n      const p0 = i * 2\n      if (inward) idx.push(p0, p0 + 2, p0 + 1, p0 + 1, p0 + 2, p0 + 3)\n      else idx.push(p0, p0 + 1, p0 + 2, p0 + 1, p0 + 3, p0 + 2)\n    }\n    const g = new THREE.BufferGeometry()\n    g.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3))\n    g.setAttribute('normal', new THREE.Float32BufferAttribute(nor, 3))\n    g.setIndex(idx)\n    return g\n  }\n  k.add(mat, wallGeo(rbo, false))\n  k.add('pipeBore', wallGeo(rbi, true))\n  annulus(k, mat, rbi, rbo, 0.002, C.clone().addScaledVector(e2, bLen - 0.001).toArray() as V3, e2.toArray() as V3, seg)\n  return C.clone().addScaledVector(e2, bLen).toArray() as V3\n}\n\n// ------------------------------------------------------------------ face-frame assemblies (x along face, y up, z outward)\n\n/** Louvre panel centred at (cx, cy). */\nexport function louvre(k: Kit, w: number, h: number, cx: number, cy: number, frame: MatKey = SKID) {\n  k.box(screen(), [w, h, 0.01], [cx, cy, 0.005])\n  const fw = 0.05\n  for (const s of [-1, 1]) {\n    k.box(frame, [w + 2 * fw, fw, 0.12], [cx, cy + s * (h / 2 + fw / 2), 0.06])\n    k.box(frame, [fw, h, 0.12], [cx + s * (w / 2 + fw / 2), cy, 0.06])\n  }\n  const n = Math.max(3, Math.floor(h / 0.09))\n  for (let i = 0; i < n; i++) k.box(frame, [w, 0.008, 0.12], [cx, cy - h / 2 + ((i + 0.5) * h) / n, 0.06], eul(0.75, 0, 0))\n}\n\n/** Rows of weather hoods over a dark bird screen (filter-house inlet face). */\nexport function hoods(k: Kit, w: number, h: number, cx: number, cy: number, mat: MatKey) {\n  const g = new THREE.PlaneGeometry(w, h)\n  uvScale(g, w / 0.12, h / 0.12)\n  k.add(meshGuardMaterial('#3a3f44'), g, mat4([cx, cy, 0.005]))\n  const rows = Math.max(2, Math.floor(h / 0.42))\n  const pitch = h / rows\n  const a = 0.55\n  const lp = 0.34\n  const dz = Math.cos(a) * lp\n  const dy = Math.sin(a) * lp\n  for (let i = 0; i < rows; i++) {\n    const yt = cy - h / 2 + (i + 1) * pitch\n    k.box(mat, [w, 0.012, lp], [cx, yt - dy / 2, dz / 2], eul(a, 0, 0))\n    k.box(mat, [w, 0.05, 0.012], [cx, yt - dy - 0.02, dz])\n    for (const s of [-1, 1]) k.box(mat, [0.012, dy, dz], [cx + s * (w / 2), yt - dy / 2, dz / 2])\n  }\n  const nm = Math.max(1, Math.round(w / 1.4))\n  for (let j = 1; j < nm; j++) k.box(mat, [0.012, h, dz], [cx - w / 2 + (j * w) / nm, cy, dz / 2])\n}\n\n/** Personnel door (single or double leaf), sill at y = 0: frame, leaves (swung open by `open` rad), handles, vision panels, signs. */\nexport function door(k: Kit, w: number, h: number, cx: number, double: boolean, leaf: MatKey, frame: MatKey = SKID, open = 0) {\n  k.box(frame, [w + 0.14, 0.07, 0.05], [cx, h + 0.035, 0.025])\n  for (const s of [-1, 1]) k.box(frame, [0.07, h, 0.05], [cx + s * (w / 2 + 0.035), h / 2, 0.025])\n  k.box(frame, [w, 0.03, 0.12], [cx, 0.015, 0.06])\n  const leaves = double ? 2 : 1\n  const lw = w / leaves\n  for (let i = 0; i < leaves; i++) {\n    const lx = cx - w / 2 + lw * (i + 0.5)\n    const latch = double ? (i === 0 ? 1 : -1) : 1\n    const hingeX = lx - latch * (lw / 2)\n    for (const yy of [0.2, h / 2, h - 0.2]) k.cyl(frame, 0.018, 0.12, [hingeX - latch * 0.01, yy, 0.06], 'y', 8)\n    const m = new THREE.Matrix4().makeTranslation(hingeX, 0, 0.06).multiply(new THREE.Matrix4().makeRotationY(-latch * open)).multiply(new THREE.Matrix4().makeTranslation(-hingeX, 0, -0.06))\n    k.with(m, () => {\n      k.box(leaf, [lw - 0.012, h - 0.012, 0.05], [lx, h / 2, 0.03])\n      for (const yy of [0.35, 0.7]) k.box(leaf, [lw - 0.18, 0.035, 0.012], [lx, h * yy, 0.06])\n      const hx = lx + latch * (lw / 2 - 0.1)\n      k.box('stainless', [0.04, 0.14, 0.02], [hx, 1.05, 0.065])\n      k.box('stainless', [0.14, 0.022, 0.024], [hx - latch * 0.06, 1.08, 0.088])\n      k.box('stainless', [0.03, 0.18, 0.02], [hx, 1.05, -0.005])\n      for (const yy of [0.2, h / 2, h - 0.2]) k.box(leaf, [0.1, 0.06, 0.012], [hingeX + latch * 0.05, yy, 0.058])\n      k.box('glass', [0.22, 0.3, 0.052], [lx, Math.min(1.6, h - 0.35), 0.03])\n      k.plane(signMaterial('noentry'), 0.36, 0.18, [lx, Math.min(1.32, h - 0.65), 0.0565])\n    })\n  }\n}\n\nexport function gasDetector(k: Kit, p: V3) {\n  k.rbox('stainless', [0.12, 0.14, 0.08], 0.01, p)\n  k.cyl('actuatorYellow', 0.035, 0.11, [p[0], p[1] - 0.12, p[2]], 'y', 14)\n  k.cyl('castIron', 0.042, 0.03, [p[0], p[1] - 0.19, p[2]], 'y', 14)\n}\n\nexport function beacon(k: Kit, p: V3) {\n  k.box(SKID, [0.12, 0.04, 0.12], p)\n  k.cyl('actuatorRed', 0.045, 0.08, [p[0], p[1] + 0.06, p[2]], 'y', 14)\n  k.dome('glass', 0.045, [p[0], p[1] + 0.1, p[2]], 'y', Math.PI / 2, 1.2, 14)\n}\n\n// ------------------------------------------------------------------ in-line valves on a pipe along X\n\nexport function ballValve(k: Kit, x: number, y: number, z: number, nps: number, rating: Rating) {\n  const od = pipeOD(nps)\n  const L = Math.max(0.24, od * 3.2)\n  k.sphere('valveBody', od * 1.0, [x, y, z], undefined, 18)\n  annulus(k, 'valveBody', od / 2, od * 0.75, L * 0.8, [x, y, z], 'x', 24)\n  joint(k, nps, rating, [x - L / 2, y, z], 'x')\n  joint(k, nps, rating, [x + L / 2, y, z], 'x')\n  const ya = y + od\n  k.cyl('valveBody', od * 0.35, 0.1, [x, ya + 0.05, z], 'y', 12)\n  k.rbox('actuatorYellow', [0.16, 0.14, 0.14], 0.02, [x, ya + 0.17, z])\n  k.cyl('actuatorYellow', 0.065, 0.32, [x + 0.2, ya + 0.17, z], 'x', 16)\n  k.rbox('stainless', [0.08, 0.07, 0.06], 0.01, [x - 0.04, ya + 0.28, z])\n  return L\n}\n\n/** Globe control valve on a pipe along X; `through` draws the body bore when no pipe runs through it. */\nexport function controlValve(k: Kit, x: number, y: number, z: number, nps: number, rating: Rating, act: MatKey = 'actuatorBlue', through = false) {\n  const od = pipeOD(nps)\n  const L = Math.max(0.3, od * 3.6)\n  k.sphere('valveBody', od * 1.2, [x, y, z], [1.25, 1, 1], 18)\n  annulus(k, 'valveBody', od / 2, od * 0.7, L * 0.8, [x, y, z], 'x', 24)\n  joint(k, nps, rating, [x - L / 2, y, z], 'x')\n  joint(k, nps, rating, [x + L / 2, y, z], 'x')\n  if (through) k.pipe('valveBody', [x - L / 2, y, z], [x + L / 2, y, z], od, pipeWall(nps))\n  const yb = y + od * 1.15\n  k.cyl('valveBody', od * 0.5, 0.12, [x, yb + 0.06, z], 'y', 14)\n  for (const s of [-1, 1]) k.rod('valveBody', [x, yb + 0.12, z + s * 0.05], [x, yb + 0.33, z + s * 0.05], 0.012, 6)\n  const r = clamp(0.1 + od, 0.15, 0.32)\n  const yd = yb + 0.33 + r * 0.25\n  k.cyl(act, r, 0.05, [x, yd, z], 'y', 24)\n  k.dome(act, r, [x, yd + 0.025, z], 'y', Math.PI / 2, 0.35, 20)\n  k.dome(act, r, [x, yd - 0.025, z], '-y', Math.PI / 2, 0.25, 20)\n  k.rbox('stainless', [0.1, 0.12, 0.07], 0.01, [x + r * 0.8, yd - 0.15, z])\n  return L\n}\n\n// ------------------------------------------------------------------ base frame\n\n/** Deep fabricated base frame: four longitudinal I-beams, cross members, drip-pan deck (top at y = hs), upstand, padeyes, drain, earth boss. */\nexport function baseFrame(k: Kit, SL: number, SW: number, hs: number, o: { upstand?: boolean; drain?: boolean; holes?: { x: number; z: number; w: number }[] } = {}) {\n  const holes = o.holes ?? []\n  const beam = (x0: number, x1: number, z: number) => {\n    const L = x1 - x0\n    const xm = (x0 + x1) / 2\n    k.box(SKID, [L, 0.03, 0.3], [xm, 0.015, z])\n    k.box(SKID, [L, 0.03, 0.3], [xm, hs - 0.027, z])\n    k.box(SKID, [L, hs - 0.072, 0.018], [xm, hs / 2 - 0.006, z])\n  }\n  for (const z of [-SW / 2 + 0.15, -SW * 0.2, SW * 0.2, SW / 2 - 0.15]) beam(-SL / 2, SL / 2, z)\n  const nCross = Math.max(3, Math.round(SL / 1.6))\n  for (let i = 0; i <= nCross; i++) {\n    const x = -SL / 2 + 0.01 + (i * (SL - 0.02)) / nCross\n    if (holes.some((h) => Math.abs(x - h.x) < h.w / 2 + 0.03)) continue\n    k.box(SKID, [0.018, hs - 0.07, SW - 0.3], [x, hs / 2 - 0.006, 0])\n  }\n  // drip-pan deck, cut around deck openings (nozzles passing through the skid) with trimmers and coamings\n  let xa = -SL / 2\n  for (const h of [...holes].sort((a, b) => a.x - b.x)) {\n    const h0 = h.x - h.w / 2\n    const h1 = h.x + h.w / 2\n    if (h0 > xa) k.box('deckPlate', [h0 - xa, 0.012, SW], [(xa + h0) / 2, hs - 0.006, 0])\n    for (const [z0, z1] of [[-SW / 2, h.z - h.w / 2], [h.z + h.w / 2, SW / 2]]) if (z1 > z0) k.box('deckPlate', [h.w, 0.012, z1 - z0], [h.x, hs - 0.006, (z0 + z1) / 2])\n    for (const s of [-1, 1]) {\n      k.box(SKID, [0.018, hs - 0.07, SW - 0.3], [h.x + s * (h.w / 2 + 0.01), hs / 2 - 0.006, 0])\n      k.box(SKID, [h.w + 0.03, 0.08, 0.012], [h.x, hs + 0.03, h.z + s * (h.w / 2 + 0.006)])\n      k.box(SKID, [0.012, 0.08, h.w], [h.x + s * (h.w / 2 + 0.006), hs + 0.03, h.z])\n    }\n    xa = Math.max(xa, h1)\n  }\n  if (SL / 2 > xa) k.box('deckPlate', [SL / 2 - xa, 0.012, SW], [(xa + SL / 2) / 2, hs - 0.006, 0])\n  if (o.upstand !== false)\n    for (const s of [-1, 1]) {\n      k.box(SKID, [SL, 0.1, 0.012], [0, hs + 0.02, s * (SW / 2 - 0.006)])\n      k.box(SKID, [0.012, 0.1, SW], [s * (SL / 2 - 0.006), hs + 0.02, 0])\n    }\n  if (o.drain !== false) lineRun(k, [[-SL / 2 + 0.35, hs - 0.02, SW / 2 - 0.35], [-SL / 2 + 0.35, 0.12, SW / 2 - 0.35], [-SL / 2 - 0.08, 0.12, SW / 2 - 0.35]], 2, SKID)\n  if (o.drain !== false) k.cyl('flangeSteel', 0.075, 0.02, [-SL / 2 - 0.08, 0.12, SW / 2 - 0.35], 'x', 20)\n  for (const sx of [-1, 1])\n    for (const sz of [-1, 1]) {\n      const px = sx * (SL / 2 - Math.min(1.1, SL * 0.12))\n      k.box(SKID, [0.34, 0.26, 0.04], [px, hs * 0.5, sz * (SW / 2 + 0.02)])\n      k.torus('nut', 0.07, 0.02, [px, hs * 0.5 + 0.03, sz * (SW / 2 + 0.045)], 'z', 6, 18)\n    }\n  k.box('brass', [0.12, 0.08, 0.02], [SL / 2 - 0.5, hs * 0.5, SW / 2 + 0.01])\n}\n\n// ------------------------------------------------------------------ rotating auxiliaries\n\n/** Vertical TEFC motor on a pump head, flange at y. */\nexport function verticalMotorPump(k: Kit, x: number, y: number, z: number, r = 0.13) {\n  k.cyl(OIL, r * 0.75, r * 1.2, [x, y + r * 0.6, z], 'y', 20)\n  k.cyl(OIL, r * 0.9, 0.03, [x, y + r * 1.2, z], 'y', 20)\n  const yb = y + r * 1.22\n  k.cyl('motorPaint', r, r * 2.9, [x, yb + r * 1.45, z], 'y', 24)\n  for (let f = 0; f < 12; f++) {\n    const a = (f / 12) * Math.PI * 2\n    k.box('motorPaint', [0.01, r * 2.5, 0.025], [x + Math.cos(a) * (r + 0.006), yb + r * 1.45, z + Math.sin(a) * (r + 0.006)], new THREE.Quaternion().setFromAxisAngle(UP, -a))\n  }\n  k.cyl('motorPaint', r * 1.12, r * 0.75, [x, yb + r * 3.25, z], 'y', 24, r * 0.9)\n  k.disc(fanGuardMaterial(), r * 0.9, [x, yb + r * 3.63, z], 'y', 24)\n  k.rbox('motorPaint', [r * 1.1, r * 0.9, r * 0.8], 0.015, [x, yb + r * 1.6, z + r + r * 0.35])\n}\n\nexport interface LubeOpts {\n  x: number\n  z: number\n  /** reservoir length along X and depth along Z, m */\n  L: number\n  D: number\n  hs: number\n  /** oil cooler: fin-fan on a stand above the tank, or twin vertical shell-and-tube */\n  cooler?: 'finfan' | 'shell'\n  /** underside of the fin-fan stand top */\n  coolerY?: number\n  /** side carrying the level glass and instruments (+1 = +Z) */\n  front?: 1 | -1\n}\n\n/** API 614 lube-oil console: reservoir, twin motor pumps, duplex filters with transfer valve, accumulator, heater, coolers. */\nexport function lubeConsole(k: Kit, o: LubeOpts): RotorSpec[] {\n  const { x, z, L, D, hs } = o\n  const f = o.front ?? 1\n  const tH = clamp(0.55 + L * 0.1, 0.65, 0.95)\n  const yt = hs + tH\n  const rotors: RotorSpec[] = []\n  k.rbox(OIL, [L, tH, D], 0.03, [x, hs + tH / 2, z])\n  k.box(OIL, [L - 0.1, 0.03, D - 0.1], [x, yt + 0.015, z])\n  const pr = clamp(D * 0.2, 0.09, 0.15)\n  const xf = x + L * 0.2\n  for (const [i, px] of [x - L * 0.34, x - L * 0.1].entries()) {\n    verticalMotorPump(k, px, yt + 0.03, z, pr)\n    const yy = yt + 0.25 + i * 0.07\n    lineRun(k, [[px + pr * 0.75, yt + 0.12, z], [px + pr * 0.75 + 0.08, yt + 0.12, z], [px + pr * 0.75 + 0.08, yy, z], [xf, yy, z]], 1.5, OIL)\n  }\n  for (const s of [-1, 1]) {\n    const fz = z + s * clamp(D * 0.18, 0.09, 0.2)\n    k.cyl(OIL, 0.085, 0.5, [xf, yt + 0.3, fz], 'y', 20)\n    k.dome(OIL, 0.085, [xf, yt + 0.55, fz], 'y', Math.PI / 2, 0.5, 16)\n    k.cyl(OIL, 0.1, 0.03, [xf, yt + 0.48, fz], 'y', 20)\n  }\n  k.box('valveBody', [0.12, 0.12, 0.2], [xf, yt + 0.1, z])\n  k.rod('handwheel', [xf, yt + 0.16, z], [xf, yt + 0.32, z], 0.012, 6)\n  k.box('handwheel', [0.22, 0.02, 0.02], [xf, yt + 0.32, z])\n  gauge(k, [xf + 0.14, yt + 0.42, z], 'x', 0.08, 'bar', 10)\n  // accumulator, fill breather, heater terminal box, level glass\n  k.cyl(OIL, 0.09, 0.45, [x + L * 0.38, yt + 0.25, z - f * D * 0.2], 'y', 18)\n  k.dome(OIL, 0.09, [x + L * 0.38, yt + 0.475, z - f * D * 0.2], 'y', Math.PI / 2, 1, 14)\n  k.cyl('brass', 0.04, 0.12, [x - L * 0.45, yt + 0.06, z - f * D * 0.25], 'y', 12)\n  k.rbox('stainless', [0.25, 0.25, 0.12], 0.02, [x - L / 2 - 0.06, hs + tH * 0.55, z], undefined)\n  k.box('stainless', [0.05, tH * 0.7, 0.03], [x + L * 0.4, hs + tH / 2, z + f * (D / 2 + 0.015)])\n  k.box('glass', [0.025, tH * 0.6, 0.006], [x + L * 0.4, hs + tH / 2, z + f * (D / 2 + 0.033)])\n  if ((o.cooler ?? 'finfan') === 'shell') {\n    for (const s of [-1, 1]) {\n      const cz = z + s * D * 0.28\n      const cx = x + L * 0.42\n      k.cyl(OIL, 0.1, 0.8, [cx, yt + 0.45, cz], 'y', 20)\n      for (const e of [-1, 1]) k.cyl(OIL, 0.125, 0.04, [cx, yt + 0.45 + e * 0.38, cz], 'y', 20)\n      k.dome(OIL, 0.1, [cx, yt + 0.87, cz], 'y', Math.PI / 2, 0.5, 14)\n      lineRun(k, [[cx + 0.1, yt + 0.75, cz], [cx + 0.2, yt + 0.75, cz], [cx + 0.2, yt + 0.95, cz]], 1, 'pipePaint')\n    }\n    return rotors\n  }\n  const yc0 = Math.max(o.coolerY ?? yt + 1.0, yt + 0.9)\n  const cL = L * 0.92\n  for (const sx of [-1, 1])\n    for (const sz of [-1, 1]) k.box('structSteel', [0.08, yc0 - hs, 0.08], [x + sx * (cL / 2 - 0.04), (yc0 + hs) / 2, z + sz * (D / 2 - 0.04)])\n  k.box('castIron', [cL - 0.24, 0.16, D], [x, yc0 + 0.08, z])\n  for (const s of [-1, 1]) k.box(OIL, [0.12, 0.24, D + 0.04], [x + s * (cL / 2 - 0.06), yc0 + 0.1, z])\n  k.box(OIL, [cL, 0.3, D], [x, yc0 + 0.35, z])\n  const nf = cL > 2.2 ? 2 : 1\n  const fr = Math.min(D / 2 - 0.04, cL / (2 * nf) - 0.06)\n  const fan = fanRotor('stainless', fr * 0.93, [0, 0, 0], 'y', 0, { blades: 4, chord: fr * 0.4, pitch: 0.32, hub: fr * 0.2, hubLen: 0.06 })\n  for (let i = 0; i < nf; i++) {\n    const fx = x - cL / 2 + ((i + 0.5) * cL) / nf\n    k.cyl(OIL, fr + 0.02, 0.14, [fx, yc0 + 0.57, z], 'y', 32, undefined, true)\n    k.disc(fanGuardMaterial(), fr, [fx, yc0 + 0.64, z], 'y', 32)\n    k.cyl('motorPaint', fr * 0.3, 0.22, [fx, yc0 + 0.76, z], 'y', 16)\n    k.cyl('motorPaint', 0.025, 0.1, [fx, yc0 + 0.6, z], 'y', 10)\n    rotors.push({ ...fan, pos: [fx, yc0 + 0.57, z], rpm: 190 })\n  }\n  lineRun(k, [[x + L * 0.42, yt, z], [x + L * 0.42, yc0 + 0.1, z], [x + cL / 2 - 0.06, yc0 + 0.1, z]], 2, OIL)\n  lineRun(k, [[x - cL / 2 + 0.06, yc0 + 0.1, z - D * 0.25], [x - L * 0.45, yc0 + 0.1, z - D * 0.25], [x - L * 0.45, yt, z - D * 0.25]], 2, OIL)\n  return rotors\n}\n\n/** Parallel-shaft gearbox between x0 and x1: slow shaft at ySlow (-X side), fast shaft at yFast (+X side), base on y = hs. */\nexport function gearbox(k: Kit, o: { x0: number; x1: number; ySlow: number; yFast: number; hs: number; W: number; mat?: MatKey }) {\n  const { x0, x1, ySlow, yFast, hs, W } = o\n  const M = o.mat ?? OIL\n  const L = x1 - x0\n  const gx = (x0 + x1) / 2\n  const yMid = (ySlow + yFast) / 2\n  const top = Math.max(ySlow, yFast) + 0.3 + W * 0.12\n  k.rbox(M, [L, yMid - hs, W], 0.04, [gx, (yMid + hs) / 2, 0])\n  k.rbox(M, [L * 0.94, top - yMid, W * 0.86], Math.min(0.12, L * 0.1), [gx, (yMid + top) / 2, 0])\n  k.box(M, [L + 0.08, 0.06, W + 0.08], [gx, yMid, 0])\n  const nb = Math.max(4, Math.round(L / 0.16))\n  for (let i = 0; i < nb; i++)\n    for (const s of [-1, 1]) {\n      const bx = x0 + 0.06 + (i * (L - 0.12)) / (nb - 1)\n      k.cyl('boltXylan', 0.012, 0.12, [bx, yMid, s * (W / 2 + 0.02)], 'y', 6)\n      k.hex('nut', 0.04, 0.02, [bx, yMid + 0.04, s * (W / 2 + 0.02)])\n    }\n  for (const s of [-1, 1]) for (const fx of [-0.3, 0, 0.3]) k.box(M, [0.03, yMid - hs - 0.06, 0.08], [gx + fx * L, (yMid + hs) / 2, s * (W / 2 + 0.04)])\n  // feet on the base\n  k.box(M, [L + 0.2, 0.05, W + 0.2], [gx, hs + 0.025, 0])\n  k.box(M, [L * 0.45, 0.04, W * 0.4], [gx, top + 0.02, 0])\n  k.around(8, Math.min(L, W) * 0.16, [gx, top + 0.045, 0], 'y', (p) => k.hex('nut', 0.03, 0.015, p))\n  k.cyl('brass', 0.03, 0.12, [gx + L * 0.3, top + 0.06, W * 0.2], 'y', 10)\n  k.cyl('brass', 0.05, 0.015, [gx, yMid - 0.25, W / 2 + 0.01], 'z', 16)\n  k.disc('glass', 0.04, [gx, yMid - 0.25, W / 2 + 0.018], 'z', 16)\n  k.cyl(M, 0.2, 0.1, [x1 + 0.05, yFast, 0], 'x', 28)\n  k.cyl(M, 0.24, 0.1, [x0 - 0.05, ySlow, 0], 'x', 28)\n  // vibration probes on the bearing caps\n  for (const xx of [x0 + 0.12, x1 - 0.12]) k.rod('stainless', [xx, top, -W * 0.25], [xx, top + 0.1, -W * 0.25], 0.012, 8)\n  k.plane(signMaterial('ler', 'GEARBOX'), 0.4, 0.1, [gx, yMid + 0.12, W * 0.43 + 0.001])\n}\n\n/** Roof extract fan in a cowl over a damper plate; returns the turning impeller. */\nexport function roofFan(k: Kit, x: number, z: number, H: number, R: number, mat: MatKey): RotorSpec {\n  k.box(mat, [R * 2 + 0.2, 0.25, R * 2 + 0.2], [x, H + 0.125, z])\n  k.cyl(mat, R, 0.7, [x, H + 0.6, z], 'y', 32, undefined, true)\n  k.disc('castIron', R, [x, H + 0.5, z], 'y', 32)\n  k.cyl(mat, R + 0.03, 0.05, [x, H + 0.95, z], 'y', 32, undefined, true)\n  k.disc(fanGuardMaterial(), R, [x, H + 0.976, z], 'y', 32)\n  k.cyl('motorPaint', R * 0.32, 0.3, [x, H + 0.8, z], 'y', 16)\n  k.rbox('stainless', [0.18, 0.2, 0.12], 0.01, [x + R + 0.14, H + 0.35, z])\n  return fanRotor('stainless', R * 0.94, [x, H + 0.62, z], 'y', 170, { blades: 6, chord: R * 0.36, pitch: 0.42, hub: R * 0.3, hubLen: 0.1 })\n}\n\n/** Large induction motor (box frame, CACA cooler on top, NDE fan hood, sleeve-bearing pedestals, terminal boxes). Frame mB0..mB1 on y = hs, shaft at ym ending at xEnd. */\nexport function largeMotor(k: Kit, o: { mB0: number; mB1: number; xEnd: number; ym: number; hs: number; mW: number; rpm: number }): RotorSpec {\n  const { mB0, mB1, xEnd, ym, hs, mW } = o\n  const MOTOR: MatKey = 'motorPaint'\n  const mx = (mB0 + mB1) / 2\n  const mTop = ym + mW * 0.5\n  k.rbox(MOTOR, [mB1 - mB0, mTop - hs - 0.12, mW], 0.06, [mx, (mTop + hs + 0.12) / 2, 0])\n  for (let i = 0; i <= 5; i++) for (const sz of [-1, 1]) k.box(MOTOR, [0.05, mTop - hs - 0.2, 0.06], [mB0 + 0.1 + (i * (mB1 - mB0 - 0.2)) / 5, (mTop + hs) / 2, sz * (mW / 2 + 0.02)])\n  k.box(SKID, [mB1 - mB0 + 0.2, 0.12, mW + 0.2], [mx, hs + 0.06, 0])\n  const cH = clamp(0.4 + mW * 0.18, 0.5, 0.85)\n  k.rbox(MOTOR, [(mB1 - mB0) * 0.92, cH, mW * 0.92], 0.05, [mx, mTop + cH / 2, 0])\n  for (const sz of [-1, 1])\n    for (let i = 0; i < 9; i++) k.box(MOTOR, [(mB1 - mB0) * 0.7, 0.02, 0.04], [mx, mTop + cH * 0.15 + i * (cH * 0.75) / 9, sz * (mW * 0.46 + 0.01)], new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), sz * 0.6))\n  const yHood = mTop + cH * 0.5\n  const hr = Math.min(mW * 0.32, cH * 0.6)\n  k.cyl(MOTOR, hr, 0.4, [mB0 - 0.15, yHood, 0], 'x', 32, undefined, true)\n  k.cyl(MOTOR, hr + 0.02, 0.04, [mB0 - 0.33, yHood, 0], 'x', 32, undefined, true)\n  k.disc('rubberBlack', hr, [mB0 - 0.2, yHood, 0], '-x', 32)\n  k.disc(fanGuardMaterial(), hr, [mB0 - 0.352, yHood, 0], '-x', 32)\n  k.cyl('stainless', 0.04, 0.12, [mB0 - 0.24, yHood, 0], 'x', 12)\n  const fan = fanRotor('flangeSteel', hr * 0.9, [mB0 - 0.29, yHood, 0], 'x', o.rpm, { blades: 7, chord: hr * 0.28, pitch: 0.3, hub: hr * 0.22, hubLen: 0.05 })\n  for (const sx of [mB0 + 0.2, mB1 - 0.2]) for (const sz of [-1, 1]) k.torus('nut', 0.06, 0.018, [sx, mTop + cH + 0.04, sz * mW * 0.4], 'x', 6, 16)\n  for (const xbp of [mB1 + 0.22, mB0 - 0.22]) {\n    k.rbox(MOTOR, [0.4, ym - hs + 0.25, 0.7], 0.04, [xbp, (ym + 0.25 + hs) / 2, 0])\n    k.cyl(MOTOR, 0.26, 0.42, [xbp, ym, 0], 'x', 32)\n    k.cyl('brass', 0.045, 0.02, [xbp, ym - 0.1, 0.36], 'z', 16)\n    k.disc('glass', 0.035, [xbp, ym - 0.1, 0.371], 'z', 16)\n    lineRun(k, [[xbp, ym + 0.26, 0], [xbp, ym + 0.45, 0], [xbp, ym + 0.45, -mW / 2 - 0.1], [xbp, hs + 0.15, -mW / 2 - 0.1]], 1, OIL)\n  }\n  k.cyl('stainless', 0.11, xEnd - mB1 - 0.4, [(mB1 + 0.4 + xEnd) / 2, ym, 0], 'x', 20)\n  const tz = mW / 2\n  const ty = (hs + mTop) / 2\n  k.rbox(MOTOR, [1.1, 0.8, 0.4], 0.04, [mx + 0.3, ty + 0.05, tz + 0.2])\n  k.rbox(MOTOR, [0.5, 0.5, 0.3], 0.04, [mx - 0.8, ty, tz + 0.15])\n  k.plane(signMaterial('hv'), 0.4, 0.2, [mx + 0.3, ty + 0.25, tz + 0.401])\n  for (let i = 0; i < 3; i++) {\n    const cxg = mx + i * 0.3\n    k.hex('brass', 0.09, 0.06, [cxg, ty - 0.38, tz + 0.2], 'y')\n    k.spline('cable', [[cxg, ty - 0.42, tz + 0.2], [cxg, hs + 0.25, tz + 0.23], [cxg + 0.1, hs + 0.02, tz + 0.13]], 0.035, 8, 16)\n  }\n  k.box('nameplate', [0.5, 0.3, 0.01], [mx - 0.2, mTop - 0.25, -mW / 2 - 0.006])\n  return fan\n}\n"
    }
  ],
  "categories": [
    "rotating"
  ],
  "meta": {
    "export": "ReciprocatingCompressor",
    "anchor": "base",
    "standards": [
      "API 618",
      "API 11P",
      "IEC 60034",
      "ASME VIII (bottles)",
      "ASME B16.5"
    ]
  }
}