{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "screw-compressor",
  "type": "registry:component",
  "title": "Oil-injected screw compressor package (API 619)",
  "description": "Packaged rotary screw compressor for instrument air or gas boost: twin-rotor block with inlet (unloader) valve, TEFC two-pole motor and guarded coupling, horizontal oil separator on saddles with safety valve and minimum-pressure valve, combined oil cooler / aftercooler with fan, duplex oil filters, thermostatic valve and (air service) an intake filter housing. With `enclosure` it sits in an acoustic canopy with intake louvres, a cooling-air discharge grille, HMI and front doors that can be opened; without it, a free-standing control panel. Block, separator and cooler scale with `kw`.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "rotating/screw-compressor.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/rotating/screw-compressor.tsx",
      "content": "// Oil-injected screw compressor package (API 619 / ISO 1217): twin-rotor block, TEFC motor, horizontal oil separator, combined oil / aftercooler, filters, control panel, optional acoustic enclosure.\nimport * as THREE from 'three'\nimport { useEffect, useMemo } from 'react'\nimport { Kit, mat4, uvScale, type MatKey, type V3 } from '../lib/kit'\nimport { KitMesh, type GroupProps } from '../lib/mesh'\nimport { flange, gauge, motor, motorSize, saddle, skid, tagPlate } from '../lib/parts'\nimport { port, type Port } from '../lib/ports'\nimport { Rotors, cowlFan, fanRotor, guardedCoupling, type RotorSpec } from '../lib/rotors'\nimport { fanGuardMaterial, lcdMaterial, louvreMaterial, signMaterial } from '../lib/textures'\nimport { elbow90, pipeOD, type Rating } from '../lib/standards'\nimport { OIL, SKID, annulus, clamp, faceQ, lineRun, louvre, neckTo, runToFlange } from './_compressors-kit'\n\nexport interface ScrewCompressorProps {\n  /** motor rated power, kW */\n  kw?: number\n  service?: 'air' | 'gas'\n  enclosure?: boolean\n  /** swing the front enclosure doors open */\n  doorsOpen?: boolean\n  inletNps?: number\n  outletNps?: number\n  rating?: Rating\n  tag?: string\n  running?: boolean\n}\n\nexport const defaults = {\n  kw: 250,\n  service: 'air' as 'air' | 'gas',\n  enclosure: true,\n  doorsOpen: false,\n  inletNps: 6,\n  outletNps: 3,\n  rating: 300 as Rating,\n  tag: 'K-4600',\n  running: true,\n}\n\ntype P = typeof defaults\n\nfunction layout(p: P) {\n  const kw = clamp(p.kw, 30, 1500)\n  const ms = motorSize(Math.min(kw, 1000), 2)\n  const Rr = clamp(0.1 + kw * 0.0003, 0.12, 0.42) // rotor housing radius, typical\n  const Lb = Rr * 5.2\n  const cowl = Math.max(0.12, ms.R * 0.6)\n  const Ds = clamp(0.45 + kw / 1400, 0.5, 1.3)\n  const Hcool = clamp(0.9 + kw / 700, 1.0, 2.4)\n  const hs = 0.208\n  const ys = hs + Math.max(ms.H + 0.12, Rr * 1.4 + 0.35)\n  // stations from the -X end\n  const xmL = 0.25\n  const xm = xmL + cowl + ms.L\n  const xc0 = xm + 0.12 + ms.R * 0.2\n  const xc1 = xc0 + 0.3\n  const xb0 = xc1 + 0.15\n  const xb1 = xb0 + Lb\n  const xcool = xb1 + 0.7 + Rr\n  const SL = xcool + 0.5\n  const shift = -SL / 2\n  const S = (x: number) => x + shift\n  const trainHalf = Math.max(ms.R + 0.25, 2.6 * Rr + 0.15)\n  const SW = Math.max(1.6, 2 * trainHalf + Ds + 0.7)\n  const zt = SW / 2 - trainHalf - 0.1\n  const zs = -SW / 2 + Ds / 2 + 0.2\n  const ysep = hs + Ds / 2 + 0.35\n  const blockTop = ys + Rr * 1.3\n  const Hc = Math.max(hs + Hcool + 0.55, ysep + Ds / 2 + 0.75, blockTop + 0.75, ys + ms.R + 0.6) + 0.15\n  const yIn = p.enclosure ? Hc + 0.35 : blockTop + 0.85\n  const xi = S(xb0 + Lb * 0.3)\n  const yOut = hs + 0.45\n  const xo = S(xcool - 0.45)\n  return { kw, ms, Rr, Lb, cowl, Ds, Hcool, hs, ys, xmL: S(xmL), xm: S(xm), xc0: S(xc0), xc1: S(xc1), xb0: S(xb0), xb1: S(xb1), xcool: S(xcool), SL, SW, zt, zs, ysep, blockTop, Hc, yIn, xi, yOut, xo }\n}\n\nexport function ports(props: ScrewCompressorProps = {}): Port[] {\n  const p = { ...defaults, ...props }\n  const L = layout(p)\n  const air = p.service === 'air'\n  return [\n    port('inlet', air ? 'Air intake' : 'Gas inlet', [L.xi, L.yIn, L.zt], [0, 1, 0], p.inletNps, p.rating, air ? 'vent' : 'process'),\n    port('outlet', air ? 'Compressed air outlet' : 'Gas outlet', [L.xo, L.yOut, -L.SW / 2 - 0.2], [0, 0, -1], p.outletNps, p.rating),\n  ]\n}\n\nexport function ScrewCompressor(props: ScrewCompressorProps & GroupProps) {\n  const { kw, service, enclosure, doorsOpen, inletNps, outletNps, rating, tag, running, ...group } = { ...defaults, ...props }\n  const { built, rotors } = useMemo(() => build({ kw, service, enclosure, doorsOpen, inletNps, outletNps, rating, tag, running }), [kw, service, enclosure, doorsOpen, inletNps, outletNps, 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 ScrewCompressor\n\nconst BLOCK: MatKey = 'equipmentPaintDark'\nconst VESSEL: MatKey = 'equipmentPaint'\n\nfunction build(p: P) {\n  const k = new Kit()\n  const L = layout(p)\n  const { Rr, Lb, hs, ys, zt, zs, Ds, ysep, SL, SW, ms } = L\n  const rotors: RotorSpec[] = []\n  skid(k, SL, SW, 'HEB 200', SKID)\n\n  // motor on a pedestal, coupling under its guard\n  const ped = ys - ms.H - hs\n  k.box(SKID, [ms.L + 0.1, ped, ms.R * 2 + 0.2], [L.xm - ms.L / 2, hs + ped / 2, zt])\n  k.at([L.xm, ys, zt], undefined, () => motor(k, { R: ms.R, L: ms.L, H: ms.H, tbox: 'side', cable: true }))\n  rotors.push(cowlFan(ms.R, ms.L, 300, [L.xm, ys, zt]))\n  rotors.push(guardedCoupling(k, L.xc0, L.xc1, ys, Math.max(0.14, Rr * 0.8), hs, 'safetyYellow', 300, zt, Math.max(0.03, ms.shaftDia / 2)))\n\n  // twin-rotor block: male rotor on the drive shaft, female alongside (+Z), cast ribs, end covers, feet\n  const xbm = (L.xb0 + L.xb1) / 2\n  const zf = zt + 1.5 * Rr\n  k.cyl(BLOCK, Rr * 1.15, Lb, [xbm, ys, zt], 'x', 40)\n  k.cyl(BLOCK, Rr, Lb, [xbm, ys, zf], 'x', 40)\n  k.box(BLOCK, [Lb, Rr * 1.7, 1.5 * Rr], [xbm, ys, (zt + zf) / 2])\n  for (let i = 1; i < 5; i++) {\n    const x = L.xb0 + (i * Lb) / 5\n    k.cyl(BLOCK, Rr * 1.2, 0.035, [x, ys, zt], 'x', 40)\n    k.cyl(BLOCK, Rr * 1.05, 0.035, [x, ys, zf], 'x', 40)\n  }\n  for (const [x, d] of [[L.xb0, -1], [L.xb1, 1]] as const) {\n    k.rbox(BLOCK, [0.1, Rr * 2.6, 2.9 * Rr], 0.03, [x + d * 0.05, ys, (zt + zf) / 2])\n    k.boltCircle('boltXylan', 'nut', 10, Rr * 0.95, [x + d * 0.06, ys, zt], d > 0 ? 'x' : '-x', 0.014, 0.06, [0.05])\n    k.boltCircle('boltXylan', 'nut', 8, Rr * 0.85, [x + d * 0.06, ys, zf], d > 0 ? 'x' : '-x', 0.014, 0.06, [0.05])\n  }\n  k.dome(BLOCK, Rr * 0.6, [L.xb1 + 0.1, ys, zt], 'x', Math.PI / 2, 0.5, 24)\n  k.dome(BLOCK, Rr * 0.55, [L.xb1 + 0.1, ys, zf], 'x', Math.PI / 2, 0.5, 24)\n  k.cyl(BLOCK, Rr * 0.5, 0.15, [L.xb0 - 0.075, ys, zt], 'x', 24)\n  for (const x of [L.xb0 + Lb * 0.15, L.xb1 - Lb * 0.15]) {\n    k.box(BLOCK, [0.14, ys - Rr * 0.8 - hs, Rr * 2.8], [x, (ys - Rr * 0.8 + hs) / 2, (zt + zf) / 2])\n    k.box(SKID, [0.25, 0.02, Rr * 3.1], [x, hs + 0.01, (zt + zf) / 2])\n  }\n  k.box('nameplate', [0.22, 0.14, 0.006], [xbm, ys, zf + Rr + 0.003])\n  // inlet: boss, unloader / inlet valve with actuator, riser to the inlet flange\n  const iN = p.inletNps\n  const iOD = pipeOD(iN)\n  k.cyl(BLOCK, iOD * 0.7, Rr * 0.4, [L.xi, ys + Rr * 1.05, zt], 'y', 24)\n  const yv = L.blockTop + 0.12\n  k.cyl('valveBody', iOD * 0.85, 0.12, [L.xi, yv, zt], 'y', 28)\n  k.rbox('actuatorYellow', [0.2, 0.16, 0.16], 0.02, [L.xi, yv, zt - iOD * 0.85 - 0.12])\n  k.cyl('actuatorYellow', 0.06, 0.3, [L.xi + 0.18, yv, zt - iOD * 0.85 - 0.12], 'x', 14)\n  neckTo(k, [L.xi, yv + 0.06, zt], [L.xi, L.yIn, zt], [0, 1, 0], iN, p.rating, 'pipePaint')\n  if (p.service === 'air') {\n    // intake filter housing on the riser: body ring, bolted cover band, restriction indicator\n    const fy = Math.min(yv + 0.45, (yv + L.yIn) / 2)\n    annulus(k, 'motorPaint', iOD / 2, iOD * 1.45, 0.42, [L.xi, fy, zt], 'y', 32)\n    for (const dy of [-0.19, 0.19]) annulus(k, 'flangeSteel', iOD * 1.45, iOD * 1.5, 0.03, [L.xi, fy + dy, zt], 'y', 32)\n    k.cyl('actuatorYellow', 0.025, 0.06, [L.xi + iOD * 1.45 + 0.03, fy + 0.1, zt], 'x', 12)\n    k.disc('glass', 0.02, [L.xi + iOD * 1.45 + 0.061, fy + 0.1, zt], 'x', 12)\n  }\n  if (p.enclosure) k.box(SKID, [iOD + 0.12, 0.04, iOD + 0.12], [L.xi, L.Hc + 0.06, zt])\n\n  // horizontal oil separator on saddles: element cover at +X, PSV and minimum pressure valve on top, sight glass\n  const Ls = Math.max(1.4, L.xb1 - L.xm + 0.2)\n  const xs = (L.xm + L.xb1) / 2 - 0.1\n  const hd = Ds / 4\n  const prof: [number, number][] = [[0, -Ls / 2 - hd]]\n  for (let i = 1; i <= 12; i++) {\n    const t = (i / 12) * (Math.PI / 2)\n    prof.push([(Ds / 2) * Math.sin(t), -Ls / 2 - hd * Math.cos(t)])\n  }\n  prof.push([Ds / 2, Ls / 2], [Ds / 2 + 0.06, Ls / 2], [Ds / 2 + 0.06, Ls / 2 + 0.08], [0, Ls / 2 + 0.08])\n  k.lathe(VESSEL, prof, [xs, ysep, zs], 'x', 40)\n  k.boltCircle('boltXylan', 'nut', 16, Ds / 2 + 0.03, [xs + Ls / 2 - 0.04, ysep, zs], 'x', 0.016, 0.16, [0.03, 0.13])\n  k.torus('nut', 0.05, 0.012, [xs + Ls / 2 + 0.1, ysep, zs], 'x', 6, 14)\n  k.at([xs, ysep, zs], undefined, () => {\n    saddle(k, -Ls * 0.32, Ds / 2, -(ysep), { mat: VESSEL })\n    saddle(k, Ls * 0.32, Ds / 2, -(ysep), { mat: VESSEL, sliding: true })\n  })\n  k.cyl('brass', 0.035, 0.02, [xs - Ls * 0.1, ysep - Ds * 0.15, zs + Ds / 2], 'z', 14)\n  k.box('glass', [0.05, Ds * 0.35, 0.012], [xs + Ls * 0.05, ysep - Ds * 0.1, zs + Ds / 2 + 0.006])\n  tagPlate(k, 'OIL SEPARATOR', [xs - Ls * 0.25, ysep + Ds * 0.15, zs + Ds * 0.49], 'z', 0.24)\n  const yST = ysep + Ds / 2\n  k.rod(VESSEL, [xs - Ls * 0.3, yST - 0.02, zs], [xs - Ls * 0.3, yST + 0.12, zs], pipeOD(1) / 2, 10)\n  k.cyl('actuatorRed', 0.04, 0.2, [xs - Ls * 0.3, yST + 0.22, zs], 'y', 14)\n  k.cyl('actuatorRed', 0.025, 0.12, [xs - Ls * 0.3, yST + 0.38, zs], 'y', 10)\n  gauge(k, [xs, yST + 0.2, zs + 0.06], 'z', 0.1, 'bar', 16)\n  k.rod('stainless', [xs, yST - 0.02, zs], [xs, yST + 0.2, zs], 0.008, 8)\n  // discharge: block bottom to the separator inlet\n  const dN = Math.max(p.outletNps, 2)\n  const xd = L.xb1 - Lb * 0.25\n  k.cyl(BLOCK, pipeOD(dN) * 0.7, Rr * 0.3, [xd, ys - Rr * 1.05, zt], 'y', 20)\n  lineRun(k, [[xd, ys - Rr * 1.1, zt], [xd, ysep, zt], [xd, ysep, zs + Ds / 2 - 0.02]], dN, 'pipePaint')\n  // minimum pressure valve and line to the aftercooler\n  const xmpv = xs + Ls * 0.3\n  k.rod(VESSEL, [xmpv, yST - 0.02, zs], [xmpv, yST + 0.1, zs], pipeOD(dN) / 2, 14)\n  k.rbox('valveBody', [0.16, 0.14, 0.16], 0.02, [xmpv, yST + 0.17, zs])\n  const yAir = Math.min(hs + L.Hcool, yST + 0.42)\n  lineRun(k, [[xmpv, yST + 0.24, zs], [xmpv, yAir, zs], [L.xcool - 0.2, yAir, zs]], dN, 'pipePaint')\n\n  // combined cooler at the +X end: oil section below, aftercooler above, fan shroud on the -X side\n  const Wc = SW - 0.3\n  const yc0 = hs + 0.1\n  const Hc = L.Hcool\n  const xcl = L.xcool\n  k.box('aluminium', [0.22, Hc, Wc - 0.12], [xcl, yc0 + Hc / 2, 0])\n  // finned core face: fine fin pitch via the louvre texture tiled every ~25 mm\n  const core = new THREE.PlaneGeometry(Wc - 0.16, Hc - 0.04)\n  uvScale(core, 1, (Hc - 0.04) / 0.025)\n  k.add(louvreMaterial('#9aa1a6'), core, mat4([xcl + 0.112, yc0 + Hc / 2, 0], faceQ('x')))\n  for (const s of [-1, 1]) k.box(SKID, [0.26, Hc + 0.06, 0.06], [xcl, yc0 + Hc / 2, s * (Wc / 2 - 0.03)])\n  k.box(SKID, [0.26, 0.05, Wc], [xcl, yc0 + Hc * 0.42, 0])\n  k.box(SKID, [0.26, 0.06, Wc], [xcl, yc0 + Hc + 0.03, 0])\n  for (const s of [-1, 1]) k.box(SKID, [0.3, yc0 - hs, 0.12], [xcl, (yc0 + hs) / 2, s * (Wc / 2 - 0.1)])\n  const fr = Math.min(Hc, Wc) * 0.42\n  const yfan = yc0 + Hc / 2\n  k.lathe(SKID, [[fr + 0.05, 0], [fr + 0.02, 0.25], [fr * 1.25, 0.32], [fr * 1.25, 0.34], [fr + 0.01, 0.34]], [xcl - 0.45, yfan, 0], 'x', 40)\n  k.box(SKID, [0.02, Hc * 0.98, Wc * 0.9], [xcl - 0.12, yfan, 0])\n  k.disc(fanGuardMaterial(), fr + 0.03, [xcl - 0.46, yfan, 0], '-x', 32)\n  k.cyl('motorPaint', fr * 0.22, 0.28, [xcl - 0.62, yfan, 0], 'x', 20)\n  for (const s of [-1, 1]) k.rod(SKID, [xcl - 0.52, yfan, 0], [xcl - 0.46, yfan + s * fr, 0], 0.012, 6)\n  rotors.push(fanRotor('aluminium', fr * 0.94, [xcl - 0.33, yfan, 0], 'x', 600, { blades: 7, chord: fr * 0.3, pitch: 0.35, hub: fr * 0.2, hubLen: 0.08 }))\n  // aftercooler outlet header to the outlet flange (-Z), moisture separator drain\n  const oN = p.outletNps\n  k.cyl(SKID, pipeOD(oN) * 0.8, Hc * 0.5, [xcl - 0.16, yc0 + Hc * 0.72, -Wc / 2 + 0.12], 'y', 16)\n  runToFlange(k, [[xcl - 0.16, yc0 + Hc * 0.5, -Wc / 2 + 0.12], [xcl - 0.16, L.yOut, -Wc / 2 + 0.12], [L.xo, L.yOut, -Wc / 2 + 0.12]], [L.xo, L.yOut, -SW / 2 - 0.2], [0, 0, -1], oN, p.rating)\n  void elbow90\n  // oil circuit: separator sump -> thermostatic valve -> oil cooler -> duplex filters -> injection\n  const yOil = hs + 0.18\n  k.rbox('valveBody', [0.16, 0.16, 0.16], 0.02, [xs + Ls * 0.42, yOil + 0.02, zs + Ds * 0.3])\n  lineRun(k, [[xs + Ls * 0.42, ysep - Ds / 2 + 0.02, zs], [xs + Ls * 0.42, yOil, zs], [xs + Ls * 0.42, yOil, zs + Ds * 0.3], [xcl - 0.16, yOil, zs + Ds * 0.3], [xcl - 0.16, yc0 + 0.15, zs + Ds * 0.3]], 1.5, OIL)\n  const xfl = L.xb1 + 0.3\n  for (const s of [-1, 1]) {\n    k.cyl(OIL, 0.07, 0.32, [xfl, hs + 0.55, zt + s * 0.1], 'y', 18)\n    k.cyl(OIL, 0.08, 0.04, [xfl, hs + 0.73, zt + s * 0.1], 'y', 18)\n  }\n  k.rbox(OIL, [0.12, 0.08, 0.36], 0.01, [xfl, hs + 0.79, zt])\n  k.rod(SKID, [xfl, hs, zt], [xfl, hs + 0.39, zt], 0.03, 8)\n  lineRun(k, [[xcl - 0.16, yc0 + 0.3, zt], [xfl + 0.15, yc0 + 0.3, zt], [xfl + 0.15, hs + 0.79, zt], [xfl + 0.06, hs + 0.79, zt]], 1.5, OIL)\n  lineRun(k, [[xfl, hs + 0.83, zt], [xfl, ys + Rr * 1.3, zt], [L.xb1 - Lb * 0.4, ys + Rr * 1.3, zt], [L.xb1 - Lb * 0.4, ys + Rr * 0.95, zt]], 1, OIL)\n  gauge(k, [xfl, hs + 1.0, zt + 0.06], 'z', 0.09, 'bar', 16)\n\n  if (!p.enclosure) {\n    // free-standing control panel with HMI at the -X front corner\n    const px = L.xmL + 0.35\n    const pz = SW / 2 - 0.3\n    k.box('stainless', [0.6, 1.3, 0.3], [px, hs + 0.2 + 0.65, pz])\n    k.box(SKID, [0.5, 0.2, 0.25], [px, hs + 0.1, pz])\n    k.plane(lcdMaterial(), 0.3, 0.2, [px, hs + 1.15, pz + 0.151])\n    k.cyl('actuatorRed', 0.025, 0.03, [px + 0.2, hs + 0.9, pz + 0.16], 'z', 16)\n    k.plane(signMaterial('ler', p.tag), 0.5, 0.1, [px, hs + 1.42, pz + 0.151])\n    return { built: k.build(), rotors }\n  }\n\n  // acoustic enclosure: framed panels with seams and handles, intake louvres, cooler discharge grille, HMI door\n  const H = L.Hc\n  const ex0 = -SL / 2 + 0.04\n  const ex1 = SL / 2 - 0.04\n  const ez = SW / 2 - 0.04\n  const t = 0.05\n  const PANEL: MatKey = 'cladding'\n  const TRIM: MatKey = 'motorPaint'\n  const wallH = H - hs\n  const yw = hs + wallH / 2\n  const EL = ex1 - ex0\n  const zm = 0\n  const iz = (z: number, w: number, x: number) => k.box(PANEL, [w, wallH, t], [x, yw, z])\n  // -Z wall with the outlet penetration\n  iz(-ez + t / 2, EL, (ex0 + ex1) / 2)\n  k.box(TRIM, [0.3, 0.3, 0.02], [L.xo, L.yOut, -ez - 0.01])\n  // +Z front: panels, HMI, intake louvres\n  // front wall: door pair in the middle (swung open on request), HMI panel on the solid part\n  const d0 = ex0 + EL * 0.38\n  const d1 = ex0 + EL * 0.86\n  iz(ez - t / 2, d0 - ex0, (ex0 + d0) / 2)\n  iz(ez - t / 2, ex1 - d1, (d1 + ex1) / 2)\n  const dlw = (d1 - d0) / 2\n  const op = p.doorsOpen ? 1.75 : 0\n  for (const [hx, sgn] of [[d0, 1], [d1, -1]] as const) {\n    const m = new THREE.Matrix4().makeTranslation(hx, 0, ez).multiply(new THREE.Matrix4().makeRotationY(-sgn * op)).multiply(new THREE.Matrix4().makeTranslation(-hx, 0, -ez))\n    k.with(m, () => {\n      k.box(PANEL, [dlw - 0.01, wallH - 0.02, t], [hx + sgn * dlw / 2, yw, ez - t / 2])\n      k.box(TRIM, [0.04, wallH - 0.02, 0.015], [hx + sgn * (dlw - 0.02), yw, ez + 0.006])\n      for (const yy of [hs + wallH * 0.3, hs + wallH * 0.75]) k.box('stainless', [0.12, 0.025, 0.03], [hx + sgn * (dlw - 0.12), yy, ez + 0.02])\n    })\n    for (const yy of [hs + 0.3, hs + wallH - 0.3]) k.cyl(TRIM, 0.015, 0.1, [hx, yy, ez + 0.01], 'y', 8)\n  }\n  k.box(PANEL, [t, wallH, 2 * ez], [ex0 + t / 2, yw, zm])\n  k.box(PANEL, [t, wallH * 0.12, 2 * ez], [ex1 - t / 2, H - wallH * 0.06, zm])\n  k.box(PANEL, [t, 0.08, 2 * ez], [ex1 - t / 2, hs + 0.04, zm])\n  k.box(PANEL, [EL + 0.04, 0.06, 2 * ez + 0.04], [(ex0 + ex1) / 2, H + 0.03, zm])\n  const np = Math.max(3, Math.round(EL / 0.9))\n  for (let i = 0; i <= np; i++) {\n    const x = ex0 + (i * EL) / np\n    for (const s of [-1, 1]) if (s < 0 || x < d0 - 0.03 || x > d1 + 0.03) k.box(TRIM, [0.04, wallH, 0.015], [x, yw, s * (ez + 0.006)])\n    k.box(TRIM, [0.04, 0.02, 2 * ez], [x, H + 0.07, zm])\n    if (i < np) {\n      const xc = x + EL / np / 2\n      for (const s of [-1, 1]) for (const yy of [hs + wallH * 0.3, hs + wallH * 0.75]) if (s < 0 || xc + EL / np * 0.32 < d0 || xc + EL / np * 0.32 > d1) k.box('stainless', [0.12, 0.025, 0.03], [xc + EL / np * 0.32, yy, s * (ez + 0.02)])\n    }\n  }\n  for (const s of [-1, 1]) {\n    k.box(TRIM, [EL + 0.04, 0.08, 0.03], [(ex0 + ex1) / 2, hs + 0.04, s * (ez + 0.01)])\n    k.box(TRIM, [EL + 0.04, 0.06, 0.03], [(ex0 + ex1) / 2, H - 0.03, s * (ez + 0.01)])\n  }\n  // intake louvres low on both long sides near the motor\n  const lw = Math.min(1.2, EL * 0.3)\n  k.at([ex0 + 0.25 + lw / 2, hs, ez], faceQ('z'), () => louvre(k, lw, wallH * 0.3, 0, wallH * 0.25, TRIM))\n  k.at([ex0 + 0.25 + lw / 2, hs, -ez], faceQ('-z'), () => louvre(k, lw, wallH * 0.3, 0, wallH * 0.25, TRIM))\n  // cooling-air discharge grille over the cooler face (+X end)\n  k.at([ex1, hs, 0], faceQ('x'), () => louvre(k, 2 * ez - 0.2, wallH * 0.76 - 0.12, 0, wallH * 0.12 + 0.06 + (wallH * 0.76 - 0.12) / 2 + 0.02, TRIM))\n  // HMI and emergency stop on the front, warning and tag signs\n  const hx = ex0 + 0.6\n  k.rbox('stainless', [0.5, 0.42, 0.06], 0.02, [hx, hs + 1.2, ez + 0.03])\n  k.plane(lcdMaterial(), 0.32, 0.22, [hx, hs + 1.24, ez + 0.061])\n  k.cyl('actuatorYellow', 0.04, 0.01, [hx + 0.17, hs + 1.08, ez + 0.065], 'z', 18)\n  k.cyl('actuatorRed', 0.025, 0.035, [hx + 0.17, hs + 1.08, ez + 0.08], 'z', 16)\n  k.plane(signMaterial('ler', `${p.tag}  ${p.service === 'air' ? 'INSTRUMENT AIR' : 'GAS BOOST'} COMPRESSOR`), Math.min(1.4, d0 - ex0 - 0.2), 0.2, [(ex0 + d0) / 2, H - 0.3, ez + 0.012])\n  tagPlate(k, p.tag, [hx, hs + 0.85, ez + 0.012], 'z', 0.2)\n  for (const sx of [-1, 1]) for (const sz of [-1, 1]) k.torus('nut', 0.05, 0.014, [sx * (EL / 2 - 0.2), H + 0.12, sz * (ez - 0.2)], 'z', 6, 14)\n  void ([] as V3[])\n  void flange\n  return { built: k.build(), rotors }\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": "ScrewCompressor",
    "anchor": "base",
    "standards": [
      "API 619",
      "ISO 1217",
      "ISO 8573-1",
      "IEC 60034",
      "ASME VIII (separator)",
      "ASME B16.5"
    ]
  }
}