{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "packaged-boiler",
  "type": "registry:component",
  "title": "Packaged fire-tube steam boiler",
  "description": "Three-pass wetback shell (fire-tube) steam boiler on a skid and housekeeping pad: ribbed clad shell on saddles, bolted front and rear smoke-box doors with davits, flue outlet and stack, duoblock burner with windbox, FD fan with motor and inlet silencer, gas train (isolation, filter, regulator, double block shut-offs), main steam stop valve, twin safety valves with escape pipes, manway, water column with two gauge glasses and level controls, feedwater and bottom-blowdown connections, control panel, and an optional top platform with caged ladder. Shell size scales with `steam` (t/h).",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json",
    "https://deck-twin-library.demoareeb.workers.dev/r/gate-valve.json"
  ],
  "files": [
    {
      "path": "onshore/packaged-boiler.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/onshore/packaged-boiler.tsx",
      "content": "// Packaged three-pass wetback fire-tube steam boiler on a skid: duoblock burner with windbox and FD fan, gas train, trim and stack. EN 12953, ASME Section I.\nimport * as THREE from 'three'\nimport type { Kit, V3 } from '../lib/kit'\nimport { KitMesh, useKit, type GroupProps } from '../lib/mesh'\nimport { gasket, gauge, grating, handrail, ladder, manway, member, nozzle, pipeRun, tagPlate } from '../lib/parts'\nimport { paint } from '../lib/materials'\nimport { port, type Port } from '../lib/ports'\nimport { faceToFace, pipeOD, type Rating } from '../lib/standards'\nimport { louvreMaterial, textMaterial } from '../lib/textures'\nimport { GateValve } from '../valves/gate-valve'\nimport { anchorBolt, clamp, earthBond, plinth, ring } from './_onshore'\n\nexport interface PackagedBoilerProps {\n  /** steam output, t/h */\n  steam?: number\n  /** design pressure, barg */\n  pressure?: number\n  rating?: Rating\n  fuel?: 'gas' | 'dual'\n  /** top access platform with ladder */\n  platform?: boolean\n  tag?: string\n}\n\nexport const defaults = { steam: 8, pressure: 12, rating: 300 as Rating, fuel: 'gas' as 'gas' | 'dual', platform: true, tag: '' }\n\nfunction layout(p: typeof defaults) {\n  const t = clamp(p.steam, 1, 25)\n  // typical shell sizes for three-pass wetback units (8 t/h ≈ 2.4 m OD × 5.8 m)\n  const D = clamp(1.3 + 0.14 * t, 1.4, 4.2)\n  const L = clamp(2.4 * D, 3, 9.5)\n  const R = D / 2\n  const yS = 0.15 + 0.22 + 0.3 + R\n  const steamNps = t <= 3 ? 3 : t <= 6 ? 4 : t <= 10 ? 6 : t <= 16 ? 8 : 10\n  const feedNps = t <= 10 ? 2 : 3\n  const bdNps = t <= 6 ? 1.5 : 2\n  const fuelNps = t <= 4 ? 1.5 : t <= 12 ? 2 : 3\n  const sx = 0.12 * L\n  const steamTop = yS + R + 0.25\n  const Lv = faceToFace('gate', steamNps, p.rating)\n  const feed = { base: [0.12 * L, yS + 0.3 * R, Math.sqrt(R * R - (0.3 * R) ** 2)] as V3, standout: 0.25 }\n  const bd = { x: 0.38 * L, y: 0.38, z: R + 0.35 }\n  const gas = { x: -L / 2 + 1.0, y: 0.95, z: R + 0.75 }\n  return { t, D, L, R, yS, steamNps, feedNps, bdNps, fuelNps, sx, steamTop, Lv, feed, bd, gas }\n}\n\nexport function ports(props: PackagedBoilerProps = {}): Port[] {\n  const p = { ...defaults, ...props }\n  const g = layout(p)\n  return [\n    port('steam', 'Main steam', [g.sx, g.steamTop + 0.0045 + g.Lv, 0], [0, 1, 0], g.steamNps, p.rating),\n    port('feedwater', 'Feedwater', [g.feed.base[0], g.feed.base[1], g.feed.base[2] + g.feed.standout], [0, 0, 1], g.feedNps, p.rating),\n    port('blowdown', 'Bottom blowdown', [g.bd.x, g.bd.y, g.bd.z], [0, 0, 1], g.bdNps, p.rating, 'drain'),\n    port('fuel', 'Fuel gas', [g.gas.x, g.gas.y, g.gas.z], [0, 0, 1], g.fuelNps, 150),\n  ]\n}\n\nexport function PackagedBoiler(props: PackagedBoilerProps & GroupProps) {\n  const { steam, pressure, rating, fuel, platform, tag, ...group } = { ...defaults, ...props }\n  const p = { steam, pressure, rating, fuel, platform, tag }\n  const g = layout(p)\n  const data = useKit((k) => build(k, p, g), [steam, pressure, rating, fuel, platform, tag])\n  return (\n    <group {...group}>\n      <KitMesh data={data} />\n      <GateValve nps={g.steamNps} rating={rating} operator={g.steamNps >= 8 ? 'gear' : 'handwheel'} position={[g.sx, g.steamTop + 0.0045 + g.Lv / 2, 0]} rotation={[0, 0, Math.PI / 2]} tag={tag ? `${tag}-MSV` : ''} />\n    </group>\n  )\n}\n\nexport default PackagedBoiler\n\ntype P = typeof defaults\ntype G = ReturnType<typeof layout>\n\nfunction build(k: Kit, p: P, g: G) {\n  const { D, L, R, yS, t } = g\n  const jacket = paint('#c4c9cc', { metalness: 0.55, roughness: 0.35, clearcoat: 0.2 })\n  const head = paint('#a83226', { clearcoat: 0.4 })\n  const burnerC = paint('#2f4f7a', { clearcoat: 0.5 })\n  const SK = 'structSteelDark'\n  // housekeeping pad, skid, saddles\n  const xf = -L / 2 - 0.55\n  const xr = L / 2 + 0.45\n  const skL = xr - xf + 2.2\n  const skC = (xf - 2.0 + xr + 0.2) / 2\n  plinth(k, skC, 0, skL + 0.4, D + 1.4, 0.15)\n  const hk = 0.22\n  for (const s of [-1, 1]) member(k, [skC - skL / 2, 0.15 + hk / 2, s * (R * 0.7)], [skC + skL / 2, 0.15 + hk / 2, s * (R * 0.7)], 'HEB 220', SK)\n  for (let i = 0; i <= 4; i++) {\n    const x = skC - skL / 2 + 0.11 + (i * (skL - 0.22)) / 4\n    member(k, [x, 0.15 + hk / 2, -R * 0.7 + 0.11], [x, 0.15 + hk / 2, R * 0.7 - 0.11], 'HEB 220', SK)\n  }\n  for (const s of [-1, 1]) for (const x of [skC - skL / 2 + 0.2, skC + skL / 2 - 0.2]) anchorBolt(k, [x, 0.15 + hk, s * (R * 0.7 + 0.08)], 0.02)\n  for (const x of [-L * 0.32, L * 0.32]) {\n    const sh = new THREE.Shape()\n    const w = R * 1.6\n    sh.moveTo(-w / 2, 0)\n    sh.lineTo(w / 2, 0)\n    sh.lineTo(w / 2, 0.3 + R * 0.35)\n    sh.absarc(0, 0.3 + R, R * 1.02, -Math.PI / 2 + 0.83, -Math.PI / 2 - 0.83, true)\n    sh.lineTo(-w / 2, 0.3 + R * 0.35)\n    sh.closePath()\n    k.extrude(SK, sh, 0.3, new THREE.Matrix4().makeBasis(new THREE.Vector3(0, 0, 1), new THREE.Vector3(0, 1, 0), new THREE.Vector3(-1, 0, 0)).setPosition(x + 0.15, 0.15 + hk, 0))\n  }\n  // shell with ribbed cladding, front and rear smoke boxes / doors\n  k.cyl(jacket, R + 0.08, L, [0, yS, 0], 'x', 64)\n  for (let i = 0; i <= Math.round(L / 0.9); i++) k.torus(jacket, R + 0.085, 0.012, [-L / 2 + (i * L) / Math.round(L / 0.9), yS, 0], 'x', 6, 64)\n  k.cyl(head, R + 0.02, 0.5, [-L / 2 - 0.25, yS, 0], 'x', 64)\n  k.cyl(head, R + 0.06, 0.08, [-L / 2 - 0.5, yS, 0], 'x', 64)\n  k.cyl(head, R, 0.4, [L / 2 + 0.2, yS, 0], 'x', 64)\n  k.lathe(head, [[R, 0], [R * 0.9, 0.12], [R * 0.55, 0.22], [0, 0.25]], [L / 2 + 0.4, yS, 0], 'x', 48)\n  for (const [x, ax] of [[-L / 2 - 0.5, '-x'], [L / 2 + 0.4, 'x']] as const) {\n    k.boltCircle('boltXylan', 'nut', 16, R - 0.02, [x - (ax === 'x' ? 0.05 : -0.05), yS, 0], ax, 0.02, 0.08, [0.06])\n    // davit hinge for the door\n    k.rod(head, [x, yS + R * 0.5, R + 0.05], [x, yS - R * 0.5, R + 0.05], 0.04, 10)\n  }\n  k.cyl('glass', 0.04, 0.02, [L / 2 + 0.64, yS, 0], 'x', 16)\n  // flue outlet on the rear smoke box and stack\n  const stackR = clamp(0.18 + 0.03 * t, 0.25, 0.8)\n  const stackTop = yS + R + 3.5 + 0.15 * t\n  ring(k, head, stackR, stackR + 0.08, 0.06, [L / 2 + 0.2, yS + R + 0.18, 0], 'y')\n  k.cyl(head, stackR + 0.02, 0.18, [L / 2 + 0.2, yS + R + 0.09, 0], 'y', 32, undefined, true)\n  k.pipe('galvanised', [L / 2 + 0.2, yS + R + 0.21, 0], [L / 2 + 0.2, stackTop, 0], stackR * 2, 0.006, 40)\n  ring(k, 'galvanised', stackR, stackR + 0.07, 0.05, [L / 2 + 0.2, yS + R + 0.24, 0], 'y')\n  for (let y = yS + R + 1.5; y < stackTop - 0.5; y += 1.5) ring(k, 'galvanised', stackR, stackR + 0.03, 0.04, [L / 2 + 0.2, y, 0], 'y')\n  k.cyl('stainless', 0.02, 0.15, [L / 2 + 0.2 + stackR + 0.07, yS + R + 1.0, 0], 'x', 8)\n\n  // duoblock burner: windbox on the front door, FD fan with motor and inlet silencer on the skid front\n  const xb = -L / 2 - 0.55\n  const yb = yS - 0.12 * D\n  const wb = clamp(0.5 + 0.06 * t, 0.7, 1.8)\n  k.rbox(burnerC, [0.55, wb, wb], 0.04, [xb - 0.28, yb, 0])\n  k.cyl(burnerC, wb * 0.32, 0.5, [xb - 0.8, yb, 0], 'x', 32)\n  k.cyl('stainless', 0.06, 0.6, [xb - 1.2, yb, 0], 'x', 16)\n  k.box('actuatorBlue', [0.14, 0.14, 0.14], [xb - 0.5, yb + wb * 0.5 + 0.07, 0.15])\n  k.cyl('glass', 0.03, 0.04, [xb - 0.56, yb + 0.2, wb * 0.5 + 0.01], 'z', 14)\n  const fx = xb - 0.28 - 0.3 * wb\n  const fz = -(R + 0.25)\n  const fy = 0.15 + wb * 0.62\n  const yd = yb + wb / 2 + 0.15\n  const ox = fx + 0.3 * wb\n  k.cyl(burnerC, wb * 0.5, wb * 0.45, [fx, fy, fz], 'z', 40)\n  k.box(burnerC, [wb * 0.35, yd - fy, wb * 0.45], [ox, (fy + yd) / 2, fz])\n  k.box(burnerC, [wb * 0.35, 0.3, -fz + wb * 0.22], [ox, yd, fz / 2 - wb * 0.11])\n  k.cyl(burnerC, wb * 0.18, 0.08, [fx, fy, fz - wb * 0.27], 'z', 28)\n  k.cyl('motorPaint', wb * 0.2, wb * 0.55, [fx, fy, fz - wb * 0.57], 'z', 28)\n  const sil = new THREE.CylinderGeometry(wb * 0.26, wb * 0.26, 0.5, 28, 1, false)\n  const uv = sil.getAttribute('uv') as THREE.BufferAttribute\n  for (let i = 0; i < uv.count; i++) uv.setXY(i, uv.getX(i) * 6, uv.getY(i) * 3)\n  k.add(louvreMaterial('#5a6f8a'), sil, new THREE.Matrix4().compose(new THREE.Vector3(fx - wb * 0.5 - 0.25, fy, fz), new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), Math.PI / 2), new THREE.Vector3(1, 1, 1)))\n  k.box(SK, [wb * 1.1, fy - wb * 0.5 - 0.15, wb * 0.5], [fx, (fy - wb * 0.5 + 0.15) / 2, fz])\n  // gas train: ball valve, filter, regulator, double block shut-offs, pressure switches, into the burner head\n  const gy = g.gas.y\n  const gz = g.gas.z - 0.25\n  const gnps = g.fuelNps\n  const god = pipeOD(gnps)\n  const face = nozzle(k, { base: [g.gas.x, gy, gz], dir: [0, 0, 1], nps: gnps, rating: 150, standout: 0.25, mat: 'safetyYellow' })\n  void face\n  const gx1 = xb - 0.8\n  pipeRun(k, [[g.gas.x, gy, gz], [g.gas.x, gy, gz - 0.15], [gx1, gy, gz - 0.15], [gx1, gy, wb * 0.25], [gx1, yb - 0.2, wb * 0.25], [gx1 + 0.2, yb - 0.2, wb * 0.25]], gnps, { mat: 'safetyYellow' })\n  const comps: [number, string, number][] = [[0.15, 'ball', 0.08], [0.45, 'filter', 0.12], [0.8, 'reg', 0.16], [1.15, 'ssov', 0.13], [1.45, 'ssov', 0.13]]\n  for (const [d, kind, s] of comps) {\n    const x = g.gas.x - 0.1 - d\n    if (x < gx1 + 0.2) continue\n    if (kind === 'ball') {\n      k.cyl('valveBody', god * 0.8, s * 1.5, [x, gy, gz - 0.15], 'x', 18)\n      k.box('actuatorRed', [0.2, 0.02, 0.03], [x, gy + god * 0.8 + 0.03, gz - 0.07])\n    } else if (kind === 'filter') k.rbox('aluminium', [s * 1.4, s * 1.6, s * 1.4], 0.02, [x, gy, gz - 0.15])\n    else if (kind === 'reg') {\n      k.cyl('castIron', god * 0.9, s * 1.4, [x, gy, gz - 0.15], 'x', 18)\n      k.cyl(paint('#3d5f8f'), s * 0.9, s * 0.35, [x, gy + god + s * 0.2, gz - 0.15], 'y', 28)\n    } else {\n      k.rbox('aluminium', [s * 1.3, s * 1.1, s * 1.1], 0.02, [x, gy, gz - 0.15])\n      k.rbox('actuatorYellow', [s * 0.9, s * 1.2, s * 0.9], 0.02, [x, gy + s * 1.15, gz - 0.15])\n    }\n  }\n  for (const x of [g.gas.x - 0.05, gx1 + 0.12]) {\n    k.box('galvanised', [0.06, gy - god / 2 - 0.15, 0.06], [x, 0.15 + (gy - god / 2 - 0.15) / 2, gz - 0.15])\n    k.box('galvanised', [0.2, 0.012, 0.2], [x, 0.156, gz - 0.15])\n  }\n  if (p.fuel === 'dual') {\n    k.tube('stainless', [[gx1 - 0.3, 0.6, -wb * 0.2], [gx1 - 0.3, yb - 0.1, -wb * 0.2], [xb - 0.95, yb - 0.1, -0.05]], 0.012)\n    k.rbox(paint('#7a3b1d'), [0.4, 0.35, 0.3], 0.02, [gx1 - 0.3, 0.6, -wb * 0.2])\n  }\n  // control panel beside the burner\n  const cx = xf - 1.55\n  const cz = R + 0.15\n  k.rbox(paint('#d9dbd6'), [0.8, 1.7, 0.35], 0.02, [cx, 0.15 + 0.1 + 0.85, cz])\n  k.plane(textMaterial('BOILER  ' + Math.round(t) + ' t/h', '#0e1b2a', '#8fd0ff', 512, 96), 0.4, 0.08, [cx, 0.15 + 1.45, cz + 0.176])\n  for (const [i, c] of ['#27c24a', '#e5b114', '#d22a1f'].entries()) k.cyl(paint(c, { emissive: c, emissiveIntensity: 0.5 }), 0.02, 0.02, [cx - 0.15 + i * 0.15, 0.15 + 1.25, cz + 0.18], 'z', 12)\n  k.box(SK, [0.9, 0.1, 0.45], [cx, 0.2, cz])\n  k.tube('cable', [[cx, 0.3, cz - 0.17], [cx + 0.3, 0.3, cz - 0.4], [xb - 0.6, yb - wb * 0.5 + 0.05, wb * 0.3]], 0.015)\n  // top: main steam nozzle, safety valves with escape pipes, manway, air vent, pressure trim\n  nozzle(k, { base: [g.sx, yS + R + 0.06, 0], dir: [0, 1, 0], nps: g.steamNps, rating: p.rating, standout: g.steamTop - (yS + R + 0.06), mat: jacket })\n  gasket(k, g.steamNps, p.rating, [g.sx, g.steamTop + 0.00225, 0], 'y')\n  for (const dz of [-0.35, 0.35]) {\n    const x = 0.3 * L\n    const vNps = g.steamNps >= 6 ? 3 : 2\n    nozzle(k, { base: [x, yS + R + 0.05, dz], dir: [0, 1, 0], nps: vNps, rating: p.rating, standout: 0.22, mat: jacket })\n    const yv = yS + R + 0.27\n    k.cyl('valveBody', pipeOD(vNps) * 0.75, 0.25, [x, yv + 0.125, dz], 'y', 20)\n    k.cyl('valveBody', pipeOD(vNps) * 0.5, 0.35, [x, yv + 0.42, dz], 'y', 16)\n    k.cyl('actuatorRed', pipeOD(vNps) * 0.35, 0.08, [x, yv + 0.63, dz], 'y', 14)\n    k.rod('handwheel', [x, yv + 0.55, dz], [x + 0.18, yv + 0.62, dz], 0.008, 6)\n    pipeRun(k, [[x, yv + 0.15, dz], [x + 0.3, yv + 0.15, dz], [x + 0.3, yv + 2.4, dz]], vNps + 1, { mat: 'galvanised' })\n    k.cyl('galvanised', pipeOD(vNps + 1) * 0.85, 0.3, [x + 0.3, yv + 0.5, dz], 'y', 16)\n  }\n  manway(k, [-0.15 * L, yS + R + 0.05, 0], [0, 1, 0], 0.15, 16, 300, jacket)\n  k.cyl('valveBody', 0.03, 0.25, [0.42 * L, yS + R + 0.15, 0], 'y', 10)\n  k.handwheel('handwheel', 0.1, [0.42 * L, yS + R + 0.29, 0], 'y', 4)\n  k.tube('stainless', [[-0.3 * L, yS + R + 0.05, 0.3], [-0.3 * L, yS + R + 0.25, 0.3], [-0.3 * L + 0.1, yS + R + 0.25, 0.3], [-0.3 * L + 0.1, yS + R + 0.35, 0.3]], 0.008)\n  gauge(k, [-0.3 * L + 0.1, yS + R + 0.42, 0.3], 'z', 0.15, 'bar', 25)\n  for (const dx of [0.2, 0.35]) k.box('stainless', [0.1, 0.12, 0.1], [-0.3 * L + dx, yS + R + 0.12, 0.3])\n  // water column with two gauge glasses and level controls on the +Z side near the front\n  const wx = -0.38 * L\n  const wz = R + 0.3\n  const wy0 = yS - 0.1 * R\n  const wy1 = yS + 0.55 * R\n  k.cyl('castIron', 0.05, wy1 - wy0 + 0.3, [wx, (wy0 + wy1) / 2, wz], 'y', 16)\n  for (const yy of [wy0, wy1]) pipeRun(k, [[wx, yy, wz], [wx, yy, R * Math.sqrt(1 - ((yy - yS) / R) ** 2) * 0.98]], 1)\n  for (const dx of [-0.16, 0.16]) {\n    k.box('stainless', [0.06, wy1 - wy0, 0.05], [wx + dx, (wy0 + wy1) / 2, wz + 0.04])\n    k.box('glass', [0.025, wy1 - wy0 - 0.1, 0.01], [wx + dx, (wy0 + wy1) / 2, wz + 0.07])\n    for (const yy of [wy0, wy1]) k.rod('stainless', [wx, yy - 0.05 + (yy === wy0 ? 0.1 : -0.1) * 0.5, wz], [wx + dx, yy - 0.05 + (yy === wy0 ? 0.1 : -0.1) * 0.5, wz + 0.04], 0.012, 8)\n  }\n  for (const yy of [yS + 0.05 * R, yS + 0.3 * R]) {\n    k.cyl('castIron', 0.09, 0.3, [wx + 0.45, yy, wz], 'y', 20)\n    k.rod('castIron', [wx, yy, wz], [wx + 0.45, yy, wz], 0.025, 8)\n    k.rbox('actuatorBlue', [0.12, 0.12, 0.1], 0.01, [wx + 0.45, yy + 0.22, wz])\n  }\n  // feedwater nozzle with check and stop valves on the neck\n  nozzle(k, { base: g.feed.base, dir: [0, 0, 1], nps: g.feedNps, rating: p.rating, standout: g.feed.standout, mat: jacket })\n  // bottom blowdown: drain from the shell bottom, two blowdown valves, flanged outlet\n  const bnps = g.bdNps\n  const yBot = yS - R\n  pipeRun(k, [[g.bd.x, yBot + 0.05, 0], [g.bd.x, g.bd.y, 0], [g.bd.x, g.bd.y, g.bd.z - 0.2]], bnps)\n  nozzle(k, { base: [g.bd.x, g.bd.y, g.bd.z - 0.2], dir: [0, 0, 1], nps: bnps, rating: p.rating, standout: 0.2, mat: 'pipePaint' })\n  for (const z of [R * 0.35, R * 0.75]) {\n    k.cyl('valveBody', pipeOD(bnps) * 0.85, 0.14, [g.bd.x, g.bd.y, z], 'z', 16)\n    k.rod('handwheel', [g.bd.x, g.bd.y + 0.05, z], [g.bd.x + 0.35, g.bd.y + 0.15, z], 0.012, 6)\n  }\n  // top access platform and ladder at the rear\n  if (p.platform) {\n    const yp = yS + R + 0.12\n    const zc = -R + 0.2\n    const x0 = 0.02 * L\n    const x1 = L / 2 - 0.1\n    for (const x of [x0 + 0.1, (x0 + x1) / 2, x1 - 0.1]) {\n      const zs = -Math.sqrt(R * R - (R * 0.35) ** 2)\n      k.box(SK, [0.06, yp - (yS - zs * 0) - R * 0.94 + 0.06, 0.06], [x, (yp + yS + R * 0.94) / 2 - 0.03, -R * 0.35])\n      member(k, [x, yp - 0.08, -R * 0.15], [x, yp - 0.08, zc - 0.45], 'UPN 120', 'galvanised', [0, 1, 0])\n    }\n    grating(k, [(x0 + x1) / 2, yp, zc], x1 - x0, 0.9)\n    handrail(k, [[x1 - 0.6, yp, zc - 0.45], [x0, yp, zc - 0.45], [x0, yp, zc + 0.45]], 'safetyYellow')\n    handrail(k, [[x1, yp, zc - 0.45], [x1, yp, zc + 0.45]], 'safetyYellow')\n    ladder(k, [x1 - 0.3, 0.15, zc - 0.47], yp, [0, 0, -1])\n  }\n  // lifting lugs, earth boss, nameplate\n  for (const x of [-0.42 * L, 0.42 * L]) for (const s of [-1, 1]) k.box(SK, [0.2, 0.18, 0.04], [x, 0.37 + 0.1, s * (R * 0.7 + 0.13)])\n  earthBond(k, [skC - skL / 2 + 0.5, 0.15 + hk / 2, R * 0.7 + 0.11], [0, 0, 1], 0, 0.6)\n  tagPlate(k, p.tag || `EN 12953  ${p.pressure} barg`, [-L / 2 - 0.545, yS + R * 0.78, 0], '-x', 0.3)\n}\n"
    },
    {
      "path": "onshore/_onshore.ts",
      "type": "registry:file",
      "target": "components/deck-twin/onshore/_onshore.ts",
      "content": "// Shared onshore parts: plinths, anchor bolts, earth bonds, ISO 14122 stairs, shedded insulators, lattice steel and swivel joints.\nimport * as THREE from 'three'\nimport { axisVec, type Axis, type Kit, type MatKey, type V3 } from '../lib/kit'\nimport { paint } from '../lib/materials'\nimport { grating, handrail, member } from '../lib/parts'\nimport { stairFlight } from '../lib/standards'\n\nexport const add = (a: V3, b: V3, s = 1): V3 => [a[0] + b[0] * s, a[1] + b[1] * s, a[2] + b[2] * s]\nexport const deg = (d: number) => (d * Math.PI) / 180\nexport const clamp = (v: number, a: number, b: number) => Math.min(b, Math.max(a, v))\n\nexport const porcelain = () => paint('#6e3420', { metalness: 0.05, roughness: 0.12, clearcoat: 0.9, clearcoatRoughness: 0.1, weather: 0 })\nexport const silicone = () => paint('#8b9399', { metalness: 0, roughness: 0.55, clearcoat: 0.2, clearcoatRoughness: 0.5, weather: 0.3 })\nexport const earthLead = () => paint('#5c9e33', { metalness: 0, roughness: 0.6, clearcoat: 0, weather: 0 })\nexport const lightGrey = () => paint('#cfd2cf', { metalness: 0.2, roughness: 0.5, clearcoat: 0.25, weather: 1 })\n\n/** Concrete plinth from y0 up to y0 + h with a small arris, centred on (cx, cz). */\nexport function plinth(k: Kit, cx: number, cz: number, sx: number, sz: number, h: number, y0 = 0) {\n  const c = Math.min(0.025, h * 0.2)\n  k.box('concrete', [sx, h - c, sz], [cx, y0 + (h - c) / 2, cz])\n  k.box('concrete', [sx - 2 * c, c, sz - 2 * c], [cx, y0 + h - c / 2, cz])\n}\n\n/** Cast-in anchor bolt standing on a surface at p: washer, double nut, projecting thread. */\nexport function anchorBolt(k: Kit, p: V3, dia = 0.024) {\n  k.cyl('galvanised', dia * 1.1, 0.005, [p[0], p[1] + 0.0025, p[2]], 'y', 12)\n  k.hex('nut', dia * 1.85, dia * 0.9, [p[0], p[1] + 0.005 + dia * 0.45, p[2]])\n  k.hex('nut', dia * 1.85, dia * 0.9, [p[0], p[1] + 0.005 + dia * 1.4, p[2]])\n  k.cyl('boltXylan', dia / 2, dia * 2.6, [p[0], p[1] + dia * 1.3, p[2]], 'y', 8)\n}\n\n/** Steel base plate with its top at y, on a grout bed, anchored near each corner. */\nexport function basePlate(k: Kit, cx: number, y: number, cz: number, sx: number, sz: number, t = 0.025, dia = 0.024, mat: MatKey = 'galvanised') {\n  k.box(mat, [sx, t, sz], [cx, y - t / 2, cz])\n  const ix = sx / 2 - dia * 2.2\n  const iz = sz / 2 - dia * 2.2\n  for (const a of [-1, 1]) for (const b of [-1, 1]) anchorBolt(k, [cx + a * ix, y, cz + b * iz], dia)\n}\n\n/** Earth boss with a bolted lug and a green/yellow bonding lead down to an inspection pit at grade. */\nexport function earthBond(k: Kit, pos: V3, out: V3, ground = 0, pitOffset = 0.3) {\n  const o = new THREE.Vector3(...out).normalize().toArray() as V3\n  k.cyl('brass', 0.017, 0.03, add(pos, o, 0.015), o, 10)\n  k.box('copper', [0.03, 0.05, 0.004], add(add(pos, o, 0.032), [0, -0.012, 0]), o)\n  k.hex('nut', 0.022, 0.012, add(pos, o, 0.04), o)\n  const lug = add(add(pos, o, 0.035), [0, -0.035, 0])\n  const pit: V3 = [pos[0] + o[0] * pitOffset, ground, pos[2] + o[2] * pitOffset]\n  if (pos[1] - ground > 0.12) {\n    k.spline(earthLead(), [lug, add(lug, [0, -0.08, 0]), add([pit[0], ground + 0.25, pit[2]], o, -pitOffset * 0.3), [pit[0], ground + 0.1, pit[2]], [pit[0], ground + 0.03, pit[2]]], 0.0065, 6, 24)\n    k.cyl('concrete', 0.14, 0.05, [pit[0], ground + 0.025, pit[2]], 'y', 16)\n    k.cyl('castIron', 0.1, 0.008, [pit[0], ground + 0.054, pit[2]], 'y', 16)\n  }\n}\n\n/** ISO 14122-3 straight stair: first riser at `foot`, climbing along `dir` by `rise`. Channel stringers, grating treads, handrails. Returns the horizontal run. */\nexport function stair(k: Kit, foot: V3, dir: Axis, rise: number, width = 0.9, rails: 'both' | 'left' | 'right' | 'none' = 'both') {\n  const f = stairFlight(rise)\n  const n = f.steps\n  const g = f.going\n  const q = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(1, 0, 0), axisVec(dir).setY(0).normalize())\n  const w = width / 2\n  const slope = f.riser / g\n  k.with(new THREE.Matrix4().compose(new THREE.Vector3(...foot), q, new THREE.Vector3(1, 1, 1)), () => {\n    const sd = 0.18\n    const yAt = (x: number) => f.riser + x * slope - 0.07\n    const x0 = (sd * 0.45 + 0.07 - f.riser) / slope\n    const run = (n - 1) * g\n    for (const s of [-1, 1]) {\n      const z = s * (w + 0.04)\n      member(k, [x0, yAt(x0), z], [run, yAt(run), z], 'UPN 180', 'galvanised', [-slope, 1, 0])\n      k.box('galvanised', [0.22, 0.015, 0.16], [x0 + 0.06, 0.0075, z])\n    }\n    for (let i = 1; i < n; i++) {\n      const y = i * f.riser\n      grating(k, [(i - 1) * g + g / 2 + 0.0125, y, 0], g + 0.025, width)\n      k.box('safetyYellow', [0.03, 0.032, width], [(i - 1) * g + 0.015, y - 0.016, 0])\n    }\n    const pts = (z: number): V3[] => [[0, f.riser * 0.5, z], [run, rise, z]]\n    if (rails === 'both' || rails === 'left') handrail(k, pts(w + 0.04), 'safetyYellow')\n    if (rails === 'both' || rails === 'right') handrail(k, pts(-w - 0.04), 'safetyYellow')\n  })\n  return (n - 1) * g\n}\n\n/** Shedded post insulator from `base` along `axis` for L: galvanised end caps, alternating sheds of radius ~2.2 r. */\nexport function insulator(k: Kit, base: V3, L: number, r: number, mat: MatKey, axis: Axis = 'y') {\n  const v = axisVec(axis).toArray() as V3\n  const cap = Math.min(0.08, L * 0.08)\n  const prof: [number, number][] = [[0, cap], [r, cap]]\n  const body = L - 2 * cap\n  const n = Math.max(3, Math.round(body / Math.max(0.045, r * 0.75)))\n  const pitch = body / n\n  let y = cap\n  for (let i = 0; i < n; i++) {\n    const big = i % 2 === 0 ? 2.3 : 1.85\n    prof.push([r * 1.05, y + pitch * 0.15], [r * big, y + pitch * 0.42], [r * (big - 0.1), y + pitch * 0.55], [r, y + pitch * 0.62])\n    y += pitch\n    prof.push([r, y])\n  }\n  prof.push([0, L - cap])\n  k.lathe(mat, prof, base, v, 20)\n  for (const t of [cap / 2, L - cap / 2]) k.cyl('galvanised', r * 1.25, cap, add(base, v, t), v, 18)\n  for (const t of [0.006, L - 0.006]) k.cyl('galvanised', r * 1.9, 0.012, add(base, v, t), v, 18)\n}\n\n/** Square lattice column of four angle legs with zig-zag bracing on every face, (cx, cz) centre, from y0 to y1. */\nexport function latticeColumn(k: Kit, cx: number, cz: number, y0: number, y1: number, w: number, leg = 'L100x100x10', brace = 'L50x50x5', mat: MatKey = 'galvanised') {\n  const h = w / 2\n  const c: [number, number][] = [[-h, -h], [h, -h], [h, h], [-h, h]]\n  for (const [x, z] of c) member(k, [cx + x, y0, cz + z], [cx + x, y1, cz + z], leg, mat, [-x, 0, -z])\n  const H = y1 - y0\n  const nb = Math.max(1, Math.round(H / w))\n  const dy = H / nb\n  for (let f = 0; f < 4; f++) {\n    const [ax, az] = c[f]\n    const [bx, bz] = c[(f + 1) % 4]\n    for (let i = 0; i < nb; i++) {\n      const ya = y0 + i * dy\n      const flip = (i + f) % 2 === 0\n      const p0: V3 = [cx + (flip ? ax : bx), ya, cz + (flip ? az : bz)]\n      const p1: V3 = [cx + (flip ? bx : ax), ya + dy, cz + (flip ? bz : az)]\n      member(k, p0, p1, brace, mat, [ax + bx, 0, az + bz])\n    }\n    member(k, [cx + ax, y1 - 0.03, cz + az], [cx + bx, y1 - 0.03, cz + bz], brace, mat)\n  }\n}\n\n/** Swivel joint between a and b: two ball-race housings, seal ring, grease nipples and plug bolts. od = pipe OD. */\nexport function swivel(k: Kit, a: V3, b: V3, od: number, mat: MatKey = 'flangeSteel') {\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 v = d.toArray() as V3\n  const at = (t: number) => A.clone().addScaledVector(d, t).toArray() as V3\n  const R = od * 0.82\n  const ri = od * 0.44\n  ring(k, mat, ri, od * 0.56, L, at(L / 2), v)\n  k.cyl('pipeBore', ri, L, at(L / 2), v, 32, undefined, true)\n  ring(k, mat, od * 0.55, R, L * 0.36, at(L * 0.3), v)\n  ring(k, 'machined', od * 0.55, R * 0.96, L * 0.04, at(L * 0.5), v)\n  ring(k, mat, od * 0.55, R * 0.92, L * 0.3, at(L * 0.69), v)\n  for (const t of [0.12, 0.88]) ring(k, mat, od * 0.55, od * 0.7, L * 0.06, at(L * t), v)\n  const [u, w] = perpOf(d)\n  for (let i = 0; i < 6; i++) {\n    const ang = (i / 6) * Math.PI * 2\n    const r = new THREE.Vector3().addScaledVector(u, Math.cos(ang) * R).addScaledVector(w, Math.sin(ang) * R)\n    const p = new THREE.Vector3(...at(L * 0.3)).add(r)\n    k.hex('nut', Math.max(0.018, od * 0.06), 0.012, p.toArray() as V3, r.clone().normalize().toArray() as V3)\n  }\n  const gp = new THREE.Vector3(...at(L * 0.69)).addScaledVector(u, R * 0.92)\n  k.cyl('brass', Math.max(0.006, od * 0.02), 0.03, gp.clone().addScaledVector(u, 0.012).toArray() as V3, u.toArray() as V3, 8)\n}\n\nfunction perpOf(d: THREE.Vector3): [THREE.Vector3, THREE.Vector3] {\n  const ref = Math.abs(d.y) > 0.9 ? new THREE.Vector3(1, 0, 0) : new THREE.Vector3(0, 1, 0)\n  const u = new THREE.Vector3().crossVectors(d, ref).normalize()\n  return [u, new THREE.Vector3().crossVectors(d, u).normalize()]\n}\n\n/** Annular ring (hollow short cylinder) centred on pos along axis, open through the middle. */\nexport function ring(k: Kit, mat: MatKey, ri: number, ro: number, h: number, pos: V3, axis: Axis = 'y', seg = 36) {\n  const v = axisVec(axis)\n  const base = new THREE.Vector3(...pos).addScaledVector(v, -h / 2).toArray() as V3\n  k.lathe(mat, [[ro, 0], [ro, h], [ri, h], [ri, 0], [ro, 0]], base, v.toArray() as V3, seg)\n}\n\n/** Hydraulic cylinder from a (barrel end clevis) to b (rod eye). */\nexport function hydraulicCylinder(k: Kit, a: V3, b: V3, bore: number, mat: MatKey = 'actuatorBlue') {\n  const A = new THREE.Vector3(...a)\n  const B = new THREE.Vector3(...b)\n  const L = A.distanceTo(B)\n  if (L < 1e-3) return\n  const d = B.clone().sub(A).normalize()\n  const bl = Math.min(L * 0.62, L - 0.1)\n  const p = (t: number) => A.clone().addScaledVector(d, t).toArray() as V3\n  k.rod(mat, p(0.04), p(bl), bore / 2, 16)\n  k.rod(mat, p(0.04), p(0.1), bore * 0.62, 16)\n  k.rod(mat, p(bl - 0.06), p(bl), bore * 0.62, 16)\n  k.rod('stainless', p(bl), p(L - 0.04), bore * 0.28, 12)\n  const side = Math.abs(d.x) > 0.9 ? 'z' : 'x'\n  for (const t of [0.02, L - 0.02]) k.cyl('flangeSteel', bore * 0.45, bore * 0.6, p(t), side, 12)\n  k.rod('tubing', add(p(0.12), [0, bore * 0.6, 0]), add(p(bl - 0.08), [0, bore * 0.6, 0]), 0.006, 6)\n}\n\n/**\n * Trapezoidal profiled sheet (wall or roof cladding): profile runs along `u` for `length`, ribs stand out along `n`,\n * extruded `height` along `w`; `origin` is the (0, 0, 0) corner. Typical 35 mm rib at 200 mm pitch.\n */\nexport function profiledSheet(k: Kit, mat: MatKey, length: number, height: number, origin: V3, u: V3, n: V3, w: V3, pitch = 0.2, depth = 0.035) {\n  const top = pitch * 0.2\n  const slope = pitch * 0.15\n  const pts: [number, number][] = [[0, 0]]\n  const ribs = Math.max(1, Math.round(length / pitch))\n  const sc = length / (ribs * pitch)\n  for (let i = 0; i < ribs; i++) {\n    const a = i * pitch * sc\n    const m = a + (pitch * sc) / 2\n    pts.push([m - (top / 2 + slope) * sc, 0], [m - (top / 2) * sc, depth], [m + (top / 2) * sc, depth], [m + (top / 2 + slope) * sc, 0])\n  }\n  pts.push([length, 0])\n  const sh = new THREE.Shape()\n  pts.forEach(([x, y], i) => (i ? sh.lineTo(x, y) : sh.moveTo(x, y)))\n  for (let i = pts.length - 1; i >= 0; i--) sh.lineTo(pts[i][0], pts[i][1] - 0.0015)\n  sh.closePath()\n  const m = new THREE.Matrix4().makeBasis(new THREE.Vector3(...u), new THREE.Vector3(...n), new THREE.Vector3(...w)).setPosition(...origin)\n  k.extrude(mat, sh, height, m, 1)\n}\n"
    }
  ],
  "categories": [
    "onshore"
  ],
  "meta": {
    "export": "PackagedBoiler",
    "anchor": "base",
    "standards": [
      "EN 12953",
      "ASME Section I",
      "EN 676",
      "ASME B16.5",
      "ISO 14122"
    ]
  }
}