{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "truck-loading-bay",
  "type": "registry:component",
  "title": "Truck loading bay (gantry)",
  "description": "Road-tanker loading gantry with one island per lane under a portal-frame canopy. Each bay has a meter run (block valve, basket strainer, air eliminator, PD meter with register, digital set-stop valve), API RP 1004 bottom-loading arms with balance cylinders and API couplers, a vapour recovery arm, a counterbalanced top-loading arm with drop tube, a gantry platform with stair and fold-down gangway, a batch controller, an overfill and earth-verification monitor with plug and clamp, and an emergency stop. Product sets the colour bands; `loading` picks bottom, top or both.",
  "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/truck-loading-bay.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/onshore/truck-loading-bay.tsx",
      "content": "// Road-tanker loading gantry: canopy, island platform with fold-down gangway, bottom and top loading arms, meter run, overfill and earth monitor. API RP 1004, EN 13083, API 2610.\nimport * as THREE from 'three'\nimport type { Kit, V3 } from '../lib/kit'\nimport { KitMesh, useKit, type GroupProps } from '../lib/mesh'\nimport { flange, gasket, gauge, grating, handrail, member, pipe, 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 { hazardStripeMaterial, textMaterial } from '../lib/textures'\nimport { GateValve } from '../valves/gate-valve'\nimport { anchorBolt, basePlate, clamp, earthBond, plinth, stair, swivel } from './_onshore'\n\ntype Product = 'gasoline' | 'diesel' | 'jet' | 'crude'\ntype Loading = 'bottom' | 'top' | 'both'\n\nexport interface TruckLoadingBayProps {\n  /** loading lanes, each with its own island */\n  bays?: number\n  product?: Product\n  loading?: Loading\n  /** product line size, inches */\n  nps?: number\n  rating?: Rating\n  /** gangways lowered onto the tanker */\n  gangwayDown?: boolean\n  tag?: string\n}\n\nexport const defaults = { bays: 2, product: 'gasoline' as Product, loading: 'both' as Loading, nps: 4, rating: 150 as Rating, gangwayDown: true, tag: '' }\n\n// typical: product identification colours after API RP 1637 / EI 1542\nconst BAND = { gasoline: '#c0262a', diesel: '#e3b40f', jet: '#1d1d1d', crude: '#5b3a1e' }\nconst LANE = 4.5\nconst ISLAND = 1.6\nconst PITCH = LANE + ISLAND\nconst X0 = -8 // island west end\nconst X1 = 9\nconst YH = 0.75 // meter run centreline\nconst PLAT = 3.9 // gantry deck, typical tanker top + 0.3 m\nconst ROOF = 7.6\n\nfunction layout(p: typeof defaults) {\n  const n = clamp(Math.round(p.bays), 1, 6)\n  const W = n * PITCH\n  const zi = (i: number) => -W / 2 + ISLAND / 2 + i * PITCH\n  return { n, W, zi, xIn: X0 - 0.6, zm: (i: number) => zi(i) - 0.4 }\n}\n\nexport function ports(props: TruckLoadingBayProps = {}): Port[] {\n  const p = { ...defaults, ...props }\n  const g = layout(p)\n  return Array.from({ length: g.n }, (_, i) => port(`inlet-${i + 1}`, `Bay ${i + 1} product inlet`, [g.xIn, YH, g.zm(i)], [-1, 0, 0], p.nps, p.rating))\n}\n\nexport function TruckLoadingBay(props: TruckLoadingBayProps & GroupProps) {\n  const { bays, product, loading, nps, rating, gangwayDown, tag, ...group } = { ...defaults, ...props }\n  const p = { bays, product, loading, nps, rating, gangwayDown, tag }\n  const g = layout(p)\n  const data = useKit((k) => build(k, p, g), [bays, product, loading, nps, rating, gangwayDown, tag])\n  const L = faceToFace('gate', nps, rating)\n  return (\n    <group {...group}>\n      <KitMesh data={data} />\n      {Array.from({ length: g.n }, (_, i) => (\n        <GateValve key={i} nps={nps} rating={rating} position={[g.xIn + 0.45 + L / 2, YH, g.zm(i)]} tag={`XV-${i + 1}01`} />\n      ))}\n    </group>\n  )\n}\n\nexport default TruckLoadingBay\n\ntype P = typeof defaults\ntype G = ReturnType<typeof layout>\n\nfunction build(k: Kit, p: P, g: G) {\n  const band = paint(BAND[p.product])\n  const kerb = paint('#e7e3d6', { metalness: 0, roughness: 0.85, clearcoat: 0 })\n  // paved apron with a grated drain channel along each lane\n  const zA = -g.W / 2 - 0.5\n  const zB = g.W / 2 + 0.5\n  k.box('concrete', [X1 - X0 + 6, 0.06, zB - zA], [(X0 + X1) / 2, 0.03, (zA + zB) / 2])\n  for (let i = 0; i < g.n; i++) {\n    const zc = g.zi(i) + ISLAND / 2 + LANE / 2\n    k.box('castIron', [X1 - X0 + 4, 0.004, 0.3], [(X0 + X1) / 2, 0.062, zc])\n    for (const s of [-1, 1]) k.box(kerb, [X1 - X0 + 5, 0.003, 0.1], [(X0 + X1) / 2, 0.0615, zc + s * (LANE / 2 - 0.1)])\n    bay(k, p, g, i, band)\n  }\n  canopy(k, g)\n  if (p.tag) {\n    const zEnd = -g.W / 2 - 0.05\n    k.box('stainless', [2.4, 0.5, 0.02], [0, ROOF - 0.9, zEnd - 0.2])\n    k.plane(textMaterial(p.tag, '#1f4e9a', '#ffffff'), 2.3, 0.42, [0, ROOF - 0.9, zEnd - 0.212], new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI))\n  }\n}\n\nfunction canopy(k: Kit, g: G) {\n  const xs = [-5.6, 8.4]\n  const z0 = -g.W / 2 - 0.3\n  const z1 = g.W / 2 + 0.3\n  for (const x of xs) {\n    for (let i = 0; i < g.n; i++) {\n      const z = g.zi(i) + 0.45\n      plinth(k, x, z, 0.7, 0.7, 0.25)\n      basePlate(k, x, 0.29, z, 0.5, 0.5, 0.03, 0.03)\n      member(k, [x, 0.29, z], [x, ROOF - 0.5, z], 'HEB 300', 'structSteel', [1, 0, 0])\n      // knee braces to the rafter\n      for (const s of [-1, 1]) member(k, [x, ROOF - 1.8, z], [x, ROOF - 0.62, z + s * 1.4], 'L80x80x8', 'structSteel')\n      earthBond(k, [x, 0.6, z + 0.16], [0, 0, 1], 0.06, 0.25)\n    }\n    member(k, [x, ROOF - 0.3, z0], [x, ROOF - 0.3, z1], 'IPE 500', 'structSteel', [0, 1, 0])\n  }\n  // purlins, deck sheet, fascia and gutters\n  const xa = -7\n  const xb = 9.8\n  for (let z = z0 + 0.1; z <= z1 - 0.05; z += 1.5) member(k, [xa, ROOF - 0.01, z], [xb, ROOF - 0.01, z], 'UPN 160', 'galvanised', [0, 1, 0])\n  k.box('cladding', [xb - xa, 0.04, z1 - z0], [(xa + xb) / 2, ROOF + 0.1, (z0 + z1) / 2])\n  for (const z of [z0, z1]) {\n    k.box('cladding', [xb - xa, 0.6, 0.03], [(xa + xb) / 2, ROOF - 0.15, z])\n    k.box('galvanised', [xb - xa, 0.12, 0.14], [(xa + xb) / 2, ROOF - 0.5, z + (z > 0 ? 0.08 : -0.08)])\n  }\n  for (const x of [xa, xb]) k.box('cladding', [0.03, 0.6, z1 - z0], [x, ROOF - 0.15, (z0 + z1) / 2])\n  // lighting under the canopy\n  for (const x of [-3, 1.5, 6])\n    for (let i = 0; i < g.n; i++) {\n      const z = g.zi(i) + ISLAND / 2 + LANE / 2\n      k.rbox('aluminium', [1.2, 0.1, 0.3], 0.02, [x, ROOF - 0.12, z])\n      k.box('lampLens', [1.1, 0.01, 0.24], [x, ROOF - 0.175, z])\n    }\n}\n\nfunction bay(k: Kit, p: P, g: G, i: number, band: THREE.Material) {\n  const zi = g.zi(i)\n  const zm = g.zm(i)\n  const zE = zi + ISLAND / 2 // lane-side kerb\n  const top = 0.2\n  // island with painted kerb ends\n  k.box('concrete', [X1 - X0, top - 0.06, ISLAND], [(X0 + X1) / 2, 0.06 + (top - 0.06) / 2, zi])\n  for (const x of [X0, X1]) {\n    const geo = new THREE.BoxGeometry(0.3, top - 0.06 + 0.002, ISLAND + 0.002)\n    const uv = geo.getAttribute('uv') as THREE.BufferAttribute\n    for (let j = 0; j < uv.count; j++) uv.setXY(j, uv.getX(j) * 4, uv.getY(j) * 0.5)\n    k.add(hazardStripeMaterial(), geo, new THREE.Matrix4().makeTranslation(x + (x < 0 ? 0.15 : -0.15), 0.06 + (top - 0.06) / 2, zi))\n  }\n  // crash bollards at both island ends\n  for (const x of [X0 - 0.5, X1 + 0.5])\n    for (const s of [-0.5, 0.5]) {\n      k.cyl(paint('#e5b114'), 0.085, 1.1, [x, 0.6, zi + s], 'y', 16)\n      k.dome(paint('#e5b114'), 0.085, [x, 1.15, zi + s], 'y')\n    }\n\n  // meter run: inlet spool, block valve (component), strainer, air eliminator, meter, set-stop valve\n  const nps = p.nps\n  const od = pipeOD(nps)\n  const L = faceToFace('gate', nps, p.rating)\n  const xv = g.xIn + 0.45\n  const fi = flange(k, nps, p.rating, [g.xIn, YH, zm], '-x', true)\n  const ha = flange(k, nps, p.rating, [xv - 0.0045, YH, zm], 'x', true)\n  k.pipe('pipePaint', [g.xIn + fi, YH, zm], [xv - 0.0045 - ha, YH, zm], od, od * 0.07)\n  gasket(k, nps, p.rating, [xv - 0.00225, YH, zm], 'x')\n  const hb = flange(k, nps, p.rating, [xv + L + 0.0045, YH, zm], '-x', true)\n  gasket(k, nps, p.rating, [xv + L + 0.00225, YH, zm], 'x')\n  for (const x of [g.xIn + 0.2, X0 + 0.25]) {\n    plinth(k, x, zm, 0.3, 0.4, x < X0 ? 0.55 : 0.35, x < X0 ? 0 : top)\n    k.box('galvanised', [0.2, YH - od / 2 - (x < X0 ? 0.55 : 0.35 + top), 0.3], [x, (YH - od / 2 + (x < X0 ? 0.55 : 0.35 + top)) / 2, zm])\n  }\n  let x = xv + L + 0.0045 + hb\n  const seg = (x1: number) => {\n    k.pipe('pipePaint', [x, YH, zm], [x1, YH, zm], od, od * 0.07)\n    x = x1\n  }\n  seg(X0 + 0.9)\n  // basket strainer with quick-opening cover\n  k.cyl('valveBody', od * 1.1, od * 2.6, [x + od * 1.2, YH - od * 0.4, zm], 'y', 24)\n  k.cyl('valveBody', od * 1.35, 0.04, [x + od * 1.2, YH + od * 0.95, zm], 'y', 24)\n  k.cyl('handwheel', 0.02, 0.12, [x + od * 1.2, YH + od * 1.05, zm], 'z', 8)\n  k.cyl('valveBody', od * 0.6, od * 2.4, [x + od * 1.2, YH, zm], 'x', 20)\n  x += od * 2.4\n  seg(x + 0.4)\n  // air eliminator above the meter inlet\n  k.cyl('equipmentPaint', 0.11, 0.7, [x - 0.2, YH + 0.45, zm], 'y', 24)\n  k.dome('equipmentPaint', 0.11, [x - 0.2, YH + 0.8, zm], 'y', Math.PI / 2, 0.5)\n  k.cyl('valveBody', 0.025, 0.12, [x - 0.2, YH + 0.9, zm], 'y', 10)\n  // positive-displacement meter with register and pulser\n  const mL = 0.55\n  k.cyl('castIron', 0.24, mL, [x + mL / 2, YH + 0.05, zm], 'x', 32)\n  for (const s of [-1, 1]) k.cyl('castIron', 0.27, 0.04, [x + mL / 2 + s * mL * 0.45, YH + 0.05, zm], 'x', 32)\n  k.box('castIron', [0.32, 0.25, 0.28], [x + mL / 2, YH + 0.4, zm])\n  k.rbox('stainless', [0.28, 0.24, 0.12], 0.02, [x + mL / 2, YH + 0.64, zm + 0.04])\n  k.plane(textMaterial('0 0 0 4 2 7 L', '#101418', '#9ef0a0', 256, 64), 0.2, 0.05, [x + mL / 2, YH + 0.66, zm + 0.101])\n  k.box('castIron', [0.22, od * 0.8, 0.22], [x + mL / 2, YH + 0.05 - 0.28, zm])\n  x += mL\n  seg(x + 0.25)\n  // digital set-stop control valve with solenoid pilots\n  k.sphere('valveBody', od * 1.0, [x + od, YH, zm], [1.2, 1, 1], 20)\n  k.cyl('valveBody', od * 0.9, od * 0.7, [x + od, YH + od * 0.95, zm], 'y', 20)\n  for (const s of [-1, 1]) {\n    k.cyl('actuatorBlue', 0.03, 0.12, [x + od + s * 0.06, YH + od * 1.45, zm], 'y', 12)\n    k.box('actuatorBlue', [0.06, 0.05, 0.06], [x + od + s * 0.06, YH + od * 1.45 + 0.08, zm])\n  }\n  k.tube('tubing', [[x + od - 0.06, YH + od * 1.3, zm], [x + od - 0.06, YH + od * 1.3, zm + 0.12], [x - 0.1, YH + 0.1, zm + 0.12], [x - 0.1, YH + 0.06, zm]], 0.004)\n  x += od * 2\n  gauge(k, [x + 0.12, YH + od / 2 + 0.12, zm], 'z', 0.08, 'bar', 16)\n  k.rod('stainless', [x + 0.12, YH + od / 2, zm], [x + 0.12, YH + od / 2 + 0.08, zm], 0.007, 8)\n  // run on to the arm manifold, rise and supports\n  const xr = -1.2\n  seg(xr - 0.3)\n  for (const xs of [X0 + 2, X0 + 4]) {\n    k.box('galvanised', [0.1, YH - od / 2 - top, 0.25], [xs, (YH - od / 2 + top) / 2, zm])\n    k.box('galvanised', [0.25, 0.012, 0.3], [xs, top + 0.006, zm])\n  }\n  const hdr: V3 = [xr - 0.3, YH, zm]\n  const bottom = p.loading !== 'top'\n  const topArm = p.loading !== 'bottom'\n  // product header along the island feeding the bottom arms\n  if (bottom) {\n    pipeRun(k, [hdr, [2.3, YH, zm]], nps)\n    k.dome('pipePaint', od / 2, [2.3, YH, zm], 'x', Math.PI / 2, 0.5)\n    for (const [j, xb] of [0.4, 1.6].entries()) {\n      bottomArm(k, nps, [xb, YH, zm], zE, band, j === 0 ? 1 : -1)\n    }\n    // vapour recovery arm and header back to the vapour line\n    const vz = zi + 0.05\n    pipeRun(k, [[X0 + 0.3, top + 0.35, vz], [3.7, top + 0.35, vz]], nps, { mat: 'equipmentPaint' })\n    k.dome('equipmentPaint', od / 2, [3.7, top + 0.35, vz], 'x', Math.PI / 2, 0.5)\n    k.dome('equipmentPaint', od / 2, [X0 + 0.3, top + 0.35, vz], '-x', Math.PI / 2, 0.5)\n    bottomArm(k, nps, [3.4, top + 0.35, vz], zE, paint('#e8e8e2'), 1, true)\n    for (const xs of [X0 + 1.5, 0, 3.0]) k.box('galvanised', [0.08, 0.35 - od / 2, 0.15], [xs, top + (0.35 - od / 2) / 2, vz])\n  }\n  // top-loading arm: riser through the gantry, swivel, counterbalanced boom and drop tube\n  if (topArm) {\n    const hr = PLAT + 1.6\n    pipeRun(k, [hdr, [xr, YH, zm], [xr, hr, zm]], nps)\n    swivel(k, [xr, hr, zm], [xr, hr + 0.3, zm], od)\n    const ang = Math.PI / 3\n    const reach = 2.6\n    const d: V3 = [Math.cos(ang), 0, Math.sin(ang)]\n    const tip: V3 = [xr + d[0] * reach, hr + 0.55, zm + d[2] * reach]\n    pipeRun(k, [[xr, hr + 0.3, zm], [xr, hr + 0.55, zm], tip, [tip[0], hr - 0.05, tip[2]]], nps, { mat: 'aluminium' })\n    swivel(k, [tip[0], hr - 0.05, tip[2]], [tip[0], hr - 0.3, tip[2]], od)\n    k.pipe('aluminium', [tip[0], hr - 0.3, tip[2]], [tip[0], PLAT + 0.55, tip[2]], od, od * 0.06)\n    k.cyl('aluminium', 0.28, 0.14, [tip[0], hr - 0.55, tip[2]], 'y', 28, 0.18)\n    k.rod('handwheel', [tip[0], hr - 0.45, tip[2]], [tip[0] + 0.35, hr - 0.45, tip[2]], 0.015, 8)\n    k.torus(band, od / 2 + 0.006, 0.012, [tip[0], hr - 0.8, tip[2]], 'y', 6, 20)\n    // balance spring cylinder from the riser to the boom\n    k.rod('actuatorYellow', [xr + 0.08, hr - 0.4, zm], [xr + d[0] * 1.0, hr + 0.48, zm + d[2] * 1.0], 0.045, 12)\n    k.rod('stainless', [xr + d[0] * 0.75, hr + 0.32, zm + d[2] * 0.75], [xr + d[0] * 1.0, hr + 0.48, zm + d[2] * 1.0], 0.018, 8)\n    k.torus(band, od / 2 + 0.006, 0.012, [xr, PLAT + 0.5, zm], 'y', 6, 20)\n  }\n\n  // gantry platform over the island, stair, fold-down gangway\n  const xp0 = -3\n  const xp1 = 3\n  const zp0 = zi - 0.7\n  const zp1 = zi + 0.7\n  for (const xc of [xp0 + 0.1, xp1 - 0.1])\n    for (const zc of [zp0 + 0.1, zp1 - 0.1]) {\n      basePlate(k, xc, top + 0.02, zc, 0.3, 0.3, 0.02, 0.02)\n      member(k, [xc, top + 0.02, zc], [xc, PLAT - 0.2, zc], 'SHS 150x150x8', 'galvanised')\n    }\n  for (const zc of [zp0 + 0.1, zp1 - 0.1]) member(k, [xp0, PLAT - 0.1, zc], [xp1, PLAT - 0.1, zc], 'UPN 200', 'galvanised', [0, 1, 0])\n  for (let xc = xp0 + 0.1; xc <= xp1 - 0.05; xc += 1.2) member(k, [xc, PLAT - 0.1, zp0 + 0.1], [xc, PLAT - 0.1, zp1 - 0.1], 'UPN 160', 'galvanised', [0, 1, 0])\n  grating(k, [0, PLAT, zi], xp1 - xp0, zp1 - zp0)\n  const gw = 0.8\n  const gx = 0.6\n  handrail(k, [[xp1, PLAT, zp0], [xp0, PLAT, zp0], [xp0, PLAT, zp1], [gx - gw / 2 - 0.05, PLAT, zp1]], 'safetyYellow')\n  handrail(k, [[gx + gw / 2 + 0.05, PLAT, zp1], [xp1 - 1.05, PLAT, zp1]], 'safetyYellow')\n  stair(k, [xp1 + 0.01 + stairRun(PLAT - top), top, zi - 0.2], '-x', PLAT - top, 0.8)\n  // gangway: hinged at the deck edge, counterbalanced, cage rails\n  const down = p.gangwayDown ? -0.42 : 1.45\n  const Lg = 1.9\n  const q = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), -down)\n  k.with(new THREE.Matrix4().compose(new THREE.Vector3(gx, PLAT, zp1), q, new THREE.Vector3(1, 1, 1)), () => {\n    k.cyl('stainless', 0.03, gw + 0.1, [0, -0.03, 0], 'x', 10)\n    grating(k, [0, 0, Lg / 2], gw, Lg)\n    for (const s of [-1, 1]) {\n      k.box('galvanised', [0.01, 0.12, Lg], [s * gw / 2, -0.05, Lg / 2])\n      k.rod('safetyYellow', [s * gw / 2, 0, 0.05], [s * gw / 2, 1.0, 0.05], 0.02, 8)\n      k.rod('safetyYellow', [s * gw / 2, 0, Lg - 0.05], [s * gw / 2, 1.0, Lg - 0.05], 0.02, 8)\n      k.rod('safetyYellow', [s * gw / 2, 1.0, 0.05], [s * gw / 2, 1.0, Lg - 0.05], 0.02, 8)\n      k.rod('safetyYellow', [s * gw / 2, 0.5, 0.05], [s * gw / 2, 0.5, Lg - 0.05], 0.016, 8)\n    }\n    k.box('rubberBlack', [gw, 0.05, 0.08], [0, -0.04, Lg])\n  })\n  const att = new THREE.Vector3(gw / 2 + 0.04, 0.5, Lg * 0.35).applyQuaternion(q).add(new THREE.Vector3(gx, PLAT, zp1))\n  k.rod('actuatorYellow', [gx + gw / 2 + 0.12, PLAT + 1.7, zp1 - 0.1], att.toArray() as V3, 0.025, 10)\n  member(k, [gx + gw / 2 + 0.12, PLAT, zp1 - 0.1], [gx + gw / 2 + 0.12, PLAT + 1.8, zp1 - 0.1], 'SHS 100x100x6', 'galvanised')\n\n  // batch controller, overfill / earth monitor with plug and clamp reel, emergency stop\n  const pedestal = (xp: number, h: number) => {\n    k.box('galvanised', [0.25, 0.012, 0.25], [xp, top + 0.006, zE - 0.25])\n    for (const a of [-1, 1]) for (const b of [-1, 1]) anchorBolt(k, [xp + a * 0.09, top + 0.012, zE - 0.25 + b * 0.09], 0.012)\n    k.box('galvanised', [0.08, h, 0.08], [xp, top + h / 2, zE - 0.25])\n  }\n  pedestal(-0.4, 1.3)\n  k.rbox('stainless', [0.42, 0.5, 0.18], 0.02, [-0.4, top + 1.45, zE - 0.12])\n  k.plane(textMaterial('BAY ' + (i + 1) + '  PRESET 32000 L', '#0f2a1a', '#8ff59b', 512, 96), 0.34, 0.07, [-0.4, top + 1.6, zE - 0.029])\n  for (let r = 0; r < 3; r++) for (let c = 0; c < 4; c++) k.box('plasticGrey', [0.05, 0.035, 0.01], [-0.4 - 0.09 + c * 0.06, top + 1.47 - r * 0.05, zE - 0.025])\n  pedestal(4.2, 1.2)\n  k.rbox(paint('#d5d8d6'), [0.4, 0.5, 0.2], 0.02, [4.2, top + 1.35, zE - 0.12])\n  for (const [j, c] of ['#27c24a', '#e5b114', '#d22a1f'].entries()) k.cyl(paint(c, { emissive: c, emissiveIntensity: 0.6 }), 0.025, 0.02, [4.08 + j * 0.12, top + 1.52, zE - 0.015], 'z', 14)\n  k.spline('cable', [[4.1, top + 1.12, zE - 0.06], [4.1, top + 0.7, zE + 0.02], [4.25, top + 0.45, zE + 0.03], [4.38, top + 0.8, zE + 0.0], [4.42, top + 1.12, zE - 0.03]], 0.008, 6, 30)\n  k.cyl('plasticGrey', 0.03, 0.1, [4.42, top + 1.18, zE - 0.03], 'y', 12)\n  k.cyl('safetyGreen', 0.12, 0.08, [4.2, top + 0.75, zE - 0.06], 'z', 20)\n  k.box('copper', [0.04, 0.06, 0.03], [4.38, top + 0.55, zE - 0.02])\n  pedestal(5.5, 1.1)\n  k.rbox('safetyYellow', [0.14, 0.18, 0.1], 0.015, [5.5, top + 1.2, zE - 0.2])\n  k.cyl('actuatorRed', 0.04, 0.04, [5.5, top + 1.22, zE - 0.13], 'z', 16)\n  // island earth bar\n  k.box('copper', [0.5, 0.05, 0.008], [X0 + 1.6, top + 0.3, zE - 0.004])\n  for (let j = 0; j < 5; j++) k.hex('nut', 0.018, 0.01, [X0 + 1.4 + j * 0.1, top + 0.3, zE + 0.004], 'z')\n  if (p.tag) tagPlate(k, `${p.tag}-B${i + 1}`, [X0 + 2.4, top + 0.18, zE + 0.002], 'z', 0.18)\n}\n\nfunction stairRun(rise: number) {\n  const n = Math.max(1, Math.round(rise / 0.18))\n  const going = rise / n / Math.tan((38 * Math.PI) / 180)\n  return (n - 1) * going\n}\n\n/** API RP 1004 bottom-loading arm parked along the island: base swivel, two arms, API coupler. */\nfunction bottomArm(k: Kit, nps: number, base: V3, zE: number, band: THREE.Material, side: 1 | -1, vapour = false) {\n  const od = pipeOD(nps)\n  const mat = vapour ? 'equipmentPaint' : 'aluminium'\n  const [x, y0, z0] = base\n  const y1 = y0 + 0.3\n  const yb = y1 + 0.55\n  pipe(k, [x, y0, z0], [x, y1, z0], nps)\n  swivel(k, [x, y1, z0], [x, y1 + 0.25, z0], od)\n  const ze = zE + 0.15\n  pipeRun(k, [[x, y1 + 0.25, z0], [x, yb, z0], [x, yb, ze]], nps, { mat })\n  swivel(k, [x, yb, ze], [x, yb, ze + 0.25], od)\n  const Li = 1.9\n  const elbow: V3 = [x + side * Li, yb, ze + 0.6]\n  pipeRun(k, [[x, yb, ze + 0.25], [x, yb, ze + 0.45], elbow], nps, { mat })\n  swivel(k, elbow, [elbow[0], elbow[1] - 0.25, elbow[2]], od)\n  const back: V3 = [elbow[0] - side * 1.6, elbow[1] - 0.45, elbow[2] + 0.2]\n  pipeRun(k, [[elbow[0], elbow[1] - 0.25, elbow[2]], [elbow[0], elbow[1] - 0.45, elbow[2]], back], nps, { mat })\n  // API coupler with operating handle, parked on its hook\n  k.cyl('aluminium', od * 0.85, 0.22, [back[0] - side * 0.11, back[1], back[2]], 'x', 24)\n  k.torus('aluminium', od * 0.95, 0.02, [back[0] - side * 0.2, back[1], back[2]], 'x', 8, 24)\n  k.rod('handwheel', [back[0] - side * 0.1, back[1], back[2] + od], [back[0] - side * 0.1, back[1] + 0.25, back[2] + od + 0.12], 0.016, 8)\n  k.torus(band, od / 2 + 0.006, 0.014, [elbow[0] - side * 0.4, elbow[1], elbow[2]], 'x', 6, 20)\n  // balance spring cylinder and parking hook post\n  k.rod('actuatorYellow', [x, y1 + 0.1, z0 + 0.1], [x + side * 0.7, yb - 0.04, ze + 0.5], 0.035, 10)\n  const hx = back[0] - side * 0.35\n  k.box('galvanised', [0.06, back[1] - od / 2 - 0.2, 0.06], [hx, 0.2 + (back[1] - od / 2 - 0.2) / 2, zE - 0.06])\n  k.box('galvanised', [0.06, 0.06, back[2] - zE + 0.06], [hx, back[1] - od / 2 - 0.03, (back[2] + zE - 0.06) / 2])\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": "TruckLoadingBay",
    "anchor": "base",
    "standards": [
      "API RP 1004",
      "EN 13083",
      "API 2610",
      "NFPA 30",
      "ISO 14122",
      "API RP 2003"
    ]
  }
}