{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "marine-loading-arm",
  "type": "registry:component",
  "title": "Marine loading arm",
  "description": "Counterbalanced marine loading arm on a jetty riser (stand post): base slewing swivel with ring gear, luffing swivel, inboard arm with its counterweight, rigid-link pantograph (lever, tie rod, apex crank) carrying the outboard counterweight, outboard arm, triple swivel, insulating flange, emergency release coupler with twin ball valves, and a hydraulic QC/DC coupler. Hydraulic cylinders, power unit, platform with caged ladder and earth bond. `luff`, `slew` and `outboard` move the arms; the coupler hangs plumb. Stand post height and arm length follow published data per size.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "onshore/marine-loading-arm.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/onshore/marine-loading-arm.tsx",
      "content": "// Marine loading arm on a jetty riser: counterbalanced inboard and outboard arms, style 80 swivels, hydraulics, ERS and QC/DC coupler. OCIMF MLA, EN 1474.\nimport * as THREE from 'three'\nimport { type Kit, type V3 } from '../lib/kit'\nimport { KitMesh, useKit, type GroupProps } from '../lib/mesh'\nimport { flangedJoint, gauge, junctionBox, ladder, nozzle, pipe, ringPlatform, tagPlate } from '../lib/parts'\nimport { paint } from '../lib/materials'\nimport { port, type Port } from '../lib/ports'\nimport { elbow90, pipeOD, pipeWall, type Rating } from '../lib/standards'\nimport { hazardStripeMaterial } from '../lib/textures'\nimport { add, basePlate, clamp, deg, earthBond, hydraulicCylinder, plinth, ring, swivel } from './_onshore'\n\nexport interface MarineLoadingArmProps {\n  /** product line size, inches */\n  nps?: number\n  rating?: Rating\n  /** inboard and outboard arm length, m */\n  reach?: number\n  /** inboard arm angle above horizontal, degrees */\n  luff?: number\n  /** slew about the riser, degrees (0 = reaching +Z) */\n  slew?: number\n  /** outboard arm angle from plumb, degrees (+ swings out) */\n  outboard?: number\n  /** emergency release system (ERC with twin ball valves) */\n  ers?: boolean\n  /** hydraulic operation with power unit; false = manual */\n  hydraulic?: boolean\n  tag?: string\n}\n\nexport const defaults = { nps: 12, rating: 150 as Rating, reach: 11, luff: 55, slew: 0, outboard: 30, ers: true, hydraulic: true, tag: '' }\n\n// stand post height and arm length by size (B0030-type data; NPS 20 extrapolated, typical)\nconst SIZES: [number, number, number][] = [[6, 4.5, 8], [8, 4.5, 9], [10, 6, 10], [12, 7, 11], [16, 7, 11], [20, 7.5, 12]]\nconst RISER: Record<number, number> = { 6: 10, 8: 12, 10: 16, 12: 18, 16: 20, 20: 24 }\n\nfunction layout(p: typeof defaults) {\n  const row = SIZES.reduce((a, b) => (Math.abs(b[0] - p.nps) < Math.abs(a[0] - p.nps) ? b : a))\n  const nps = row[0]\n  const od = pipeOD(nps)\n  const R = elbow90(nps, 'LR')\n  const sw = Math.max(0.35, od * 1.6) // swivel length, typical\n  const C = clamp(p.reach, 5, 16)\n  const riserNps = RISER[nps]\n  const Ro = pipeOD(riserNps) / 2\n  const ph = 0.3\n  const Hr = row[1]\n  const hs = sw\n  const Hp = Hr + hs + R + 0.35\n  const xa = 2 * R + sw\n  const xt = xa + R + sw * 0.4\n  const xo = xa + 2 * R + sw\n  const inlet = { base: [0, ph + 0.9, -Ro] as V3, standout: 0.25 + od * 0.5 }\n  const s = clamp(0.55 + nps / 28, 0.7, 1.35)\n  // bearing cheek between the inboard counterweight (half width 0.25 s) and the outboard lever\n  const xc = (xa + 0.25 * s + 0.03 + xt - 0.05) / 2\n  return { nps, od, R, sw, C, D: C, riserNps, Ro, ph, Hr, hs, Hp, xa, xt, xc, xo, inlet, s, a: 0.9 * s, b: 0.24 * C }\n}\ntype L = ReturnType<typeof layout>\n\nexport function ports(props: MarineLoadingArmProps = {}): Port[] {\n  const p = { ...defaults, ...props }\n  const g = layout(p)\n  return [port('riser', 'Riser inlet', [0, g.inlet.base[1], -(g.Ro + g.inlet.standout)], [0, 0, -1], g.nps, p.rating)]\n}\n\nexport function MarineLoadingArm(props: MarineLoadingArmProps & GroupProps) {\n  const { nps, rating, reach, luff, slew, outboard, ers, hydraulic, tag, ...group } = { ...defaults, ...props }\n  const p = { nps, rating, reach, luff, slew, outboard, ers, hydraulic, tag }\n  const g = layout(p)\n  const L = deg(clamp(luff, 20, 85))\n  const O = deg(clamp(outboard, -15, 90))\n  const key = [nps, rating, reach, ers, hydraulic, tag]\n  const fixed = useKit((k) => buildFixed(k, g, p), key)\n  const turret = useKit((k) => buildTurret(k, g, p), key)\n  const inboard = useKit((k) => buildInboard(k, g), key)\n  const lever = useKit((k) => buildLever(k, g), key)\n  const out = useKit((k) => buildOutboard(k, g), key)\n  const coupler = useKit((k) => buildCoupler(k, g, p), key)\n  const dyn = useKit((k) => buildDyn(k, g, L, O, hydraulic), [...key, L, O])\n  return (\n    <group {...group}>\n      <KitMesh data={fixed} />\n      <group rotation-y={deg(clamp(slew, -60, 60))}>\n        <KitMesh data={turret} />\n        <KitMesh data={dyn} />\n        <group position={[0, g.Hp, 0]} rotation-x={-O}>\n          <KitMesh data={lever} />\n        </group>\n        <group position={[0, g.Hp, 0]} rotation-x={-L}>\n          <KitMesh data={inboard} />\n          <group position={[0, 0, g.C]} rotation-x={L - O}>\n            <KitMesh data={out} />\n            <group position={[g.xo, -g.D, 0]} rotation-x={O}>\n              <KitMesh data={coupler} />\n            </group>\n          </group>\n        </group>\n      </group>\n    </group>\n  )\n}\n\nexport default MarineLoadingArm\n\nconst wall = (g: L) => pipeWall(g.nps, 'STD')\nconst ARM = 'equipmentPaint'\nconst STEEL = 'structSteelDark'\n\nfunction counterweight(k: Kit, c: V3, size: V3) {\n  const geo = new THREE.BoxGeometry(...size)\n  const uv = geo.getAttribute('uv') as THREE.BufferAttribute\n  for (let i = 0; i < uv.count; i++) uv.setXY(i, uv.getX(i) * 3, uv.getY(i) * 3)\n  k.add(hazardStripeMaterial(), geo, new THREE.Matrix4().makeTranslation(c[0], c[1], c[2]))\n  k.box(STEEL, [size[0] + 0.03, 0.04, size[2] + 0.03], [c[0], c[1] + size[1] / 2 + 0.02, c[2]])\n  for (const s of [-1, 1]) k.torus('nut', 0.05, 0.012, [c[0] + s * size[0] * 0.3, c[1] + size[1] / 2 + 0.07, c[2]], 'z', 6, 14)\n}\n\nfunction buildFixed(k: Kit, g: L, p: typeof defaults) {\n  const { Ro, ph, Hr, od } = g\n  // plinth, base plate with gussets, closed riser bottom\n  plinth(k, 0, 0, Ro * 2 + 0.9, Ro * 2 + 0.9, ph)\n  basePlate(k, 0, ph + 0.04, 0, Ro * 2 + 0.6, Ro * 2 + 0.6, 0.04, 0.036)\n  for (let i = 0; i < 8; i++) {\n    const a = (i / 8) * Math.PI * 2 + Math.PI / 8\n    k.box(STEEL, [0.2, 0.35, 0.02], [Math.cos(a) * (Ro + 0.1), ph + 0.04 + 0.175, Math.sin(a) * (Ro + 0.1)], new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), -a))\n  }\n  const rw = pipeWall(g.riserNps, 'STD')\n  const yRed = Hr - 0.55\n  k.pipe(ARM, [0, ph + 0.04, 0], [0, yRed, 0], Ro * 2, rw, 48)\n  // concentric reducer to the arm size, flanged joint, base slewing swivel\n  k.lathe(ARM, [[Ro, yRed], [od / 2, Hr - 0.2], [od / 2 - 0.01, Hr - 0.2]].map(([r, y]) => [r, y - yRed]) as [number, number][], [0, yRed, 0], 'y', 48)\n  pipe(k, [0, Hr - 0.2, 0], [0, Hr - 0.12, 0], g.nps, ARM)\n  flangedJoint(k, g.nps, p.rating, [0, Hr - 0.06, 0], 'y')\n  swivel(k, [0, Hr, 0], [0, Hr + g.hs, 0], od)\n  // slew ring gear\n  k.cyl(STEEL, Ro + 0.12, 0.06, [0, Hr - 0.6, 0], 'y', 48)\n  for (let i = 0; i < 48; i++) {\n    const a = (i / 48) * Math.PI * 2\n    k.box(STEEL, [0.04, 0.06, 0.03], [Math.cos(a) * (Ro + 0.135), Hr - 0.6, Math.sin(a) * (Ro + 0.135)], new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), -a))\n  }\n  // inlet nozzle, drain, vent\n  nozzle(k, { base: g.inlet.base, dir: [0, 0, -1], nps: g.nps, rating: p.rating, standout: g.inlet.standout, mat: ARM })\n  k.pipe(ARM, [Ro * 0.7, ph + 0.18, 0], [Ro + 0.25, ph + 0.18, 0], 0.06, 0.005)\n  k.box('valveBody', [0.12, 0.1, 0.1], [Ro + 0.32, ph + 0.18, 0])\n  k.box('actuatorRed', [0.03, 0.02, 0.2], [Ro + 0.32, ph + 0.25, 0.08])\n  k.cyl('brass', 0.035, 0.06, [Ro + 0.41, ph + 0.18, 0], 'x', 12)\n  // platform under the swivels, caged ladder, tag, earth\n  const yp = Hr - 1.2\n  ringPlatform(k, yp, Ro + 0.01, 0.85, Math.PI / 2, (3 * Math.PI) / 2)\n  const lx = -(Ro + 0.95)\n  ladder(k, [lx, ph, 0], yp, [-1, 0, 0])\n  for (const y of [ph + 1.2, yp - 1.5]) k.box(STEEL, [0.95 - 0.02, 0.06, 0.06], [lx / 2 - Ro / 2, y, 0])\n  if (p.tag) tagPlate(k, p.tag, [0, ph + 1.7, Ro + 0.003], 'z', 0.32)\n  earthBond(k, [Ro * 0.7, ph + 0.35, Ro * 0.72], [0.7, 0, 0.72], 0, Ro + 0.25)\n  // hydraulic power unit with accumulators and tubing up the riser\n  if (p.hydraulic) {\n    const hx = Ro + 1.6\n    const hz = -0.9\n    plinth(k, hx, hz, 1.9, 1.2, 0.15)\n    k.box(STEEL, [1.7, 0.12, 1.0], [hx, 0.21, hz])\n    k.rbox('actuatorBlue', [0.9, 0.6, 0.7], 0.02, [hx - 0.25, 0.57, hz])\n    k.cyl('motorPaint', 0.16, 0.45, [hx + 0.45, 0.45, hz], 'x', 24)\n    k.cyl('motorPaint', 0.11, 0.2, [hx + 0.2, 0.45, hz], 'x', 20)\n    for (const dx of [-0.55, -0.35]) {\n      k.cyl('actuatorRed', 0.09, 0.7, [hx + dx, 1.22, hz - 0.3], 'y', 20)\n      k.dome('actuatorRed', 0.09, [hx + dx, 1.57, hz - 0.3], 'y')\n    }\n    k.box(STEEL, [0.05, 0.9, 0.05], [hx + 0.3, 0.72, hz + 0.45])\n    junctionBox(k, [hx + 0.3, 1.0, hz + 0.475], 'z', [0.4, 0.35, 0.14], 'stainless')\n    gauge(k, [hx - 0.25, 0.95, hz + 0.36], 'z', 0.08, 'bar', 250)\n    const ty = 0.35\n    k.tube('tubing', [[hx - 0.6, ty, hz + 0.2], [Ro + 0.06, ty, hz + 0.2], [Ro + 0.06, ty, -0.05], [Ro + 0.04, ty + 0.2, -0.05], [Ro + 0.04, Hr - 0.7, -0.05]], 0.009)\n    k.tube('tubing', [[hx - 0.6, ty + 0.05, hz + 0.25], [Ro + 0.1, ty + 0.05, hz + 0.25], [Ro + 0.1, ty + 0.05, 0.05], [Ro + 0.04, ty + 0.25, 0.05], [Ro + 0.04, Hr - 0.7, 0.05]], 0.009)\n    for (let y = 1; y < Hr - 0.8; y += 1.5) k.box(STEEL, [0.06, 0.04, 0.16], [Ro + 0.03, y, 0])\n  }\n}\n\nfunction buildTurret(k: Kit, g: L, p: typeof defaults) {\n  const { R, Hr, hs, Hp, xc, Ro } = g\n  const y0 = Hr + hs\n  // vertical riser stub and bend to the luffing swivel (axis +X)\n  pipe(k, [0, y0, 0], [0, Hp - R, 0], g.nps, ARM)\n  k.bend(ARM, [0, Hp - R, 0], [0, 1, 0], [1, 0, 0], R, g.od, Math.PI / 2, wall(g))\n  swivel(k, [R, Hp, 0], [R + g.sw, Hp, 0], g.od)\n  // slewing head on the swivel, box girder forward of the pivot (clear of the counterweight sweep), bearing cheek\n  const x0 = -(Ro + 0.35)\n  const x1 = g.xt + 0.2\n  const z0 = g.od * 0.5 + 0.02\n  const zg = 0.6\n  k.cyl(STEEL, g.od * 0.9, 0.12, [0, y0 + 0.06, 0], 'y', 32)\n  k.box(STEEL, [x1 - x0, 0.3, zg], [(x1 + x0) / 2, y0 + 0.15, z0 + zg / 2])\n  k.box(STEEL, [0.5, 0.3, z0 + 0.1], [0, y0 + 0.15, z0 / 2])\n  for (const x of [x0 + 0.02, -0.25, g.xa - 0.35, x1 - 0.02]) k.box(STEEL, [0.02, 0.26, zg - 0.04], [x, y0 + 0.15, z0 + zg / 2])\n  const cheek = new THREE.Shape()\n  cheek.moveTo(-0.25, Hp - y0 + 0.25)\n  cheek.lineTo(0.22, Hp - y0 + 0.25)\n  cheek.lineTo(z0 + zg, 0.3)\n  cheek.lineTo(z0, 0.3)\n  cheek.lineTo(-0.25, Hp - y0 - 0.3)\n  cheek.closePath()\n  k.extrude(STEEL, cheek, 0.05, new THREE.Matrix4().makeBasis(new THREE.Vector3(0, 0, 1), new THREE.Vector3(0, 1, 0), new THREE.Vector3(-1, 0, 0)).setPosition(xc + 0.025, y0, 0))\n  k.cyl('machined', 0.21, 0.16, [xc, Hp, 0], 'x', 28)\n  k.boltCircle('boltXylan', 'nut', 6, 0.16, [xc - 0.1, Hp, 0], 'x', 0.02, 0.2, [0.02, 0.18])\n  k.cyl('stainless', 0.09, g.xt - xc + 0.06, [(xc + g.xt) / 2, Hp, 0], 'x', 16)\n  // slew drive: hydraulic motor and pinion on a bracket\n  if (p.hydraulic) {\n    const mx = -(Ro + 0.2)\n    k.box(STEEL, [0.3, y0 - (Hr - 0.6) + 0.1, 0.04], [mx, (y0 + Hr - 0.6) / 2, Ro * 0.3 + 0.12])\n    k.cyl(STEEL, 0.07, 0.07, [mx, Hr - 0.6, Ro * 0.3], 'y', 14)\n    k.cyl('actuatorBlue', 0.1, 0.35, [mx, Hr - 0.35, Ro * 0.3], 'y', 20)\n  }\n  // luff end stops and envelope limit switches\n  for (const x of [g.xa - 0.2, x1 - 0.1]) {\n    k.box('rubberBlack', [0.12, 0.15, 0.15], [x, y0 + 0.375, z0 + 0.1])\n    k.box('actuatorYellow', [0.08, 0.12, 0.08], [x, y0 + 0.36, z0 + zg - 0.08])\n  }\n}\n\nfunction buildInboard(k: Kit, g: L) {\n  const { R, sw, xa, xt, C, od, s } = g\n  // heel bend from the swivel into the arm, trunnion hub\n  k.bend(ARM, [R + sw, 0, 0], [1, 0, 0], [0, 0, 1], R, od, Math.PI / 2, wall(g))\n  k.cyl(STEEL, od * 0.6 + 0.05, xt - (R + sw) - 0.05, [(xt + R + sw) / 2 - 0.025, 0, 0], 'x', 28)\n  // inboard arm, apex bend, outboard swivel, vacuum breaker\n  pipe(k, [xa, 0, R], [xa, 0, C - R], g.nps, ARM)\n  k.bend(ARM, [xa, 0, C - R], [0, 0, 1], [1, 0, 0], R, od, Math.PI / 2, wall(g))\n  swivel(k, [xa + R, 0, C], [xa + R + sw, 0, C], od)\n  k.cyl('valveBody', 0.05, 0.16, [xa + R * 0.35, od / 2 + 0.06, C - R * 0.06], 'y', 14)\n  k.cyl('actuatorRed', 0.065, 0.04, [xa + R * 0.35, od / 2 + 0.16, C - R * 0.06], 'y', 14)\n  // counterweight boom and block behind the pivot\n  const Lc = 0.3 * C\n  for (const dx of [-0.12, 0.12]) k.box(STEEL, [0.08, 0.3, Lc + 0.3], [xa + dx, 0, -Lc / 2 + 0.15])\n  k.box(STEEL, [0.34, 0.06, 0.6], [xa, od / 2 + 0.02, R + 0.3])\n  counterweight(k, [xa, -0.15 * s, -Lc], [0.5 * s, 1.1 * s, 1.0 * s])\n  // lug for the luffing cylinder, hose bundle along the arm\n  k.box(STEEL, [0.04, 0.25, 0.3], [xa - 0.25, -od / 2 - 0.08, 0.28 * C])\n  k.box(STEEL, [0.3, 0.04, 0.3], [xa - 0.12, -od / 2 + 0.01, 0.28 * C])\n  k.rod('rubberBlack', [xa, od / 2 + 0.03, R + 0.3], [xa, od / 2 + 0.03, C - R], 0.024, 8)\n  for (let z = R + 1; z < C - R; z += 1.6) k.torus('stainless', od / 2 + 0.004, 0.008, [xa, 0, z], 'z', 6, 24)\n}\n\nfunction buildLever(k: Kit, g: L) {\n  const { xt, a, b, s } = g\n  // outboard pantograph lever: crank up to the tie rod, rear arm with the outboard counterweight\n  k.cyl(STEEL, 0.17, 0.08, [xt, 0, 0], 'x', 24)\n  k.box(STEEL, [0.04, a + 0.15, 0.18], [xt, a / 2, 0])\n  k.cyl('machined', 0.05, 0.12, [xt, a, 0], 'x', 14)\n  k.box(STEEL, [0.04, 0.2, b], [xt, 0, -b / 2])\n  k.box(STEEL, [0.04, 0.2, b], [xt + 0.12, 0, -b / 2])\n  counterweight(k, [xt + 0.3 * s, 0, -b], [0.55 * s, 0.8 * s, 0.75 * s])\n}\n\nfunction buildOutboard(k: Kit, g: L) {\n  const { xa, R, sw, xo, D, od, a, xt } = g\n  k.bend(ARM, [xa + R + sw, 0, 0], [1, 0, 0], [0, -1, 0], R, od, Math.PI / 2, wall(g))\n  pipe(k, [xo, -R, 0], [xo, -D, 0], g.nps, ARM)\n  // crank at the apex, parallel to the base lever\n  k.box(STEEL, [xo - xt + 0.05, 0.16, 0.16], [(xo + xt) / 2, 0.06, 0])\n  k.box(STEEL, [0.04, a + 0.1, 0.18], [xt, a / 2, 0])\n  k.cyl('machined', 0.05, 0.12, [xt, a, 0], 'x', 14)\n  k.rod('rubberBlack', [xo, -R - 0.3, od / 2 + 0.03], [xo, -D + 0.3, od / 2 + 0.03], 0.024, 8)\n  for (let y = -R - 1; y > -D + 0.4; y -= 1.6) k.torus('stainless', od / 2 + 0.004, 0.008, [xo, y, 0], 'y', 6, 24)\n  // parking pin bracket near the lower end\n  k.box(STEEL, [0.2, 0.25, 0.05], [xo, -D + 0.6, -od / 2 - 0.03])\n}\n\nfunction buildCoupler(k: Kit, g: L, p: typeof defaults) {\n  const { R, sw, od, nps } = g\n  // triple swivel: Y, X then Z axes\n  swivel(k, [0, 0, 0], [0, -sw, 0], od)\n  k.bend(ARM, [0, -sw, 0], [0, -1, 0], [1, 0, 0], R, od, Math.PI / 2, wall(g))\n  swivel(k, [R, -sw - R, 0], [R + sw, -sw - R, 0], od)\n  k.bend(ARM, [R + sw, -sw - R, 0], [1, 0, 0], [0, -1, 0], R, od, Math.PI / 2, wall(g))\n  const x = 2 * R + sw\n  let y = -sw - 2 * R\n  // insulating flange (orange sleeve kit)\n  pipe(k, [x, y, 0], [x, y - 0.15, 0], nps, ARM)\n  flangedJoint(k, nps, p.rating, [x, y - 0.2, 0], '-y')\n  ring(k, paint('#d9661f', { clearcoat: 0 }), od / 2 + 0.01, od * 0.68, 0.012, [x, y - 0.2, 0], 'y')\n  y -= 0.25\n  pipe(k, [x, y, 0], [x, y - 0.08, 0], nps, ARM)\n  y -= 0.08\n  if (p.ers) {\n    // emergency release coupler: twin ball valves either side of the release collar\n    const vb = od * 1.5\n    for (const [yc, sgn] of [[y - vb * 0.6, 1], [y - vb * 1.9, -1]] as const) {\n      ring(k, 'valveBody', od / 2 - wall(g), od * 0.95, vb * 0.9, [x, yc, 0], 'y')\n      k.cyl('pipeBore', od / 2 - wall(g), vb * 0.9, [x, yc, 0], 'y', 32, undefined, true)\n      k.rbox('actuatorYellow', [0.22, 0.2, 0.25], 0.02, [x, yc, od * 0.95 + 0.12])\n      k.cyl('actuatorBlue', 0.05, 0.3, [x + 0.1, yc + sgn * 0.02, od * 0.95 + 0.12], 'x', 14)\n    }\n    const yr = y - vb * 1.25\n    ring(k, 'flangeSteel', od / 2 - wall(g), od * 1.0, 0.14, [x, yr, 0], 'y')\n    ring(k, 'actuatorRed', od / 2, od * 1.05, 0.05, [x, yr, 0], 'y')\n    for (let i = 0; i < 4; i++) {\n      const a = (i / 4) * Math.PI * 2 + Math.PI / 4\n      k.box('flangeSteel', [0.05, 0.22, 0.05], [x + Math.cos(a) * od * 1.05, yr, Math.sin(a) * od * 1.05])\n    }\n    hydraulicCylinder(k, [x + od * 1.15, y - vb * 0.6, 0.08], [x + od * 1.15, y - vb * 1.9, 0.08], 0.05)\n    y -= vb * 2.5\n  }\n  pipe(k, [x, y, 0], [x, y - 0.05, 0], nps, ARM)\n  y -= 0.05\n  k.bend(ARM, [x, y, 0], [0, -1, 0], [0, 0, 1], R, od, Math.PI / 2, wall(g))\n  const z0 = R\n  swivel(k, [x, y - R, z0], [x, y - R, z0 + sw], od)\n  // hydraulic QC/DC coupler: body, clamp jaws, drive motor, presentation flange face\n  const zq = z0 + sw\n  const lq = Math.max(0.25, od * 1.1)\n  const yq = y - R\n  ring(k, 'flangeSteel', od / 2 - wall(g), od * 0.75, lq, [x, yq, zq + lq / 2], 'z')\n  k.cyl('pipeBore', od / 2 - wall(g), lq, [x, yq, zq + lq / 2], 'z', 32, undefined, true)\n  k.torus('machined', od * 0.75, 0.012, [x, yq, zq + lq * 0.15], 'z', 6, 36)\n  const nj = nps >= 12 ? 6 : 4\n  for (let i = 0; i < nj; i++) {\n    const a = (i / nj) * Math.PI * 2\n    const c = [x + Math.cos(a) * od * 0.88, yq + Math.sin(a) * od * 0.88] as const\n    const q = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), a)\n    k.box('actuatorYellow', [0.07, 0.08, lq * 0.9], [c[0], c[1], zq + lq * 0.5], q)\n    k.box('flangeSteel', [0.09, 0.06, 0.05], [x + Math.cos(a) * od * 0.82, yq + Math.sin(a) * od * 0.82, zq + lq + 0.02], q)\n  }\n  k.cyl('actuatorBlue', 0.07, 0.22, [x, yq + od * 0.88 + 0.1, zq + lq * 0.4], 'y', 18)\n  ring(k, 'machined', od / 2 - wall(g), od * 0.52, 0.012, [x, yq, zq + lq + 0.004], 'z')\n  k.spline('rubberBlack', [[x, yq + od * 0.88 + 0.2, zq + lq * 0.4], [x, yq + od * 0.88 + 0.5, zq * 0.5], [0, -sw - 0.2, od * 0.9], [0, -0.3, od / 2 + 0.03]], 0.018, 8, 30)\n  // drain at the low point\n  k.cyl('valveBody', 0.04, 0.12, [x, yq - od / 2 - 0.08, z0 + sw * 0.4], 'y', 12)\n  k.box('actuatorRed', [0.12, 0.015, 0.025], [x + 0.05, yq - od / 2 - 0.12, z0 + sw * 0.4])\n}\n\nfunction buildDyn(k: Kit, g: L, L: number, O: number, hydraulic: boolean) {\n  const { Hp, Hr, hs, C, a, xa, xt } = g\n  const rx = (v: V3, ang: number): V3 => [v[0], v[1] * Math.cos(ang) - v[2] * Math.sin(ang), v[1] * Math.sin(ang) + v[2] * Math.cos(ang)]\n  const piv: V3 = [0, Hp, 0]\n  const luffPt = (v: V3) => add(piv, rx(v, -L))\n  const leverPt = (v: V3) => add(piv, rx(v, -O))\n  const apex = luffPt([0, 0, C])\n  // pantograph tie rod: base lever crank pin to apex crank pin, parallel to the inboard arm\n  const crank = rx([0, a, 0], -O)\n  const t0 = add(piv, [xt, crank[1], crank[2]])\n  const t1 = add(apex, [xt, crank[1], crank[2]])\n  k.rod('structSteel', t0, t1, 0.055, 12)\n  for (const t of [t0, t1]) k.cyl('flangeSteel', 0.07, 0.1, add(t, [0.07, 0, 0]), 'x', 14)\n  if (!hydraulic) return\n  const y0 = Hr + hs + 0.3\n  const zf = g.od * 0.5 + 0.5\n  hydraulicCylinder(k, [xa - 0.35, y0, zf], luffPt([xa - 0.25, -g.od / 2 - 0.15, 0.28 * C]), 0.14)\n  hydraulicCylinder(k, [xt + 0.08, y0, zf], leverPt([xt + 0.08, a * 0.6, 0]), 0.11)\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": "MarineLoadingArm",
    "anchor": "base",
    "standards": [
      "OCIMF Design and Construction Specification for Marine Loading Arms",
      "EN 1474",
      "ASME B16.5",
      "ISO 14122"
    ]
  }
}