{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pedestal-crane",
  "type": "registry:component",
  "title": "Offshore pedestal crane",
  "description": "Diesel-hydraulic offshore pedestal crane with a wire-luffed lattice boom (hollow tubular chords and lacing). Flanged pedestal with bolted slew bearing and external gear, twin slew drives, caged ladders, rest and ring platforms, machinery house with louvres, cooler and exhaust, main and auxiliary winches, counterweight, operator cab beside the boom foot, A-frame gantry with lead sheaves and the upper luffing block, 6-fall luffing tackle with a floating spreader and LMI load cell, pendants, boom backstops, boom-angle indicator, head with rope keepers, anemometer and aviation lights, anti-two-block weight and switch, 4-fall hook block with a latched hook, whip line and ball, and an optional braced boom rest along +X. Sheave pitch diameters meet API 2C D/d ≥ 18. Slew (deg, counter-clockwise from above, 0 = boom along +X), luff (boom angle) and hook (drop below the head sheaves) move the boom, ropes and blocks; luff is clamped between the rest angle and 78°.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "lifting/pedestal-crane.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/lifting/pedestal-crane.tsx",
      "content": "// Offshore pedestal crane (diesel-hydraulic, lattice boom) after API 2C / EN 13852-1: pedestal, slew bearing, machinery house, cab, A-frame, luffing tackle, hook block and boom rest.\nimport * as THREE from 'three'\nimport { useMemo } from 'react'\nimport { Kit, type MatKey, type V3 } from '../lib/kit'\nimport { KitMesh, useKit, type GroupProps } from '../lib/mesh'\nimport { paint } from '../lib/materials'\nimport { grating, handrail, ladder, tagPlate } from '../lib/parts'\nimport { ACCESS } from '../lib/standards'\nimport { gratingMaterial, louvreMaterial, textMaterial } from '../lib/textures'\nimport { Rope, bar, boxMM, craneYellow, hazardPanel, hook, ropeMaterial } from './_rigging'\n\nexport interface PedestalCraneProps {\n  /** lattice boom length, foot pin to head, m */\n  boomLength?: number\n  /** slew, deg, counter-clockwise from above; 0 = boom along +X */\n  slew?: number\n  /** boom angle above horizontal, deg */\n  luff?: number\n  /** hook block below the boom-head sheaves, m */\n  hook?: number\n  /** slew bearing seat above the deck, m */\n  pedestalHeight?: number\n  /** safe working load, t (nameplate and boom marking) */\n  swl?: number\n  boomRest?: boolean\n  tag?: string\n}\n\nexport const defaults = {\n  boomLength: 36,\n  slew: 0,\n  luff: 42,\n  hook: 14,\n  pedestalHeight: 10,\n  swl: 25,\n  boomRest: true,\n  tag: 'CR-01',\n}\n\nconst DEG = Math.PI / 180\nconst clamp = (v: number, a: number, b: number) => Math.max(a, Math.min(b, v))\n\ntype Dims = ReturnType<typeof dims>\n\nfunction dims(boomLength: number, pedestalHeight: number) {\n  const L = clamp(boomLength, 15, 70)\n  const k = clamp(0.62 + L / 100, 0.82, 1.1)\n  const yB = clamp(pedestalHeight, 5, 25)\n  const yD = yB + 0.95\n  const foot = 2.9\n  const yFoot = yD + 0.8\n  const fA = -0.5\n  const yA = yD + 8.6 * clamp(L / 32, 0.78, 1)\n  const taper = 5.5 * Math.min(1, L / 32 + 0.2)\n  const bodyW = 1.9 * k\n  const st = [\n    { t: 0, w: Math.max(2.2 * k, 1.6), d: 0.35 },\n    { t: taper, w: bodyW, d: bodyW },\n    { t: L - taper, w: bodyW, d: bodyW },\n    { t: L, w: 1.0, d: 0.9 },\n  ]\n  // boom rest station and the drooped rest angle (rest top kept ≥ 1.2 m above the deck)\n  const tRest = 0.62 * L\n  const dRest = sectionAt(st, tRest).d\n  const restLuff = -Math.min(7 * DEG, Math.asin(clamp((yFoot - dRest / 2 - 1.4) / tRest, -0.5, 0.99)))\n  return {\n    L, k, yB, yTT: yB + 0.25, yD, foot, yFoot, fA, yA, st, tRest, restLuff,\n    tip: { t: L + 0.75, n: -0.05 },\n    lug: { t: L - 0.15, n: 0.55 },\n    aux: { t: L + 2.7, n: 0.78 },\n    guide: { t: 0.9, n: 0.85 },\n    uHub: [fA + 0.35, yA - 0.72] as [number, number],\n    lead: [fA, yA + 0.68] as [number, number],\n    // sheave radii: pitch diameter ≥ 18 × rope diameter (API 2C 7.4.1.1, 44 mm rope)\n    rLead: 0.42,\n    rGuide: 0.42,\n    rBlock: 0.42,\n    rAux: 0.38,\n    yR: yD + 3.3,\n    sheaveR: 0.5,\n  }\n}\n\nfunction sectionAt(st: { t: number; w: number; d: number }[], t: number) {\n  for (let i = 0; i < st.length - 1; i++)\n    if (t <= st[i + 1].t) {\n      const u = (t - st[i].t) / (st[i + 1].t - st[i].t)\n      return { w: st[i].w + (st[i + 1].w - st[i].w) * u, d: st[i].d + (st[i + 1].d - st[i].d) * u }\n    }\n  return st[st.length - 1]\n}\n\n/** boom frame (t along, n normal) → slewing frame (x, y) at luff (rad) */\nfunction bp(D: Dims, luff: number, t: number, n: number): [number, number] {\n  const c = Math.cos(luff)\n  const s = Math.sin(luff)\n  return [D.foot + t * c - n * s, D.yFoot + t * s + n * c]\n}\n\nexport function PedestalCrane({ boomLength = defaults.boomLength, slew = defaults.slew, luff = defaults.luff, hook: hookDrop = defaults.hook, pedestalHeight = defaults.pedestalHeight, swl = defaults.swl, boomRest = defaults.boomRest, tag = defaults.tag, ...group }: PedestalCraneProps & GroupProps) {\n  const D = useMemo(() => dims(boomLength, pedestalHeight), [boomLength, pedestalHeight])\n  const base = useKit((k) => buildBase(k, D, boomRest, swl, tag), [D, boomRest, swl, tag])\n  const slewing = useKit((k) => buildSlewing(k, D, swl, tag), [D, swl, tag])\n  const boom = useKit((k) => buildBoom(k, D), [D])\n  const block = useKit((k) => buildHookBlock(k, swl), [swl])\n  const spreader = useKit((k) => buildSpreader(k), [])\n  const ball = useKit((k) => buildBall(k), [])\n  const a2b = useKit((k) => buildA2B(k), [])\n  const rig = useMemo(() => rigging(D, clamp(luff, D.restLuff / DEG, 78) * DEG, hookDrop), [D, luff, hookDrop])\n  const rope = ropeMaterial()\n  return (\n    <group {...group}>\n      <KitMesh data={base} />\n      <group rotation={[0, slew * DEG, 0]}>\n        <KitMesh data={slewing} />\n        <group position={[D.foot, D.yFoot, 0]} rotation={[0, 0, rig.luff]}>\n          <KitMesh data={boom} />\n        </group>\n        {rig.ropes.map(([a, b, r], i) => (\n          <Rope key={i} a={a} b={b} r={r} material={rope} />\n        ))}\n        <group position={rig.spreader} quaternion={rig.spreaderQ}>\n          <KitMesh data={spreader} />\n        </group>\n        <group position={rig.block}>\n          <KitMesh data={block} />\n        </group>\n        <group position={rig.a2b}>\n          <KitMesh data={a2b} />\n        </group>\n        <group position={rig.ball}>\n          <KitMesh data={ball} />\n        </group>\n      </group>\n    </group>\n  )\n}\n\nexport default PedestalCrane\n\n// Moving ropes in the slewing frame: hoist lead, luffing tackle + pendants, main falls.\nfunction rigging(D: Dims, luff: number, hookDrop: number) {\n  const ropes: [V3, V3, number][] = []\n  const R = 0.022\n  const [gx, gy] = bp(D, luff, D.guide.t, D.guide.n + D.rGuide)\n  for (const s of [-1, 1]) ropes.push([[D.lead[0] + D.rLead, D.lead[1], s * 0.95], [gx, gy, s * 0.25], R])\n  const U = new THREE.Vector3(D.uHub[0], D.uHub[1], 0)\n  const [lx, ly] = bp(D, luff, D.lug.t, D.lug.n)\n  const P = new THREE.Vector3(lx, ly, 0)\n  const dir = P.clone().sub(U).normalize()\n  const nrm = new THREE.Vector3(-dir.y, dir.x, 0)\n  const S = U.clone().lerp(P, 0.3)\n  for (let i = 0; i < 6; i++) {\n    const z = -0.45 + 0.18 * i\n    const o = i % 2 ? 1 : -1\n    const a = U.clone().addScaledVector(nrm, o * D.rBlock)\n    const b = S.clone().addScaledVector(nrm, o * D.rBlock)\n    ropes.push([[a.x, a.y, z], [b.x, b.y, z], R])\n  }\n  for (const s of [-1, 1]) {\n    const a = S.clone().addScaledVector(dir, 0.45)\n    ropes.push([[a.x, a.y, s * 0.62], [P.x, P.y, s * 0.5], 0.035])\n  }\n  const [tx, ty] = bp(D, luff, D.tip.t, D.tip.n)\n  const hb = ty - Math.max(2.6, hookDrop)\n  for (const z of [-0.22, 0.22]) for (const dx of [-D.sheaveR, D.sheaveR]) ropes.push([[tx + dx, ty, z], [tx + dx, hb, z], R])\n  const [sx2, sy2] = bp(D, luff, D.tip.t + 0.3, -0.48)\n  const a2b: V3 = [tx + D.sheaveR, ty - 1.5, -0.22]\n  ropes.push([[sx2, sy2, -0.38], [a2b[0], a2b[1] + 0.2, -0.36], 0.008])\n  const [ax, ay] = bp(D, luff, D.aux.t, D.aux.n)\n  return {\n    luff,\n    ropes,\n    spreader: S.toArray() as V3,\n    spreaderQ: new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir),\n    block: [tx, hb, 0] as V3,\n    a2b,\n    ball: [ax + D.rAux, ay, 0] as V3,\n  }\n}\n\n// ---------------------------------------------------------------- pedestal + boom rest (static)\nfunction buildBase(k: Kit, D: Dims, withRest: boolean, swl: number, tag: string) {\n  const Y = craneYellow()\n  const dark: MatKey = 'castIron'\n  const kk = D.k\n  const yB = D.yB\n  const P = {\n    rCan: 1.45 * kk,\n    rShaft: 1.25 * kk,\n    rTop: 1.75 * kk,\n    rFlange: 1.95 * kk,\n    rBear: 1.8 * kk,\n    hCan: yB * 0.283,\n    hCone: yB * 0.358,\n    hShaft: yB - 1.2,\n    hTop: yB - 0.2,\n    ring: yB - 2.2,\n  }\n  const rAt = (y: number) => (y < P.hCan ? P.rCan : y < P.hCone ? P.rCan + ((P.rShaft - P.rCan) * (y - P.hCan)) / (P.hCone - P.hCan) : y < P.hShaft ? P.rShaft : P.rShaft + ((P.rTop - P.rShaft) * (y - P.hShaft)) / (P.hTop - P.hShaft))\n  const polar = (t: number, r: number, y: number): V3 => [r * Math.cos(t), y, -r * Math.sin(t)]\n  const ring = (mat: MatKey, rTop: number, rBot: number, y0: number, y1: number, seg = 40) => k.cyl(mat, rBot, y1 - y0, [0, (y0 + y1) / 2, 0], 'y', seg, rTop)\n\n  // base flange (underside on the deck), bolt ring, stiffened can, cone, shaft, flared top, top flange\n  const rBase = P.rCan + 0.28\n  ring(Y, rBase, rBase, 0, 0.07, 48)\n  k.boltCircle('boltXylan', 'nut', 32, rBase - 0.12, [0, 0, 0], 'y', 0.036, 0.14, [0.09])\n  ring(Y, P.rCan, P.rCan, 0.07, P.hCan)\n  for (let i = 0; i < 16; i++) {\n    const t = (i + 0.5) * (Math.PI / 8)\n    const sh = new THREE.Shape([new THREE.Vector2(0, 0), new THREE.Vector2(0.26, 0), new THREE.Vector2(0, Math.min(0.9, P.hCan * 0.5))])\n    const q = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), t)\n    k.extrude(Y, sh, 0.03, new THREE.Matrix4().compose(new THREE.Vector3(...polar(t, P.rCan - 0.01, 0.07)).add(new THREE.Vector3(Math.sin(t), 0, Math.cos(t)).multiplyScalar(-0.015)), q, new THREE.Vector3(1, 1, 1)))\n  }\n  ring(Y, P.rCan + 0.06, P.rCan + 0.06, P.hCan - 0.06, P.hCan + 0.06)\n  ring(Y, P.rShaft, P.rCan, P.hCan, P.hCone)\n  ring(Y, P.rShaft, P.rShaft, P.hCone, P.hShaft)\n  ring(Y, P.rShaft + 0.05, P.rShaft + 0.05, (P.hCone + P.hShaft) / 2, (P.hCone + P.hShaft) / 2 + 0.1)\n  ring(Y, P.rTop, P.rShaft, P.hShaft, P.hTop)\n  ring(Y, P.rFlange, P.rFlange, P.hTop, yB, 56)\n  k.boltCircle('boltXylan', 'nut', 48, P.rFlange - 0.09, [0, P.hTop - 0.1, 0], 'y', 0.03, 0.12, [0.03])\n  // slew bearing outer ring with the external gear\n  ring(dark, P.rBear, P.rBear, yB, D.yTT, 56)\n  for (let i = 0; i < 72; i++) {\n    const t = (i / 72) * Math.PI * 2\n    k.box(dark, [0.06, 0.2, 0.08], polar(t, P.rBear + 0.035, yB + 0.12), new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), t))\n  }\n  ring('rubberBlack', P.rCan + 0.004, P.rCan + 0.004, 0.9, 1.0)\n  // pedestal door, nameplate, SWL plate, tag\n  const td = 112 * DEG\n  const dq = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), td + Math.PI / 2)\n  const dIn = new THREE.Vector3(...polar(td, 1, 0)).setY(0)\n  k.box('galvanised', [0.85, 1.95, 0.05], polar(td, P.rShaft + 0.01, P.ring + 1.05), dq)\n  k.box(dark, [0.08, 0.12, 0.05], new THREE.Vector3(...polar(td + 0.22 / P.rShaft, P.rShaft + 0.05, P.ring + 1.05)).toArray() as V3, dq)\n  const plate = (y: number, text: string, w: number, h: number, bg: string, fg: string) => {\n    const p = new THREE.Vector3(...polar(15 * DEG, rAt(y) + 0.012, y))\n    const q = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 0, 1), new THREE.Vector3(Math.cos(15 * DEG), 0, -Math.sin(15 * DEG)))\n    k.box('stainless', [w + 0.03, h + 0.03, 0.006], p.toArray() as V3, q)\n    k.plane(textMaterial(text, bg, fg, 512, Math.round((512 * h) / w)), w, h, p.addScaledVector(dIn.set(Math.cos(15 * DEG), 0, -Math.sin(15 * DEG)), 0.0035).toArray() as V3, q)\n  }\n  plate(1.55, `SWL ${swl} t`, 0.9, 0.3, '#f4f4ef', '#111111')\n  if (tag) tagPlate(k, tag, polar(-15 * DEG, rAt(1.55) + 0.003, 1.55), [Math.cos(15 * DEG), 0, Math.sin(15 * DEG)], 0.36)\n\n  // ladders and rest platforms (≤ 6 m flights, ISO 14122-4), ring platform under the slew bearing\n  const rl = P.rCan + 0.22\n  const n = Math.max(1, Math.ceil(P.ring / ACCESS.ladder.maxFlight))\n  const lv = Array.from({ length: n + 1 }, (_, i) => (P.ring * i) / n)\n  const ang = (i: number) => (i - 1) * 52 * DEG\n  const annulus = (y: number, r0: number, r1: number, a0: number, a1: number) => {\n    const g = new THREE.RingGeometry(r0, r1, Math.max(6, Math.round(((a1 - a0) * r1) / 0.4)), 1, a0, a1 - a0)\n    g.rotateX(-Math.PI / 2)\n    const pos = g.getAttribute('position') as THREE.BufferAttribute\n    const uv = g.getAttribute('uv') as THREE.BufferAttribute\n    for (let i = 0; i < uv.count; i++) uv.setXY(i, pos.getX(i) / 0.3, pos.getZ(i) / 0.3)\n    k.add(gratingMaterial(), g, new THREE.Matrix4().makeTranslation(0, y, 0))\n    k.lathe('galvanised', [[r1 - 0.006, y - 0.1], [r1, y - 0.1], [r1, y], [r1 - 0.006, y]], [0, 0, 0], 'y', 48, a0 + Math.PI / 2, a1 - a0)\n  }\n  const arcRail = (y: number, r: number, a0: number, a1: number) => {\n    const m = Math.max(2, Math.ceil((r * (a1 - a0)) / 0.5))\n    handrail(k, Array.from({ length: m + 1 }, (_, i) => polar(a0 + ((a1 - a0) * i) / m, r, y)))\n  }\n  const brackets = (y: number, r1: number, a0: number, a1: number, m: number) => {\n    for (let i = 0; i <= m; i++) {\n      const t = a0 + ((a1 - a0) * i) / m\n      bar(k, Y, polar(t, rAt(y - 0.09), y - 0.09), polar(t, r1, y - 0.09), 0.08, 0.14)\n      k.rod(Y, polar(t, rAt(y - 0.95), y - 0.95), polar(t, r1 - 0.45, y - 0.15), 0.035, 6)\n    }\n  }\n  for (let i = 1; i <= n; i++) {\n    const t = ang(i)\n    ladder(k, polar(t, rl, lv[i - 1]), lv[i], [Math.cos(t), 0, -Math.sin(t)], { mat: 'galvanised' })\n    for (let y = lv[i - 1] + 1.2; y < lv[i] - 0.3; y += 2.4)\n      for (const sd of [-0.2, 0.2]) {\n        const tt = t + sd / rl\n        bar(k, 'galvanised', polar(tt, rAt(y) - 0.02, y), polar(tt, rl, y), 0.06, 0.06)\n      }\n    if (i < n) {\n      const y = lv[i]\n      const a0 = t + 14 * DEG\n      const a1 = ang(i + 1) + 20 * DEG\n      const r1 = rl + 1.05\n      annulus(y, rAt(y) + 0.02, r1, a0, a1)\n      arcRail(y, r1 - 0.04, a0, a1)\n      handrail(k, [polar(a1, rAt(y) + 0.06, y), polar(a1, r1 - 0.04, y)])\n      brackets(y, r1, a0, a1, 2)\n    }\n  }\n  {\n    const y = P.ring\n    const tn = ang(n)\n    const notch = 12 * DEG\n    const rRing = rl + 1.45\n    annulus(y, rAt(y) + 0.02, rRing, tn + notch, tn - notch + Math.PI * 2)\n    annulus(y, rl + 0.8, rRing, tn - notch, tn + notch)\n    arcRail(y, rRing - 0.04, 0, Math.PI * 2)\n    brackets(y, rRing, tn + notch + 0.1, tn - notch - 0.1 + Math.PI * 2, 10)\n  }\n\n  if (withRest) buildRest(k, D, Y)\n}\n\n// Braced 4-leg boom rest under the parked boom (slew 0, along +X).\nfunction buildRest(k: Kit, D: Dims, Y: MatKey) {\n  const sec = sectionAt(D.st, D.tRest)\n  const [fr, yc] = bp(D, D.restLuff, D.tRest, -sec.d / 2)\n  const chordR = 0.095 * Math.sqrt(D.k)\n  const padTop = yc - chordR\n  const beamTop = padTop - 0.12\n  const yTop = beamTop - 0.4\n  const half = Math.max(0.95, sec.w / 2 + 0.4)\n  const lf = [fr - 0.75, fr + 0.75]\n  const ls = [-half, half]\n  for (const f of lf)\n    for (const s of ls) {\n      boxMM(k, Y, [f - 0.12, 0.04, s - 0.12], [f + 0.12, yTop, s + 0.12])\n      boxMM(k, Y, [f - 0.3, 0, s - 0.3], [f + 0.3, 0.04, s + 0.3])\n      for (const dx of [-0.2, 0.2]) for (const dz of [-0.2, 0.2]) k.hex('nut', 0.05, 0.03, [f + dx, 0.055, s + dz])\n    }\n  boxMM(k, Y, [fr - 0.25, yTop, -half - 0.25], [fr + 0.25, beamTop, half + 0.25])\n  for (const s of ls) boxMM(k, Y, [lf[0] - 0.12, yTop - 0.3, s - 0.1], [lf[1] + 0.12, yTop, s + 0.1])\n  const pad = sec.w / 2\n  for (const sg of [-1, 1]) {\n    boxMM(k, 'rubberBlack', [fr - 0.3, beamTop, sg * pad - 0.2], [fr + 0.3, padTop, sg * pad + 0.2])\n    bar(k, Y, [fr, beamTop, sg * (pad + 0.27)], [fr, beamTop + 0.65, sg * (pad + 0.47)], 0.4, 0.05, [0, 0, 1])\n  }\n  const tiers = [0.3, yTop / 3, (2 * yTop) / 3, yTop - 0.3]\n  const faces: [number, number, number, number][] = [\n    [lf[0], ls[0], lf[1], ls[0]],\n    [lf[0], ls[1], lf[1], ls[1]],\n    [lf[0], ls[0], lf[0], ls[1]],\n    [lf[1], ls[0], lf[1], ls[1]],\n  ]\n  if (yTop > 1.2)\n    for (let i = 0; i < 3; i++)\n      for (const [f0, s0, f1, s1] of faces) {\n        k.rod(Y, [f0, tiers[i], s0], [f1, tiers[i + 1], s1], 0.05, 6)\n        k.rod(Y, [f1, tiers[i], s1], [f0, tiers[i + 1], s0], 0.05, 6)\n        if (i > 0) boxMM(k, Y, [Math.min(f0, f1) - 0.06, tiers[i] - 0.06, Math.min(s0, s1) - 0.06], [Math.max(f0, f1) + 0.06, tiers[i] + 0.06, Math.max(s0, s1) + 0.06])\n      }\n  const yP = padTop - 2.0\n  if (yP > 2.2) {\n    const pf0 = lf[1] + 0.12\n    const pf1 = lf[1] + 1.0\n    grating(k, [(pf0 + pf1) / 2, yP, 0], pf1 - pf0, 2 * half)\n    for (const s of ls) {\n      boxMM(k, Y, [lf[1], yP - 0.2, s - 0.06], [pf1, yP - 0.03, s + 0.06])\n      k.rod(Y, [lf[1], yP - 1.0, s], [pf1 - 0.1, yP - 0.12, s], 0.04, 6)\n    }\n    boxMM(k, Y, [pf1 - 0.06, yP - 0.2, -half], [pf1, yP - 0.03, half])\n    const ld = Math.min(0.5, half - 0.45)\n    handrail(k, [[pf0, yP, -half + 0.03], [pf1 - 0.03, yP, -half + 0.03], [pf1 - 0.03, yP, ld - 0.38]])\n    handrail(k, [[pf1 - 0.03, yP, ld + 0.38], [pf1 - 0.03, yP, half - 0.03], [pf0, yP, half - 0.03]])\n    ladder(k, [pf1 + 0.12, 0, ld], yP, [1, 0, 0], { mat: 'galvanised' })\n  }\n}\n\n// ---------------------------------------------------------------- slewing assembly (rotates about Y)\nfunction buildSlewing(k: Kit, D: Dims, swl: number, tag: string) {\n  const Y = craneYellow()\n  const W = paint('#e8e9e4', { weather: 0.8 })\n  const dark: MatKey = 'castIron'\n  const blue: MatKey = 'motorPaint'\n  const B = (f0: number, y0: number, s0: number, f1: number, y1: number, s1: number, mat: MatKey = Y) => boxMM(k, mat, [f0, y0, s0], [f1, y1, s1])\n  const cylY = (mat: MatKey, f: number, s: number, y0: number, y1: number, r: number, seg = 16) => k.cyl(mat, r, y1 - y0, [f, (y0 + y1) / 2, s], 'y', seg)\n  const cylS = (mat: MatKey, f: number, y: number, s0: number, s1: number, r: number, seg = 16) => k.cyl(mat, r, s1 - s0, [f, y, (s0 + s1) / 2], 'z', seg)\n  const { yD, yTT, yR, fA, yA } = D\n  const rBear = 1.8 * D.k\n  const s0 = -Math.max(1.75, rBear + 0.05)\n\n  k.boltCircle('boltXylan', 'nut', 48, rBear - 0.08, [0, yTT - 0.02, 0], 'y', 0.03, 0.1, [0.05])\n  k.cyl(dark, rBear - 0.02, 0.06, [0, yTT - 0.03, 0], 'y', 56)\n  k.cyl(Y, 1.65 * D.k, yD - 0.5 - yTT, [0, (yTT + yD - 0.5) / 2, 0], 'y', 48)\n  B(-2.95, yD - 0.5, s0, 3.3, yD, 2.0)\n  // two slew drives: pinion on the bearing gear, gearbox, hydraulic motor\n  const rPin = rBear + 0.26\n  for (const sg of [-1, 1]) {\n    const f = rPin * Math.cos(36 * DEG)\n    const s = sg * rPin * Math.sin(36 * DEG)\n    cylY(dark, f, s, D.yB + 0.02, yTT - 0.02, 0.17, 14)\n    cylY(dark, f, s, yTT, yD - 0.5, 0.22, 14)\n    cylY(dark, f, s, yD, yD + 0.1, 0.22, 14)\n    cylY(blue, f, s, yD + 0.1, yD + 0.62, 0.16, 14)\n  }\n  B(-2.95, yD - 0.3, 2.0, -0.02, yD - 0.02, 2.95)\n  for (const f of [-2.0, -1.0]) B(f - 0.05, yD - 0.25, 2.0, f + 0.05, yD - 0.02, 2.85)\n\n  // machinery house with louvres, doors, roof cooler, exhaust stack and luffing winch\n  const H = { f0: -0.9, f1: 1.45, s0: -1.75, s1: 1.95 }\n  B(H.f0, yD, H.s0, H.f1, yR, H.s1, W)\n  B(H.f0 - 0.05, yR, H.s0 - 0.05, H.f1 + 0.05, yR + 0.08, H.s1 + 0.05, 'galvanised')\n  const lv = louvreMaterial('#8c9196')\n  for (const [f0, f1] of [[-0.65, 0.1], [0.35, 1.2]]) {\n    B(f0 - 0.04, yD + 0.98, H.s0 - 0.03, f1 + 0.04, yD + 2.62, H.s0 - 0.002, dark)\n    k.add(lv, new THREE.PlaneGeometry(f1 - f0, 1.6), new THREE.Matrix4().makeRotationY(Math.PI).setPosition((f0 + f1) / 2, yD + 1.8, H.s0 - 0.032))\n  }\n  k.add(lv, new THREE.PlaneGeometry(1.1, 1.2), new THREE.Matrix4().makeRotationY(-Math.PI / 2).setPosition(H.f0 - 0.006, yD + 2.0, -0.85))\n  const sign = textMaterial(`SWL ${swl} t`, '#111111', '#f2c40f', 1024, 220)\n  k.plane(sign, 2.2, 0.47, [0.3, yD + 2.95, H.s0 - 0.006], new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI))\n  B(-0.8, yD + 0.02, H.s1, -0.08, yD + 2.05, H.s1 + 0.03, 'galvanised')\n  B(-0.25, yD + 1.0, H.s1 + 0.03, -0.15, yD + 1.06, H.s1 + 0.07, dark)\n  B(H.f0 - 0.03, yD + 0.02, 0.4, H.f0, yD + 2.05, 1.15, 'galvanised')\n  B(0.75, yR + 0.08, -1.35, 1.4, yR + 0.75, -0.2, W)\n  for (const s of [-1.0, -0.52]) {\n    cylY('rubberBlack', 1.075, s, yR + 0.75, yR + 0.78, 0.22, 18)\n    k.disc(paint('#22252a'), 0.2, [1.075, yR + 0.785, s], 'y', 18)\n  }\n  k.cyl(dark, 0.24, 1.05, [-0.275, yR + 0.42, -1.2], 'x', 16)\n  for (const f of [-0.6, 0.05]) B(f - 0.04, yR + 0.08, -1.42, f + 0.04, yR + 0.3, -0.98, dark)\n  cylY(dark, -0.75, -1.2, yR + 0.42, yR + 2.3, 0.11, 12)\n  cylY('rubberBlack', -0.75, -1.2, yR + 2.1, yR + 2.3, 0.125, 12)\n  const yLw = yR + 0.62\n  cylS(dark, 0.05, yLw, -0.5, 0.7, 0.4, 24)\n  cylS(ropeMaterial(), 0.05, yLw, -0.45, 0.65, 0.44, 24)\n  for (const s of [-0.55, 0.7]) cylS(Y, 0.05, yLw, s, s + 0.05, 0.56, 24)\n  for (const s of [-0.68, 0.78]) B(-0.5, yR + 0.08, s, 0.6, yLw + 0.15, s + 0.1)\n  cylS(dark, 0.05, yLw, 0.88, 1.05, 0.24, 14)\n  cylS(blue, 0.05, yLw, 1.05, 1.4, 0.18, 14)\n\n  // open winch bay: main and auxiliary hoist winches (drums across the house)\n  const winch = (a: number, b: number, r: number, out: number): V3 => {\n    const f = -1.6\n    const y = yD + 0.95\n    cylS(dark, f, y, a + 0.1, b - 0.1, r, 24)\n    cylS(ropeMaterial(), f, y, a + 0.16, b - 0.16, r + 0.05, 24)\n    for (const s of [a + 0.1, b - 0.14]) cylS(Y, f, y, s, s + 0.04, r + 0.17, 28)\n    for (const s of [a, b - 0.1]) B(f - r - 0.3, yD, s, f + r + 0.3, y + 0.2, s + 0.1)\n    const sm = out < 0 ? a : b\n    cylS(dark, f, y, Math.min(sm, sm + out * 0.2), Math.max(sm, sm + out * 0.2), 0.3, 16)\n    cylS(blue, f, y, Math.min(sm + out * 0.2, sm + out * 0.55), Math.max(sm + out * 0.2, sm + out * 0.55), 0.2, 14)\n    return [f, y + r + 0.05, (a + b) / 2]\n  }\n  const mainDrum = winch(-1.2, 0.0, 0.45, -1)\n  const auxDrum = winch(0.25, 1.35, 0.38, 1)\n  // counterweight with hazard striping\n  const CW = { f0: -2.95, f1: -2.3, y0: yD - 0.9, y1: yD + 1.0 }\n  B(CW.f0, CW.y0, H.s0, CW.f1, CW.y1, H.s1)\n  hazardPanel(k, [CW.f0, (CW.y0 + CW.y1) / 2, (H.s0 + H.s1) / 2], H.s1 - H.s0, CW.y1 - CW.y0, [-1, 0, 0])\n  for (const sg of [-1, 1]) hazardPanel(k, [(CW.f0 + CW.f1) / 2, (CW.y0 + CW.y1) / 2, sg > 0 ? H.s1 : H.s0], CW.f1 - CW.f0, CW.y1 - CW.y0, [0, 0, sg])\n\n  // walkway with handrail and the ladder down to the pedestal ring platform\n  grating(k, [(-2.95 - 1.3) / 2, yD, 2.475], 1.65, 0.95)\n  grating(k, [(-0.7 - 0.02) / 2, yD, 2.475], 0.68, 0.95)\n  handrail(k, [[-2.93, yD, 2.02], [-2.93, yD, 2.93], [-0.05, yD, 2.93]])\n  ladder(k, [-1.0, D.yB - 2.2, 2.2], yD, [0, 0, 1], { mat: 'galvanised', cage: false })\n\n  // operator cab beside the boom foot\n  const C = { f0: 0.0, f1: 2.5, s0: 1.98, s1: 3.1, y0: yD + 0.3, y1: yD + 2.75 }\n  B(C.f0 + 0.1, yD - 0.3, C.s0, C.f1 - 0.1, C.y0, C.s1 - 0.05)\n  B(C.f0, C.y0, C.s0, C.f1, C.y1, C.s1)\n  B(C.f1, yD + 1.0, C.s0 + 0.1, C.f1 + 0.025, C.y1 - 0.12, C.s1 - 0.1, 'glass')\n  B(C.f1, C.y0 + 0.12, C.s0 + 0.14, C.f1 + 0.025, yD + 0.88, C.s1 - 0.14, 'glass')\n  B(C.f1, yD + 1.0, 2.52, C.f1 + 0.035, C.y1 - 0.12, 2.58)\n  B(0.75, yD + 1.0, C.s1, C.f1 - 0.1, C.y1 - 0.12, C.s1 + 0.025, 'glass')\n  B(1.55, yD + 1.0, C.s1, 1.62, C.y1 - 0.12, C.s1 + 0.035)\n  B(H.f1 + 0.1, yD + 1.0, C.s0 - 0.025, C.f1 - 0.1, C.y1 - 0.12, C.s0, 'glass')\n  B(1.25, C.y1, C.s0 + 0.12, C.f1 - 0.1, C.y1 + 0.025, C.s1 - 0.12, 'glass')\n  bar(k, dark, [C.f1, C.y1 - 0.02, (C.s0 + C.s1) / 2], [C.f1 + 0.4, C.y1 - 0.16, (C.s0 + C.s1) / 2], 1.2, 0.03)\n  for (const s of [2.25, 2.8]) bar(k, 'rubberBlack', [C.f1 + 0.04, yD + 1.08, s], [C.f1 + 0.04, yD + 1.95, s + 0.18], 0.02, 0.03, [1, 0, 0])\n  B(C.f0 - 0.03, C.y0 + 0.03, 2.15, C.f0, yD + 2.45, 2.9, 'galvanised')\n  B(C.f0 - 0.04, yD + 1.5, 2.3, C.f0 - 0.03, yD + 2.25, 2.75, 'glass')\n  B(C.f0 - 0.08, yD + 1.25, 2.72, C.f0 - 0.03, yD + 1.3, 2.84, dark)\n  B(0.1, C.y1, 2.1, 0.95, C.y1 + 0.38, 2.95, W)\n  B(C.f0 - 0.6, yD, 2.1, C.f0, yD + 0.3, 2.85, 'galvanised')\n  for (const z of [-0.8, 0.8]) B(H.f1, yD + 2.55, z - 0.2, H.f1 + 0.06, yD + 3.1, z + 0.2, dark)\n  if (tag) k.plane(textMaterial(tag, '#111111', '#f2c40f', 512, 128), 1.0, 0.25, [1.25, yD + 0.65, C.s1 + 0.004])\n  k.add(paint('#2b2f33'), new THREE.BoxGeometry(0.6, 0.9, 0.5), new THREE.Matrix4().makeTranslation(1.6, yD + 0.75, 2.55))\n\n  // A-frame gantry: front legs from the house roof, back stays to the counterweight, crosshead\n  const legS = [-1.6, 1.75]\n  for (const s of legS) {\n    bar(k, Y, [1.2, yR + 0.08, s], [fA, yA, s], 0.28, 0.28, [0, 0, 1])\n    bar(k, Y, [-2.62, CW.y1, s], [fA, yA, s], 0.26, 0.26, [0, 0, 1])\n    B(0.95, yR + 0.08, s - 0.22, 1.45, yR + 0.14, s + 0.22, dark)\n    B(-2.9, CW.y1, s - 0.2, -2.35, CW.y1 + 0.06, s + 0.2, dark)\n  }\n  B(fA - 0.25, yA - 0.2, -1.85, fA + 0.25, yA + 0.2, 2.0)\n  const tm = 0.49\n  const fMid = 1.2 + (fA - 1.2) * tm\n  const yMid = yR + 0.08 + (yA - yR - 0.08) * tm\n  bar(k, Y, [fMid, yMid, legS[0]], [fMid, yMid, legS[1]], 0.16, 0.16)\n  k.rod(Y, [1.2, yR + 0.08, legS[0]], [fMid, yMid, legS[1]], 0.06, 8)\n  k.rod(Y, [1.2, yR + 0.08, legS[1]], [fMid, yMid, legS[0]], 0.06, 8)\n  const fBk = -2.62 + (fA + 2.62) * 0.55\n  const yBk = CW.y1 + (yA - CW.y1) * 0.55\n  bar(k, Y, [fBk, yBk, legS[0]], [fBk, yBk, legS[1]], 0.16, 0.16)\n  for (const s of legS) {\n    B(fMid + 0.15, yMid + 0.3, s - 0.18, fMid + 0.35, yMid + 0.55, s + 0.18, dark)\n    k.plane('lampLens', 0.3, 0.2, [fMid + 0.352, yMid + 0.425, s], new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2))\n  }\n  k.cyl(dark, 0.09, 0.08, [fA, yA + 0.46, 0.2], 'y', 10)\n  k.sphere(paint('#ff2a1a', { emissive: '#ff1a0a', emissiveIntensity: 1.6 }), 0.13, [fA, yA + 0.55, 0.2], undefined, 12)\n  // hoist lead sheaves above the crosshead\n  for (const s of [-0.95, 0.95]) {\n    B(fA - 0.14, yA + 0.2, s - 0.12, fA + 0.14, D.lead[1] + 0.08, s - 0.09)\n    B(fA - 0.14, yA + 0.2, s + 0.09, fA + 0.14, D.lead[1] + 0.08, s + 0.12)\n    cylS(dark, D.lead[0], D.lead[1], s - 0.08, s + 0.08, D.rLead, 24)\n    k.torus(ropeMaterial(), D.rLead, 0.022, [D.lead[0], D.lead[1], s], 'z', 6, 24, Math.PI)\n    k.torus(Y, D.rLead + 0.07, 0.025, [D.lead[0], D.lead[1], s], 'z', 6, 24, Math.PI)\n    k.cyl('stainless', 0.05, 0.3, [D.lead[0], D.lead[1], s], 'z', 10)\n  }\n  // upper luffing block under the crosshead\n  const [uf, uy] = D.uHub\n  cylS(dark, uf, uy, -0.55, 0.55, D.rBlock, 26)\n  for (let i = 0; i < 6; i++) k.torus(ropeMaterial(), D.rBlock, 0.022, [uf, uy, -0.45 + 0.18 * i], 'z', 6, 26)\n  for (const s of [-0.62, 0.57]) B(fA - 0.05, yA - 1.0, s, fA + 0.75, yA - 0.2, s + 0.05)\n  // boom foot lugs + pin\n  const w = Math.max(0.98, 1.1 * D.k)\n  for (const sg of [-1, 1]) {\n    B(D.foot - 0.5, yD, sg * w, D.foot + 0.4, D.yFoot + 0.3, sg * (w + 0.2))\n    B(D.foot - 0.5, yD, sg * (w + 0.3), D.foot + 0.4, D.yFoot + 0.3, sg * (w + 0.5))\n  }\n  cylS(dark, D.foot, D.yFoot, -w - 0.55, w + 0.55, 0.11, 14)\n  // fixed ropes: drums → lead sheaves, luffing winch → upper block\n  const rm = ropeMaterial()\n  k.rod(rm, mainDrum, [D.lead[0] - D.rLead, D.lead[1], -0.95], 0.022, 6)\n  k.rod(rm, auxDrum, [D.lead[0] - D.rLead, D.lead[1], 0.95], 0.022, 6)\n  k.rod(rm, [0.05, yLw + 0.44, 0.1], [uf, uy - D.rBlock, 0.1], 0.022, 6)\n}\n\n// ---------------------------------------------------------------- lattice boom (foot-pin frame, along +X)\nfunction buildBoom(k: Kit, D: Dims) {\n  const Y = craneYellow()\n  const dark: MatKey = 'castIron'\n  const { L, st } = D\n  const B = (t: number, n: number, s: number): V3 => [t, n, s]\n  const chordR = 0.095 * Math.sqrt(D.k)\n  const CORN: [number, number][] = [[-1, -1], [1, -1], [1, 1], [-1, 1]]\n  const cp = (q: { t: number; w: number; d: number }, i: number) => B(q.t, (CORN[i][1] * q.d) / 2, (CORN[i][0] * q.w) / 2)\n  for (let i = 0; i < 4; i++) for (let j = 0; j < st.length - 1; j++) k.pipe(Y, cp(st[j], i), cp(st[j + 1], i), 2 * chordR, 0.016, 16)\n  const stations: { t: number; w: number; d: number }[] = []\n  for (let j = 0; j < st.length - 1; j++) {\n    const a = st[j]\n    const e = st[j + 1]\n    const nb = Math.max(1, Math.round((e.t - a.t) / (1.85 * Math.max(D.k, 0.7))))\n    for (let q = 0; q < nb; q++) {\n      const u = q / nb\n      stations.push({ t: a.t + (e.t - a.t) * u, w: a.w + (e.w - a.w) * u, d: a.d + (e.d - a.d) * u })\n    }\n  }\n  stations.push(st[st.length - 1])\n  for (let j = 0; j < stations.length; j++) {\n    const a = stations[j]\n    for (let i = 0; i < 4; i++) k.rod(Y, cp(a, i), cp(a, (i + 1) % 4), 0.04, 6)\n    if (j % 3 === 1) k.rod(Y, cp(a, 0), cp(a, 2), 0.035, 6)\n    if (j === stations.length - 1) break\n    const e = stations[j + 1]\n    for (let i = 0; i < 4; i++) {\n      const i2 = (i + 1) % 4\n      if (j % 2 === 0) k.rod(Y, cp(a, i), cp(e, i2), 0.045, 6)\n      else k.rod(Y, cp(a, i2), cp(e, i), 0.045, 6)\n    }\n  }\n  // foot pin bosses\n  for (const sg of [-1, 1]) k.cyl(Y, 0.2, 0.18, [0, 0, sg * (st[0].w / 2)], 'z', 18)\n  // rope guide sheaves on the boom foot\n  for (const s of [-0.25, 0.25]) {\n    k.cyl(dark, D.rGuide, 0.14, [D.guide.t, D.guide.n, s], 'z', 24)\n    k.torus(ropeMaterial(), D.rGuide, 0.022, [D.guide.t, D.guide.n, s], 'z', 6, 24, Math.PI)\n    for (const ds of [-0.11, 0.11]) {\n      bar(k, Y, [0.45, 0.2, s + ds], [D.guide.t, D.guide.n, s + ds], 0.03, 0.12)\n      bar(k, Y, [1.2, 0.28, s + ds], [D.guide.t, D.guide.n, s + ds], 0.03, 0.12)\n    }\n  }\n  // boom angle indicator facing the cab\n  {\n    const s = st[1].w / 2 + 0.19\n    bar(k, Y, [3.0, 0.1, st[1].w / 2], [3.0, 0.1, s], 0.06, 0.06)\n    k.cyl('equipmentPaint', 0.24, 0.04, [3.0, 0.1, s + 0.02], 'z', 20)\n    bar(k, 'rubberBlack', [3.0, 0.1, s + 0.045], [3.0, -0.1, s + 0.045], 0.02, 0.025, [0, 0, 1])\n  }\n  // boom head: striped side plates, main sheaves, top plate, whip jib, anemometer, light\n  for (const sg of [-1, 1]) {\n    boxMM(k, Y, [L - 0.3, -0.55, sg * 0.51], [L + 1.05, 0.5, sg * 0.55])\n    hazardPanel(k, [L + 0.375, -0.025, sg * 0.55], 1.35, 1.05, [0, 0, sg], 0.22)\n  }\n  boxMM(k, Y, [L - 0.3, 0.46, -0.51], [L + 0.7, 0.52, 0.51])\n  for (const s of [-0.22, 0.22]) {\n    k.cyl(dark, D.sheaveR, 0.16, [D.tip.t, D.tip.n, s], 'z', 28)\n    k.torus(ropeMaterial(), D.sheaveR - 0.01, 0.025, [D.tip.t, D.tip.n, s], 'z', 6, 32)\n  }\n  k.cyl(dark, 0.08, 1.2, [D.tip.t, D.tip.n, 0], 'z', 10)\n  for (const a of [Math.PI / 3, (2 * Math.PI) / 3]) k.cyl('galvanised', 0.02, 0.98, [D.tip.t + Math.cos(a) * (D.sheaveR + 0.07), D.tip.n + Math.sin(a) * (D.sheaveR + 0.07), 0], 'z', 8)\n  // anti-two-block limit switch under the head (weight hangs on the hoist falls)\n  boxMM(k, 'actuatorRed', [D.tip.t + 0.2, -0.6, -0.46], [D.tip.t + 0.4, -0.45, -0.3])\n  k.rod(dark, [D.tip.t + 0.3, -0.45, -0.38], [D.tip.t + 0.3, -0.3, -0.48], 0.02, 6)\n  // boom backstops (telescopic struts bearing on the house front at maximum boom angle)\n  for (const sg of [-1, 1]) {\n    const z = sg * 0.8\n    const a: V3 = [4.4, sectionAt(st, 4.4).d / 2, z]\n    const b: V3 = [1.55, 1.95, z]\n    const m: V3 = [a[0] + (b[0] - a[0]) * 0.55, a[1] + (b[1] - a[1]) * 0.55, z]\n    k.rod(Y, a, m, 0.075, 12)\n    k.rod('stainless', m, b, 0.05, 12)\n    k.cyl('rubberBlack', 0.11, 0.08, b, [b[0] - a[0], b[1] - a[1], 0], 14)\n  }\n  // floodlights on the foot section lighting the hook\n  for (const sg of [-1, 1]) {\n    const p: V3 = [2.6, sectionAt(st, 2.6).d / 2 + 0.18, sg * 0.55]\n    k.rod(dark, [p[0], p[1] - 0.18, p[2]], p, 0.025, 6)\n    k.box(dark, [0.18, 0.26, 0.34], [p[0], p[1] + 0.1, p[2]])\n    k.plane('lampLens', 0.3, 0.22, [p[0] + 0.091, p[1] + 0.1, p[2]], new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2))\n  }\n  for (const sg of [-1, 1]) {\n    bar(k, Y, [L + 0.3, 0.6, sg * 0.2], [L + 2.75, 0.78, sg * 0.2], 0.05, 0.36)\n    bar(k, Y, [L + 0.95, -0.2, sg * 0.42], [L + 2.3, 0.62, sg * 0.2], 0.05, 0.14)\n  }\n  k.cyl(dark, D.rAux, 0.14, [D.aux.t, D.aux.n, 0], 'z', 24)\n  k.torus(ropeMaterial(), D.rAux, 0.02, [D.aux.t, D.aux.n, 0], 'z', 6, 24, Math.PI / 2)\n  k.cyl(dark, 0.05, 0.5, [D.aux.t, D.aux.n, 0], 'z', 10)\n  {\n    const b0: V3 = [L + 0.2, 0.52, -0.3]\n    const top: V3 = [b0[0], b0[1] + 1.3, b0[2]]\n    k.rod('galvanised', b0, top, 0.03, 6)\n    k.rod('galvanised', [top[0], top[1], top[2] - 0.25], [top[0], top[1], top[2] + 0.25], 0.02, 6)\n    const an: V3 = [top[0], top[1], top[2] - 0.25]\n    k.rod(dark, an, [an[0], an[1] + 0.12, an[2]], 0.015, 6)\n    for (let i = 0; i < 3; i++) {\n      const t = (i / 3) * Math.PI * 2\n      const tp: V3 = [an[0] + Math.cos(t) * 0.12, an[1] + 0.12, an[2] + Math.sin(t) * 0.12]\n      k.rod(dark, [an[0], an[1] + 0.12, an[2]], tp, 0.008, 4)\n      k.sphere('equipmentPaint', 0.035, tp, undefined, 8)\n    }\n    const lb: V3 = [L + 0.2, 0.52, 0.3]\n    k.rod('galvanised', lb, [lb[0], lb[1] + 0.5, lb[2]], 0.03, 6)\n    k.sphere(paint('#ff2a1a', { emissive: '#ff1a0a', emissiveIntensity: 1.6 }), 0.13, [lb[0], lb[1] + 0.62, lb[2]], undefined, 12)\n  }\n  // ropes along the boom: guide sheaves → head sheaves (main) / whip sheave (aux)\n  const rm = ropeMaterial()\n  k.rod(rm, [D.guide.t, D.guide.n + D.rGuide, -0.25], [D.tip.t, D.tip.n + D.sheaveR, -0.22], 0.022, 6)\n  k.rod(rm, [D.guide.t, D.guide.n + D.rGuide, 0.25], [D.aux.t, D.aux.n + D.rAux, 0], 0.022, 6)\n  // pendant lugs\n  for (const sg of [-1, 1]) boxMM(k, Y, [L - 0.35, 0.45, sg * 0.5 - 0.04], [L + 0.05, 0.62, sg * 0.5 + 0.04])\n  for (const sg of [-1, 1]) k.cyl(dark, 0.05, 0.14, [D.lug.t, D.lug.n, sg * 0.5], 'z', 10)\n}\n\n// ---------------------------------------------------------------- moving blocks\nfunction buildHookBlock(k: Kit, swl: number) {\n  const Y = craneYellow()\n  const dark: MatKey = 'castIron'\n  for (const sg of [-1, 1]) {\n    boxMM(k, Y, [-0.62, -0.75, sg * 0.39], [0.62, 0.55, sg * 0.45])\n    hazardPanel(k, [0, -0.1, sg * 0.45], 1.24, 1.3, [0, 0, sg], 0.22)\n    k.cyl(dark, 0.5, 0.22, [0, 0, sg * 0.22], 'z', 28)\n    k.torus(ropeMaterial(), 0.5, 0.022, [0, 0, sg * 0.22], 'z', 6, 32)\n  }\n  k.cyl(dark, 0.09, 1.0, [0, 0, 0], 'z', 12)\n  for (const sg of [-1, 1]) k.hex('nut', 0.2, 0.06, [0, 0, sg * 0.48], 'z')\n  boxMM(k, dark, [-0.35, -0.98, -0.39], [0.35, -0.75, 0.39])\n  k.cyl(dark, 0.1, 0.4, [0, -1.15, 0], 'y', 14)\n  const hs = clamp(0.16 + swl / 250, 0.17, 0.32)\n  hook(k, [0, -1.35 - hs * 0.4, 0], hs, dark)\n  k.box(textMaterial(`SWL ${swl} t`, '#f2c40f', '#111111', 256, 64), [0.5, 0.125, 0.004], [0, 0.3, 0.458])\n}\n\nfunction buildSpreader(k: Kit) {\n  const Y = craneYellow()\n  k.cyl('castIron', 0.42, 1.04, [0, 0, 0], 'z', 26)\n  for (let i = 0; i < 6; i++) k.torus(ropeMaterial(), 0.42, 0.022, [0, 0, -0.45 + 0.18 * i], 'z', 6, 26)\n  k.box(Y, [0.22, 0.22, 1.5], [0, 0.45, 0])\n  for (const sg of [-1, 1]) k.box(Y, [1.0, 1.0, 0.05], [0, 0.15, sg * 0.58])\n  k.cyl('stainless', 0.06, 1.3, [0, 0, 0], 'z', 10)\n  // load cell (LMI) in the pendant equaliser\n  k.cyl('actuatorBlue', 0.06, 0.28, [0, 0.72, 0.62], 'y', 12)\n}\n\nfunction buildBall(k: Kit) {\n  const Y = craneYellow()\n  k.rod(ropeMaterial(), [0, 0, 0], [0, -2.58, 0], 0.02, 6)\n  k.sphere(Y, 0.32, [0, -2.9, 0], undefined, 18)\n  k.cyl('rubberBlack', 0.325, 0.12, [0, -2.9, 0], 'y', 18)\n  k.cyl('castIron', 0.05, 0.2, [0, -3.3, 0], 'y', 8)\n  hook(k, [0, -3.55, 0], 0.1, 'castIron')\n}\n\n// Anti-two-block weight: split collar around one hoist fall, eye for the switch chain.\nfunction buildA2B(k: Kit) {\n  k.lathe('actuatorRed', [[0.035, -0.15], [0.12, -0.15], [0.12, 0.15], [0.035, 0.15], [0.035, -0.15]], [0, 0, 0], 'y', 20)\n  k.torus('galvanised', 0.035, 0.008, [0, 0.19, -0.13], 'x', 6, 12)\n  k.rod('galvanised', [0, 0.15, -0.1], [0, 0.17, -0.13], 0.008, 6)\n}\n"
    },
    {
      "path": "lifting/_rigging.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/lifting/_rigging.tsx",
      "content": "// Shared rigging for the lifting category: wire rope, shackles, master links, 4-leg sling sets, hooks, hazard panels and oriented bars.\nimport * as THREE from 'three'\nimport { useMemo } from 'react'\nimport { Kit, mat4, uvScale, type MatKey, type V3 } from '../lib/kit'\nimport { paint } from '../lib/materials'\nimport { liftingLug } from '../lib/parts'\nimport { hazardStripeMaterial } from '../lib/textures'\n\nconst UNIT_ROPE = new THREE.CylinderGeometry(1, 1, 1, 8, 1, false)\n\n/** Galvanised wire rope (dark, oily). */\nexport const ropeMaterial = () => paint('#3a3d40', { metalness: 0.75, roughness: 0.42, clearcoat: 0, weather: 0 })\n/** Offshore crane / lifting-gear yellow (RAL 1003). */\nexport const craneYellow = () => paint('#eab308', { weather: 0.9 })\n\n/** Axis-aligned box from min to max corners. */\nexport function boxMM(k: Kit, mat: MatKey, a: V3, b: V3) {\n  k.box(mat, [Math.abs(b[0] - a[0]), Math.abs(b[1] - a[1]), Math.abs(b[2] - a[2])], [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2])\n}\n\n/** Rectangular bar from a to b, w across (perpendicular to up), h along `up`. */\nexport function bar(k: Kit, mat: MatKey, a: V3, b: V3, w: number, h: number, up: V3 = [0, 1, 0]) {\n  const A = new THREE.Vector3(...a)\n  const d = new THREE.Vector3(...b).sub(A)\n  const L = d.length()\n  if (L < 1e-6) return\n  const x = d.normalize()\n  let u = new THREE.Vector3(...up).normalize()\n  if (Math.abs(u.dot(x)) > 0.99) u = Math.abs(x.y) > 0.9 ? new THREE.Vector3(1, 0, 0) : new THREE.Vector3(0, 1, 0)\n  const z = new THREE.Vector3().crossVectors(x, u).normalize()\n  const y = new THREE.Vector3().crossVectors(z, x).normalize()\n  const m = new THREE.Matrix4().makeBasis(x, y, z).setPosition(A.addScaledVector(x, L / 2))\n  k.add(mat, new THREE.BoxGeometry(L, h, w), m)\n}\n\n/** Black/yellow hazard chevron panel of w × h, centred at pos, facing `normal`, laid 4 mm proud. */\nexport function hazardPanel(k: Kit, pos: V3, w: number, h: number, normal: V3, stripe = 0.3) {\n  const n = new THREE.Vector3(...normal).normalize()\n  const q = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 0, 1), n)\n  const g = new THREE.PlaneGeometry(w, h)\n  uvScale(g, w / (stripe * 2), h / (stripe * 2))\n  const p = new THREE.Vector3(...pos).addScaledVector(n, 0.004)\n  k.add(hazardStripeMaterial(), g, mat4(p.toArray() as V3, q))\n}\n\n/** Bow shackle: bow and pin in the plane of `along` and +Y, pin centre at pos (pairs with a padeye plate across `along`). s = jaw width. */\nexport function shackle(k: Kit, pos: V3, s = 0.06, along: 'x' | 'z' = 'x', mat: MatKey = 'galvanised') {\n  const r = s * 0.22\n  const q = along === 'x' ? new THREE.Quaternion() : new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2)\n  const m = (p: V3) => new THREE.Matrix4().compose(new THREE.Vector3(...p).applyQuaternion(q).add(new THREE.Vector3(...pos)), q, new THREE.Vector3(1, 1, 1))\n  const bow = new THREE.TorusGeometry(s / 2, r, 6, 16, Math.PI)\n  k.add(mat, bow, m([0, s * 0.9, 0]))\n  for (const sx of [-1, 1]) k.add(mat, new THREE.CylinderGeometry(r, r, s * 0.9, 6), m([sx * (s / 2), s * 0.45, 0]))\n  k.add('nut', new THREE.CylinderGeometry(r * 0.95, r * 0.95, s + r * 6, 8).rotateZ(Math.PI / 2), m([0, 0, 0]))\n  for (const sx of [-1, 1]) k.add(mat, new THREE.SphereGeometry(r * 1.25, 8, 6), m([sx * (s / 2), 0, 0]))\n}\n\n/** Top of a shackle's bow (where a sling eye sits). */\nexport const shackleTop = (pos: V3, s = 0.06): V3 => [pos[0], pos[1] + s * 1.4, pos[2]]\n\n/** Oblong master link, long axis vertical, centre at pos. */\nexport function masterLink(k: Kit, pos: V3, s = 0.25, mat: MatKey = 'galvanised') {\n  const g = new THREE.TorusGeometry(s * 0.32, s * 0.08, 8, 24)\n  g.scale(1, 1.55, 1)\n  k.add(mat, g, mat4(pos))\n}\n\n/** Four-leg sling set: legs from each attachment point to a master link at `apex`, with thimble eyes and ferrules. */\nexport function slingSet(k: Kit, eyes: V3[], apex: V3, s = 0.25, rope = 0.012) {\n  const rm = ropeMaterial()\n  const link = s * 0.32 * 1.55\n  const bottom: V3 = [apex[0], apex[1] - link + s * 0.04, apex[2]]\n  masterLink(k, apex, s)\n  // intermediate links (one per pair of legs)\n  for (const sx of [-1, 1]) {\n    const g = new THREE.TorusGeometry(s * 0.16, s * 0.05, 6, 18)\n    g.scale(1, 1.4, 1)\n    k.add('galvanised', g, mat4([bottom[0] + sx * s * 0.12, bottom[1] - s * 0.12, bottom[2]], new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2)))\n  }\n  const hang: V3 = [bottom[0], bottom[1] - s * 0.3, bottom[2]]\n  for (const e of eyes) {\n    const top: V3 = [hang[0] + Math.sign(e[0] - hang[0]) * s * 0.12, hang[1], hang[2] + Math.sign(e[2] - hang[2]) * s * 0.02]\n    const E = new THREE.Vector3(...e)\n    const T = new THREE.Vector3(...top)\n    const dir = T.clone().sub(E).normalize()\n    // thimble eye at the shackle, ferrule a little up the leg\n    const eyeC = E.clone().addScaledVector(dir, 0.06)\n    k.rod(rm, eyeC.toArray() as V3, T.toArray() as V3, rope, 6)\n    k.rod('aluminium', eyeC.clone().addScaledVector(dir, 0.12).toArray() as V3, eyeC.clone().addScaledVector(dir, 0.22).toArray() as V3, rope * 1.9, 8)\n    k.torus('galvanised', 0.035, rope * 0.9, eyeC.toArray() as V3, new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), new THREE.Vector3(-dir.z, 0, dir.x).normalize()), 6, 14)\n    k.rod(rm, T.toArray() as V3, [top[0], top[1] + 0.04, top[2]], rope, 6)\n  }\n}\n\n/** Forged single hook (DIN 15401 shape): shank up from pos, bowl below, latch. size = bowl inner radius. */\nexport function hook(k: Kit, pos: V3, size = 0.12, mat: MatKey = 'castIron', latch: MatKey = 'safetyYellow') {\n  const [x, y, z] = pos\n  const r = size\n  const t = size * 0.42\n  k.cyl(mat, t * 0.55, r * 1.3, [x, y + r * 0.65, z], 'y', 12)\n  k.hex('nut', t * 1.6, t * 0.6, [x, y + r * 1.35, z])\n  // bowl: 300° arc of a tapered section, open towards +X\n  const arc = new THREE.TorusGeometry(r, t * 0.6, 10, 24, (5 * Math.PI) / 3)\n  arc.rotateZ(Math.PI / 2 + Math.PI / 6)\n  k.add(mat, arc, mat4([x, y - r * 1.0, z], undefined, [1, 1.05, 1.3]))\n  k.sphere(mat, t * 0.55, [x + r * Math.cos(Math.PI / 6 + Math.PI / 2 + (5 * Math.PI) / 3), y - r + r * Math.sin(Math.PI / 6 + Math.PI / 2 + (5 * Math.PI) / 3), z], undefined, 10)\n  k.rod(latch, [x + t * 0.5, y + r * 0.1, z], [x + r * 0.95, y - r * 0.45, z], t * 0.12, 6)\n}\n\n/** Stretched-rope mesh between two points (moves with props without rebuilding geometry). */\nexport function Rope({ a, b, r = 0.022, material }: { a: V3; b: V3; r?: number; material?: THREE.Material }) {\n  const t = useMemo(() => {\n    const A = new THREE.Vector3(...a)\n    const d = new THREE.Vector3(...b).sub(A)\n    const L = Math.max(d.length(), 1e-4)\n    const q = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), d.normalize())\n    return { p: A.addScaledVector(d, L / 2), q, s: new THREE.Vector3(r, L, r) }\n  }, [a[0], a[1], a[2], b[0], b[1], b[2], r]) // eslint-disable-line react-hooks/exhaustive-deps\n  const m = material ?? ropeMaterial()\n  return <mesh geometry={UNIT_ROPE} material={m} position={t.p} quaternion={t.q} scale={t.s} castShadow />\n}\n\n/** Padeye plate (across `across`) with a bow shackle through its hole; returns the bow top for the sling eye. */\nexport function padeyeShackle(k: Kit, pos: V3, across: 'x' | 'z' = 'x', s = 1, mat: MatKey = 'structSteel'): V3 {\n  liftingLug(k, pos, across === 'x' ? 'z' : 'x', s, mat)\n  const pin: V3 = [pos[0], pos[1] + 0.12 * s * 0.65, pos[2]]\n  shackle(k, pin, 0.065 * s, across)\n  return shackleTop(pin, 0.065 * s)\n}\n"
    }
  ],
  "categories": [
    "lifting"
  ],
  "meta": {
    "export": "PedestalCrane",
    "anchor": "base",
    "standards": [
      "API Spec 2C",
      "EN 13852-1",
      "ISO 14122-3/4",
      "DNV-ST-0378"
    ]
  }
}