{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "deck-module",
  "type": "registry:component",
  "title": "Offshore deck module",
  "description": "Offshore topsides deck section: primary girders (HEB or welded plate girders) on the leg lines with fin-plate connections and web stiffeners, IPE secondaries, 12 mm deck plate with grated drain gutters and 3\" drain outlets through liquid-seal pots (ports drain-1…n, flange faces pointing down) or bar grating, coaming, ISO 14122 guard rail with self-closing gated openings, escape-route markings, CHS legs with node cans and chevron bracing below, and stub columns up to a stacked module. Walking surface: top of the floor at y = 0, so equipment placed at y = 0 sits on it. Girders and legs go below (leg foot at y = -legHeight, to sit on a jacket at y = 0); stack another module at y = upperDeckAt with legHeight: 0 (otherwise its legs run down through this floor).",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "structure/deck-module.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/structure/deck-module.tsx",
      "content": "// Offshore deck module: HEB / plate-girder grid with IPE secondaries, deck plate with drain gutters or bar grating, coaming, ISO 14122 guard rail with gated openings, legs down and stub columns up.\nimport { type Kit, eul, type V3 } from '../lib/kit'\nimport { KitMesh, useKit, type GroupProps } from '../lib/mesh'\nimport { flange } from '../lib/parts'\nimport { port, type Port } from '../lib/ports'\nimport { pipeOD, pipeWall } from '../lib/standards'\nimport { textMaterial } from '../lib/textures'\nimport { paint } from '../lib/materials'\nimport { beamTOS, boxMM, column, finPlate, gate, gratingBay, gusset, hazardBand, railPath, sec, toePlate, webStiffeners } from './_steel'\n\nexport type DeckSide = 'minX' | 'maxX' | 'minZ' | 'maxZ'\n\nexport interface DeckOpening {\n  side: DeckSide\n  /** centre of the opening along that side (x for Z sides, z for X sides), m */\n  at: number\n  width: number\n  /** self-closing gate across the opening */\n  gate?: boolean\n}\n\nexport interface DeckModuleProps {\n  /** along X, m */\n  length?: number\n  /** along Z, m */\n  width?: number\n  floor?: 'plate' | 'grating'\n  /** primary girders: EN/AISC name or welded plate girder 'PG <depth mm>' */\n  primary?: string\n  secondary?: string\n  /** max secondary beam pitch, m */\n  secondarySpacing?: number\n  /** leg count (4, 6 or 8), also sets the primary grid */\n  legs?: number\n  /** leg centre spacing along X / Z, m */\n  legSpanX?: number\n  legSpanZ?: number\n  /** legs below the floor (0 = none), m; leg foot at y = -legHeight */\n  legHeight?: number\n  legDiameter?: number\n  /** floor-to-floor height of an identical module stacked above (0 = no stub columns), m */\n  upperDeckAt?: number\n  column?: string\n  handrails?: boolean\n  railMinX?: boolean\n  railMaxX?: boolean\n  railMinZ?: boolean\n  railMaxZ?: boolean\n  openings?: DeckOpening[]\n  /** drain gutters with 3\" outlets (plate floor) */\n  drains?: boolean\n  markings?: boolean\n}\n\nexport const defaults = {\n  length: 24,\n  width: 16,\n  floor: 'plate' as 'plate' | 'grating',\n  primary: 'HEB 600',\n  secondary: 'IPE 300',\n  secondarySpacing: 1.5,\n  legs: 4,\n  legSpanX: 16,\n  legSpanZ: 12,\n  legHeight: 6,\n  legDiameter: 1.5,\n  upperDeckAt: 0,\n  column: 'HEB 400',\n  handrails: true,\n  railMinX: true,\n  railMaxX: true,\n  railMinZ: true,\n  railMaxZ: true,\n  openings: [{ side: 'maxX', at: 0, width: 1.0, gate: true }] as DeckOpening[],\n  drains: true,\n  markings: true,\n}\n\ntype P = typeof defaults\n\nconst DRAIN_NPS = 3\nconst GUTTER = { w: 0.2, depth: 0.15, drop: 0.75 }\n\n/** Grid lines through the edges and leg lines, subdivided so no span exceeds `max`. */\nfunction lines(edge: number, inner: number[], max: number) {\n  const base = [-edge, ...inner.filter((v) => Math.abs(v) < edge - 0.8), edge].sort((a, b) => a - b)\n  const out: number[] = []\n  for (let i = 0; i < base.length; i++) {\n    if (i > 0) {\n      const gap = base[i] - base[i - 1]\n      const n = Math.ceil(gap / max - 1e-6)\n      for (let j = 1; j < n; j++) out.push(base[i - 1] + (gap * j) / n)\n    }\n    if (!out.length || base[i] - out[out.length - 1] > 0.8) out.push(base[i])\n  }\n  return out\n}\n\nfunction layout(p: P) {\n  const L = Math.max(4, p.length)\n  const W = Math.max(3, p.width)\n  const Pm = sec(p.primary)\n  const S = sec(p.secondary)\n  const plate = p.floor === 'plate'\n  const tTop = plate ? 0.012 : 0.03\n  const tos = -tTop\n  const nx = Math.max(2, Math.round((p.legs || 4) / 2))\n  const spanX = Math.min(p.legSpanX, L - 1)\n  const spanZ = Math.min(p.legSpanZ, W - 1)\n  const legX = Array.from({ length: nx }, (_, i) => -spanX / 2 + (spanX * i) / (nx - 1))\n  const legZ = [-spanZ / 2, spanZ / 2]\n  const ex = L / 2 - Pm.b / 2\n  const ez = W / 2 - Pm.b / 2\n  const xLines = lines(ex, legX, 8)\n  const zLines = lines(ez, legZ, 8)\n  const sec2: number[] = []\n  const pitch = Math.max(0.6, p.secondarySpacing)\n  for (let j = 0; j < zLines.length - 1; j++) {\n    const n = Math.ceil((zLines[j + 1] - zLines[j]) / pitch - 1e-6)\n    for (let i = 1; i < n; i++) sec2.push(zLines[j] + ((zLines[j + 1] - zLines[j]) * i) / n)\n  }\n  // gutter strip centre (inside the edge girder flange) and the outlet x in each bay\n  const gz = W / 2 - Pm.b - 0.06 - GUTTER.w / 2\n  const drains = plate && p.drains\n  const outlets: V3[] = []\n  if (drains)\n    for (let i = 0; i < xLines.length - 1; i++) {\n      const xm = (xLines[i] + xLines[i + 1]) / 2\n      for (const s of [-1, 1]) outlets.push([xm, tos - GUTTER.depth - GUTTER.drop, s * gz])\n    }\n  return { L, W, Pm, S, plate, tTop, tos, legX, legZ, xLines, zLines, sec2, gz, drains, outlets }\n}\n\nexport function ports(props: DeckModuleProps = {}): Port[] {\n  const lay = layout({ ...defaults, ...props } as P)\n  return lay.outlets.map((o, i) => port(`drain-${i + 1}`, `Deck drain ${i + 1}`, o, [0, -1, 0], DRAIN_NPS, 150, 'drain'))\n}\n\nexport function DeckModule(props: DeckModuleProps & GroupProps) {\n  const { length, width, floor, primary, secondary, secondarySpacing, legs, legSpanX, legSpanZ, legHeight, legDiameter, upperDeckAt, column: col, handrails, railMinX, railMaxX, railMinZ, railMaxZ, openings, drains, markings, ...group } = {\n    ...defaults,\n    ...props,\n  }\n  const p: P = { length, width, floor, primary, secondary, secondarySpacing, legs, legSpanX, legSpanZ, legHeight, legDiameter, upperDeckAt, column: col, handrails, railMinX, railMaxX, railMinZ, railMaxZ, openings, drains, markings }\n  const data = useKit((k) => build(k, p), [length, width, floor, primary, secondary, secondarySpacing, legs, legSpanX, legSpanZ, legHeight, legDiameter, upperDeckAt, col, handrails, railMinX, railMaxX, railMinZ, railMaxZ, JSON.stringify(openings), drains, markings])\n  return (\n    <group {...group}>\n      <KitMesh data={data} />\n    </group>\n  )\n}\n\nexport default DeckModule\n\nfunction build(k: Kit, p: P) {\n  const lay = layout(p)\n  const { L, W, Pm, S, plate, tTop, tos, legX, legZ, xLines, zLines, sec2, gz, drains, outlets } = lay\n  const st = 'structSteel'\n  const yP = tos - Pm.d / 2\n  const yS = tos - S.d / 2\n\n  // ---- primary grid: main girders along X (continuous), cross girders along Z framing in with fin plates\n  for (const z of zLines) beamTOS(k, -L / 2, z, L / 2, z, tos, p.primary, st)\n  for (const x of xLines) {\n    for (let j = 0; j < zLines.length - 1; j++) {\n      const za = zLines[j] + Pm.b / 2 + 0.006\n      const zb = zLines[j + 1] - Pm.b / 2 - 0.006\n      beamTOS(k, x, za, x, zb, tos, p.primary, st)\n      for (const [zl, d] of [[zLines[j], 1], [zLines[j + 1], -1]] as const) {\n        webStiffeners(k, [x, yP, zl], 'x', Pm)\n        finPlate(k, [x, yP, zl + d * Pm.tw / 2], 'z', d, Pm.d, Pm.b / 2 + 0.11)\n      }\n    }\n  }\n  // ---- secondaries along X between cross girders\n  for (const z of sec2) {\n    for (let i = 0; i < xLines.length - 1; i++) {\n      const xa = xLines[i] + Pm.b / 2 + 0.006\n      const xb = xLines[i + 1] - Pm.b / 2 - 0.006\n      beamTOS(k, xa, z, xb, z, tos, p.secondary, st)\n      finPlate(k, [xLines[i] + Pm.tw / 2, yS, z], 'x', 1, S.d, Pm.b / 2 + 0.1)\n      finPlate(k, [xLines[i + 1] - Pm.tw / 2, yS, z], 'x', -1, S.d, Pm.b / 2 + 0.1)\n    }\n  }\n\n  // ---- floor\n  const xEdges = [-L / 2, ...xLines.slice(1, -1), L / 2]\n  if (plate) {\n    const gzi = gz - GUTTER.w / 2\n    const gzo = gz + GUTTER.w / 2\n    const bands: [number, number][] = drains ? [[-W / 2, -gzo], [-gzi, gzi], [gzo, W / 2]] : [[-W / 2, W / 2]]\n    for (const [z0, z1] of bands) boxMM(k, 'deckPlate', [-L / 2, -tTop, z0], [L / 2, 0, z1])\n    if (drains) {\n      for (const s of [-1, 1]) {\n        // plate over the cross girders between gutter bays\n        for (const x of xLines) boxMM(k, 'deckPlate', [Math.max(-L / 2, x - Pm.b / 2 - 0.03), -tTop, s * gz - GUTTER.w / 2], [Math.min(L / 2, x + Pm.b / 2 + 0.03), 0, s * gz + GUTTER.w / 2])\n        for (let i = 0; i < xLines.length - 1; i++) gutter(k, xLines[i] + Pm.b / 2 + 0.03, xLines[i + 1] - Pm.b / 2 - 0.03, s * gz, tTop)\n      }\n      for (const o of outlets) drainOutlet(k, o, tos - GUTTER.depth)\n    }\n    // butt-weld seams over the girder lines\n    const seam = 'structSteelDark'\n    for (const z of zLines) k.box(seam, [L, 0.0015, 0.008], [0, 0.0007, z])\n    for (const x of xLines) k.box(seam, [0.008, 0.0015, W], [x, 0.0007, 0])\n  } else {\n    const zs = [-W / 2, ...[...zLines.slice(1, -1), ...sec2].sort((a, b) => a - b), W / 2]\n    for (let i = 0; i < xEdges.length - 1; i++)\n      for (let j = 0; j < zs.length - 1; j++) gratingBay(k, xEdges[i], zs[j], xEdges[i + 1], zs[j + 1], 0, 'z')\n  }\n\n  // ---- coaming, guard rails, openings\n  const ins = 0.05\n  const sides: Record<DeckSide, { a: [number, number]; b: [number, number]; on: boolean; s: (v: number) => number }> = {\n    minZ: { a: [-L / 2 + ins, -W / 2 + ins], b: [L / 2 - ins, -W / 2 + ins], on: p.railMinZ, s: (x) => x + L / 2 - ins },\n    maxZ: { a: [-L / 2 + ins, W / 2 - ins], b: [L / 2 - ins, W / 2 - ins], on: p.railMaxZ, s: (x) => x + L / 2 - ins },\n    minX: { a: [-L / 2 + ins, -W / 2 + ins], b: [-L / 2 + ins, W / 2 - ins], on: p.railMinX, s: (z) => z + W / 2 - ins },\n    maxX: { a: [L / 2 - ins, -W / 2 + ins], b: [L / 2 - ins, W / 2 - ins], on: p.railMaxX, s: (z) => z + W / 2 - ins },\n  }\n  for (const [name, sd] of Object.entries(sides) as [DeckSide, (typeof sides)[DeckSide]][]) {\n    const gaps: [number, number][] = (p.openings ?? []).filter((o) => o.side === name).map((o) => [sd.s(o.at) - o.width / 2, sd.s(o.at) + o.width / 2])\n    // coaming at the plate edge (also the toe plate), open at access openings\n    const alongX = name === 'minZ' || name === 'maxZ'\n    const ce = (alongX ? W : L) / 2 - 0.004\n    const sg = name.startsWith('min') ? -1 : 1\n    const cut = (v: number): [number, number][] => {\n      let pcs: [number, number][] = [[-v, v]]\n      for (const o of (p.openings ?? []).filter((q) => q.side === name)) {\n        const a = o.at - o.width / 2\n        const b = o.at + o.width / 2\n        pcs = pcs.flatMap(([p0, p1]) => (b <= p0 || a >= p1 ? [[p0, p1]] : [...(a > p0 ? [[p0, a]] : []), ...(b < p1 ? [[b, p1]] : [])]) as [number, number][])\n      }\n      return pcs\n    }\n    for (const [u0, u1] of cut(alongX ? L / 2 : W / 2 - 0.008)) {\n      if (alongX) toePlate(k, [u0, 0, sg * ce], [u1, 0, sg * ce])\n      else toePlate(k, [sg * ce, 0, u0], [sg * ce, 0, u1])\n    }\n    if (p.handrails && sd.on) {\n      const adjacentStart = alongX ? p.railMinX : p.railMinZ\n      railPath(k, [sd.a, sd.b], 0, gaps, false, { toe: false }, [alongX || !adjacentStart, alongX || !(alongX ? p.railMaxX : p.railMaxZ)])\n    }\n    for (const o of (p.openings ?? []).filter((q) => q.side === name)) {\n      const a = o.at - o.width / 2\n      const b = o.at + o.width / 2\n      const e = (alongX ? W : L) / 2\n      const pt = (u: number, inset: number): V3 => (alongX ? [u, 0.0015, sg * (e - inset)] : [sg * (e - inset), 0.0015, u])\n      hazardBand(k, pt(a, 0.1), pt(b, 0.1), 0.2, 0.003)\n      if (o.gate && p.handrails && sd.on) {\n        const h = pt(a + 0.0, ins)\n        const t = pt(b, ins)\n        gate(k, [h[0], 0, h[2]], [t[0], 0, t[2]])\n      }\n    }\n  }\n\n  // ---- markings: escape-route lines and load stencils\n  if (plate && p.markings) {\n    const yel = paint('#e2b018', { clearcoat: 0, roughness: 0.7, weather: 1 })\n    const e = 1.4\n    const lw = 0.075\n    for (const s of [-1, 1]) {\n      k.box(yel, [L - 2 * e + lw, 0.002, lw], [0, 0.001, s * (W / 2 - e)])\n      k.box(yel, [lw, 0.002, W - 2 * e - lw], [s * (L / 2 - e), 0.001, 0])\n    }\n    const txt = textMaterial('MAX UDL 10 kN/m²', '#666c70', '#f1f1ec', 512, 96)\n    k.plane(txt, 3.2, 0.6, [0, 0.0012, -W / 2 + e + 0.7], eul(-Math.PI / 2, 0, 0))\n    const esc = textMaterial('ESCAPE ROUTE', '#666c70', '#e2b018', 512, 96)\n    for (const s of [-1, 1]) k.plane(esc, 2.6, 0.48, [s * (L / 4), 0.0012, W / 2 - e / 2 - 0.15], eul(-Math.PI / 2, 0, 0))\n  }\n\n  // ---- legs below, with node cans, girder gussets and chevron bracing\n  if (p.legHeight > 0) {\n    const R = p.legDiameter / 2\n    const yF = -p.legHeight\n    const lm = 'structSteel'\n    for (const x of legX)\n      for (const z of legZ) {\n        k.pipe(lm, [x, yF, z], [x, tos, z], 2 * R, Math.max(0.04, p.legDiameter / 30), 32)\n        k.cyl(lm, R + 0.03, Pm.d + 0.5, [x, tos - (Pm.d + 0.5) / 2, z], 'y', 32)\n        k.cyl(lm, R + 0.002, 0.25, [x, tos - Pm.d - 0.625, z], 'y', 32, R + 0.03)\n        k.torus('structSteelDark', R + 0.004, 0.012, [x, yF + 0.6, z], 'y', 6, 40)\n        for (const [dx, dz] of [[1, 0], [-1, 0], [0, 1], [0, -1]] as const)\n          gusset(k, [x + dx * (R + 0.03), tos - Pm.d, z + dz * (R + 0.03)], [dx, 0, dz], [0, -1, 0], Math.min(0.9, R), Math.min(1.1, p.legHeight * 0.4), 0.025, lm)\n      }\n    if (p.legHeight > 2.5) {\n      const rb = Math.min(0.3, Math.max(0.12, R * 0.22))\n      const yb = tos - Pm.d\n      const brace = (a: V3, b: V3) => {\n        const apex: V3 = [(a[0] + b[0]) / 2, yb - 0.05, (a[2] + b[2]) / 2]\n        for (const q of [a, b]) k.rod(lm, [q[0], yF + 1.0, q[2]], apex, rb, 16)\n        k.box(lm, Math.abs(a[0] - b[0]) > 0.1 ? [1.0, 0.6, 0.03] : [0.03, 0.6, 1.0], [apex[0], yb - 0.3, apex[2]])\n      }\n      for (const z of legZ) for (let i = 0; i < legX.length - 1; i++) brace([legX[i], 0, z], [legX[i + 1], 0, z])\n      for (const x of [legX[0], legX[legX.length - 1]]) brace([x, 0, legZ[0]], [x, 0, legZ[1]])\n    }\n  }\n\n  // ---- stub columns up to the girder underside of an identical module at y = upperDeckAt\n  if (p.upperDeckAt > 0) {\n    const C = sec(p.column)\n    const yTop = p.upperDeckAt - tTop - Pm.d\n    if (yTop > 1) {\n      for (const x of legX)\n        for (const z of legZ) {\n          boxMM(k, st, [x - C.d / 2 - 0.1, 0, z - C.b / 2 - 0.1], [x + C.d / 2 + 0.1, 0.03, z + C.b / 2 + 0.1])\n          boxMM(k, st, [x - C.d / 2 - 0.1, yTop - 0.03, z - C.b / 2 - 0.1], [x + C.d / 2 + 0.1, yTop, z + C.b / 2 + 0.1])\n          column(k, x, z, 0.03, yTop - 0.03, p.column, st, 'x')\n          for (const s of [-1, 1]) {\n            gusset(k, [x + s * C.d / 2, 0.03, z], [s, 0, 0], [0, 1, 0], 0.09, 0.3, 0.015)\n            gusset(k, [x + s * C.d / 2, yTop - 0.03, z], [s, 0, 0], [0, -1, 0], 0.09, 0.3, 0.015)\n            gusset(k, [x, 0.03, z + s * (C.tw / 2)], [0, 0, s], [0, 1, 0], (C.b - C.tw) / 2, 0.3, 0.015)\n          }\n          // hazard band around the base\n          const hx = C.d / 2 + 0.25\n          const hz = C.b / 2 + 0.25\n          hazardBand(k, [x - hx, 0.0015, z - hz + 0.075], [x + hx, 0.0015, z - hz + 0.075], 0.15, 0.003)\n          hazardBand(k, [x - hx, 0.0015, z + hz - 0.075], [x + hx, 0.0015, z + hz - 0.075], 0.15, 0.003)\n          hazardBand(k, [x - hx + 0.075, 0.0015, z - hz + 0.15], [x - hx + 0.075, 0.0015, z + hz - 0.15], 0.15, 0.003)\n          hazardBand(k, [x + hx - 0.075, 0.0015, z - hz + 0.15], [x + hx - 0.075, 0.0015, z + hz - 0.15], 0.15, 0.003)\n        }\n    }\n  }\n}\n\n/** Gutter trough between two cross girders, centred on z = zc, grating cover flush with the floor. */\nfunction gutter(k: Kit, x0: number, x1: number, zc: number, tTop: number) {\n  const { w, depth } = GUTTER\n  const yb = -tTop - depth\n  const t = 0.006\n  boxMM(k, 'structSteel', [x0, yb - t, zc - w / 2 - t], [x1, yb, zc + w / 2 + t])\n  for (const s of [-1, 1]) boxMM(k, 'structSteel', [x0, yb, zc + s * (w / 2) - (s < 0 ? t : 0)], [x1, -tTop, zc + s * (w / 2) + (s > 0 ? t : 0)])\n  for (const x of [x0, x1]) boxMM(k, 'structSteel', [x - t / 2, yb, zc - w / 2], [x + t / 2, -tTop, zc + w / 2])\n  // ledges for the cover\n  for (const s of [-1, 1]) boxMM(k, 'galvanised', [x0, -0.042, zc + s * (w / 2 - 0.012) - 0.006], [x1, -0.03, zc + s * (w / 2 - 0.012) + 0.006])\n  gratingBay(k, x0 + 0.004, zc - w / 2 + 0.003, x1 - 0.004, zc + w / 2 - 0.003, 0, 'z')\n}\n\n/** 3\" drain outlet below a gutter at yTop: pipe, liquid-seal pot (stops gas back-flow between decks), pipe and a downward 150# flange at `face`. */\nfunction drainOutlet(k: Kit, face: V3, yTop: number) {\n  const od = pipeOD(DRAIN_NPS)\n  const L = flange(k, DRAIN_NPS, 150, face, [0, -1, 0], true)\n  const pot = { top: yTop - 0.2, h: 0.32, r: 0.11 }\n  k.pipe('pipePaint', [face[0], yTop - 0.006, face[2]], [face[0], pot.top, face[2]], od, pipeWall(DRAIN_NPS, '40'))\n  k.cyl('pipePaint', pot.r, pot.h, [face[0], pot.top - pot.h / 2, face[2]], 'y', 20)\n  for (const y of [pot.top, pot.top - pot.h]) k.cyl('pipePaint', pot.r + 0.01, 0.02, [face[0], y, face[2]], 'y', 20)\n  k.cyl('pipePaint', 0.025, 0.06, [face[0] + pot.r, pot.top - pot.h + 0.06, face[2]], 'x', 8) // clean-out plug\n  k.pipe('pipePaint', [face[0], pot.top - pot.h, face[2]], [face[0], face[1] + L, face[2]], od, pipeWall(DRAIN_NPS, '40'))\n}\n"
    },
    {
      "path": "structure/_steel.ts",
      "type": "registry:file",
      "target": "components/deck-twin/structure/_steel.ts",
      "content": "// Shared structural steel details: sections placed by top of steel, plate girders, base/cap/end/fin plates, bolts, gussets, grating bays, ISO 14122 guard rails and gates.\nimport * as THREE from 'three'\nimport { mat4, uvScale, type Kit, type MatKey, type V3 } from '../lib/kit'\nimport { sectionShape } from '../lib/parts'\nimport { ACCESS, SECTIONS, type Section } from '../lib/standards'\nimport { gratingMaterial, hazardStripeMaterial } from '../lib/textures'\n\nexport const clamp = (v: number, a: number, b: number) => Math.min(b, Math.max(a, v))\n\n/** Section by name. Also accepts welded plate girders \"PG <depth mm>[x<flange mm>]\", e.g. \"PG 1200\" or \"PG 1500x500\". */\nexport function sec(name: string): Section {\n  const m = /^PG\\s*(\\d+)(?:\\s*[xX]\\s*(\\d+))?$/.exec(String(name).trim())\n  if (m) {\n    const d = +m[1] / 1000\n    const b = m[2] ? +m[2] / 1000 : Math.round(d * 0.38 * 20) / 20 // typical\n    const tw = clamp(Math.round(d / 70 / 0.002) * 0.002, 0.012, 0.03) // typical\n    const tf = clamp(Math.round(d / 32 / 0.005) * 0.005, 0.025, 0.07) // typical\n    return { kind: 'I', d, b, tw, tf, r: 0, mass: (2 * b * tf + (d - 2 * tf) * tw) * 7850 }\n  }\n  return SECTIONS[name] ?? SECTIONS['HEB 300']\n}\n\nfunction shapeOf(name: string): THREE.Shape {\n  if (!/^PG/i.test(String(name).trim())) return sectionShape(SECTIONS[name] ? name : 'HEB 300')\n  const { d, b, tw, tf } = sec(name)\n  const sh = new THREE.Shape()\n  sh.moveTo(-b / 2, -d / 2).lineTo(b / 2, -d / 2).lineTo(b / 2, -d / 2 + tf).lineTo(tw / 2, -d / 2 + tf)\n  sh.lineTo(tw / 2, d / 2 - tf).lineTo(b / 2, d / 2 - tf).lineTo(b / 2, d / 2).lineTo(-b / 2, d / 2)\n  sh.lineTo(-b / 2, d / 2 - tf).lineTo(-tw / 2, d / 2 - tf).lineTo(-tw / 2, -d / 2 + tf).lineTo(-b / 2, -d / 2 + tf).closePath()\n  return sh\n}\n\n/** Member of any section (incl. plate girders) between a and b; `up` is the depth (web) direction. */\nexport function bar(k: Kit, a: V3, b: V3, name: string, mat: MatKey = 'structSteel', up: V3 = [0, 1, 0]) {\n  const A = new THREE.Vector3(...a)\n  const z = new THREE.Vector3(...b).sub(A)\n  const L = z.length()\n  if (L < 1e-4) return\n  z.normalize()\n  let u = new THREE.Vector3(...up).normalize()\n  if (Math.abs(u.dot(z)) > 0.99) u = Math.abs(z.y) > 0.9 ? new THREE.Vector3(1, 0, 0) : new THREE.Vector3(0, 1, 0)\n  const x = new THREE.Vector3().crossVectors(u, z).normalize()\n  const y = new THREE.Vector3().crossVectors(z, x).normalize()\n  const s = sec(name)\n  k.extrude(mat, shapeOf(name), L, new THREE.Matrix4().makeBasis(x, y, z).setPosition(A), s.kind === 'CHS' ? 10 : 4)\n}\n\n/** Horizontal beam with its top of steel at `tos`, web vertical. */\nexport function beamTOS(k: Kit, x0: number, z0: number, x1: number, z1: number, tos: number, name: string, mat: MatKey = 'structSteel') {\n  const h = sec(name).d / 2\n  bar(k, [x0, tos - h, z0], [x1, tos - h, z1], name, mat, [0, 1, 0])\n}\n\n/** Vertical column from y0 to y1; `web` is the axis the web (depth) runs along. */\nexport function column(k: Kit, x: number, z: number, y0: number, y1: number, name: string, mat: MatKey = 'structSteel', web: 'x' | 'z' = 'x') {\n  bar(k, [x, y0, z], [x, y1, z], name, mat, web === 'x' ? [1, 0, 0] : [0, 0, 1])\n}\n\n/** Axis-aligned box from min to max corner. */\nexport function boxMM(k: Kit, mat: MatKey, a: V3, b: V3) {\n  const s: V3 = [Math.abs(b[0] - a[0]), Math.abs(b[1] - a[1]), Math.abs(b[2] - a[2])]\n  if (s[0] < 1e-5 || s[1] < 1e-5 || s[2] < 1e-5) return\n  k.box(mat, s, [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2])\n}\n\n/** Box oriented along a→b (length), with cross-section w (horizontal, ⟂) × h (in the plane containing `up`). */\nexport function strip(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 z = new THREE.Vector3(...b).sub(A)\n  const L = z.length()\n  if (L < 1e-5) return\n  z.normalize()\n  let u = new THREE.Vector3(...up).normalize()\n  if (Math.abs(u.dot(z)) > 0.99) u = new THREE.Vector3(1, 0, 0)\n  const x = new THREE.Vector3().crossVectors(u, z).normalize()\n  const y = new THREE.Vector3().crossVectors(z, x).normalize()\n  const m = new THREE.Matrix4().makeBasis(x, y, z).setPosition(A.add(new THREE.Vector3(...b)).multiplyScalar(0.5))\n  k.add(mat, new THREE.BoxGeometry(w, h, L), m)\n}\n\n/** Bolt (stud + nut each side) through a plate pack centred at p, along `axis`. */\nexport function bolt(k: Kit, p: V3, axis: V3, dia = 0.02, grip = 0.03, mat: MatKey = 'galvanised') {\n  const v = new THREE.Vector3(...axis).normalize()\n  const P = new THREE.Vector3(...p)\n  const e = grip / 2 + dia * 0.5\n  k.rod(mat, P.clone().addScaledVector(v, -e - dia * 0.3).toArray() as V3, P.clone().addScaledVector(v, e + dia * 0.3).toArray() as V3, dia / 2, 6)\n  for (const s of [-1, 1]) k.hex('nut', dia * 1.8, dia * 0.8, P.clone().addScaledVector(v, s * (grip / 2 + dia * 0.4)).toArray() as V3, axis)\n}\n\n/** Anchor bolt projecting above a base plate whose top is at y: stud, washer and nut. */\nexport function anchorBolt(k: Kit, x: number, y: number, z: number, dia = 0.024) {\n  k.cyl('galvanised', dia / 2, dia * 3, [x, y + dia * 1.5 - 0.01, z], 'y', 6)\n  k.cyl('galvanised', dia * 1.1, 0.004, [x, y + 0.002, z], 'y', 10)\n  k.hex('nut', dia * 1.8, dia * 0.8, [x, y + 0.004 + dia * 0.4, z], 'y')\n}\n\n/** Base plate (bottom at y) with anchor bolts at the corners and an optional grout bed below. Returns the plate top. */\nexport function basePlate(k: Kit, x: number, y: number, z: number, sx: number, sz: number, t = 0.025, dia = 0.024, mat: MatKey = 'structSteel', grout = 0) {\n  if (grout > 0) k.box('concrete', [sx + 0.08, grout, sz + 0.08], [x, y - grout / 2, z])\n  k.box(mat, [sx, t, sz], [x, y + t / 2, z])\n  const e = Math.max(0.045, dia * 2)\n  for (const a of [-1, 1]) for (const b of [-1, 1]) anchorBolt(k, x + a * (sx / 2 - e), y + t, z + b * (sz / 2 - e), dia)\n  return y + t\n}\n\n/** Triangular gusset plate: right angle at `corner`, legs la along dirA and lb along dirB, thickness t. */\nexport function gusset(k: Kit, corner: V3, dirA: V3, dirB: V3, la: number, lb: number, t = 0.012, mat: MatKey = 'structSteel') {\n  const a = new THREE.Vector3(...dirA).normalize()\n  const b = new THREE.Vector3(...dirB).normalize()\n  const n = new THREE.Vector3().crossVectors(a, b).normalize()\n  const c = Math.min(la, lb) * 0.12 // snipe at the root\n  const sh = new THREE.Shape()\n  sh.moveTo(c, 0).lineTo(la, 0).lineTo(la, lb * 0.1).lineTo(la * 0.1, lb).lineTo(0, lb).lineTo(0, c).closePath()\n  const o = new THREE.Vector3(...corner).addScaledVector(n, -t / 2)\n  k.extrude(mat, sh, t, new THREE.Matrix4().makeBasis(a, b, n).setPosition(o), 1)\n}\n\n/** Full-depth web stiffener pair on a horizontal beam (along `along` = 'x'|'z') at position p (centre of the beam). */\nexport function webStiffeners(k: Kit, p: V3, along: 'x' | 'z', s: Section, mat: MatKey = 'structSteel') {\n  const h = s.d - 2 * s.tf\n  const w = (s.b - s.tw) / 2 - 0.004\n  const t = clamp(s.tw * 1.2, 0.008, 0.025)\n  for (const sg of [-1, 1]) {\n    const off = sg * (s.tw / 2 + w / 2)\n    if (along === 'x') k.box(mat, [t, h, w], [p[0], p[1], p[2] + off])\n    else k.box(mat, [w, h, t], [p[0] + off, p[1], p[2]])\n  }\n}\n\n/**\n * Bolted end-plate connection at the end of a horizontal beam (along x or z): the beam ends at `p` (web centre) and the\n * plate runs from p to p + dir·t, where `dir` = +1/-1 points from the beam out through the plate.\n */\nexport function endPlate(k: Kit, p: V3, along: 'x' | 'z', dir: number, s: Section, mat: MatKey = 'structSteel') {\n  const t = s.d > 0.5 ? 0.025 : 0.02\n  const h = s.d + 0.03\n  const w = s.b + 0.02\n  const c: V3 = along === 'x' ? [p[0] + dir * t / 2, p[1], p[2]] : [p[0], p[1], p[2] + dir * t / 2]\n  k.box(mat, along === 'x' ? [t, h, w] : [w, h, t], c)\n  const rows = Math.max(2, Math.min(6, Math.floor((s.d - 0.06) / 0.09)))\n  const g = Math.min(w * 0.6, 0.14) / 2\n  const dia = s.d > 0.5 ? 0.024 : 0.02\n  for (let i = 0; i < rows; i++) {\n    const y = p[1] - s.d / 2 + 0.05 + ((s.d - 0.1) * i) / (rows - 1)\n    for (const sg of [-1, 1]) {\n      const q: V3 = along === 'x' ? [c[0], y, c[2] + sg * g] : [c[0] + sg * g, y, c[2]]\n      bolt(k, q, along === 'x' ? [1, 0, 0] : [0, 0, 1], dia, t * 2)\n    }\n  }\n}\n\n/** Fin-plate shear connection: plate welded to a supporting web face at `p`, projecting `len` along ±x|z, bolted to the supported web. */\nexport function finPlate(k: Kit, p: V3, along: 'x' | 'z', dir: number, depth: number, len = 0.13, mat: MatKey = 'structSteel') {\n  const h = clamp(depth * 0.7, 0.12, 0.6)\n  const t = 0.01\n  const c: V3 = along === 'x' ? [p[0] + dir * len / 2, p[1], p[2] + 0.011] : [p[0] + 0.011, p[1], p[2] + dir * len / 2]\n  k.box(mat, along === 'x' ? [len, h, t] : [t, h, len], c)\n  const n = Math.max(2, Math.floor(h / 0.075))\n  for (let i = 0; i < n; i++) {\n    const y = p[1] - h / 2 + 0.04 + ((h - 0.08) * i) / Math.max(1, n - 1)\n    const q: V3 = along === 'x' ? [p[0] + dir * (len - 0.045), y, p[2] + 0.006] : [p[0] + 0.006, y, p[2] + dir * (len - 0.045)]\n    bolt(k, q, along === 'x' ? [0, 0, 1] : [1, 0, 0], 0.016, 0.026)\n  }\n}\n\n/**\n * Bar-grating bay from (x0,z0) to (x1,z1), top at `top`: one textured box plus flat-bar banding.\n * `bars` = direction the bearing bars run (they span between the supports).\n */\nexport function gratingBay(k: Kit, x0: number, z0: number, x1: number, z1: number, top: number, bars: 'x' | 'z' = 'z', t = 0.03, band = true) {\n  const sx = Math.abs(x1 - x0)\n  const sz = Math.abs(z1 - z0)\n  if (sx < 0.02 || sz < 0.02) return\n  const g = new THREE.BoxGeometry(sx, t, sz)\n  uvScale(g, sx / 0.3, sz / 0.3)\n  if (bars === 'x') {\n    const uv = g.getAttribute('uv') as THREE.BufferAttribute\n    for (let i = 0; i < uv.count; i++) uv.setXY(i, uv.getY(i), uv.getX(i))\n  }\n  const cx = (x0 + x1) / 2\n  const cz = (z0 + z1) / 2\n  k.add(gratingMaterial(), g, mat4([cx, top - t / 2, cz]))\n  if (!band) return\n  const bt = 0.004\n  for (const s of [-1, 1]) {\n    k.box('galvanised', [sx, t, bt], [cx, top - t / 2, cz + s * (sz / 2 - bt / 2)])\n    k.box('galvanised', [bt, t, sz - 2 * bt], [cx + s * (sx / 2 - bt / 2), top - t / 2, cz])\n  }\n}\n\n/** Hazard-striped (black/yellow) band from a to b, w wide and h tall, oriented like strip(). Stripe pitch ~0.2 m. */\nexport function hazardBand(k: Kit, a: V3, b: V3, w: number, h: number, up: V3 = [0, 1, 0]) {\n  const L = Math.hypot(b[0] - a[0], b[1] - a[1], b[2] - a[2])\n  const A = new THREE.Vector3(...a)\n  const z = new THREE.Vector3(...b).sub(A).normalize()\n  let u = new THREE.Vector3(...up).normalize()\n  if (Math.abs(u.dot(z)) > 0.99) u = new THREE.Vector3(1, 0, 0)\n  const x = new THREE.Vector3().crossVectors(u, z).normalize()\n  const y = new THREE.Vector3().crossVectors(z, x).normalize()\n  // box with its length along local X so the stripe texture repeats along U\n  const g = new THREE.BoxGeometry(L, h, w)\n  uvScale(g, L / 0.25, 1)\n  const m = new THREE.Matrix4().makeBasis(z, y, x.clone().negate()).setPosition(A.add(new THREE.Vector3(...b)).multiplyScalar(0.5))\n  k.add(hazardStripeMaterial(), g, m)\n}\n\n// ------------------------------------------------------------------ guard rails (ISO 14122-3)\n\nexport const RAIL = {\n  top: ACCESS.handrail.height,\n  knee: ACCESS.handrail.kneeRail,\n  toe: ACCESS.handrail.toePlate,\n  postR: 0.0242, // 48.3 mm tube\n  railR: 0.0212, // 42.4 mm\n  kneeR: 0.0169, // 33.7 mm\n  maxPost: ACCESS.handrail.postSpacingMax,\n}\n\nexport interface RailOpts {\n  top?: number\n  knee?: number\n  /** toe plate on the walking edge */\n  toe?: boolean\n  startPost?: boolean\n  endPost?: boolean\n  /** square base plate under each post */\n  basePlates?: boolean\n  mat?: MatKey\n}\n\n/** Straight guard-rail run a → b; a and b are on the walking surface (may slope). */\nexport function railRun(k: Kit, a: V3, b: V3, o: RailOpts = {}) {\n  const { top = RAIL.top, knee = RAIL.knee, toe = true, startPost = true, endPost = true, basePlates = true, mat = 'safetyYellow' } = o\n  const dx = b[0] - a[0]\n  const dy = b[1] - a[1]\n  const dz = b[2] - a[2]\n  const L = Math.hypot(dx, dz)\n  if (L < 0.08) return\n  const n = Math.max(1, Math.ceil(L / RAIL.maxPost))\n  for (let i = 0; i <= n; i++) {\n    if ((i === 0 && !startPost) || (i === n && !endPost)) continue\n    const t = i / n\n    const p: V3 = [a[0] + dx * t, a[1] + dy * t, a[2] + dz * t]\n    k.rod(mat, p, [p[0], p[1] + top, p[2]], RAIL.postR, 8)\n    if (basePlates) k.box(mat, [0.1, 0.01, 0.1], [p[0], p[1] + 0.005, p[2]])\n  }\n  k.rod(mat, [a[0], a[1] + top, a[2]], [b[0], b[1] + top, b[2]], RAIL.railR, 8)\n  k.rod(mat, [a[0], a[1] + knee, a[2]], [b[0], b[1] + knee, b[2]], RAIL.kneeR, 8)\n  if (startPost) k.sphere(mat, RAIL.railR * 1.15, [a[0], a[1] + top, a[2]], undefined, 8)\n  if (endPost) k.sphere(mat, RAIL.railR * 1.15, [b[0], b[1] + top, b[2]], undefined, 8)\n  if (toe) strip(k, mat, [a[0], a[1] + 0.005 + RAIL.toe / 2, a[2]], [b[0], b[1] + 0.005 + RAIL.toe / 2, b[2]], 0.006, RAIL.toe)\n}\n\n/**\n * Guard rail along a polyline of (x, z) points at height y, leaving gaps [s0, s1] (metres along the line) open.\n * `ends` drops the post at the very start / end of the polyline (where a neighbouring run already has one).\n */\nexport function railPath(k: Kit, pts: [number, number][], y: number, gaps: [number, number][] = [], closed = false, o: RailOpts = {}, ends: [boolean, boolean] = [true, true]) {\n  const P = closed ? [...pts, pts[0]] : pts\n  let s = 0\n  for (let i = 0; i < P.length - 1; i++) {\n    const [x0, z0] = P[i]\n    const [x1, z1] = P[i + 1]\n    const L = Math.hypot(x1 - x0, z1 - z0)\n    let pieces: [number, number][] = [[0, L]]\n    for (const [g0, g1] of gaps) {\n      const a = g0 - s\n      const b = g1 - s\n      const next: [number, number][] = []\n      for (const [p0, p1] of pieces) {\n        if (b <= p0 || a >= p1) next.push([p0, p1])\n        else {\n          if (a > p0) next.push([p0, a])\n          if (b < p1) next.push([b, p1])\n        }\n      }\n      pieces = next\n    }\n    const ux = (x1 - x0) / L\n    const uz = (z1 - z0) / L\n    for (const [p0, p1] of pieces) {\n      if (p1 - p0 < 0.1) continue\n      const startPost = !(i === 0 && p0 < 1e-6 && !ends[0] && !closed)\n      const endPost = !(i === P.length - 2 && p1 > L - 1e-6 && !ends[1] && !closed)\n      railRun(k, [x0 + ux * p0, y, z0 + uz * p0], [x0 + ux * p1, y, z0 + uz * p1], { ...o, startPost, endPost })\n    }\n    s += L\n  }\n}\n\n/** Self-closing swing gate (shown closed) across an opening from hinge point h to latch point e on the walking surface. */\nexport function gate(k: Kit, h: V3, e: V3, mat: MatKey = 'safetyYellow') {\n  const d = new THREE.Vector3(e[0] - h[0], 0, e[2] - h[2])\n  const L = d.length()\n  d.normalize()\n  const at = (s: number, y: number): V3 => [h[0] + d.x * s, h[1] + y, h[2] + d.z * s]\n  const g0 = 0.06\n  const g1 = L - 0.05\n  const r = 0.0169\n  // frame: two verticals, top rail, mid rail, diagonal\n  for (const s of [g0, g1]) k.rod(mat, at(s, 0.15), at(s, RAIL.top - 0.02), r, 8)\n  k.rod(mat, at(g0, RAIL.top - 0.02), at(g1, RAIL.top - 0.02), r, 8)\n  k.rod(mat, at(g0, RAIL.knee), at(g1, RAIL.knee), r * 0.9, 8)\n  k.rod(mat, at(g0, 0.15), at(g1, 0.15), r * 0.9, 8)\n  // hinge barrels with spring housings on the hinge post side\n  for (const y of [0.3, 0.9]) {\n    k.cyl('galvanised', 0.022, 0.09, at(0.03, y), 'y', 10)\n    k.box('galvanised', [0.03, 0.012, 0.03], at(0.045, y))\n  }\n  k.cyl('nut', 0.026, 0.05, at(0.03, 0.96), 'y', 10)\n  // latch stop on the far post\n  k.box(mat, [0.05, 0.05, 0.05], at(L - 0.01, RAIL.knee + 0.1))\n}\n\n/** Toe plate / coaming only (no rail), a → b at walking-surface level. */\nexport function toePlate(k: Kit, a: V3, b: V3, h: number = RAIL.toe, mat: MatKey = 'safetyYellow') {\n  strip(k, mat, [a[0], a[1] + h / 2, a[2]], [b[0], b[1] + h / 2, b[2]], 0.008, h)\n}\n"
    }
  ],
  "categories": [
    "structure"
  ],
  "meta": {
    "export": "DeckModule",
    "anchor": "base",
    "standards": [
      "ISO 19902",
      "ISO 14122-3",
      "EN 10365",
      "NORSOK N-004",
      "NORSOK C-001"
    ]
  }
}