{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "diesel-generator",
  "type": "registry:component",
  "title": "Diesel generator set (emergency / standby)",
  "description": "Turbocharged diesel generator set on a double-wall sub-base day tank: inline-6 or V12/V16/V20 engine with rocker covers, insulated exhaust manifolds, turbochargers and air cleaners, flywheel housing with starters, alternator with terminal box, front radiator with pusher fan, anti-vibration isolators, closed crankcase ventilation filter, batteries and controller. Exhaust rises through bellows to a silencer with a flanged outlet. `enclosure` puts it in an ISO 20 ft / 40 ft high-cube container with corrugated walls, sand-trap intake louvres with damper actuators, radiator discharge louvre, opening side doors, clean-agent fire suppression and the silencer on the roof. Engine, alternator and radiator size with `kw`.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "rotating/diesel-generator.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/rotating/diesel-generator.tsx",
      "content": "// Emergency / standby diesel generator set (ISO 8528, IEC 60034): turbocharged diesel engine, alternator, radiator, sub-base day tank, exhaust silencer, open skid or ISO container.\nimport * as THREE from 'three'\nimport { useEffect, useMemo } from 'react'\nimport { Kit, eul, mat4, uvScale, type MatKey, type V3 } from '../lib/kit'\nimport { KitMesh, type GroupProps } from '../lib/mesh'\nimport { gauge, ladder, tagPlate } from '../lib/parts'\nimport { port, type Port } from '../lib/ports'\nimport { Rotors, fanRotor, type RotorSpec } from '../lib/rotors'\nimport { fanGuardMaterial, lcdMaterial, louvreMaterial, signMaterial } from '../lib/textures'\nimport { pipeOD, pipeWall } from '../lib/standards'\nimport { SKID, annulus, clamp, door, faceQ, lineRun, louvre, neckTo, runToFlange } from './_compressors-kit'\n\nexport interface DieselGeneratorProps {\n  /** electrical rating, kWe */\n  kw?: number\n  enclosure?: 'open' | 'container'\n  /** container: swing the side doors open */\n  doorsOpen?: boolean\n  fuelNps?: number\n  tag?: string\n  running?: boolean\n}\n\nexport const defaults = {\n  kw: 1000,\n  enclosure: 'open' as 'open' | 'container',\n  doorsOpen: false,\n  fuelNps: 1,\n  tag: 'G-9310',\n  running: true,\n}\n\ntype P = typeof defaults\n\nconst FLOOR = 0.15 // container floor above deck\n\nfunction layout(p: P) {\n  const kw = clamp(p.kw, 100, 3000)\n  const box = p.enclosure === 'container'\n  const Lset = 3.2 + 0.0017 * kw // typical open-set length: 4.0 m at 500 kWe, 6.6 m at 2 MWe\n  const Wset = 1.5 + 0.0004 * kw\n  const hb = box ? 0.3 : clamp(0.3 + kw * 0.0002, 0.35, 0.7) // sub-base day tank (shallow inside a container)\n  const hs = hb + 0.2\n  const yk = hs + 0.55 + Math.min(kw * 0.0001, box ? 0.12 : 0.3)\n  const roof = box ? 2.896 - FLOOR - 0.12 : 99\n  const ncyl = kw <= 600 ? 6 : kw <= 1300 ? 12 : kw <= 2200 ? 16 : 20\n  const vee = ncyl > 6\n  const Ra = clamp(0.35 + kw * 0.00018, 0.4, 0.85)\n  const La = clamp(1.0 + kw * 0.0005, 1.1, 2.2)\n  const Le = Lset - 0.15 - La - 0.25 - 0.75\n  const bw = clamp(0.7 + kw * 0.0002, 0.75, 1.2)\n  const Hr = Math.min(clamp(1.4 + kw * 0.0004, 1.5, 2.4), roof - hs - 0.45)\n  const exNps = [6, 8, 10, 12, 14, 16, 18, 20].find((n) => n >= 4 + kw * 0.008) ?? 20\n  const Ds = Math.max(clamp(0.35 + kw * 0.0002, 0.4, 0.9), pipeOD(exNps) * 1.8)\n  const engTop = yk + 0.25 + (vee ? 0.75 : 0.85)\n  // container: ISO 668 20 ft or 40 ft high cube\n  const CL = kw <= 1100 ? 6.058 : 12.192\n  const CW = 2.438\n  const CH = 2.896\n  const y0 = box ? FLOOR : 0\n  const xoff = box ? CL / 2 - 0.2 - Lset / 2 : 0\n  const ySil = box ? CH + 0.35 + Ds / 2 : Math.max(engTop + 0.45, yk + Ra + 0.6) + Ds / 2\n  const Lsil = Ds * 2.6\n  const xSil = box ? xoff : -Lset / 2 + La * 0.6\n  const yEx = ySil + Ds / 2 + 0.6\n  const xEx = xSil + Lsil / 2 - Ds * 0.35\n  const zFuel = -(box ? CW / 2 : Wset / 2) - 0.25\n  const xFuel = xoff - Lset * 0.25\n  const yFuel = y0 + hb * 0.5\n  return { roof, kw, box, Lset, Wset, hb, hs, yk, ncyl, vee, Ra, La, Le, bw, Hr, exNps, Ds, engTop, CL, CW, CH, y0, xoff, ySil, Lsil, xSil, yEx, xEx, zFuel, xFuel, yFuel }\n}\n\nexport function ports(props: DieselGeneratorProps = {}): Port[] {\n  const p = { ...defaults, ...props }\n  const L = layout(p)\n  return [\n    port('exhaust', 'Exhaust outlet', [L.xEx, L.yEx, 0], [0, 1, 0], L.exNps, 150, 'vent'),\n    port('fuel', 'Diesel supply', [L.xFuel, L.yFuel, L.zFuel], [0, 0, -1], p.fuelNps, 150, 'utility'),\n  ]\n}\n\nexport function DieselGenerator(props: DieselGeneratorProps & GroupProps) {\n  const { kw, enclosure, doorsOpen, fuelNps, tag, running, ...group } = { ...defaults, ...props }\n  const { built, rotors } = useMemo(() => build({ kw, enclosure, doorsOpen, fuelNps, tag, running }), [kw, enclosure, doorsOpen, fuelNps, 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 DieselGenerator\n\nconst ENGINE: MatKey = 'equipmentPaint'\nconst ALT: MatKey = 'motorPaint'\n\nfunction build(p: P) {\n  const k = new Kit()\n  const L = layout(p)\n  const rotors: RotorSpec[] = []\n  k.at([L.xoff, L.y0, 0], undefined, () => genset(k, p, L, rotors))\n  for (const r of rotors) r.pos = [r.pos[0] + L.xoff, r.pos[1] + L.y0, r.pos[2]]\n  if (L.box) container(k, p, L)\n  exhaust(k, L)\n  return { built: k.build(), rotors }\n}\n\n/** The set in its own frame: sub-base tank, rails, alternator at -X, engine, radiator at +X. */\nfunction genset(k: Kit, p: P, L: ReturnType<typeof layout>, rotors: RotorSpec[]) {\n  const { Lset, Wset, hb, hs, yk, Ra, La, Le, bw, Hr, vee, ncyl } = L\n  const x0 = -Lset / 2 + 0.15\n  const xa1 = x0 + La\n  const xe0 = xa1 + 0.25\n  const xe1 = xe0 + Le\n  // double-wall sub-base day tank: fill cap, vent, level gauge, leak detector, rails on top with isolators\n  k.rbox(SKID, [Lset, hb, Wset], 0.02, [0, hb / 2, 0])\n  for (const s of [-1, 1]) k.box(SKID, [Lset, 0.2, 0.1], [0, hb + 0.1, s * (Wset / 2 - 0.12)])\n  for (let i = 0; i <= 4; i++) k.box(SKID, [0.08, 0.2, Wset - 0.24], [-Lset / 2 + 0.04 + (i * (Lset - 0.08)) / 4, hb + 0.1, 0])\n  k.cyl('flangeSteel', 0.07, 0.08, [Lset * 0.3, hb + 0.04, Wset / 2 - 0.3], 'y', 16)\n  k.cyl('handwheel', 0.08, 0.03, [Lset * 0.3, hb + 0.09, Wset / 2 - 0.3], 'y', 16)\n  lineRun(k, [[Lset * 0.36, hb, Wset / 2 - 0.3], [Lset * 0.36, hs + 1.6, Wset / 2 - 0.3]], 1, SKID)\n  k.dome(SKID, 0.05, [Lset * 0.36, hs + 1.62, Wset / 2 - 0.3], '-y', Math.PI / 2, 1, 12)\n  k.box('stainless', [0.08, hb * 0.7, 0.02], [Lset * 0.15, hb / 2, Wset / 2 + 0.01])\n  k.box('glass', [0.04, hb * 0.6, 0.006], [Lset * 0.15, hb / 2, Wset / 2 + 0.022])\n  tagPlate(k, 'DAY TANK  DIESEL', [-Lset * 0.2, hb / 2, Wset / 2 + 0.002], 'z', 0.36)\n  for (const s of [-1, 1]) for (const sx of [-1, 1]) k.torus('nut', 0.05, 0.015, [sx * (Lset / 2 - 0.3), hb * 0.5, s * (Wset / 2 + 0.01)], 'z', 6, 14)\n  // fuel supply connection on -Z: neck through the tank wall to the flange\n  neckTo(k, [L.xFuel - L.xoff, L.yFuel - L.y0, -Wset / 2], [L.xFuel - L.xoff, L.yFuel - L.y0, L.zFuel], [0, 0, -1], p.fuelNps, 150, SKID)\n  // anti-vibration isolators\n  const iso = (x: number, z: number) => {\n    k.box('flangeSteel', [0.2, 0.02, 0.16], [x, hs + 0.01, z])\n    k.cyl('rubberBlack', 0.06, 0.08, [x, hs + 0.06, z], 'y', 14)\n    k.box('flangeSteel', [0.16, 0.02, 0.12], [x, hs + 0.11, z])\n  }\n\n  // alternator: ribbed frame, feet, DE screen, NDE exciter cover, terminal box with cables, nameplate\n  const xam = (x0 + xa1) / 2\n  k.cyl(ALT, Ra, La, [xam, yk, 0], 'x', 40)\n  for (let i = 0; i <= 6; i++) k.cyl(ALT, Ra * 1.03, 0.03, [x0 + 0.05 + (i * (La - 0.1)) / 6, yk, 0], 'x', 40)\n  k.cyl(ALT, Ra * 0.85, 0.12, [x0 - 0.06, yk, 0], 'x', 32)\n  k.dome(ALT, Ra * 0.6, [x0 - 0.12, yk, 0], '-x', Math.PI / 2, 0.3, 24)\n  const scr = new THREE.CylinderGeometry(Ra * 1.01, Ra * 1.01, La * 0.22, 32, 1, true)\n  uvScale(scr, 10, 1.5)\n  k.add(louvreMaterial('#2c3238'), scr, mat4([xa1 - La * 0.14, yk, 0], 'x'))\n  for (const s of [-1, 1]) {\n    k.box(ALT, [La * 0.7, yk - hs - 0.12 - Ra * 0.6, 0.12], [xam, (yk - Ra * 0.6 + hs + 0.12) / 2, s * Ra * 0.75])\n    for (const xx of [xam - La * 0.3, xam + La * 0.3]) iso(xx, s * Ra * 0.75)\n  }\n  const tb: V3 = [xam - La * 0.1, yk + Ra + 0.22, 0]\n  k.rbox(ALT, [La * 0.45, 0.42, Ra * 1.2], 0.03, tb)\n  k.box(ALT, [La * 0.3, 0.06, Ra * 0.9], [tb[0], yk + Ra + 0.02, 0])\n  k.plane(signMaterial('hv'), 0.3, 0.15, [tb[0], tb[1], Ra * 0.6 + 0.001])\n  for (let i = 0; i < 3; i++) {\n    const cz = -Ra * 0.35 + i * Ra * 0.35\n    k.hex('brass', 0.07, 0.05, [tb[0] - La * 0.18, tb[1], cz], 'x')\n    k.spline('cable', [[tb[0] - La * 0.21, tb[1], cz], [x0 - 0.05, tb[1] - 0.1, cz], [x0 - 0.15, hs + 0.1, cz * 1.3], [x0 - 0.1, hb + 0.02, cz * 1.5]], 0.025, 8, 18)\n  }\n  k.box('nameplate', [0.3, 0.18, 0.006], [xam + La * 0.25, yk + Ra * 0.3, Ra * 0.96])\n  // genset controller on a stand at the alternator end (+Z side)\n  const cx = x0 + 0.25\n  const cz = Wset / 2 - 0.2\n  k.rod(SKID, [cx, hs, cz], [cx, hs + 1.0, cz], 0.03, 10)\n  k.rbox('stainless', [0.5, 0.6, 0.25], 0.02, [cx, hs + 1.25, cz])\n  k.plane(lcdMaterial(), 0.26, 0.18, [cx, hs + 1.33, cz + 0.126])\n  k.cyl('actuatorRed', 0.025, 0.03, [cx + 0.17, hs + 1.08, cz + 0.135], 'z', 16)\n  k.plane(signMaterial('ler', p.tag), 0.4, 0.08, [cx, hs + 1.5, cz + 0.126])\n\n  // flywheel housing (SAE bell) with starters\n  k.lathe(ENGINE, [[Ra * 0.95, 0], [Ra * 0.9, 0.08], [bw * 0.7, 0.25], [0, 0.25]], [xa1, yk, 0], 'x', 40)\n  for (const s of [-1, 1]) k.cyl('motorPaint', 0.09, 0.4, [xe0 - 0.05, yk - Ra * 0.5, s * bw * 0.45], 'x', 16)\n\n  // engine: crankcase, oil pan, banks with rocker covers, aftercooler in the vee, exhaust manifolds, turbochargers\n  const xem = (xe0 + xe1) / 2\n  const ccTop = yk + 0.25\n  k.rbox(ENGINE, [Le, 0.6, bw], 0.04, [xem, yk - 0.05, 0])\n  const panB = Math.max(hs + 0.06, yk - 0.75)\n  k.rbox(ENGINE, [Le * 0.9, yk - 0.35 - panB, bw * 0.85], 0.03, [xem, (yk - 0.35 + panB) / 2, 0])\n  for (const s of [-1, 1]) {\n    k.box(ENGINE, [Le * 0.18, 0.08, 0.25], [xe0 + Le * 0.2, yk - 0.3, s * (bw / 2 + 0.1)])\n    k.box(ENGINE, [Le * 0.18, 0.08, 0.25], [xe1 - Le * 0.15, yk - 0.3, s * (bw / 2 + 0.1)])\n    for (const xx of [xe0 + Le * 0.2, xe1 - Le * 0.15]) {\n      k.box(SKID, [0.14, yk - 0.34 - hs - 0.12, 0.14], [xx, (yk - 0.34 + hs + 0.12) / 2, s * (bw / 2 + 0.1)])\n      iso(xx, s * (bw / 2 + 0.1))\n    }\n  }\n  const perBank = vee ? ncyl / 2 : ncyl\n  const banks = vee ? [-1, 1] : [0]\n  const bH = vee ? 0.55 : 0.6\n  const bW = vee ? bw * 0.42 : bw * 0.7\n  const pitchC = (Le * 0.88) / perBank\n  for (const s of banks) {\n    const ang = vee ? s * 0.55 : 0\n    const base: V3 = [xem, ccTop, s * bw * 0.18]\n    k.at(base, eul(ang, 0, 0), () => {\n      k.rbox(ENGINE, [Le * 0.92, bH, bW], 0.03, [0, bH / 2, 0])\n      for (let i = 0; i < perBank; i++) {\n        const x = -Le * 0.44 + pitchC * (i + 0.5)\n        k.rbox('aluminium', [pitchC * 0.86, 0.12, bW * 0.9], 0.03, [x, bH + 0.06, 0])\n        k.hex('nut', 0.035, 0.02, [x, bH + 0.13, 0])\n        // injector / fuel line\n        k.rod('tubing', [x + pitchC * 0.3, bH - 0.05, (s >= 0 ? 1 : -1) * bW * 0.5], [x + pitchC * 0.3, bH - 0.05, (s >= 0 ? 1 : -1) * bW * 0.62], 0.006, 6)\n      }\n      // exhaust manifold (insulated) along the outboard face\n      const side = vee ? s : -1\n      k.cyl('insulation', 0.09, Le * 0.9, [-Le * 0.02, bH * 0.65, side * (bW / 2 + 0.09)], 'x', 20)\n      for (let i = 0; i < perBank; i++) k.cyl('insulation', 0.05, 0.1, [-Le * 0.44 + pitchC * (i + 0.5), bH * 0.65, side * (bW / 2 + 0.03)], 'z', 12)\n    })\n  }\n  const engTop = L.engTop\n  if (vee) k.rbox('aluminium', [Le * 0.8, 0.25, bw * 0.38], 0.04, [xem, ccTop + 0.35, 0])\n  // turbochargers at the flywheel end, air cleaners on top, outlets up into the exhaust\n  const tz = vee ? [-1, 1].map((s) => s * bw * 0.48) : [-bw * 0.45]\n  const yT = engTop - 0.05\n  const xT = xe0 + 0.25\n  for (const z of tz) {\n    k.cyl('castIron', 0.17, 0.2, [xT, yT, z], 'x', 24)\n    k.cyl('aluminium', 0.2, 0.18, [xT + 0.2, yT, z], 'x', 24)\n    k.lathe('insulation', [[0.1, 0], [0.12, 0.1], [0.1, 0.25]], [xT - 0.12, yT, z], '-x', 16)\n    const hAc = clamp(L.roof - (yT + 0.2) - 0.1, 0.2, 0.45)\n    k.cyl('equipmentPaintDark', 0.17, hAc, [xT + 0.25, yT + 0.18 + hAc / 2, z], 'y', 24)\n    k.dome('equipmentPaintDark', 0.17, [xT + 0.25, yT + 0.18 + hAc, z], 'y', Math.PI / 2, 0.3, 20)\n    k.rod('rubberBlack', [xT + 0.22, yT + 0.12, z], [xT + 0.25, yT + 0.18, z], 0.08, 12)\n  }\n  // front end: damper, belts, water pumps, thermostat housing, oil and fuel filters\n  k.cyl('castIron', 0.22, 0.1, [xe1 + 0.05, yk, 0], 'x', 32)\n  k.cyl('flangeSteel', 0.14, 0.08, [xe1 + 0.14, yk, 0], 'x', 24)\n  for (const s of [-1, 1]) k.cyl(ENGINE, 0.13, 0.18, [xe1 + 0.05, yk + 0.35, s * bw * 0.3], 'x', 20)\n  k.rbox(ENGINE, [0.2, 0.18, bw * 0.5], 0.03, [xe1 - 0.05, engTop - 0.1, 0])\n  for (let i = 0; i < 3; i++) k.cyl('actuatorYellow', 0.06, 0.25, [xe0 + Le * (0.45 + i * 0.1), yk - 0.1, bw / 2 + 0.08], 'y', 16)\n  for (let i = 0; i < 2; i++) k.cyl('handwheel', 0.05, 0.2, [xe0 + Le * (0.75 + i * 0.08), yk - 0.1, -bw / 2 - 0.07], 'y', 14)\n  gauge(k, [xe0 + Le * 0.6, ccTop, bw / 2 + 0.05], 'z', 0.08, 'bar', 10)\n  lineRun(k, [[Lset * 0.36 - 0.1, hb, Wset / 2 - 0.3], [Lset * 0.36 - 0.1, yk - 0.2, Wset / 2 - 0.3], [xe0 + Le * 0.8, yk - 0.2, Wset / 2 - 0.3], [xe0 + Le * 0.8, yk - 0.2, -bw / 2 - 0.07]], 0.5, 'tubing')\n  tagPlate(k, p.tag, [xe0 + Le * 0.3, yk - 0.05, bw / 2 + 0.002], 'z', 0.25)\n  // closed crankcase ventilation filter with the fume line out towards the radiator discharge\n  const cvx = xe1 - Le * 0.18\n  k.cyl('equipmentPaintDark', 0.11, 0.3, [cvx, ccTop + 0.15, -bw / 2 - 0.12], 'y', 18)\n  k.dome('equipmentPaintDark', 0.11, [cvx, ccTop + 0.3, -bw / 2 - 0.12], 'y', Math.PI / 2, 0.4, 14)\n  k.box(ENGINE, [0.06, 0.06, 0.12], [cvx, ccTop, -bw / 2 - 0.03])\n  lineRun(k, [[cvx, ccTop + 0.34, -bw / 2 - 0.12], [cvx, ccTop + 0.45, -bw / 2 - 0.12], [Lset / 2 - 0.45, ccTop + 0.45, -bw / 2 - 0.12], [Lset / 2 - 0.45, hs + 0.2, -Wset / 2 + 0.12]], 1.5, 'rubberBlack')\n\n  // radiator: core with fins, top and bottom tanks, frame, fan shroud with guard, coolant hoses\n  const xr = Lset / 2 - 0.2\n  const Wr = Wset * 0.95\n  const yr0 = hs + 0.12\n  k.box('aluminium', [0.2, Hr, Wr - 0.2], [xr, yr0 + Hr / 2, 0])\n  const core = new THREE.PlaneGeometry(Wr - 0.2, Hr)\n  uvScale(core, 1, Hr / 0.02)\n  k.add(louvreMaterial('#8a9196'), core, mat4([xr + 0.101, yr0 + Hr / 2, 0], faceQ('x')))\n  k.box(SKID, [0.3, 0.14, Wr], [xr, yr0 + Hr + 0.07, 0])\n  k.box(SKID, [0.3, 0.12, Wr], [xr, yr0 - 0.06, 0])\n  for (const s of [-1, 1]) k.box(SKID, [0.3, Hr + 0.26, 0.1], [xr, yr0 + Hr / 2, s * (Wr / 2 - 0.05)])\n  for (const s of [-1, 1]) k.rod(SKID, [xr - 0.12, yr0 + Hr, s * (Wr / 2 - 0.05)], [xe1 - 0.1, engTop, s * bw * 0.4], 0.025, 8)\n  k.cyl('brass', 0.05, 0.08, [xr, yr0 + Hr + 0.18, Wr * 0.3], 'y', 14)\n  const fr = Math.min(Hr, Wr) * 0.43\n  const yf = yr0 + Hr / 2\n  k.box(SKID, [0.04, Hr, Wr - 0.2], [xr - 0.12, yf, 0])\n  k.lathe(SKID, [[fr + 0.04, 0], [fr + 0.04, 0.22], [fr * 1.2, 0.3]], [xr - 0.42, yf, 0], 'x', 40)\n  k.disc(fanGuardMaterial(), fr + 0.05, [xr - 0.43, yf, 0], '-x', 32)\n  rotors.push(fanRotor('safetyYellow', fr * 0.92, [xr - 0.3, yf, 0], 'x', 300, { blades: 8, chord: fr * 0.32, pitch: 0.4, hub: fr * 0.18, hubLen: 0.1 }))\n  k.rod('flangeSteel', [xe1 + 0.14, yf, 0], [xr - 0.36, yf, 0], 0.04, 12)\n  lineRun(k, [[xe1 - 0.05, engTop - 0.05, 0], [xe1 + 0.2, engTop - 0.05, 0], [xe1 + 0.2, yr0 + Hr + 0.05, Wr * 0.3], [xr - 0.12, yr0 + Hr + 0.05, Wr * 0.3]], 4, 'rubberBlack')\n  lineRun(k, [[xr - 0.12, yr0 + 0.08, -Wr * 0.3], [xe1 + 0.2, yr0 + 0.08, -Wr * 0.3], [xe1 + 0.2, yk + 0.35, -bw * 0.3], [xe1 + 0.14, yk + 0.35, -bw * 0.3]], 4, 'rubberBlack')\n  // batteries and charger\n  k.rbox('plasticGrey', [0.6, 0.3, 0.35], 0.02, [x0 + 0.5, hs + 0.15, -Wset / 2 + 0.3])\n  k.rbox('stainless', [0.3, 0.4, 0.15], 0.02, [x0 + 0.5, hs + 0.55, -Wset / 2 + 0.2])\n}\n\n/** Exhaust: turbo outlets up through bellows into the silencer, silencer outlet riser to the exhaust flange. */\nfunction exhaust(k: Kit, L: ReturnType<typeof layout>) {\n  const { exNps, Ds, Lsil, xSil, ySil, xEx, yEx, xoff, y0, Wset, Lset, box } = L\n  const od = pipeOD(exNps)\n  // silencer: insulated drum with flanged ends\n  const x0 = xSil - Lsil / 2\n  const x1 = xSil + Lsil / 2\n  k.cyl('stainless', Ds / 2, Lsil, [xSil, ySil, 0], 'x', 40)\n  for (const x of [x0, x1]) {\n    k.dome('stainless', Ds / 2, [x, ySil, 0], x > xSil ? 'x' : '-x', Math.PI / 2, 0.35, 32)\n    k.torus('stainless', Ds / 2 + 0.005, 0.01, [x, ySil, 0], 'x', 6, 40)\n  }\n  for (let i = 1; i < 4; i++) k.torus('stainless', Ds / 2 + 0.004, 0.008, [x0 + (i * Lsil) / 4, ySil, 0], 'x', 6, 40)\n  tagPlate(k, 'EXHAUST SILENCER', [xSil, ySil, Ds / 2 + 0.002], 'z', 0.3)\n  // silencer supports\n  const yb = box ? L.CH : L.engTop + 0.05\n  for (const xx of [x0 + Lsil * 0.2, x1 - Lsil * 0.2]) {\n    k.box(SKID, [0.12, ySil - yb - Ds * 0.3, Ds * 0.7], [xx, (ySil - Ds * 0.3 + yb) / 2, 0])\n    k.box(SKID, [0.25, 0.02, Ds * 0.9], [xx, yb + 0.01, 0])\n  }\n  if (!box) {\n    // support frame from the skid rails up to the silencer cradles\n    const yr = L.hs\n    for (const xx of [x0 + Lsil * 0.2, x1 - Lsil * 0.2])\n      for (const s of [-1, 1]) k.box(SKID, [0.1, yb - yr, 0.1], [xx + xoff, (yb + yr) / 2 + y0, s * (Wset / 2 - 0.12)])\n    for (const xx of [x0 + Lsil * 0.2, x1 - Lsil * 0.2]) k.box(SKID, [0.12, 0.12, Wset - 0.14], [xx, yb + 0.06, 0])\n  }\n  // outlet riser to the exhaust flange (port), rain-proof flanged end\n  const sOut: V3 = [xEx, ySil + Ds * 0.4, 0]\n  annulus(k, 'stainless', od / 2, od * 0.7, 0.06, [xEx, ySil + Ds / 2 - 0.01, 0], 'y', 28)\n  runToFlange(k, [sOut], [xEx, yEx, 0], [0, 1, 0], exNps, 150, 'stainless')\n  // inlet from the turbos: drops to the turbo outlet(s) through a bellows\n  const xi = x0 + Ds * 0.4\n  const xT = xoff - Lset / 2 + 0.15 + L.La + 0.25 + 0.25\n  const yT = y0 + L.engTop + 0.15\n  const iN = Math.max(6, exNps - 2)\n  const iod = pipeOD(iN)\n  lineRun(k, [[xi, ySil - Ds * 0.3, 0], [xi, ySil - Ds / 2 - 0.3, 0], [xT, ySil - Ds / 2 - 0.3, 0], [xT, yT + 0.35, 0]], iN, 'insulation')\n  k.lathe('rubberBlack', [[iod / 2 + 0.01, 0], [iod / 2 + 0.04, 0.05], [iod / 2 + 0.01, 0.1], [iod / 2 + 0.04, 0.15], [iod / 2 + 0.01, 0.2]], [xT, yT + 0.12, 0], 'y', 24)\n  const tz = L.vee ? [-1, 1].map((s) => s * L.bw * 0.48) : [-L.bw * 0.45]\n  for (const z of tz) lineRun(k, [[xT, yT + 0.12, 0], [xT, yT + 0.12, z * 0.6], [xT, yT, z]], Math.max(4, iN - 2), 'insulation')\n  if (box) annulus(k, 'flangeSteel', iod / 2, iod / 2 + 0.08, 0.06, [xT, L.CH - 0.03, 0], 'y', 24)\n  void pipeWall\n}\n\n/** ISO 668 high-cube container: corrugated walls, corner castings, cargo doors, side doors, intake louvres, radiator discharge louvre. */\nfunction container(k: Kit, p: P, L: ReturnType<typeof layout>) {\n  const { CL, CW, CH } = L\n  const C: MatKey = 'equipmentPaint'\n  const R: MatKey = 'equipmentPaintDark'\n  const x0 = -CL / 2\n  const x1 = CL / 2\n  // bottom side rails, floor, cross members, corner posts, top rails, roof\n  for (const s of [-1, 1]) {\n    k.box(R, [CL, 0.16, 0.1], [0, 0.08, s * (CW / 2 - 0.05)])\n    k.box(R, [CL, 0.1, 0.06], [0, CH - 0.05, s * (CW / 2 - 0.03)])\n  }\n  k.box('deckPlate', [CL, 0.03, CW - 0.1], [0, FLOOR - 0.015, 0])\n  for (const sx of [-1, 1])\n    for (const sz of [-1, 1]) {\n      k.box(R, [0.16, CH, 0.16], [sx * (CL / 2 - 0.08), CH / 2, sz * (CW / 2 - 0.08)])\n      for (const y of [0.06, CH - 0.06]) {\n        k.box('castIron', [0.178, 0.118, 0.162], [sx * (CL / 2 - 0.089), y, sz * (CW / 2 - 0.081)])\n        k.box('rubberBlack', [0.01, 0.05, 0.08], [sx * (CL / 2 + 0.001), y, sz * (CW / 2 - 0.081)])\n      }\n    }\n  k.box(C, [CL - 0.3, 0.04, CW - 0.12], [0, CH - 0.06, 0])\n  for (let x = x0 + 0.3; x < x1 - 0.2; x += 0.6) k.box(C, [0.24, 0.025, CW - 0.2], [x, CH - 0.03, 0])\n  // corrugated side walls with openings: intake louvres near the alternator, personnel doors, fuel penetration\n  const H = CH - 0.26\n  const yMid = 0.16 + H / 2\n  const xi = L.xoff - L.Lset / 2 + L.La * 0.5\n  const xd = Math.min(L.xoff + L.Lset * 0.1, x1 - 1.2)\n  const openings = (s: number): [number, number][] => (s > 0 ? [[xi, 1.4], [xd, 1.0]] : [[xi, 1.4], [xd + (CL > 7 ? 0 : -0.2), 1.0]])\n  for (const s of [-1, 1]) {\n    const z = s * (CW / 2 - 0.03)\n    const ops = openings(s).sort((a, b) => a[0] - b[0])\n    let a = x0 + 0.16\n    for (const [c, w] of [...ops, [x1 - 0.16 + 10, 0] as [number, number]]) {\n      const b = Math.min(c - w / 2, x1 - 0.16)\n      if (b > a) {\n        k.box(C, [b - a, H, 0.02], [(a + b) / 2, yMid, z])\n        for (let x = a + 0.14; x < b - 0.07; x += 0.278) k.box(C, [0.1, H, 0.036], [x, yMid, z + s * 0.02])\n      }\n      a = c + w / 2\n    }\n    for (const [c, w] of ops) {\n      const hh = c === xi ? 1.2 : 2.05\n      k.box(C, [w, H - hh, 0.02], [c, 0.16 + hh + (H - hh) / 2, z])\n      if (c === xi) {\n        k.at([c, 0.16, z], faceQ(s > 0 ? 'z' : '-z'), () => louvre(k, w - 0.1, hh - 0.1, 0, hh / 2, R))\n        // sand-trap intake louvre with motorised damper actuator\n        k.rbox('actuatorBlue', [0.14, 0.12, 0.1], 0.015, [c + w / 2 - 0.15, 0.16 + hh + 0.12, z + s * 0.08])\n      } else k.at([c, FLOOR, z], faceQ(s > 0 ? 'z' : '-z'), () => door(k, w - 0.1, 2.0, 0, false, C, R, p.doorsOpen ? 1.7 : 0))\n    }\n  }\n  // -X end: cargo doors with locking bars\n  k.at([x0, 0.16, 0], faceQ('-x'), () => {\n    for (const s of [-1, 1]) {\n      k.box(C, [CW / 2 - 0.1, H, 0.03], [s * (CW / 4 - 0.02), H / 2, 0.01])\n      for (const xx of [0.3, 0.75]) k.rod('flangeSteel', [s * (CW / 4 - 0.02) + s * (xx - 0.5) * 0.9, 0.1, 0.05], [s * (CW / 4 - 0.02) + s * (xx - 0.5) * 0.9, H - 0.1, 0.05], 0.014, 8)\n      for (let i = 0; i < 4; i++) k.box(C, [CW / 2 - 0.2, 0.08, 0.02], [s * (CW / 4 - 0.02), 0.3 + (i * (H - 0.6)) / 3, 0.035])\n    }\n  })\n  // +X end: radiator discharge louvre with attenuator frame\n  k.box(C, [0.03, 0.15, CW - 0.2], [x1 - 0.02, CH - 0.2, 0])\n  k.at([x1, 0.16, 0], faceQ('x'), () => louvre(k, CW - 0.4, H - 0.3, 0, (H - 0.3) / 2 + 0.06, R))\n  // clean-agent fire suppression cylinder and release panel inside, by the door; smoke / heat detectors on the ceiling\n  k.cyl('actuatorRed', 0.13, 1.1, [xd - 0.85, FLOOR + 0.55, CW / 2 - 0.25], 'y', 20)\n  k.dome('actuatorRed', 0.13, [xd - 0.85, FLOOR + 1.1, CW / 2 - 0.25], 'y', Math.PI / 2, 0.7, 16)\n  k.cyl('brass', 0.03, 0.12, [xd - 0.85, FLOOR + 1.25, CW / 2 - 0.25], 'y', 10)\n  for (const sz of [-1, 1]) k.box(R, [0.04, 0.04, 0.2], [xd - 0.85, FLOOR + 0.5 + sz * 0.3, CW / 2 - 0.15])\n  k.tube('tubing', [[xd - 0.85, FLOOR + 1.31, CW / 2 - 0.25], [xd - 0.85, CH - 0.12, CW / 2 - 0.25], [L.xoff, CH - 0.12, CW / 2 - 0.25]], 0.012, 6, 0.05)\n  for (const x of [L.xoff - L.Lset * 0.3, L.xoff + L.Lset * 0.2]) k.cyl('plasticGrey', 0.05, 0.05, [x, CH - 0.115, 0], 'y', 12)\n  // fuel penetration plate, signs, tag\n  k.box(R, [0.3, 0.3, 0.02], [L.xFuel, L.yFuel, -CW / 2 - 0.01])\n  k.plane(signMaterial('ler', `${p.tag}  EMERGENCY GENERATOR`), Math.min(2.2, CL * 0.35), 0.35, [L.xoff - L.Lset * 0.15, CH - 0.45, CW / 2 + 0.03])\n  k.plane(signMaterial('hv'), 0.4, 0.2, [xd + 0.8, 1.5, CW / 2 + 0.03])\n  tagPlate(k, p.tag, [xd - 0.8, 1.5, CW / 2 + 0.03], 'z', 0.25)\n  // roof access ladder at the -X end\n  ladder(k, [x0 + 0.5, 0, -CW / 2 - 0.2], CH, [0, 0, -1], { cage: false })\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": "DieselGenerator",
    "anchor": "base",
    "standards": [
      "ISO 8528",
      "IEC 60034-1",
      "ISO 3046",
      "ISO 668 (container)",
      "NFPA 110",
      "ASME B16.5"
    ]
  }
}