{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "gas-turbine-generator",
  "type": "registry:component",
  "title": "Gas turbine generator (API 616)",
  "description": "Packaged industrial / aeroderivative gas turbine generator on a base frame with drip pan: acoustic enclosure with opening doors, louvres, roof ventilation fans, flame / heat / gas detectors, call points and an internal maintenance monorail. Roof inlet duct with a multi-stage filter house: weather hoods over a bird screen, vane separator, coalescer / pre-filter cells and final cartridges, anti-icing manifold, access platform. Exhaust collector, expansion joint, silencer and stack with sampling platform. Inside: exciter, generator with CACW cooler and terminal box, reduction gearbox, guarded couplings, inlet plenum with bell-mouth and the GT core with VSV rings and dual-fuel manifolds. Fuel gas skid, diesel duplex filter, lube console with fin-fan cooler and roof vent separator with flame trap, CO₂ cabinet and neutral earthing resistor. Size scales with `mw`.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "rotating/gas-turbine-generator.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/rotating/gas-turbine-generator.tsx",
      "content": "// Gas turbine generator package (API 616, ISO 3977): acoustic enclosure, roof filter house, exhaust silencer and stack, gearbox, generator, lube console, dual-fuel skid.\nimport { useEffect, useMemo } from 'react'\nimport { Kit, type V3 } from '../lib/kit'\nimport { KitMesh, type GroupProps } from '../lib/mesh'\nimport { ladder, tagPlate } from '../lib/parts'\nimport { port, type Port } from '../lib/ports'\nimport { Rotors, guardedCoupling, type RotorSpec } from '../lib/rotors'\nimport { signMaterial } from '../lib/textures'\nimport { pipeOD, type Rating } from '../lib/standards'\nimport { SKID, baseFrame, clamp, faceQ, gasDetector, gearbox, lineRun, louvre, lubeConsole, roofFan } from './_compressors-kit'\nimport { TRIM, co2Cabinet, enclosure, exhaustCollector, exhaustStack, filterHouse, fuelTrain, gtCore, gtMats, inletPlenum } from './_compressors-gt'\n\nexport interface GasTurbineGeneratorProps {\n  /** ISO site rating, MW */\n  mw?: number\n  fuel?: 'gas' | 'dual'\n  fuelNps?: number\n  rating?: Rating\n  /** stack height above the enclosure roof, m */\n  stackHeight?: number\n  /** leave out the +Z enclosure wall to show the machinery */\n  cutaway?: boolean\n  /** swing the enclosure doors open */\n  doorsOpen?: boolean\n  tag?: string\n  running?: boolean\n}\n\nexport const defaults = {\n  mw: 25,\n  fuel: 'dual' as 'gas' | 'dual',\n  fuelNps: 3,\n  rating: 600 as Rating,\n  stackHeight: 10,\n  cutaway: false,\n  doorsOpen: false,\n  tag: 'GT-8100A',\n  running: true,\n}\n\ntype P = typeof defaults\n\nconst NECK = 0.3\n\nfunction layout(p: P) {\n  const MW = clamp(p.mw, 1, 60)\n  const SL = clamp(8 + 0.3 * MW, 9, 22) // typical package lengths: SGT-400 ~14 m, LM2500 ~18 m\n  const SW = clamp(2.9 + 0.07 * MW, 3.1, 6.2)\n  const hs = clamp(0.4 + SL * 0.012, 0.45, 0.65)\n  const H = clamp(3.3 + 0.075 * MW, 3.6, 7.5)\n  const m = clamp(SW * 0.17, 0.55, 1.0)\n  const yFuel = 0.9\n  return { MW, SL, SW, hs, H, m, xGas: SL * 0.15, xLiq: SL * 0.05, yFuel, faceZ: -SW / 2 - NECK }\n}\n\nexport function ports(props: GasTurbineGeneratorProps = {}): Port[] {\n  const p = { ...defaults, ...props }\n  const L = layout(p)\n  const out = [port('fuel-gas', 'Fuel gas', [L.xGas, L.yFuel, L.faceZ], [0, 0, -1], p.fuelNps, p.rating)]\n  if (p.fuel === 'dual') out.push(port('diesel', 'Liquid fuel (diesel)', [L.xLiq, L.yFuel, L.faceZ], [0, 0, -1], 2, 150, 'utility'))\n  return out\n}\n\nexport function GasTurbineGenerator(props: GasTurbineGeneratorProps & GroupProps) {\n  const { mw, fuel, fuelNps, rating, stackHeight, cutaway, doorsOpen, tag, running, ...group } = { ...defaults, ...props }\n  const { built, rotors } = useMemo(\n    () => build({ mw, fuel, fuelNps, rating, stackHeight, cutaway, doorsOpen, tag, running }),\n    [mw, fuel, fuelNps, rating, stackHeight, cutaway, doorsOpen, tag, running],\n  )\n  useEffect(() => () => [built, ...rotors.map((r) => r.built)].forEach((b) => b.geometry.dispose()), [built, rotors])\n  return (\n    <group {...group}>\n      <KitMesh data={built} />\n      <Rotors rotors={rotors} running={running} />\n    </group>\n  )\n}\n\nexport default GasTurbineGenerator\n\nfunction build(p: P) {\n  const k = new Kit()\n  const M = gtMats()\n  const { MW, SL, SW, hs, H, m } = layout(p)\n  const t = 0.08\n  const z0 = -SW / 2 + m\n  const z1 = SW / 2 - m\n  const encW = z1 - z0\n  const hw = encW / 2 - t\n  const ex0 = -SL / 2 + 0.2\n  const ex1 = SL / 2 - 0.2\n\n  // machine sizes: core reference radius, stack, collector\n  const R = Math.min(0.25 + 0.11 * Math.sqrt(MW), (hw - 0.2) / 1.35, (H - hs - 0.6) / 2.7)\n  const ys = hs + 0.2 + 1.3 * R\n  const Rs = clamp(0.1 + 0.2 * Math.sqrt(MW), 0.45, Math.min(1.5, (SW / 2 - 0.05) / 1.25))\n  const Rsil = Math.min(Rs * 1.28, SW / 2 - 0.05, 1.95)\n  const xst = SL / 2 - Math.max(2.0, Rsil + 0.45)\n  const cW = 1.3 * R\n  const Rd = Math.min(Rs, cW, hw - 0.12)\n  const cHL = Rd + 0.2\n  const xc1 = xst - cHL\n\n  // stations along X: exciter, generator, gearbox, plenum, core, collector\n  const xa = ex0 + t + 0.15\n  const A = xc1 - xa\n  const sGen1 = xa + 0.34 * A\n  const sGb0 = sGen1 + 0.05 * A\n  const sGb1 = sGb0 + 0.11 * A\n  const sPl0 = sGb1 + 0.04 * A\n  const sPl1 = sPl0 + 0.11 * A\n  const xc0 = sPl1\n  const Lc = xc1 - xc0\n  const xp = (sPl0 + sPl1) / 2\n  const xcomb = xc0 + 0.48 * Lc\n  const gx = (sGb0 + sGb1) / 2\n\n  baseFrame(k, SL, SW, hs)\n\n  // door positions avoid the fuel skid, CO₂ cabinet, lube console and ladder\n  const L0 = layout(p)\n  const x1 = L0.xGas\n  const x2 = p.fuel === 'dual' ? L0.xLiq : x1\n  const gtL = clamp(SL * 0.14, 1.4, 2.2)\n  const xpen = Math.max(x1, x2) + gtL\n  const lcL = clamp(SL * 0.2, 1.8, 3.2)\n  const xl = clamp(gx, -SL / 2 + 0.4 + lcL / 2, SL / 2 - 0.4 - lcL / 2)\n  const cabL = clamp(SL * 0.09, 1.0, 1.6)\n  const cab0 = -SL / 2 + 0.3\n  const xLad = ex0 + 0.55\n  const xNer = cab0 + cabL + 0.65\n  const blockedNeg: [number, number][] = [[Math.min(x1, x2) - 0.5, xpen + 0.5], [cab0 - 0.2, xNer + 0.6]]\n  const blockedPos: [number, number][] = [[xl - lcL / 2 - 0.3, xl + lcL / 2 + 0.3], [xLad - 0.5, xLad + 0.5]]\n  const freeX = (want: number, w: number, blocked: [number, number][]) => {\n    const lo = ex0 + w / 2 + 0.25\n    const hi = ex1 - w / 2 - 0.25\n    for (let d = 0; d < SL; d += 0.1)\n      for (const x of [want + d, want - d]) {\n        if (x < lo || x > hi) continue\n        if (blocked.every(([a, b]) => x + w / 2 < a || x - w / 2 > b)) return x\n      }\n    return null\n  }\n  const pick = (xs: (number | null)[]) => {\n    const out: number[] = []\n    for (const x of xs) if (x !== null && out.every((q) => Math.abs(q - x) > 1.2)) out.push(x)\n    return out\n  }\n  const gMid = (xa + sGen1) / 2\n  const doorsPz = pick([freeX(gMid, 0.9, blockedPos), freeX(xcomb + 0.5, 0.9, blockedPos)])\n  const doorsNz = pick([freeX(gMid, 0.9, blockedNeg), freeX(xst, 0.9, blockedNeg)])\n  const endW = encW - 1.45\n  const wEndN = endW >= 1.3 ? Math.min(1.8, endW) : 0.85\n  const cEndP = encW / 2 - 0.65\n  const { dH, wallH } = enclosure(k, { ex0, ex1, z0, z1, hs, H, t, doorsPz, doorsNz, endNx: wEndN, endPx: [cEndP, 0.85], cutaway: p.cutaway, open: p.doorsOpen, monorail: [sPl1 + 0.15, xst - Rd - 0.2] })\n  k.at([ex0, hs, 0], faceQ('-x'), () => louvre(k, Math.min(1.2, encW * 0.35), 0.5, 0, Math.min(wallH - 0.45, dH + 0.55)))\n  k.at([ex1, hs, 0], faceQ('x'), () => {\n    const lh = Math.min(0.9, wallH * 0.25)\n    const lw = Math.min(1.6, encW * 0.42)\n    louvre(k, lw, lh, -encW / 2 + lw / 2 + 0.25, wallH - lh / 2 - 0.3)\n  })\n  ladder(k, [xLad, hs, z1 + 0.2], H, [0, 0, 1], { cage: m >= 0.7 })\n  gasDetector(k, [ex1 + 0.1, H - 0.9, encW / 2 - 0.25])\n  const signX = clamp(xcomb - 0.6, ex0 + 1.4, ex1 - 1.4)\n  if (!p.cutaway) {\n    k.plane(signMaterial('ler', `${p.tag}  GAS TURBINE GENERATOR  ${Math.round(MW)} MW`), 2.4, 0.6, [signX, H - 0.6, z1 + 0.02])\n    tagPlate(k, p.tag, [signX + 1.45, H - 0.6, z1 + 0.02], 'z', 0.3)\n  } else tagPlate(k, p.tag, [signX, H - 0.6, z0 - 0.02], '-z', 0.3)\n\n  // generator with brushless exciter, CACW cooler, line-side terminal box\n  const gR = sGen1 - xa\n  const eL = clamp(0.14 * gR, 0.35, 0.8)\n  const gF0 = xa + eL + 0.3\n  const gF1 = sGen1 - 0.45\n  const gFL = gF1 - gF0\n  const gfx = (gF0 + gF1) / 2\n  const gBH = clamp(1.0 + 0.035 * MW, 1.0, H - hs - 1.2)\n  const gWd = Math.min(2 * hw - 0.7, 1.2 + 0.035 * MW)\n  const gCH = clamp(H - hs - 0.45 - 0.12 - gBH, 0, 0.7)\n  const yg = hs + 0.12 + gBH * 0.5\n  const gTop = hs + 0.12 + gBH\n  k.box(SKID, [gFL + eL + 0.9, 0.12, gWd + 0.25], [(xa + sGen1) / 2, hs + 0.06, 0])\n  k.rbox(M.GEN, [gFL, gBH, gWd], 0.06, [gfx, hs + 0.12 + gBH / 2, 0])\n  const nr = Math.max(3, Math.round(gFL / 0.5))\n  for (let i = 0; i <= nr; i++)\n    for (const s of [-1, 1]) k.box(M.GEN, [0.05, gBH - 0.12, 0.06], [gF0 + 0.08 + (i * (gFL - 0.16)) / nr, hs + 0.12 + gBH / 2, s * (gWd / 2 + 0.02)])\n  for (const s of [-1, 1]) k.box(M.GEN, [gFL + 0.06, 0.08, 0.1], [gfx, gTop - 0.04, s * (gWd / 2 + 0.02)])\n  if (gCH > 0.25) {\n    k.rbox(M.GEN, [gFL * 0.86, gCH, gWd * 0.86], 0.05, [gfx, gTop + gCH / 2, 0])\n    for (const s of [-1, 1]) {\n      const cx = gfx + s * gFL * 0.3\n      k.cyl('flangeSteel', 0.07, 0.25, [cx, gTop + gCH * 0.5, -gWd * 0.43 - 0.1], 'z', 16)\n      lineRun(k, [[cx, gTop + gCH * 0.5, -gWd * 0.43 - 0.2], [cx, gTop + gCH * 0.5, -hw + 0.12], [cx, hs + 0.2, -hw + 0.12]], 3, 'stainless')\n    }\n  }\n  for (const sx of [gF0 + 0.25, gF1 - 0.25]) for (const s of [-1, 1]) k.torus('nut', 0.06, 0.018, [sx, gTop + 0.04, s * gWd * 0.42], 'x', 6, 14)\n  for (const xb of [gF0 - 0.17, gF1 + 0.17]) {\n    k.rbox(M.GEN, [0.34, yg - hs + 0.15, Math.min(0.85, gWd * 0.6)], 0.04, [xb, (yg + 0.15 + hs + 0.12) / 2, 0])\n    k.cyl(M.GEN, 0.24, 0.38, [xb, yg, 0], 'x', 28)\n    k.cyl('brass', 0.045, 0.02, [xb, yg - 0.12, 0.2], 'z', 12)\n  }\n  k.cyl('stainless', 0.1, sGen1 - gF1 - 0.3, [(gF1 + 0.3 + sGen1) / 2, yg, 0], 'x', 20)\n  const eR = Math.min(gBH, gWd) * 0.28\n  k.cyl(M.GEN, eR, eL, [xa + eL / 2 + 0.05, yg, 0], 'x', 32)\n  k.dome(M.GEN, eR, [xa + 0.05, yg, 0], '-x', Math.PI / 2, 0.35, 28)\n  for (let i = 0; i < 6; i++) k.cyl(M.GEN, eR * 1.04, 0.025, [xa + 0.1 + (i * (eL - 0.1)) / 5, yg, 0], 'x', 32)\n  const tbD = clamp(hw - gWd / 2 - 0.05, 0.15, 0.45)\n  const tbL = Math.min(1.2, gFL * 0.35)\n  const tbH = gBH * 0.55\n  const tbx = gfx + gFL * 0.15\n  const tbY = hs + 0.25 + tbH / 2\n  k.rbox(M.GEN, [tbL, tbH, tbD], 0.03, [tbx, tbY, gWd / 2 + tbD / 2])\n  k.box(M.GEN, [tbL - 0.08, tbH - 0.08, 0.02], [tbx, tbY, gWd / 2 + tbD + 0.01])\n  k.around(8, Math.min(tbL, tbH) * 0.42, [tbx, tbY, gWd / 2 + tbD + 0.025], 'z', (q) => k.hex('nut', 0.025, 0.012, q, 'z'), Math.PI / 8)\n  k.plane(signMaterial('hv'), Math.min(0.4, tbL * 0.5), Math.min(0.2, tbL * 0.25), [tbx, tbY + tbH * 0.2, gWd / 2 + tbD + 0.021])\n  for (let i = 0; i < 3; i++) {\n    const cx = tbx - tbL * 0.3 + i * tbL * 0.3\n    const yb = tbY - tbH / 2\n    k.hex('brass', 0.1, 0.06, [cx, yb - 0.03, gWd / 2 + tbD / 2], 'y')\n    k.spline('cable', [[cx, yb - 0.05, gWd / 2 + tbD / 2], [cx, hs + 0.25, gWd / 2 + tbD / 2 + 0.03], [cx + 0.08, hs + 0.02, gWd / 2 + tbD / 2 - 0.05]], 0.04, 8, 14)\n  }\n  k.rbox(M.GEN, [0.4, 0.4, 0.2], 0.02, [gfx - gFL * 0.25, hs + 0.55, -gWd / 2 - 0.1])\n  k.box('nameplate', [0.45, 0.28, 0.01], [gfx + gFL * 0.1, gTop - 0.35, -gWd / 2 - 0.006])\n\n  // reduction gearbox and couplings (generator side slow, turbine side fast)\n  gearbox(k, { x0: sGb0, x1: sGb1, ySlow: yg, yFast: ys, hs, W: Math.min(2 * hw - 0.6, 0.8 + 0.6 * R) })\n  const rotors: RotorSpec[] = [\n    guardedCoupling(k, sGen1 - 0.02, sGb0 - 0.1, yg, 0.3, hs, 'safetyYellow', 135, 0, 0.1),\n    guardedCoupling(k, sGb1 + 0.1, sPl0 - 0.03, ys, 0.27, hs, 'safetyYellow', 215, 0, 0.08),\n  ]\n\n  // air inlet plenum, gas turbine core, exhaust collector\n  const plW = 2 * (hw - 0.04)\n  inletPlenum(k, { a: sPl0, b: sPl1, ys, R, hs, top: H - 0.1, W: plW, M })\n  const core = gtCore(k, { xc0, Lc, ys, R, hs, MW, xBleed: xc1 + 0.05, M })\n  exhaustCollector(k, { xst, ys, R, cHL, cW, Rd, hs, H, rExit: core.prof(0.99), M })\n\n  // internal services: lube oil header with drops, CO₂ header and nozzles, fuel lines to the manifolds\n  const zh = z1 - t - 0.12\n  const lyP = hs + 0.18\n  lineRun(k, [[xl, hs + 0.45, z1], [xl, hs + 0.45, zh], [xl, lyP, zh], [xa + 0.3, lyP, zh]], 1.5, 'stainless')\n  lineRun(k, [[xl, lyP, zh], [core.xu(0.85), lyP, zh]], 1.5, 'stainless')\n  for (const [xb, yb, zb] of [[gF0 - 0.17, yg - 0.1, 0.25], [gF1 + 0.17, yg - 0.1, 0.25], [core.xu(0.06) + 0.12, ys - 0.55 * R, 0.4 * R], [core.xu(0.8) + 0.14, ys - 0.62 * R, 0.5 * R]] as V3[])\n    if (zh - zb > 0.08) lineRun(k, [[xb, lyP, zh], [xb, yb, zh], [xb, yb, zb]], 1, 'stainless')\n  const yco = H - 0.35\n  const zco = -hw * 0.35\n  const cabX = cab0 + cabL / 2\n  lineRun(k, [[cabX, yco, z0], [cabX, yco, zco], [xc1 + cHL * 0.4, yco, zco]], 1.5, 'stainless')\n  for (let x = cabX + 1.0; x < xc1 + cHL * 0.4; x += 2.4) {\n    k.rod('stainless', [x, yco, zco], [x, yco - 0.18, zco], 0.02, 8)\n    k.cyl('brass', 0.035, 0.06, [x, yco - 0.21, zco], 'y', 10)\n  }\n  gasDetector(k, [xcomb, H - 0.25, hw * 0.6])\n  gasDetector(k, [xst - cHL - 0.25, H - 0.25, -hw * 0.6])\n  const zf = -core.rr - clamp((hw - core.rr) * 0.5, 0.06, 0.35)\n  const zA = (-SW / 2 + z0) / 2\n  const { yL } = fuelTrain(k, { xGas: x1, xLiq: p.fuel === 'dual' ? x2 : null, y: L0.yFuel, faceZ: L0.faceZ, gasNps: p.fuelNps, rating: p.rating, xpen, z0, zA, m, hs })\n  lineRun(k, [[xpen, L0.yFuel, z0], [xpen, L0.yFuel, zf], [core.xcomb, L0.yFuel, zf], [core.xcomb, ys, zf], [core.xcomb, ys, -core.rr]], Math.min(p.fuelNps, 2), 'stainless')\n  if (p.fuel === 'dual') lineRun(k, [[xpen, yL, z0], [xpen, yL, zf], [core.xL, yL, zf], [core.xL, ys, zf], [core.xL, ys, -core.rr + 0.02]], 1, 'stainless')\n  co2Cabinet(k, { x0: cab0, L: cabL, z0, m, hs, H, yHdr: yco })\n  // neutral earthing resistor on the -Z apron, cable from the generator neutral box through the wall\n  const nD = Math.min(0.6, m - 0.15)\n  const nz = z0 - nD / 2 - 0.05\n  for (const sx of [-1, 1]) for (const sz of [-1, 1]) k.box(SKID, [0.06, 0.35, 0.06], [xNer + sx * 0.35, hs + 0.175, nz + sz * (nD / 2 - 0.05)])\n  k.box('galvanised', [0.85, 1.0, nD], [xNer, hs + 0.85, nz])\n  k.box('galvanised', [0.95, 0.04, nD + 0.1], [xNer, hs + 1.37, nz])\n  for (const sx of [-1, 1]) k.at([xNer + sx * 0.43, hs + 0.35, nz], faceQ(sx > 0 ? 'x' : '-x'), () => louvre(k, nD - 0.15, 0.8, 0, 0.5, 'galvanised'))\n  k.at([xNer, hs + 0.35, nz - nD / 2], faceQ('-z'), () => louvre(k, 0.6, 0.6, 0, 0.5, 'galvanised'))\n  k.plane(signMaterial('hv'), 0.3, 0.15, [xNer, hs + 1.15, nz - nD / 2 - 0.13])\n  tagPlate(k, 'NER', [xNer + 0.25, hs + 1.15, nz - nD / 2 - 0.13], '-z', 0.14)\n  k.spline('cable', [[xNer, hs + 1.39, nz], [xNer, hs + 1.6, nz + 0.1], [xNer, hs + 1.6, z0 - 0.02]], 0.03, 8, 12)\n  k.box(TRIM, [0.2, 0.2, 0.02], [xNer, hs + 1.6, z0 - 0.01])\n  // lube tank vent: run along the +Z wall to the generator end, up to an oil-mist separator with flame trap on the roof,\n  // discharging 2.5 m above the roof, well away from the combustion-air hoods\n  const vx0 = xl - lcL / 2 + 0.1\n  const vx = ex0 + 1.4\n  const vz = z1 - 0.45\n  lineRun(k, [[vx0, hs + 0.75, z1 + 0.12], [vx0, H - 0.45, z1 + 0.12], [vx + 0.35, H - 0.45, z1 + 0.12], [vx + 0.35, H + 0.25, z1 + 0.12], [vx + 0.35, H + 0.25, vz], [vx + 0.14, H + 0.25, vz]], 2, 'stainless')\n  for (let x = vx + 0.9; x < vx0 - 0.3; x += 2.0) k.box(SKID, [0.05, 0.05, 0.1], [x, H - 0.45 - pipeOD(2) / 2 - 0.025, z1 + 0.06])\n  k.cyl('stainless', 0.13, 0.55, [vx, H + 0.4, vz], 'y', 20)\n  k.dome('stainless', 0.13, [vx, H + 0.675, vz], 'y', Math.PI / 2, 0.4, 16)\n  for (const sx of [-1, 1]) k.box(SKID, [0.05, 0.12, 0.3], [vx + sx * 0.1, H + 0.06, vz])\n  k.box(SKID, [0.06, 0.25, 0.06], [vx + 0.35, H + 0.125, vz])\n  k.rod('stainless', [vx, H + 0.72, vz], [vx, H + 2.5, vz], pipeOD(2) / 2, 12)\n  k.cyl('flangeSteel', 0.08, 0.16, [vx, H + 2.4, vz], 'y', 16)\n  k.lathe('stainless', [[0.035, 0], [0.035, 0.12], [0.08, 0.14], [0.08, 0.16], [0.03, 0.16]], [vx, H + 2.5, vz], 'y', 16)\n  for (const sx of [-1, 1]) k.rod('structSteel', [vx + sx * 0.3, H, vz], [vx, H + 1.6, vz], 0.015, 6)\n\n  // roof: inlet duct and filter house over the plenum, ventilation fans, exhaust stack\n  const plL = sPl1 - sPl0\n  const dHt = clamp(0.9 + 0.02 * MW, 1.0, 1.6)\n  const FLmax = 2 * Math.min(xp - ex0, xst - Rsil - 1.3 - xp)\n  const FL = Math.max(Math.min(clamp(2.0 + 0.07 * MW, 2.4, 5.0), FLmax), plL + 0.2)\n  const FW = Math.min(SW - 0.65, encW + 0.9)\n  const FH = clamp(1.8 + 0.05 * MW, 2.0, 3.8)\n  filterHouse(k, { xp, dL: plL, dW: plW, H, dHt, FL, FW, FH, roofHalfW: encW / 2, platform: xst - Rsil - (xp + FL / 2) > 1.05 && xp + FL / 2 + 1.0 < ex1 })\n  const fanR = clamp(0.3 + 0.005 * MW, 0.3, 0.5)\n  const fanXmax = xp - FL / 2 - fanR - 0.3\n  for (const fx of [xa + gR * 0.3, xa + gR * 0.68]) if (fx < fanXmax) rotors.push(roofFan(k, fx, -encW * 0.18, H, fanR, TRIM))\n  exhaustStack(k, { xst, H, Rd, Rs, Rsil, stackH: p.stackHeight, legR: Math.min(Rsil * 0.72, encW / 2 - 0.1), M })\n\n  // +Z apron: lube oil console with fin-fan cooler, supply / return through the wall\n  const lD = m - 0.14\n  const lzc = (z1 + SW / 2) / 2\n  rotors.push(...lubeConsole(k, { x: xl, z: lzc, L: lcL, D: lD, hs, cooler: 'finfan', coolerY: Math.min(hs + 2.0, H + 0.2), front: 1 }))\n  for (const [i, xx] of [xl, xl + 0.25].entries()) {\n    lineRun(k, [[xx, hs + 0.45, lzc - lD / 2 + 0.01], [xx, hs + 0.45, z1 - 0.01]], i === 0 ? 1.5 : 2, 'machineryGreen')\n    if (!p.cutaway) k.box(TRIM, [0.22, 0.22, 0.02], [xx, hs + 0.45, z1 + 0.005])\n  }\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"
    },
    {
      "path": "rotating/_compressors-gt.ts",
      "type": "registry:file",
      "target": "components/deck-twin/rotating/_compressors-gt.ts",
      "content": "// Gas turbine package parts: acoustic enclosure, inlet plenum and filter house, GT core, exhaust collector and stack, fuel train, CO₂ cabinet.\nimport * as THREE from 'three'\nimport { Kit, eul, mat4, uvScale, type MatKey, type V3 } from '../lib/kit'\nimport { flange, gauge, grating, handrail, ladder, member, ringPlatform } from '../lib/parts'\nimport { paint } from '../lib/materials'\nimport { meshGuardMaterial, signMaterial } from '../lib/textures'\nimport { pipeOD, type Rating } from '../lib/standards'\nimport { SKID, OIL, annulus, ballValve, beacon, clamp, controlValve, door, faceQ, gasDetector, hoods, lineRun, runToFlange } from './_compressors-kit'\n\nexport function gtMats() {\n  return {\n    GT: paint('#a4a9ad', { metalness: 0.72, roughness: 0.33, clearcoat: 0 }),\n    HOT: paint('#7b6c5f', { metalness: 0.6, roughness: 0.55, clearcoat: 0 }),\n    GEN: paint('#3d5d79', { metalness: 0.35, roughness: 0.45 }),\n    STACK: paint('#8c9194', { metalness: 0.55, roughness: 0.5, clearcoat: 0 }),\n    SOOT: paint('#262321', { metalness: 0.2, roughness: 0.95, clearcoat: 0 }),\n    SOOT2: paint('#4b4744', { metalness: 0.35, roughness: 0.8, clearcoat: 0 }),\n  }\n}\nexport type GtMats = ReturnType<typeof gtMats>\n\nexport const CLAD: MatKey = 'cladding'\nexport const TRIM: MatKey = 'equipmentPaint'\n\n// ------------------------------------------------------------------ enclosure\n\nexport interface EnclosureOpts {\n  ex0: number\n  ex1: number\n  z0: number\n  z1: number\n  hs: number\n  H: number\n  /** door centres along X on the +Z and -Z walls */\n  doorsPz: number[]\n  doorsNz: number[]\n  /** -X end door width (0 = none); double leaf above 1.3 m */\n  endNx: number\n  /** +X end door centre (wall coordinate = -z) and width, or null */\n  endPx: [number, number] | null\n  /** leave out the +Z wall to show the machinery */\n  cutaway?: boolean\n  /** swing the personnel doors open */\n  open?: boolean\n  /** internal maintenance monorail along X: [x0, x1] or null */\n  monorail?: [number, number] | null\n  t?: number\n}\n\n/** Acoustic enclosure: insulated wall panels cut around doors, cover battens, girts, sills, roof with seam cappings and padeyes. */\nexport function enclosure(k: Kit, o: EnclosureOpts) {\n  const { ex0, ex1, z0, z1, hs, H } = o\n  const t = o.t ?? 0.08\n  const wallH = H - hs - 0.1\n  const yw = hs + wallH / 2\n  const EL = ex1 - ex0\n  const encW = z1 - z0\n  const exm = (ex0 + ex1) / 2\n  const zm = (z0 + z1) / 2\n  const dH = Math.min(2.1, wallH - 0.4)\n  const yHead = hs + dH\n  const holes = {\n    pz: o.doorsPz.map((x) => [x, 0.9] as [number, number]),\n    nz: o.doorsNz.map((x) => [x, 0.9] as [number, number]),\n    nx: o.endNx > 0 ? ([[zm, o.endNx]] as [number, number][]) : [],\n    px: o.endPx ? ([[zm - o.endPx[0], o.endPx[1]]] as [number, number][]) : [],\n  }\n  const solid = (a0: number, a1: number, hl: [number, number][], pad = 0) => {\n    const out: [number, number][] = []\n    let a = a0\n    for (const [c, w] of [...hl].sort((p, q) => p[0] - q[0])) {\n      if (c - w / 2 - pad > a) out.push([a, c - w / 2 - pad])\n      a = Math.max(a, c + w / 2 + pad)\n    }\n    if (a1 > a) out.push([a, a1])\n    return out\n  }\n  const inHole = (v: number, hl: [number, number][], pad: number) => hl.some(([c, w]) => Math.abs(v - c) < w / 2 + pad)\n  const cut = !!o.cutaway\n  const panelZ = (z: number, hl: [number, number][]) => {\n    for (const [a, b] of solid(ex0, ex1, hl)) k.box(CLAD, [b - a, wallH, t], [(a + b) / 2, yw, z])\n    for (const [c, w] of hl) k.box(CLAD, [w, hs + wallH - yHead, t], [c, (yHead + hs + wallH) / 2, z])\n  }\n  const panelX = (x: number, hl: [number, number][]) => {\n    for (const [a, b] of solid(z0 + t, z1 - t, hl)) k.box(CLAD, [t, wallH, b - a], [x, yw, (a + b) / 2])\n    for (const [c, w] of hl) k.box(CLAD, [t, hs + wallH - yHead, w], [x, (yHead + hs + wallH) / 2, c])\n  }\n  panelZ(z0 + t / 2, holes.nz)\n  if (!cut) panelZ(z1 - t / 2, holes.pz)\n  panelX(ex0 + t / 2, holes.nx)\n  panelX(ex1 - t / 2, holes.px)\n  k.box(CLAD, [EL + 0.12, 0.1, encW + 0.12], [exm, H - 0.05, zm])\n  const zWalls = (cut ? [[z0, -1, holes.nz]] : [[z0, -1, holes.nz], [z1, 1, holes.pz]]) as [number, number, [number, number][]][]\n  const nb = Math.max(4, Math.round(EL / 1.2))\n  for (let i = 0; i <= nb; i++) {\n    const x = ex0 + (i * EL) / nb\n    for (const [z, s, hl] of zWalls) if (!inHole(x, hl, 0.12)) k.box(TRIM, [0.06, wallH, 0.018], [x, yw, z + s * 0.009])\n    k.box(TRIM, [0.05, 0.03, encW + 0.1], [x, H + 0.015, zm])\n  }\n  const ne = Math.max(2, Math.round(encW / 1.2))\n  for (let i = 1; i < ne; i++)\n    for (const [x, s, hl] of [[ex0, -1, holes.nx], [ex1, 1, holes.px]] as const) {\n      const z = z0 + (i * encW) / ne\n      if (!inHole(z, hl, 0.12)) k.box(TRIM, [0.018, wallH, 0.06], [x + s * 0.009, yw, z])\n    }\n  const yGirt = hs + wallH * 0.52\n  const girtCut = yGirt - 0.035 < yHead + 0.08\n  for (const [z, s, hl] of zWalls) {\n    for (const [a, b] of girtCut ? solid(ex0, ex1, hl, 0.1) : [[ex0, ex1] as [number, number]]) k.box(TRIM, [b - a, 0.07, 0.03], [(a + b) / 2, yGirt, z + s * 0.024])\n    for (const [a, b] of solid(ex0 - 0.02, ex1 + 0.02, hl, 0.07)) k.box(SKID, [b - a, 0.14, 0.05], [(a + b) / 2, hs + 0.07, z + s * 0.02])\n    k.box(TRIM, [EL + 0.12, 0.14, 0.04], [exm, H - 0.07, z + s * 0.08])\n  }\n  for (const [x, s, hl] of [[ex0, -1, holes.nx], [ex1, 1, holes.px]] as const) {\n    for (const [a, b] of solid(z0 - 0.02, z1 + 0.02, hl, 0.07)) k.box(SKID, [0.05, 0.14, b - a], [x + s * 0.02, hs + 0.07, (a + b) / 2])\n    k.box(TRIM, [0.04, 0.14, encW + 0.2], [x + s * 0.08, H - 0.07, zm])\n  }\n  for (const sx of [-1, 1]) for (const sz of [-1, 1]) k.box(TRIM, [0.1, wallH, 0.1], [sx > 0 ? ex1 - 0.04 : ex0 + 0.04, yw, sz > 0 ? z1 - 0.04 : z0 + 0.04])\n  for (const sx of [-1, 1])\n    for (const sz of [-1, 1]) {\n      const p: V3 = [sx > 0 ? ex1 - 0.35 : ex0 + 0.35, H + 0.12, zm + sz * (encW / 2 - 0.3)]\n      k.box('structSteel', [0.22, 0.2, 0.03], p)\n      k.torus('nut', 0.05, 0.015, [p[0], p[1] + 0.03, p[2] + 0.02], 'z', 6, 14)\n    }\n  // doors with CO₂ warning beacons and manual call points\n  const op = o.open ? 1.65 : 0\n  if (!cut)\n    for (const x of o.doorsPz) {\n      k.at([x, hs, z1], faceQ('z'), () => door(k, 0.9, dH, 0, false, TRIM, SKID, op))\n      k.rbox('actuatorRed', [0.12, 0.12, 0.05], 0.01, [x - 0.75, hs + 1.4, z1 + 0.03])\n      beacon(k, [x + 0.7, hs + dH + 0.15, z1 + 0.08])\n    }\n  for (const x of o.doorsNz) {\n    k.at([x, hs, z0], faceQ('-z'), () => door(k, 0.9, dH, 0, false, TRIM, SKID, op))\n    k.rbox('actuatorRed', [0.12, 0.12, 0.05], 0.01, [x + 0.75, hs + 1.4, z0 - 0.03])\n    beacon(k, [x - 0.7, hs + dH + 0.15, z0 - 0.08])\n  }\n  if (o.endNx > 0) k.at([ex0, hs, zm], faceQ('-x'), () => door(k, o.endNx, dH, 0, o.endNx >= 1.3, TRIM, SKID, op))\n  if (o.endPx) k.at([ex1, hs, zm], faceQ('x'), () => door(k, o.endPx![1], dH, o.endPx![0], false, TRIM, SKID, op))\n  // internal fire & gas: IR flame detectors in the upper corners, heat detectors on the ceiling\n  for (const [x, z] of [[ex0 + 0.3, z0 + t + 0.15], [ex1 - 0.3, z1 - t - 0.15], [exm, z0 + t + 0.15]] as [number, number][]) {\n    k.rbox('stainless', [0.12, 0.12, 0.1], 0.015, [x, H - 0.35, z])\n    k.disc('glass', 0.035, [x + (x < exm ? 0.061 : -0.061), H - 0.35, z], x < exm ? 'x' : '-x', 14)\n  }\n  for (let x = ex0 + 1.5; x < ex1 - 0.5; x += 3) k.cyl('plasticGrey', 0.05, 0.05, [x, H - 0.125, zm + encW * 0.25], 'y', 12)\n  // maintenance monorail with trolley and chain block under the roof\n  if (o.monorail) {\n    const [m0, m1] = o.monorail\n    const yb = H - 0.1 - 0.15\n    member(k, [m0, yb, zm], [m1, yb, zm], 'IPE 300', 'safetyYellow', [0, 1, 0])\n    for (let x = m0 + 0.2; x < m1; x += 1.5) k.box('structSteel', [0.12, 0.06, 0.2], [x, H - 0.13, zm])\n    const tx = m0 + (m1 - m0) * 0.3\n    k.rbox('safetyYellow', [0.3, 0.12, 0.26], 0.02, [tx, yb - 0.21, zm])\n    k.rbox('actuatorRed', [0.22, 0.25, 0.18], 0.03, [tx, yb - 0.4, zm])\n    k.rod('nut', [tx, yb - 0.52, zm], [tx, yb - 0.9, zm], 0.008, 6)\n    k.torus('nut', 0.04, 0.01, [tx, yb - 0.94, zm], 'z', 6, 12)\n    k.plane(signMaterial('ler', 'SWL 5 t'), 0.3, 0.08, [tx + 0.5, yb, zm + 0.005])\n  }\n  return { dH, wallH }\n}\n\n// ------------------------------------------------------------------ inlet plenum, GT core, exhaust collector\n\n/** Air-inlet plenum with bell-mouth, nose cone, struts and FOD screen, between x = a and x = b (air leaves at +X). */\nexport function inletPlenum(k: Kit, o: { a: number; b: number; ys: number; R: number; hs: number; top: number; W: number; M: GtMats }) {\n  const { a, b, ys, R, hs, top, W, M } = o\n  const L = b - a\n  const xp = (a + b) / 2\n  k.box(TRIM, [0.05, top - hs, W], [a + 0.025, (top + hs) / 2, 0])\n  for (const s of [-1, 1]) k.box(TRIM, [L, top - hs, 0.05], [xp, (top + hs) / 2, s * (W / 2 - 0.025)])\n  const sh = new THREE.Shape()\n  sh.moveTo(-W / 2, hs)\n  sh.lineTo(W / 2, hs)\n  sh.lineTo(W / 2, top)\n  sh.lineTo(-W / 2, top)\n  sh.closePath()\n  const hole = new THREE.Path()\n  hole.absarc(0, ys, 0.84 * R, 0, Math.PI * 2, true)\n  sh.holes.push(hole)\n  k.extrude(TRIM, sh, 0.05, new THREE.Matrix4().compose(new THREE.Vector3(b - 0.05, 0, 0), new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2), new THREE.Vector3(1, 1, 1)), 32)\n  const bL = Math.min(L * 0.6, 0.9 * R)\n  const ri = 0.8 * R\n  const rl = Math.max(ri * 1.1, Math.min(1.3 * R, W / 2 - 0.1, top - ys - 0.1))\n  k.lathe(M.GT, [\n    [ri, bL],\n    [ri * 1.03, bL * 0.6],\n    [(ri + rl) / 2, bL * 0.25],\n    [rl, bL * 0.06],\n    [rl + 0.03, 0],\n    [rl + 0.07, 0.02],\n    [rl + 0.05, bL * 0.12],\n    [(ri + rl) / 2 + 0.05, bL * 0.3],\n    [ri * 1.03 + 0.04, bL * 0.62],\n    [ri + 0.04, bL],\n  ], [b - bL, ys, 0], 'x', 48)\n  k.dome(M.GT, 0.35 * R, [b - bL * 0.2, ys, 0], '-x', Math.PI / 2, 1.6, 28)\n  k.cyl(M.GT, 0.35 * R, bL * 0.25, [b - bL * 0.1, ys, 0], 'x', 28)\n  for (let i = 0; i < 6; i++) {\n    const t = (i / 6) * Math.PI * 2\n    k.box(M.GT, [bL * 0.3, ri * 0.66, 0.02], [b - bL * 0.15, ys + Math.sin(t) * ri * 0.66, Math.cos(t) * ri * 0.66], new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), -t + Math.PI / 2))\n  }\n  k.cyl('stainless', 0.08, L, [xp, ys, 0], 'x', 16)\n  const fodR = rl + 0.08\n  const fodL = Math.max(0.15, L - bL - 0.1)\n  const g = new THREE.CylinderGeometry(fodR, fodR, fodL, 40, 1, true)\n  uvScale(g, (2 * Math.PI * fodR) / 0.25, fodL / 0.25)\n  k.add(meshGuardMaterial('#b4b9bd'), g, mat4([a + 0.05 + fodL / 2, ys, 0], 'x'))\n}\n\n/** Gas turbine core from xc0 to xc0 + Lc on shaft height ys: compressor casing with VSV rings, combustor with dual-fuel manifolds, turbine, supports. */\nexport function gtCore(k: Kit, o: { xc0: number; Lc: number; ys: number; R: number; hs: number; MW: number; xBleed: number; M: GtMats }) {\n  const { xc0, Lc, ys, R, hs, MW, M } = o\n  const prof = (u: number) => {\n    if (u <= 0.4) return R * (0.8 - (0.2 * u) / 0.4)\n    if (u <= 0.43) return R * (0.6 + (0.14 * (u - 0.4)) / 0.03)\n    if (u <= 0.56) return R * 0.74\n    if (u <= 0.6) return R * (0.74 - (0.1 * (u - 0.56)) / 0.04)\n    if (u <= 0.68) return R * (0.64 + (0.04 * (u - 0.6)) / 0.08)\n    if (u <= 0.72) return R * (0.68 + (0.12 * (u - 0.68)) / 0.04)\n    if (u <= 0.97) return R * (0.8 + (0.12 * (u - 0.72)) / 0.25)\n    return R * 0.92\n  }\n  const xu = (u: number) => xc0 + u * Lc\n  const lathe = (mat: MatKey, u0: number, u1: number, steps: number[]) => {\n    const pts: [number, number][] = [[0, 0]]\n    for (const u of [u0, ...steps.filter((s) => s > u0 && s < u1), u1]) pts.push([prof(u), (u - u0) * Lc])\n    pts.push([0, (u1 - u0) * Lc])\n    k.lathe(mat, pts, [xu(u0), ys, 0], 'x', 48)\n  }\n  lathe(M.GT, 0, 0.4, [0.2])\n  lathe(M.HOT, 0.4, 1, [0.4, 0.43, 0.56, 0.6, 0.68, 0.72, 0.97])\n  // casing split flanges with bolting\n  for (const [u, mat, nb] of [[0.005, M.GT, 24], [0.2, M.GT, 20], [0.4, M.HOT, 20], [0.56, M.HOT, 20], [0.68, M.HOT, 22], [0.72, M.HOT, 26], [0.995, M.HOT, 26]] as [number, MatKey, number][]) {\n    const r = prof(Math.min(u, 0.999)) + (u < 0.01 ? 0.1 * R : 0.04)\n    k.cyl(mat, r, 0.05, [xu(u), ys, 0], 'x', 48)\n    k.boltCircle('boltXylan', 'nut', nb, r - 0.02, [xu(u) - 0.045, ys, 0], 'x', 0.016, 0.09, [0.078])\n  }\n  // variable stator vane rings, levers and actuator\n  for (const u of [0.05, 0.09, 0.13]) {\n    k.torus('stainless', prof(u) + 0.06, 0.012, [xu(u), ys, 0], 'x', 6, 48)\n    k.around(20, prof(u) + 0.02, [xu(u) - 0.025, ys, 0], 'x', (p) => k.box('stainless', [0.06, 0.012, 0.012], p))\n  }\n  k.cyl('actuatorYellow', 0.04, Lc * 0.12, [xu(0.12), ys + 0.1 * R, prof(0.1) + 0.12], 'x', 12)\n  k.rod('stainless', [xu(0.05), ys + 0.1 * R, prof(0.05) + 0.07], [xu(0.13), ys + 0.1 * R, prof(0.05) + 0.12], 0.012, 6)\n  // accessory gearbox and starter under the compressor\n  k.rbox(M.GT, [Lc * 0.15, 0.25 * R, 0.6 * R], 0.03, [xu(0.12), ys - 0.94 * R, 0])\n  k.cyl(M.GT, 0.12 * R, Lc * 0.08, [xu(0.12) - Lc * 0.1, ys - 0.9 * R, 0], 'x', 16)\n  k.rod(M.GT, [xu(0.12), ys - 0.82 * R, 0], [xu(0.12), ys - 0.72 * R, 0], 0.08 * R, 12)\n  // compressor bleed lines with bleed valves to the exhaust\n  for (const s of [-1, 1]) {\n    const a = s * 0.6\n    const rb = prof(0.3)\n    const zb = Math.sin(a) * rb\n    const yb = ys + Math.cos(a) * rb\n    const yt = ys + 0.95 * R + (s > 0 ? 0 : 0.12)\n    const bn = R > 0.7 ? 4 : 3\n    lineRun(k, [[xu(0.3), yb, zb], [xu(0.3), yt, zb], [o.xBleed, yt, zb]], bn, M.GT)\n    k.cyl(M.GT, pipeOD(bn) * 0.8, 0.06, [xu(0.3), yb + 0.02, zb], 'y', 16)\n    const xv = xu(0.62)\n    k.sphere('valveBody', pipeOD(bn) * 1.05, [xv, yt, zb], undefined, 14)\n    k.rbox('actuatorYellow', [0.12, 0.14, 0.12], 0.02, [xv, yt + pipeOD(bn) + 0.08, zb])\n  }\n  // combustor: gas and liquid fuel manifold rings, pigtails to the burners, igniters\n  const xcomb = xc0 + 0.48 * Lc\n  const rcomb = prof(0.5)\n  const rr = rcomb + 0.16\n  const xL = xcomb + Math.max(0.12, 0.05 * Lc)\n  k.torus('stainless', rr, pipeOD(2) / 2, [xcomb, ys, 0], 'x', 10, 64)\n  k.torus('stainless', rr - 0.02, pipeOD(1) / 2, [xL, ys, 0], 'x', 8, 64)\n  const nN = clamp(Math.round(8 + MW * 0.4), 10, 24)\n  for (let i = 0; i < nN; i++) {\n    const a = ((i + 0.5) / nN) * Math.PI * 2\n    const c = Math.cos(a)\n    const sn = Math.sin(a)\n    const xn = (xcomb + xL) / 2\n    k.cyl(M.HOT, 0.035, 0.08, [xn, ys + c * (rcomb + 0.03), sn * (rcomb + 0.03)], [0, c, sn], 10)\n    const head: V3 = [xn, ys + c * (rcomb + 0.075), sn * (rcomb + 0.075)]\n    const g1: V3 = [xcomb, ys + c * (rr - 0.05), sn * (rr - 0.05)]\n    const g2: V3 = [xL, ys + c * (rr - 0.06), sn * (rr - 0.06)]\n    k.rod('tubing', [xcomb, ys + c * rr, sn * rr], g1, 0.01, 5)\n    k.rod('tubing', g1, head, 0.01, 5)\n    k.rod('tubing', [xL, ys + c * (rr - 0.02), sn * (rr - 0.02)], g2, 0.008, 5)\n    k.rod('tubing', g2, head, 0.008, 5)\n  }\n  for (const s of [-1, 1]) {\n    const p: V3 = [xu(0.52), ys - 0.5 * R, s * 0.55 * R]\n    k.rod('stainless', p, [p[0], p[1] - 0.15, p[2] * 1.25], 0.02, 8)\n    k.spline('cable', [[p[0], p[1] - 0.15, p[2] * 1.25], [p[0] + 0.2, hs + 0.3, p[2] * 1.3], [p[0] + 0.5, hs + 0.05, p[2] * 1.3]], 0.012, 6, 16)\n  }\n  // EGT thermocouple harness\n  k.torus('tubing', prof(0.75) + 0.06, 0.008, [xu(0.75), ys, 0], 'x', 6, 48)\n  k.around(12, prof(0.75) + 0.03, [xu(0.75), ys, 0], 'x', (p) => k.cyl('stainless', 0.012, 0.07, p, 'x', 6))\n  // trunnion supports on a sub-base\n  for (const s of [-1, 1]) {\n    k.box(SKID, [0.14, ys - 0.62 * R - hs - 0.08, 0.14], [xu(0.06), (ys - 0.62 * R + hs + 0.08) / 2, s * 0.45 * R])\n    k.box(SKID, [0.16, ys - 0.7 * R - hs - 0.08, 0.16], [xu(0.8), (ys - 0.7 * R + hs + 0.08) / 2, s * 0.55 * R])\n  }\n  k.box(SKID, [0.25, 0.04, 1.2 * R], [xu(0.06), ys - 0.62 * R, 0])\n  k.box(SKID, [0.28, 0.04, 1.4 * R], [xu(0.8), ys - 0.7 * R, 0])\n  k.box(SKID, [Lc * 0.9, 0.08, 1.4 * R], [xu(0.45), hs + 0.04, 0])\n  return { prof, xu, xcomb, xL, rr }\n}\n\n/** Insulated exhaust collector around x = xst with the vertical duct up through the roof to y = H + 0.3. */\nexport function exhaustCollector(k: Kit, o: { xst: number; ys: number; R: number; cHL: number; cW: number; Rd: number; hs: number; H: number; rExit: number; M: GtMats }) {\n  const { xst, ys, R, cHL, cW, Rd, hs, H, M } = o\n  const colTop = ys + 1.3 * R\n  k.rbox('insulation', [2 * cHL, 2.6 * R, 2 * cW], 0.12, [xst, ys, 0])\n  for (const s of [-1, 1]) for (let i = 0; i < 3; i++) k.box('insulation', [0.03, 2.6 * R, 0.03], [xst - cHL * 0.6 + i * cHL * 0.6, ys, s * (cW + 0.012)])\n  k.cyl('insulation', Rd, H + 0.3 - colTop, [xst, (H + 0.3 + colTop) / 2, 0], 'y', 48)\n  for (let y = colTop + 0.6; y < H - 0.2; y += 0.8) k.cyl('insulation', Rd + 0.012, 0.03, [xst, y, 0], 'y', 48)\n  k.cyl(M.HOT, o.rExit, 0.5 * cHL, [xst - cHL - 0.25 * cHL + 0.5 * cHL, ys, 0], 'x', 40, o.rExit * 1.05)\n  for (const s of [-1, 1]) k.box(SKID, [0.2, ys - 1.3 * R - hs, 2 * cW * 0.8], [xst + s * cHL * 0.6, (ys - 1.3 * R + hs) / 2, 0])\n  return colTop\n}\n\n// ------------------------------------------------------------------ roof: filter house, exhaust stack\n\nexport interface FilterHouseOpts {\n  xp: number\n  /** inlet duct length (X) and width (Z) */\n  dL: number\n  dW: number\n  H: number\n  dHt: number\n  FL: number\n  FW: number\n  FH: number\n  /** filter house centre x when offset from the duct (default xp) */\n  xh?: number\n  /** access platform and ladder at the house door (needs ~1 m clear at +X) */\n  platform?: boolean\n  /** half width of the roof the house stands on (keeps posts on the roof) */\n  roofHalfW?: number\n}\n\n/** Inlet silencer duct and multi-stage filter house on legs: weather hoods on three faces, access door and dP gauges on the fourth. */\nexport function filterHouse(k: Kit, o: FilterHouseOpts) {\n  const { xp, dL, dW, H, dHt, FL, FW, FH } = o\n  const xh = o.xh ?? xp\n  const fy0 = H + dHt\n  const fy1 = fy0 + FH\n  k.box(TRIM, [dL, dHt, dW], [xp, H + dHt / 2, 0])\n  for (const s of [-1, 1]) k.box(TRIM, [dL + 0.08, 0.08, dW + 0.08], [xp, H + dHt / 2 + s * dHt * 0.3, 0])\n  k.box(TRIM, [FL, 0.08, FW], [xh, fy0 + 0.04, 0])\n  k.box(TRIM, [FL + 0.1, 0.1, FW + 0.1], [xh, fy1 - 0.05, 0])\n  for (const s of [-1, 1]) {\n    k.box(TRIM, [FL, FH, 0.04], [xh, (fy0 + fy1) / 2, s * (FW / 2 - 0.02)])\n    k.box(TRIM, [0.04, FH, FW], [xh + s * (FL / 2 - 0.02), (fy0 + fy1) / 2, 0])\n  }\n  // seams on the roof of the house\n  for (let i = 1; i < Math.round(FL / 1.2); i++) k.box(TRIM, [0.05, 0.03, FW + 0.1], [xh - FL / 2 + (i * FL) / Math.round(FL / 1.2), fy1 + 0.015, 0])\n  // inlet faces: weather hoods over a bird screen, then vane separator and coalescer / pre-filter stage, then the final filter cartridges\n  const media = paint('#ddd6c3', { metalness: 0, roughness: 0.9, clearcoat: 0 })\n  const vane: MatKey = 'galvanised'\n  const stages = (w: number, h: number) => {\n    for (let x = -w / 2 + 0.03; x < w / 2; x += 0.06) k.box(vane, [0.006, h, 0.1], [x, h / 2 + 0.17, -0.09], eul(0, 0.5, 0))\n    const n = Math.max(2, Math.floor(w / 0.62))\n    const m = Math.max(2, Math.floor(h / 0.62))\n    const cw = w / n\n    const ch = h / m\n    for (let i = 0; i <= n; i++) k.box('internalsSteel', [0.04, h, 0.05], [-w / 2 + i * cw, h / 2 + 0.17, -0.3])\n    for (let j = 0; j <= m; j++) k.box('internalsSteel', [w, 0.04, 0.05], [0, 0.17 + j * ch, -0.3])\n    for (let i = 0; i < n; i++)\n      for (let j = 0; j < m; j++) {\n        const cx = -w / 2 + (i + 0.5) * cw\n        const cy = 0.17 + (j + 0.5) * ch\n        k.box(media, [cw - 0.06, ch - 0.06, 0.04], [cx, cy, -0.3])\n        // final-stage cartridge pair behind each cell\n        for (const s of [-1, 1]) k.cyl(media, Math.min(cw, ch) * 0.17, 0.5, [cx + s * cw * 0.22, cy, -0.6], 'z', 16)\n      }\n  }\n  const faces: [V3, 'z' | '-z' | '-x', number][] = [[[xh, fy0, FW / 2], 'z', FL - 0.25], [[xh, fy0, -FW / 2], '-z', FL - 0.25], [[xh - FL / 2, fy0, 0], '-x', FW - 0.25]]\n  for (const [o, f, w] of faces)\n    k.at(o, faceQ(f), () => {\n      hoods(k, w, FH - 0.35, 0, FH / 2, TRIM)\n      stages(Math.min(w, (f === '-x' ? FW : FL) - 1.4), FH - 0.45)\n      // anti-icing hot-air manifold along the foot of the hoods, with nozzles up into the inlet\n      k.rod('stainless', [-w / 2, 0.12, 0.3], [w / 2, 0.12, 0.3], pipeOD(3) / 2, 12)\n      for (let x = -w / 2 + 0.3; x < w / 2; x += 0.5) k.rod('stainless', [x, 0.12, 0.3], [x, 0.24, 0.24], 0.012, 6)\n      for (const sx of [-1, 1]) k.box('structSteel', [0.04, 0.12, 0.3], [sx * (w / 2 - 0.05), 0.06, 0.15])\n    })\n  k.at([xh + FL / 2, fy0, 0], faceQ('x'), () => {\n    const fdH = Math.min(1.8, FH - 0.3)\n    door(k, 0.7, fdH, FW / 2 - 0.6, false, TRIM)\n    k.rbox('stainless', [0.5, 0.6, 0.12], 0.02, [-FW / 2 + 0.6, FH * 0.55, 0.06])\n    for (let i = 0; i < 3; i++) gauge(k, [-FW / 2 + 0.45 + i * 0.15, FH * 0.55 + 0.12, 0.15], 'z', 0.09, 'bar', 10)\n    k.plane(signMaterial('ler', 'AIR INLET FILTER'), Math.min(1.2, FW * 0.4), 0.22, [0, FH - 0.3, 0.005])\n  })\n  gasDetector(k, [xh + FL / 2 + 0.12, fy0 + 0.4, -FW / 2 + 0.3])\n  // access platform at the house door, ladder down to the roof\n  if (o.platform !== false) {\n    const pd = 0.9\n    const pw = 1.4\n    const rh = o.roofHalfW ?? FW / 2\n    const pz = Math.max(-(FW / 2 - 0.6), -rh + 0.1 + pw / 2)\n    const px = xh + FL / 2 + pd / 2 + 0.02\n    grating(k, [px, fy0, pz], pd, pw)\n    handrail(k, [[px - pd / 2, fy0, pz - pw / 2], [px + pd / 2, fy0, pz - pw / 2], [px + pd / 2, fy0, pz + pw / 2 - 0.6]])\n    handrail(k, [[px - pd / 2, fy0, pz + pw / 2], [px + pd / 2 - 0.55, fy0, pz + pw / 2]])\n    for (const [dx, dz] of [[-1, -1], [1, -1], [-1, 1], [1, 1]]) k.box('structSteel', [0.08, fy0 - H - 0.03, 0.08], [px + dx * (pd / 2 - 0.04), (fy0 - 0.03 + H) / 2, pz + dz * (pw / 2 - 0.04)])\n    ladder(k, [px + pd / 2 - 0.3, H, pz + pw / 2 + 0.05], fy0, [0, 0, 1], { cage: false })\n  }\n  // anti-icing supply riser from the roof (compressor bleed air) to the manifolds\n  const az = Math.min(FW / 2, o.roofHalfW ?? FW / 2) - 0.25\n  k.rod('stainless', [xh + FL / 2 - 0.3, H, az], [xh + FL / 2 - 0.3, fy0, az], pipeOD(3) / 2, 12)\n  k.cyl('flangeSteel', pipeOD(3) * 0.9, 0.03, [xh + FL / 2 - 0.3, fy0 - 0.015, az], 'y', 16)\n  k.sphere('valveBody', pipeOD(3) * 0.9, [xh + FL / 2 - 0.3, H + dHt * 0.45, az], undefined, 14)\n  k.rbox('actuatorYellow', [0.16, 0.14, 0.14], 0.02, [xh + FL / 2 - 0.3, H + dHt * 0.45, az - 0.16])\n  const lz = Math.min(FW / 2 - 0.1, dW / 2 + 0.25, (o.roofHalfW ?? FW / 2) - 0.2)\n  for (const sx of [-1, 1])\n    for (const sz of [-1, 1]) {\n      const lx = xh + sx * (FL / 2 - 0.12)\n      k.box('structSteel', [0.12, dHt, 0.12], [lx, H + dHt / 2, sz * lz])\n      k.box('structSteel', [0.24, 0.02, 0.24], [lx, H + 0.01, sz * lz])\n      k.rod('structSteel', [lx, H + 0.1, sz * lz], [lx - sx * Math.min(0.5, FL * 0.15), fy0 - 0.02, sz * lz], 0.03, 8)\n    }\n}\n\n/** Exhaust stack on the roof at xst: fabric expansion joint, silencer, flanged stack sections, sampling platform with caged ladder, rain cap, aviation light. */\nexport function exhaustStack(k: Kit, o: { xst: number; H: number; Rd: number; Rs: number; Rsil: number; stackH: number; legR: number; M: GtMats }) {\n  const { xst, H, Rd, Rs, Rsil, M } = o\n  const stackH = Math.max(o.stackH, 3)\n  const yE0 = H + 0.25\n  const yE1 = H + 0.65\n  const cone1 = Math.max(0.3, (Rsil - Rd) * 0.8)\n  const cone2 = Math.max(0.3, (Rsil - Rs) * 0.9)\n  let silH = clamp(stackH * 0.3, 1.2, 5)\n  silH = Math.max(0.8, Math.min(silH, stackH - 0.65 - cone1 - cone2 - 1.0))\n  const ySil0 = yE1 + cone1\n  const ySil1 = ySil0 + silH\n  const yS0 = ySil1 + cone2\n  const yTop = Math.max(H + stackH, yS0 + 2.4)\n  const sootH = clamp(0.06 * stackH, 0.35, 1.0)\n  k.cyl(M.STACK, Rd, yE0 - H, [xst, (H + yE0) / 2, 0], 'y', 48)\n  for (const y of [yE0, yE1]) k.cyl(M.STACK, Rd + 0.08, 0.05, [xst, y, 0], 'y', 48)\n  k.lathe('rubberBlack', [[Rd + 0.01, yE0], [Rd + 0.07, yE0 + 0.12], [Rd + 0.1, (yE0 + yE1) / 2], [Rd + 0.07, yE1 - 0.12], [Rd + 0.01, yE1]], [xst, 0, 0], 'y', 48)\n  k.lathe(M.STACK, [[0, yE1], [Rd, yE1], [Rsil, ySil0], [Rsil, ySil1], [Rs, yS0], [Rs, yTop - sootH * 2.2]], [xst, 0, 0], 'y', 48)\n  k.lathe(M.SOOT2, [[Rs, yTop - sootH * 2.2], [Rs, yTop - sootH]], [xst, 0, 0], 'y', 48)\n  k.lathe(M.SOOT, [[Rs, yTop - sootH], [Rs, yTop], [Rs + 0.05, yTop], [Rs + 0.05, yTop + 0.04], [Rs - 0.012, yTop + 0.04], [Rs - 0.012, yTop - 2.0]], [xst, 0, 0], 'y', 48)\n  k.disc(M.SOOT, Rs, [xst, yTop - 1.9, 0], 'y', 40)\n  const flg = (y: number, r: number, n: number) => {\n    k.cyl(M.STACK, r + 0.07, 0.06, [xst, y, 0], 'y', 48)\n    k.boltCircle('boltXylan', 'nut', n, r + 0.04, [xst, y - 0.06, 0], 'y', 0.018, 0.12, [0.108])\n  }\n  flg(ySil0, Rsil, 28)\n  flg(ySil1, Rsil, 28)\n  flg(yS0, Rs, 20)\n  const nSec = Math.max(1, Math.round((yTop - yS0) / 3.2))\n  for (let i = 1; i < nSec; i++) flg(yS0 + (i * (yTop - yS0)) / nSec, Rs, 20)\n  for (let i = 1; i < 4; i++) k.cyl(M.STACK, Rsil + 0.025, 0.08, [xst, ySil0 + (i * silH) / 4, 0], 'y', 48)\n  // silencer support legs on the roof\n  const lr = o.legR\n  for (const a of [Math.PI / 4, (3 * Math.PI) / 4, (5 * Math.PI) / 4, (7 * Math.PI) / 4]) {\n    const p: V3 = [xst + Math.cos(a) * lr, H, Math.sin(a) * lr]\n    k.box('structSteel', [0.16, ySil0 - H, 0.16], [p[0], (H + ySil0) / 2, p[2]])\n    k.box('structSteel', [0.3, 0.03, 0.3], [p[0], H + 0.015, p[2]])\n  }\n  k.torus('structSteel', lr, 0.05, [xst, ySil0 - 0.08, 0], 'y', 6, 40)\n  // sampling platform, gap towards -X for the ladder\n  const pw = Math.max(0.85, Rsil + 0.55 - Rs)\n  const yP = clamp(ySil1 + 1.2, yS0 + 0.3, yTop - 1.6)\n  const gap = 0.5\n  k.at([xst, 0, 0], undefined, () => ringPlatform(k, yP, Rs + 0.02, pw, -Math.PI + gap, Math.PI - gap))\n  ladder(k, [xst - Rsil - 0.3, H, 0], yP, [-1, 0, 0], { cage: true })\n  // ladder stand-offs to the silencer\n  for (let y = H + 1.2; y < yP - 0.2; y += 1.8) k.rod('structSteel', [xst - Rsil - 0.3, y, 0], [xst - Rsil + 0.01, y, 0], 0.025, 8)\n  for (const a of [Math.PI / 2, -Math.PI / 2, 0]) {\n    const d: V3 = [Math.cos(a), 0, Math.sin(a)]\n    const ysp = Math.min(yP + 1.2, yTop - 0.4)\n    k.rod(M.STACK, [xst + d[0] * (Rs - 0.02), ysp, d[2] * (Rs - 0.02)], [xst + d[0] * (Rs + 0.16), ysp, d[2] * (Rs + 0.16)], pipeOD(4) / 2, 16)\n    flange(k, 4, 150, [xst + d[0] * (Rs + 0.24), ysp, d[2] * (Rs + 0.24)], d, true)\n    k.cyl('flangeSteel', 0.115, 0.025, [xst + d[0] * (Rs + 0.253), ysp, d[2] * (Rs + 0.253)], d, 24)\n  }\n  const capY = yTop + 0.45 * Rs\n  k.cyl(M.SOOT, Rs * 1.15, 0.35 * Rs, [xst, capY + 0.175 * Rs, 0], 'y', 40, 0.06)\n  for (let i = 0; i < 4; i++) {\n    const a = (i / 4) * Math.PI * 2 + Math.PI / 4\n    k.rod(M.SOOT, [xst + Math.cos(a) * (Rs + 0.03), yTop - 0.3, Math.sin(a) * (Rs + 0.03)], [xst + Math.cos(a) * Rs * 1.05, capY, Math.sin(a) * Rs * 1.05], 0.03, 6)\n  }\n  k.box('structSteel', [0.06, 0.06, 0.35], [xst, yTop - 0.6, Rs + 0.17])\n  k.cyl('actuatorRed', 0.06, 0.12, [xst, yTop - 0.51, Rs + 0.3], 'y', 14)\n  k.dome('glass', 0.06, [xst, yTop - 0.45, Rs + 0.3], 'y', Math.PI / 2, 1.3, 14)\n  return yTop\n}\n\n// ------------------------------------------------------------------ fuel, fire suppression\n\nexport interface FuelTrainOpts {\n  /** gas inlet flange x, liquid inlet flange x (null = gas only) */\n  xGas: number\n  xLiq: number | null\n  y: number\n  /** flange face z (on the -Z side) */\n  faceZ: number\n  gasNps: number\n  rating: Rating\n  /** wall penetration x, enclosure -Z wall, apron centre z, apron width */\n  xpen: number\n  z0: number\n  zA: number\n  m: number\n  hs: number\n}\n\n/** Fuel gas conditioning (coalescing filter, double block-and-bleed ESDVs, metering valve) and liquid fuel duplex filter, on the -Z apron. */\nexport function fuelTrain(k: Kit, o: FuelTrainOpts) {\n  const { xGas: x1, y: yG, faceZ, gasNps, rating, xpen, z0, zA, m, hs } = o\n  const yL = Math.max(yG + 1.0, 1.85)\n  k.box(SKID, [xpen + 0.25 - Math.min(x1, o.xLiq ?? x1) + 0.35, 0.1, m - 0.08], [(xpen + 0.25 + Math.min(x1, o.xLiq ?? x1) - 0.35) / 2, hs + 0.05, zA])\n  runToFlange(k, [[xpen, yG, z0 + 0.01], [xpen, yG, zA], [x1, yG, zA]], [x1, yG, faceZ], [0, 0, -1], gasNps, rating)\n  const gs = xpen - x1\n  const rf = clamp(pipeOD(gasNps) * 1.6, 0.12, m / 2 - 0.05)\n  const xf = x1 + gs * 0.2\n  const fy0 = hs + 0.18\n  const fy1 = yG + 0.75\n  k.cyl('equipmentPaint', rf, fy1 - fy0, [xf, (fy0 + fy1) / 2, zA], 'y', 28)\n  k.dome('equipmentPaint', rf, [xf, fy1, zA], 'y', Math.PI / 2, 0.5, 24)\n  k.cyl('flangeSteel', rf + 0.04, 0.05, [xf, fy1 - 0.1, zA], 'y', 28)\n  k.around(12, rf + 0.02, [xf, fy1 - 0.07, zA], 'y', (p) => k.hex('nut', 0.025, 0.015, p))\n  for (let i = 0; i < 3; i++) {\n    const a = (i / 3) * Math.PI * 2\n    k.rod(SKID, [xf + Math.cos(a) * rf * 0.8, hs + 0.1, zA + Math.sin(a) * rf * 0.8], [xf + Math.cos(a) * rf * 0.8, fy0 + 0.05, zA + Math.sin(a) * rf * 0.8], 0.025, 6)\n  }\n  gauge(k, [xf, yG + 0.35, zA - rf - 0.04], '-z', 0.09, 'bar', 40)\n  ballValve(k, x1 + gs * 0.42, yG, zA, gasNps, rating)\n  const xvn = x1 + gs * 0.56\n  annulus(k, 'pipePaint', pipeOD(1) / 2, pipeOD(1) * 0.75, 0.03, [xvn, yG + pipeOD(gasNps) / 2 + 0.015, zA], 'y', 14)\n  k.rod('pipePaint', [xvn, yG + pipeOD(gasNps) / 2 + 0.001, zA], [xvn, yG + 0.45, zA], pipeOD(1) / 2, 10)\n  k.sphere('valveBody', 0.045, [xvn, yG + 0.25, zA], undefined, 12)\n  k.rbox('actuatorYellow', [0.1, 0.1, 0.1], 0.015, [xvn, yG + 0.25, zA - 0.11])\n  k.dome('pipePaint', pipeOD(1) / 2, [xvn, yG + 0.45, zA], 'y', Math.PI / 2, 0.5, 10)\n  ballValve(k, x1 + gs * 0.68, yG, zA, gasNps, rating)\n  controlValve(k, x1 + gs * 0.86, yG, zA, gasNps, rating)\n  for (const xs of [x1 + gs * 0.3, x1 + gs * 0.78]) {\n    k.rod('structSteel', [xs, hs + 0.1, zA], [xs, yG - pipeOD(gasNps) / 2 - 0.02, zA], 0.03, 8)\n    k.box('structSteel', [0.12, 0.02, 0.14], [xs, yG - pipeOD(gasNps) / 2 - 0.01, zA])\n  }\n  k.box(TRIM, [0.3, 0.3, 0.02], [xpen, yG, z0 - 0.005])\n  if (o.xLiq !== null) {\n    const x2 = o.xLiq\n    const xr = Math.max(x2 + 0.4, x1 - 0.28)\n    runToFlange(k, [[xpen, yL, z0 + 0.01], [xpen, yL, zA], [xr, yL, zA], [xr, yG, zA], [x2, yG, zA]], [x2, yG, faceZ], [0, 0, -1], 2, 150)\n    const ls = xr - x2\n    const xdf = x2 + ls * 0.35\n    for (const s of [-1, 1]) {\n      k.cyl(OIL, 0.065, yG + 0.12 - (hs + 0.12), [xdf + s * 0.09, (yG + 0.12 + hs + 0.12) / 2, zA - 0.12], 'y', 18)\n      k.cyl(OIL, 0.075, 0.03, [xdf + s * 0.09, yG + 0.12, zA - 0.12], 'y', 18)\n      k.rod(OIL, [xdf + s * 0.09, yG, zA - 0.12], [xdf + s * 0.09, yG, zA], 0.025, 8)\n    }\n    k.rod('handwheel', [xdf, yG + 0.06, zA - 0.12], [xdf, yG + 0.24, zA - 0.12], 0.012, 6)\n    k.box('handwheel', [0.22, 0.02, 0.02], [xdf, yG + 0.24, zA - 0.12])\n    ballValve(k, x2 + ls * 0.75, yG, zA, 2, 150)\n    k.rod('structSteel', [xr, hs + 0.1, zA + 0.1], [xr, yL, zA + 0.1], 0.025, 8)\n    k.box(TRIM, [0.3, 0.3, 0.02], [xpen, yL, z0 - 0.005])\n  }\n  const gx0 = Math.min(x1, o.xLiq ?? x1) - 0.2\n  k.rod('structSteel', [gx0, hs + 0.1, zA - 0.15], [gx0, 2.4, zA - 0.15], 0.025, 8)\n  gasDetector(k, [gx0, 2.3, zA - 0.08])\n  return { yL }\n}\n\n/** CO₂ fire-suppression cylinder cabinet against the -Z wall, discharge header to y = yHdr inside. */\nexport function co2Cabinet(k: Kit, o: { x0: number; L: number; z0: number; m: number; hs: number; H: number; yHdr: number }) {\n  const { x0, L, z0, m, hs, H } = o\n  const cd = m - 0.12\n  const cz = z0 - cd / 2 - 0.02\n  const ch = Math.min(2.0, H - hs - 0.4)\n  const cx = x0 + L / 2\n  k.box('actuatorRed', [L, ch, 0.03], [cx, hs + ch / 2, z0 - 0.035])\n  for (const s of [-1, 1]) k.box('actuatorRed', [0.03, ch, cd], [cx + s * (L / 2 - 0.015), hs + ch / 2, cz])\n  k.box('actuatorRed', [L + 0.06, 0.04, cd + 0.08], [cx, hs + ch + 0.02, cz])\n  k.box(SKID, [L, 0.06, cd], [cx, hs + 0.03, cz])\n  const nbt = Math.max(2, Math.floor((L - 0.1) / 0.28))\n  const bR = Math.min(0.12, cd / 2 - 0.04)\n  const bH = ch - 0.55\n  for (let i = 0; i < nbt; i++) {\n    const bx = cx - L / 2 + 0.05 + (i + 0.5) * ((L - 0.1) / nbt)\n    k.cyl('actuatorRed', bR, bH, [bx, hs + 0.06 + bH / 2, cz], 'y', 20)\n    k.dome('actuatorRed', bR, [bx, hs + 0.06 + bH, cz], 'y', Math.PI / 2, 0.6, 16)\n    k.cyl('brass', 0.035, 0.12, [bx, hs + bH + 0.18, cz], 'y', 10)\n    k.tube('tubing', [[bx, hs + bH + 0.24, cz], [bx, hs + bH + 0.32, cz], [bx, hs + bH + 0.38, z0 - 0.08]], 0.01, 6, 0.04)\n  }\n  k.box('structSteel', [L - 0.06, 0.04, 0.05], [cx, hs + bH * 0.6, cz - bR - 0.03])\n  lineRun(k, [[cx - L / 2 + 0.08, hs + bH + 0.38, z0 - 0.08], [cx, hs + bH + 0.38, z0 - 0.08], [cx, o.yHdr, z0 - 0.08], [cx, o.yHdr, z0 + 0.02]], 1.5, 'stainless')\n  k.box('actuatorRed', [L, 0.3, 0.02], [cx, hs + ch - 0.2, z0 - cd - 0.015])\n  k.plane(signMaterial('ler', 'CO2 FIRE SUPPRESSION'), Math.min(0.9, L - 0.1), 0.25, [cx, hs + ch - 0.2, z0 - cd - 0.03], new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI))\n  k.rbox('safetyGreen', [0.2, 0.26, 0.1], 0.02, [cx + L / 2 + 0.25, 1.4, z0 - 0.05])\n}\n"
    }
  ],
  "categories": [
    "rotating"
  ],
  "meta": {
    "export": "GasTurbineGenerator",
    "anchor": "base",
    "standards": [
      "API 616",
      "ISO 3977",
      "ISO 21789",
      "IEC 60034-3",
      "API 614",
      "ASME B16.5"
    ]
  }
}