{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "industrial-building",
  "type": "registry:component",
  "title": "Industrial building (portal frame)",
  "description": "Steel portal-frame building shell on a floor slab: IPE columns with haunched rafters on anchored base plates, purlins, a blockwork dado with profiled metal cladding above, a profiled duo-pitch roof with rooflights, ridge cap, ridge ventilators, gutters and downpipes. Roller shutters have coil boxes, motors, striped jambs, bollards and ramps; personnel doors have canopies, exit signs and lights. High-level louvres, lightning down-conductors with earth pits. `kind`: a workshop adds an overhead crane, a control building swaps the shutters for a window strip and AC condensers. The roller shutters open with `doorOpen`, showing the hollow interior.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "onshore/industrial-building.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/onshore/industrial-building.tsx",
      "content": "// Portal-frame industrial building: steel frames, profiled cladding on a dado wall, duo-pitch roof, roller shutters, personnel doors, louvres, roof vents. EN 1993-1-1, EN 14509 / EN 508, EN 13241.\nimport * as THREE from 'three'\nimport type { Kit, V3 } from '../lib/kit'\nimport { KitMesh, useKit, type GroupProps } from '../lib/mesh'\nimport { member, tagPlate } from '../lib/parts'\nimport { paint } from '../lib/materials'\nimport { hazardStripeMaterial, louvreMaterial, signMaterial, textMaterial } from '../lib/textures'\nimport { anchorBolt, clamp, earthBond, profiledSheet } from './_onshore'\n\nexport interface IndustrialBuildingProps {\n  kind?: 'warehouse' | 'workshop' | 'control'\n  /** along X, m */\n  length?: number\n  /** span along Z, m */\n  width?: number\n  /** eaves height above floor, m */\n  eaves?: number\n  /** roof pitch, degrees */\n  pitch?: number\n  /** roller shutters on the +Z wall */\n  rollerDoors?: number\n  personnelDoors?: number\n  /** roller shutters, 0 closed … 1 open */\n  doorOpen?: number\n  cladding?: string\n  tag?: string\n}\n\nexport const defaults = { kind: 'workshop' as 'warehouse' | 'workshop' | 'control', length: 30, width: 18, eaves: 7, pitch: 6, rollerDoors: 2, personnelDoors: 2, doorOpen: 0.6, cladding: '#9fb0b6', tag: '' }\n\nconst FFL = 0.15\nconst DADO = 1.2\n\nexport function IndustrialBuilding(props: IndustrialBuildingProps & GroupProps) {\n  const { kind, length, width, eaves, pitch, rollerDoors, personnelDoors, doorOpen, cladding, tag, ...group } = { ...defaults, ...props }\n  const p = { kind, length, width, eaves, pitch, rollerDoors, personnelDoors, doorOpen, cladding, tag }\n  const data = useKit((k) => build(k, p), [kind, length, width, eaves, pitch, rollerDoors, personnelDoors, doorOpen, cladding, tag])\n  return (\n    <group {...group}>\n      <KitMesh data={data} />\n    </group>\n  )\n}\n\nexport default IndustrialBuilding\n\ntype Opening = { x0: number; x1: number; top: number; kind: 'roller' | 'door' | 'window' }\n\nfunction build(k: Kit, p: typeof defaults) {\n  const L = clamp(p.length, 6, 120)\n  const W = clamp(p.width, 6, 40)\n  const H = clamp(p.eaves, 3, 15)\n  const pit = (clamp(p.pitch, 3, 20) * Math.PI) / 180\n  const rise = (W / 2) * Math.tan(pit)\n  const ctrl = p.kind === 'control'\n  const clad = paint(p.cladding, { metalness: 0.35, roughness: 0.45, clearcoat: 0.3 })\n  const roofM = paint('#8d9599', { metalness: 0.45, roughness: 0.4, clearcoat: 0.2 })\n  const trim = paint('#3e4a52', { metalness: 0.3, roughness: 0.5 })\n  const yE = FFL + H\n  const yR = yE + rise\n  // slab, dado wall, ramp aprons\n  k.box('concrete', [L + 0.8, FFL, W + 0.8], [0, FFL / 2, 0])\n  // portal frames\n  const bays = Math.max(1, Math.round(L / 6))\n  const bay = L / bays\n  const sec = W <= 15 ? 'IPE 330' : W <= 25 ? 'IPE 450' : 'IPE 600'\n  const d = W <= 15 ? 0.33 : W <= 25 ? 0.45 : 0.6\n  const zc = W / 2 - 0.12 - d / 2\n  for (let i = 0; i <= bays; i++) {\n    const x = -L / 2 + 0.15 + (i * (L - 0.3)) / bays\n    for (const s of [-1, 1]) {\n      member(k, [x, FFL + 0.03, s * zc], [x, yE - 0.1, s * zc], sec, 'structSteel', [0, 0, s])\n      k.box('structSteel', [0.35, 0.03, d + 0.15], [x, FFL + 0.015, s * zc])\n      for (const a of [-1, 1]) for (const b of [-1, 1]) anchorBolt(k, [x + a * 0.12, FFL + 0.03, s * zc + b * (d / 2 + 0.03)], 0.02)\n      member(k, [x, yE - 0.1, s * zc], [x, yR - 0.2, 0], sec, 'structSteel', [0, 1, 0])\n      // eaves haunch\n      member(k, [x, yE - 0.9, s * (zc - d / 2)], [x, yE - 0.25, s * (zc - 1.8)], 'IPE 200', 'structSteel', [0, 1, 0])\n    }\n  }\n  // purlins and eaves beams\n  const slopeL = W / 2 / Math.cos(pit)\n  for (const s of [-1, 1]) for (let t = 0.2; t < slopeL - 0.2; t += 1.6) {\n    const z = s * (W / 2 - t * Math.cos(pit) - 0.1)\n    const y = yE + t * Math.sin(pit) - 0.08\n    member(k, [-L / 2 + 0.05, y, z], [L / 2 - 0.05, y, z], 'UPN 160', 'galvanised', [0, Math.cos(pit), s * Math.sin(pit)])\n  }\n  // openings in the +Z wall\n  const opens: Opening[] = []\n  const nR = ctrl ? 0 : clamp(Math.round(p.rollerDoors), 0, Math.max(0, Math.floor(L / 8)))\n  const rw = clamp(W / 4, 3.5, 6)\n  const rh = Math.min(H - 1, 5.5)\n  for (let i = 0; i < nR; i++) {\n    const xc = -L / 2 + (L * (i + 1)) / (nR + 1)\n    opens.push({ x0: xc - rw / 2, x1: xc + rw / 2, top: FFL + rh, kind: 'roller' })\n  }\n  const nP = clamp(Math.round(p.personnelDoors), 0, 6)\n  const free = (x: number, w: number) => !opens.some((o) => x + w / 2 > o.x0 - 0.6 && x - w / 2 < o.x1 + 0.6)\n  const cands = [-L / 2 + 1.5, L / 2 - 1.5, ...Array.from({ length: bays }, (_, i) => -L / 2 + (i + 0.5) * bay)]\n  for (let i = 0, placed = 0; i < cands.length && placed < nP; i++)\n    if (free(cands[i], 1.1)) {\n      opens.push({ x0: cands[i] - 0.55, x1: cands[i] + 0.55, top: FFL + 2.2, kind: 'door' })\n      placed++\n    }\n  if (ctrl) for (let x = -L / 2 + 2.5; x < L / 2 - 2.5; x += 3) if (free(x, 1.6)) opens.push({ x0: x - 0.8, x1: x + 0.8, top: FFL + 2.4, kind: 'window' })\n  opens.sort((a, b) => a.x0 - b.x0)\n  // walls: dado blockwork to 1.2 m, profiled sheet above, cut round the openings on the front\n  const tw = 0.2\n  const wallZ = W / 2 - tw / 2\n  const dado = (x0: number, x1: number, z: number) => k.box('concrete', [x1 - x0, DADO, tw], [(x0 + x1) / 2, FFL + DADO / 2, z])\n  const sheet = (x0: number, x1: number, y0: number, y1: number, z: number, out: number) =>\n    profiledSheet(k, clad, x1 - x0, y1 - y0, [x0, y0, z], [1, 0, 0], [0, 0, out], [0, 1, 0])\n  let cur = -L / 2\n  for (const o of opens) {\n    if (o.x0 > cur + 0.01) {\n      dado(cur, o.x0, wallZ)\n      sheet(cur, o.x0, FFL + DADO, yE, W / 2, 1)\n    }\n    if (o.kind === 'window') {\n      dado(o.x0, o.x1, wallZ)\n      sheet(o.x0, o.x1, FFL + DADO, FFL + 1.25, W / 2, 1)\n    }\n    sheet(o.x0, o.x1, o.top, yE, W / 2, 1)\n    cur = o.x1\n  }\n  dado(cur, L / 2, wallZ)\n  sheet(cur, L / 2, FFL + DADO, yE, W / 2, 1)\n  dado(-L / 2, L / 2, -wallZ)\n  sheet(-L / 2, L / 2, FFL + DADO, yE, -W / 2, -1)\n  for (const s of [-1, 1]) {\n    k.box('concrete', [tw, DADO, W - 2 * tw], [s * (L / 2 - tw / 2), FFL + DADO / 2, 0])\n    profiledSheet(k, clad, W, yE - FFL - DADO, [s * L / 2, FFL + DADO, s > 0 ? W / 2 : -W / 2], [0, 0, -s], [s, 0, 0], [0, 1, 0])\n    // gable triangle\n    const g = new THREE.Shape()\n    g.moveTo(-W / 2, 0)\n    g.lineTo(W / 2, 0)\n    g.lineTo(0, rise)\n    g.closePath()\n    k.extrude(clad, g, 0.03, new THREE.Matrix4().makeBasis(new THREE.Vector3(0, 0, 1), new THREE.Vector3(0, 1, 0), new THREE.Vector3(-1, 0, 0)).setPosition(s * L / 2 + (s > 0 ? 0.03 : 0), yE, 0))\n    // corner and verge flashings\n    for (const z of [-W / 2, W / 2]) k.box(trim, [0.1, yE - FFL - DADO, 0.1], [s * (L / 2 + 0.02), (yE + FFL + DADO) / 2, z])\n  }\n  k.box(trim, [L + 0.1, 0.06, tw + 0.06], [0, FFL + DADO + 0.03, wallZ])\n  k.box(trim, [L + 0.1, 0.06, tw + 0.06], [0, FFL + DADO + 0.03, -wallZ])\n  // roof slopes, ridge, gutters, downpipes, rooflights\n  for (const s of [-1, 1]) {\n    const o: V3 = [-L / 2 - 0.15, yE + 0.02, s * (W / 2 + 0.25)]\n    const up = new THREE.Vector3(0, Math.sin(pit), -s * Math.cos(pit))\n    const nrm = new THREE.Vector3(0, Math.cos(pit), s * Math.sin(pit))\n    const len = slopeL + 0.25 / Math.cos(pit)\n    profiledSheet(k, roofM, L + 0.3, len, o, [1, 0, 0], nrm.toArray() as V3, up.toArray() as V3, 0.33, 0.04)\n    for (let i = 1; i < bays; i += 2) {\n      const x = -L / 2 + (i + 0.5) * bay\n      const c = new THREE.Vector3(x, 0, 0).add(new THREE.Vector3(...o).setX(x)).addScaledVector(up, len * 0.5).addScaledVector(nrm, 0.045)\n      k.box(paint('#dfe6e3', { transparent: true, opacity: 0.75, metalness: 0, roughness: 0.3, clearcoat: 0 }), [1.0, 0.01, len * 0.6], c.toArray() as V3, new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 0, -s), up))\n    }\n    const gz = s * (W / 2 + 0.33)\n    k.box(trim, [L + 0.3, 0.12, 0.16], [0, yE - 0.03, gz])\n    for (const x of [-L / 2 + 0.3, L / 2 - 0.3]) {\n      k.cyl(trim, 0.05, yE - 0.1, [x, (yE - 0.1) / 2 + 0.05, s * (W / 2 + 0.12)], 'y', 12)\n      k.rod(trim, [x, yE - 0.1, s * (W / 2 + 0.12)], [x, yE - 0.05, gz], 0.05, 12)\n      k.box('concrete', [0.35, 0.08, 0.35], [x, 0.04, s * (W / 2 + 0.2)])\n    }\n  }\n  k.box(trim, [L + 0.3, 0.05, 0.5], [0, yR + 0.08, 0])\n  // ridge ventilators every other bay\n  for (let i = 0; i < bays; i += 2) {\n    const x = -L / 2 + (i + 0.5) * bay\n    const g = new THREE.BoxGeometry(1.6, 0.45, 0.7)\n    const uv = g.getAttribute('uv') as THREE.BufferAttribute\n    for (let j = 0; j < uv.count; j++) uv.setXY(j, uv.getX(j), uv.getY(j) * 4)\n    k.add(louvreMaterial('#9aa3a7'), g, new THREE.Matrix4().makeTranslation(x, yR + 0.33, 0))\n    k.box(trim, [1.8, 0.05, 0.9], [x, yR + 0.58, 0])\n  }\n  // high-level louvres on the gables and the rear wall\n  for (const s of [-1, 1]) {\n    const lv = new THREE.PlaneGeometry(2.4, 1.0)\n    const uv = lv.getAttribute('uv') as THREE.BufferAttribute\n    for (let j = 0; j < uv.count; j++) uv.setXY(j, uv.getX(j), uv.getY(j) * 6)\n    k.add(louvreMaterial('#6e7b82'), lv, new THREE.Matrix4().compose(new THREE.Vector3(s * (L / 2 + 0.05), yE - 1.2, 0), new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), (s * Math.PI) / 2), new THREE.Vector3(1, 1, 1)))\n  }\n  for (let x = -L / 2 + bay / 2; x < L / 2; x += bay * 2) {\n    const lv = new THREE.PlaneGeometry(1.8, 0.8)\n    const uv = lv.getAttribute('uv') as THREE.BufferAttribute\n    for (let j = 0; j < uv.count; j++) uv.setXY(j, uv.getX(j), uv.getY(j) * 5)\n    k.add(louvreMaterial('#6e7b82'), lv, new THREE.Matrix4().compose(new THREE.Vector3(x, yE - 1.2, -W / 2 - 0.05), new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI), new THREE.Vector3(1, 1, 1)))\n  }\n  // openings: roller shutters, personnel doors, windows\n  for (const o of opens) {\n    const w = o.x1 - o.x0\n    const xc = (o.x0 + o.x1) / 2\n    const z = W / 2\n    if (o.kind === 'roller') {\n      const h = o.top - FFL\n      const curtain = h * (1 - clamp(p.doorOpen, 0, 1))\n      if (curtain > 0.05) {\n        const g = new THREE.BoxGeometry(w - 0.1, curtain, 0.03)\n        const uv = g.getAttribute('uv') as THREE.BufferAttribute\n        for (let j = 0; j < uv.count; j++) uv.setXY(j, uv.getX(j), (uv.getY(j) * curtain) / 0.1)\n        k.add(louvreMaterial('#b8bec0'), g, new THREE.Matrix4().makeTranslation(xc, o.top - curtain / 2, z + 0.06))\n        k.box(trim, [w - 0.1, 0.06, 0.06], [xc, o.top - curtain, z + 0.06])\n      }\n      k.cyl(trim, 0.32, w + 0.2, [xc, o.top + 0.34, z + 0.33], 'x', 20)\n      k.box(trim, [w + 0.2, 0.68, 0.02], [xc, o.top + 0.34, z + 0.01])\n      k.rbox('motorPaint', [0.25, 0.3, 0.3], 0.03, [o.x1 + 0.25, o.top + 0.3, z + 0.3])\n      k.rbox('stainless', [0.18, 0.25, 0.1], 0.01, [o.x1 + 0.5, FFL + 1.4, z + 0.06])\n      for (const x of [o.x0, o.x1]) {\n        k.box(trim, [0.1, h, 0.12], [x + (x === o.x0 ? -0.05 : 0.05), FFL + h / 2, z + 0.06])\n        const jamb = new THREE.BoxGeometry(0.12, 1.2, 0.14)\n        const uv = jamb.getAttribute('uv') as THREE.BufferAttribute\n        for (let j = 0; j < uv.count; j++) uv.setXY(j, uv.getX(j), uv.getY(j) * 4)\n        k.add(hazardStripeMaterial(), jamb, new THREE.Matrix4().makeTranslation(x + (x === o.x0 ? -0.05 : 0.05), FFL + 0.6, z + 0.06))\n        k.cyl(paint('#e5b114'), 0.1, 1.1, [x + (x === o.x0 ? -0.35 : 0.35), FFL + 0.55, z + 0.5], 'y', 16)\n      }\n      // ramp apron down to grade\n      k.box('concrete', [w + 0.6, 0.15, 1.2], [xc, 0.075, W / 2 + 0.6 + 0.4], new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), 0.0))\n      lamp(k, [o.x1 + 0.6, o.top + 0.6, z + 0.15])\n    } else if (o.kind === 'door') {\n      k.box(trim, [w, 2.2, 0.06], [xc, FFL + 1.1, z - 0.02])\n      k.box(paint('#d5d9d3'), [w - 0.12, 2.1, 0.04], [xc, FFL + 1.05, z + 0.02])\n      k.box('stainless', [0.14, 0.03, 0.05], [xc + w / 2 - 0.2, FFL + 1.05, z + 0.06])\n      k.box(trim, [w + 0.6, 0.06, 0.9], [xc, FFL + 2.55, z + 0.45])\n      k.plane(signMaterial('exit'), 0.4, 0.2, [xc, FFL + 2.35, z + 0.03])\n      k.box('concrete', [w + 0.6, FFL, 1.0], [xc, FFL / 2, W / 2 + 0.4 + 0.5])\n      lamp(k, [xc + w / 2 + 0.3, FFL + 2.5, z + 0.12])\n    } else {\n      k.box(trim, [w, o.top - FFL - 1.25, 0.08], [xc, (o.top + FFL + 1.25) / 2, z - 0.05])\n      k.box('glass', [w - 0.1, o.top - FFL - 1.35, 0.02], [xc, (o.top + FFL + 1.25) / 2, z])\n      k.box(trim, [w + 0.1, 0.05, 0.15], [xc, FFL + 1.25, z + 0.05])\n    }\n  }\n  // workshop: overhead crane runway beams and bridge girder; control building: condenser units\n  if (p.kind === 'workshop') {\n    const yr = yE - 1.4\n    for (const s of [-1, 1]) member(k, [-L / 2 + 0.3, yr, s * (zc - d / 2 - 0.25)], [L / 2 - 0.3, yr, s * (zc - d / 2 - 0.25)], 'HEB 300', 'structSteel', [0, 1, 0])\n    member(k, [-L / 4, yr + 0.35, -(zc - d / 2 - 0.25)], [-L / 4, yr + 0.35, zc - d / 2 - 0.25], 'HEB 500', paint('#e5b114'), [0, 1, 0])\n    k.rbox(paint('#e5b114'), [0.6, 0.5, 0.6], 0.04, [-L / 4, yr + 0.0, 0])\n    k.rod('cable', [-L / 4, yr - 0.25, 0], [-L / 4, FFL + 2.2, 0], 0.012, 6)\n    k.box('nut', [0.25, 0.3, 0.12], [-L / 4, FFL + 2.05, 0])\n  }\n  if (ctrl) for (let i = 0; i < 3; i++) {\n    const x = -L / 2 + 3 + i * 2.2\n    k.box('concrete', [1.6, 0.15, 1.0], [x, 0.075, -W / 2 - 1.0])\n    k.rbox(paint('#d5d9d3'), [1.4, 1.1, 0.8], 0.04, [x, 0.7, -W / 2 - 1.0])\n    k.cyl(paint('#3a3f44'), 0.42, 0.02, [x, 0.7, -W / 2 - 0.59], 'z', 28)\n  }\n  // lightning down-conductors and earth pits at the corners, signage\n  for (const sx of [-1, 1]) for (const sz of [-1, 1]) {\n    k.box('copper', [0.025, yE - 0.3, 0.004], [sx * (L / 2 + 0.06), (yE + 0.3) / 2, sz * (W / 2 - 0.3)])\n    earthBond(k, [sx * (L / 2 + 0.06), 0.45, sz * (W / 2 - 0.3)], [sx, 0, 0], 0, 0.35)\n  }\n  if (p.tag) {\n    k.plane(textMaterial(p.tag, '#1f4e9a', '#ffffff'), 3.2, 0.6, [0, yE - 0.6, W / 2 + 0.05])\n    tagPlate(k, p.tag, [L / 2 + 0.05, FFL + 1.6, W / 2 - 1], 'x', 0.3)\n  }\n}\n\nfunction lamp(k: Kit, pos: V3) {\n  k.rbox('aluminium', [0.35, 0.12, 0.25], 0.02, pos)\n  k.box('lampLens', [0.3, 0.01, 0.2], [pos[0], pos[1] - 0.065, pos[2]])\n}\n"
    },
    {
      "path": "onshore/_onshore.ts",
      "type": "registry:file",
      "target": "components/deck-twin/onshore/_onshore.ts",
      "content": "// Shared onshore parts: plinths, anchor bolts, earth bonds, ISO 14122 stairs, shedded insulators, lattice steel and swivel joints.\nimport * as THREE from 'three'\nimport { axisVec, type Axis, type Kit, type MatKey, type V3 } from '../lib/kit'\nimport { paint } from '../lib/materials'\nimport { grating, handrail, member } from '../lib/parts'\nimport { stairFlight } from '../lib/standards'\n\nexport const add = (a: V3, b: V3, s = 1): V3 => [a[0] + b[0] * s, a[1] + b[1] * s, a[2] + b[2] * s]\nexport const deg = (d: number) => (d * Math.PI) / 180\nexport const clamp = (v: number, a: number, b: number) => Math.min(b, Math.max(a, v))\n\nexport const porcelain = () => paint('#6e3420', { metalness: 0.05, roughness: 0.12, clearcoat: 0.9, clearcoatRoughness: 0.1, weather: 0 })\nexport const silicone = () => paint('#8b9399', { metalness: 0, roughness: 0.55, clearcoat: 0.2, clearcoatRoughness: 0.5, weather: 0.3 })\nexport const earthLead = () => paint('#5c9e33', { metalness: 0, roughness: 0.6, clearcoat: 0, weather: 0 })\nexport const lightGrey = () => paint('#cfd2cf', { metalness: 0.2, roughness: 0.5, clearcoat: 0.25, weather: 1 })\n\n/** Concrete plinth from y0 up to y0 + h with a small arris, centred on (cx, cz). */\nexport function plinth(k: Kit, cx: number, cz: number, sx: number, sz: number, h: number, y0 = 0) {\n  const c = Math.min(0.025, h * 0.2)\n  k.box('concrete', [sx, h - c, sz], [cx, y0 + (h - c) / 2, cz])\n  k.box('concrete', [sx - 2 * c, c, sz - 2 * c], [cx, y0 + h - c / 2, cz])\n}\n\n/** Cast-in anchor bolt standing on a surface at p: washer, double nut, projecting thread. */\nexport function anchorBolt(k: Kit, p: V3, dia = 0.024) {\n  k.cyl('galvanised', dia * 1.1, 0.005, [p[0], p[1] + 0.0025, p[2]], 'y', 12)\n  k.hex('nut', dia * 1.85, dia * 0.9, [p[0], p[1] + 0.005 + dia * 0.45, p[2]])\n  k.hex('nut', dia * 1.85, dia * 0.9, [p[0], p[1] + 0.005 + dia * 1.4, p[2]])\n  k.cyl('boltXylan', dia / 2, dia * 2.6, [p[0], p[1] + dia * 1.3, p[2]], 'y', 8)\n}\n\n/** Steel base plate with its top at y, on a grout bed, anchored near each corner. */\nexport function basePlate(k: Kit, cx: number, y: number, cz: number, sx: number, sz: number, t = 0.025, dia = 0.024, mat: MatKey = 'galvanised') {\n  k.box(mat, [sx, t, sz], [cx, y - t / 2, cz])\n  const ix = sx / 2 - dia * 2.2\n  const iz = sz / 2 - dia * 2.2\n  for (const a of [-1, 1]) for (const b of [-1, 1]) anchorBolt(k, [cx + a * ix, y, cz + b * iz], dia)\n}\n\n/** Earth boss with a bolted lug and a green/yellow bonding lead down to an inspection pit at grade. */\nexport function earthBond(k: Kit, pos: V3, out: V3, ground = 0, pitOffset = 0.3) {\n  const o = new THREE.Vector3(...out).normalize().toArray() as V3\n  k.cyl('brass', 0.017, 0.03, add(pos, o, 0.015), o, 10)\n  k.box('copper', [0.03, 0.05, 0.004], add(add(pos, o, 0.032), [0, -0.012, 0]), o)\n  k.hex('nut', 0.022, 0.012, add(pos, o, 0.04), o)\n  const lug = add(add(pos, o, 0.035), [0, -0.035, 0])\n  const pit: V3 = [pos[0] + o[0] * pitOffset, ground, pos[2] + o[2] * pitOffset]\n  if (pos[1] - ground > 0.12) {\n    k.spline(earthLead(), [lug, add(lug, [0, -0.08, 0]), add([pit[0], ground + 0.25, pit[2]], o, -pitOffset * 0.3), [pit[0], ground + 0.1, pit[2]], [pit[0], ground + 0.03, pit[2]]], 0.0065, 6, 24)\n    k.cyl('concrete', 0.14, 0.05, [pit[0], ground + 0.025, pit[2]], 'y', 16)\n    k.cyl('castIron', 0.1, 0.008, [pit[0], ground + 0.054, pit[2]], 'y', 16)\n  }\n}\n\n/** ISO 14122-3 straight stair: first riser at `foot`, climbing along `dir` by `rise`. Channel stringers, grating treads, handrails. Returns the horizontal run. */\nexport function stair(k: Kit, foot: V3, dir: Axis, rise: number, width = 0.9, rails: 'both' | 'left' | 'right' | 'none' = 'both') {\n  const f = stairFlight(rise)\n  const n = f.steps\n  const g = f.going\n  const q = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(1, 0, 0), axisVec(dir).setY(0).normalize())\n  const w = width / 2\n  const slope = f.riser / g\n  k.with(new THREE.Matrix4().compose(new THREE.Vector3(...foot), q, new THREE.Vector3(1, 1, 1)), () => {\n    const sd = 0.18\n    const yAt = (x: number) => f.riser + x * slope - 0.07\n    const x0 = (sd * 0.45 + 0.07 - f.riser) / slope\n    const run = (n - 1) * g\n    for (const s of [-1, 1]) {\n      const z = s * (w + 0.04)\n      member(k, [x0, yAt(x0), z], [run, yAt(run), z], 'UPN 180', 'galvanised', [-slope, 1, 0])\n      k.box('galvanised', [0.22, 0.015, 0.16], [x0 + 0.06, 0.0075, z])\n    }\n    for (let i = 1; i < n; i++) {\n      const y = i * f.riser\n      grating(k, [(i - 1) * g + g / 2 + 0.0125, y, 0], g + 0.025, width)\n      k.box('safetyYellow', [0.03, 0.032, width], [(i - 1) * g + 0.015, y - 0.016, 0])\n    }\n    const pts = (z: number): V3[] => [[0, f.riser * 0.5, z], [run, rise, z]]\n    if (rails === 'both' || rails === 'left') handrail(k, pts(w + 0.04), 'safetyYellow')\n    if (rails === 'both' || rails === 'right') handrail(k, pts(-w - 0.04), 'safetyYellow')\n  })\n  return (n - 1) * g\n}\n\n/** Shedded post insulator from `base` along `axis` for L: galvanised end caps, alternating sheds of radius ~2.2 r. */\nexport function insulator(k: Kit, base: V3, L: number, r: number, mat: MatKey, axis: Axis = 'y') {\n  const v = axisVec(axis).toArray() as V3\n  const cap = Math.min(0.08, L * 0.08)\n  const prof: [number, number][] = [[0, cap], [r, cap]]\n  const body = L - 2 * cap\n  const n = Math.max(3, Math.round(body / Math.max(0.045, r * 0.75)))\n  const pitch = body / n\n  let y = cap\n  for (let i = 0; i < n; i++) {\n    const big = i % 2 === 0 ? 2.3 : 1.85\n    prof.push([r * 1.05, y + pitch * 0.15], [r * big, y + pitch * 0.42], [r * (big - 0.1), y + pitch * 0.55], [r, y + pitch * 0.62])\n    y += pitch\n    prof.push([r, y])\n  }\n  prof.push([0, L - cap])\n  k.lathe(mat, prof, base, v, 20)\n  for (const t of [cap / 2, L - cap / 2]) k.cyl('galvanised', r * 1.25, cap, add(base, v, t), v, 18)\n  for (const t of [0.006, L - 0.006]) k.cyl('galvanised', r * 1.9, 0.012, add(base, v, t), v, 18)\n}\n\n/** Square lattice column of four angle legs with zig-zag bracing on every face, (cx, cz) centre, from y0 to y1. */\nexport function latticeColumn(k: Kit, cx: number, cz: number, y0: number, y1: number, w: number, leg = 'L100x100x10', brace = 'L50x50x5', mat: MatKey = 'galvanised') {\n  const h = w / 2\n  const c: [number, number][] = [[-h, -h], [h, -h], [h, h], [-h, h]]\n  for (const [x, z] of c) member(k, [cx + x, y0, cz + z], [cx + x, y1, cz + z], leg, mat, [-x, 0, -z])\n  const H = y1 - y0\n  const nb = Math.max(1, Math.round(H / w))\n  const dy = H / nb\n  for (let f = 0; f < 4; f++) {\n    const [ax, az] = c[f]\n    const [bx, bz] = c[(f + 1) % 4]\n    for (let i = 0; i < nb; i++) {\n      const ya = y0 + i * dy\n      const flip = (i + f) % 2 === 0\n      const p0: V3 = [cx + (flip ? ax : bx), ya, cz + (flip ? az : bz)]\n      const p1: V3 = [cx + (flip ? bx : ax), ya + dy, cz + (flip ? bz : az)]\n      member(k, p0, p1, brace, mat, [ax + bx, 0, az + bz])\n    }\n    member(k, [cx + ax, y1 - 0.03, cz + az], [cx + bx, y1 - 0.03, cz + bz], brace, mat)\n  }\n}\n\n/** Swivel joint between a and b: two ball-race housings, seal ring, grease nipples and plug bolts. od = pipe OD. */\nexport function swivel(k: Kit, a: V3, b: V3, od: number, mat: MatKey = 'flangeSteel') {\n  const A = new THREE.Vector3(...a)\n  const d = new THREE.Vector3(...b).sub(A)\n  const L = d.length()\n  d.normalize()\n  const v = d.toArray() as V3\n  const at = (t: number) => A.clone().addScaledVector(d, t).toArray() as V3\n  const R = od * 0.82\n  const ri = od * 0.44\n  ring(k, mat, ri, od * 0.56, L, at(L / 2), v)\n  k.cyl('pipeBore', ri, L, at(L / 2), v, 32, undefined, true)\n  ring(k, mat, od * 0.55, R, L * 0.36, at(L * 0.3), v)\n  ring(k, 'machined', od * 0.55, R * 0.96, L * 0.04, at(L * 0.5), v)\n  ring(k, mat, od * 0.55, R * 0.92, L * 0.3, at(L * 0.69), v)\n  for (const t of [0.12, 0.88]) ring(k, mat, od * 0.55, od * 0.7, L * 0.06, at(L * t), v)\n  const [u, w] = perpOf(d)\n  for (let i = 0; i < 6; i++) {\n    const ang = (i / 6) * Math.PI * 2\n    const r = new THREE.Vector3().addScaledVector(u, Math.cos(ang) * R).addScaledVector(w, Math.sin(ang) * R)\n    const p = new THREE.Vector3(...at(L * 0.3)).add(r)\n    k.hex('nut', Math.max(0.018, od * 0.06), 0.012, p.toArray() as V3, r.clone().normalize().toArray() as V3)\n  }\n  const gp = new THREE.Vector3(...at(L * 0.69)).addScaledVector(u, R * 0.92)\n  k.cyl('brass', Math.max(0.006, od * 0.02), 0.03, gp.clone().addScaledVector(u, 0.012).toArray() as V3, u.toArray() as V3, 8)\n}\n\nfunction perpOf(d: THREE.Vector3): [THREE.Vector3, THREE.Vector3] {\n  const ref = Math.abs(d.y) > 0.9 ? new THREE.Vector3(1, 0, 0) : new THREE.Vector3(0, 1, 0)\n  const u = new THREE.Vector3().crossVectors(d, ref).normalize()\n  return [u, new THREE.Vector3().crossVectors(d, u).normalize()]\n}\n\n/** Annular ring (hollow short cylinder) centred on pos along axis, open through the middle. */\nexport function ring(k: Kit, mat: MatKey, ri: number, ro: number, h: number, pos: V3, axis: Axis = 'y', seg = 36) {\n  const v = axisVec(axis)\n  const base = new THREE.Vector3(...pos).addScaledVector(v, -h / 2).toArray() as V3\n  k.lathe(mat, [[ro, 0], [ro, h], [ri, h], [ri, 0], [ro, 0]], base, v.toArray() as V3, seg)\n}\n\n/** Hydraulic cylinder from a (barrel end clevis) to b (rod eye). */\nexport function hydraulicCylinder(k: Kit, a: V3, b: V3, bore: number, mat: MatKey = 'actuatorBlue') {\n  const A = new THREE.Vector3(...a)\n  const B = new THREE.Vector3(...b)\n  const L = A.distanceTo(B)\n  if (L < 1e-3) return\n  const d = B.clone().sub(A).normalize()\n  const bl = Math.min(L * 0.62, L - 0.1)\n  const p = (t: number) => A.clone().addScaledVector(d, t).toArray() as V3\n  k.rod(mat, p(0.04), p(bl), bore / 2, 16)\n  k.rod(mat, p(0.04), p(0.1), bore * 0.62, 16)\n  k.rod(mat, p(bl - 0.06), p(bl), bore * 0.62, 16)\n  k.rod('stainless', p(bl), p(L - 0.04), bore * 0.28, 12)\n  const side = Math.abs(d.x) > 0.9 ? 'z' : 'x'\n  for (const t of [0.02, L - 0.02]) k.cyl('flangeSteel', bore * 0.45, bore * 0.6, p(t), side, 12)\n  k.rod('tubing', add(p(0.12), [0, bore * 0.6, 0]), add(p(bl - 0.08), [0, bore * 0.6, 0]), 0.006, 6)\n}\n\n/**\n * Trapezoidal profiled sheet (wall or roof cladding): profile runs along `u` for `length`, ribs stand out along `n`,\n * extruded `height` along `w`; `origin` is the (0, 0, 0) corner. Typical 35 mm rib at 200 mm pitch.\n */\nexport function profiledSheet(k: Kit, mat: MatKey, length: number, height: number, origin: V3, u: V3, n: V3, w: V3, pitch = 0.2, depth = 0.035) {\n  const top = pitch * 0.2\n  const slope = pitch * 0.15\n  const pts: [number, number][] = [[0, 0]]\n  const ribs = Math.max(1, Math.round(length / pitch))\n  const sc = length / (ribs * pitch)\n  for (let i = 0; i < ribs; i++) {\n    const a = i * pitch * sc\n    const m = a + (pitch * sc) / 2\n    pts.push([m - (top / 2 + slope) * sc, 0], [m - (top / 2) * sc, depth], [m + (top / 2) * sc, depth], [m + (top / 2 + slope) * sc, 0])\n  }\n  pts.push([length, 0])\n  const sh = new THREE.Shape()\n  pts.forEach(([x, y], i) => (i ? sh.lineTo(x, y) : sh.moveTo(x, y)))\n  for (let i = pts.length - 1; i >= 0; i--) sh.lineTo(pts[i][0], pts[i][1] - 0.0015)\n  sh.closePath()\n  const m = new THREE.Matrix4().makeBasis(new THREE.Vector3(...u), new THREE.Vector3(...n), new THREE.Vector3(...w)).setPosition(...origin)\n  k.extrude(mat, sh, height, m, 1)\n}\n"
    }
  ],
  "categories": [
    "onshore"
  ],
  "meta": {
    "export": "IndustrialBuilding",
    "anchor": "base",
    "standards": [
      "EN 1993-1-1",
      "EN 14509",
      "EN 508-1",
      "EN 13241",
      "EN 62305-3"
    ]
  }
}