{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "perimeter-fence",
  "type": "registry:component",
  "title": "Perimeter security fence",
  "description": "Straight fence run along X. Palisade: RSJ posts at 2.75 m on concrete footings, angle rails and bolted W-section pales with triple-pointed heads. Chain-link: tube posts at 3 m, top rail, tension wires, 50 mm diamond mesh and diagonal braces. Topping is cranked arms with three barbed strands or a razor coil. Gate options are a double-leaf swing gate or a cantilever sliding gate with roller posts and motor operator. Warning signs and gate earth bond.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "onshore/perimeter-fence.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/onshore/perimeter-fence.tsx",
      "content": "// Security perimeter fence: chain-link or steel palisade with barbed-wire or razor-coil topping and an optional swing or sliding gate. BS 1722-10 / -12, ASTM F567.\nimport type { Kit } from '../lib/kit'\nimport { KitMesh, useKit, type GroupProps } from '../lib/mesh'\nimport { paint } from '../lib/materials'\nimport { signMaterial } from '../lib/textures'\nimport { fenceRun, slidingGate, swingGate, type FenceOpts, type FenceStyle, type Topping } from './_fence'\nimport { clamp, earthBond } from './_onshore'\n\nexport interface PerimeterFenceProps {\n  /** run length along X, m */\n  length?: number\n  style?: FenceStyle\n  /** fabric or pale height, m */\n  height?: number\n  topping?: Topping\n  gate?: 'none' | 'swing' | 'sliding'\n  /** clear gate opening, m */\n  gateWidth?: number\n  finish?: 'galvanised' | 'green'\n  tag?: string\n}\n\nexport const defaults = { length: 30, style: 'palisade' as FenceStyle, height: 2.4, topping: 'razor' as Topping, gate: 'sliding' as 'none' | 'swing' | 'sliding', gateWidth: 6, finish: 'galvanised' as 'galvanised' | 'green', tag: '' }\n\nexport function PerimeterFence(props: PerimeterFenceProps & GroupProps) {\n  const { length, style, height, topping, gate, gateWidth, finish, tag, ...group } = { ...defaults, ...props }\n  const p = { length, style, height, topping, gate, gateWidth, finish, tag }\n  const data = useKit((k) => build(k, p), [length, style, height, topping, gate, gateWidth, finish, tag])\n  return (\n    <group {...group}>\n      <KitMesh data={data} />\n    </group>\n  )\n}\n\nexport default PerimeterFence\n\nfunction build(k: Kit, p: typeof defaults) {\n  const L = clamp(p.length, 2, 300)\n  const o: FenceOpts = { style: p.style, height: clamp(p.height, 1.2, 3.6), topping: p.topping, mat: p.finish === 'green' ? paint('#2f5a3a', { clearcoat: 0.2 }) : 'galvanised' }\n  const gw = p.gate === 'none' ? 0 : clamp(p.gateWidth, 1, Math.max(1, L * 0.6))\n  const a = -L / 2\n  const b = L / 2\n  if (!gw) fenceRun(k, [a, 0, 0], [b, 0, 0], o, true, [0, 0, 1])\n  else {\n    const g0 = -gw / 2\n    const g1 = gw / 2\n    if (p.gate === 'sliding') {\n      // the leaf parks behind the fence on the -X side, so keep the run clear of its tail\n      fenceRun(k, [a, 0, 0], [g0, 0, 0], o, true, [0, 0, 1])\n      slidingGate(k, [g0, 0, 0], [g1, 0, 0], o)\n      fenceRun(k, [g1 + 0.2, 0, 0], [b, 0, 0], o, true, [0, 0, 1])\n    } else {\n      fenceRun(k, [a, 0, 0], [g0, 0, 0], o, true, [0, 0, 1])\n      swingGate(k, [g0, 0, 0], [g1, 0, 0], o)\n      fenceRun(k, [g1, 0, 0], [b, 0, 0], o, true, [0, 0, 1])\n    }\n    // warning signs either side of the gate, earth bond on the gate post\n    k.plane(signMaterial('noentry'), 0.6, 0.3, [g0 - 1.2, 1.5, 0.12], undefined)\n    k.plane(signMaterial('hv', 'DANGER'), 0.5, 0.25, [g1 + 1.2, 1.5, 0.12], undefined)\n    earthBond(k, [g1 + 0.25, 0.35, 0.06], [0, 0, 1], 0, 0.35)\n  }\n  if (p.tag) k.plane(signMaterial('muster', p.tag), 0.8, 0.4, [a + 2, 1.4, 0.12], undefined)\n}\n"
    },
    {
      "path": "onshore/_fence.ts",
      "type": "registry:file",
      "target": "components/deck-twin/onshore/_fence.ts",
      "content": "// Security fence runs and gates: chain-link (BS 1722-10 / ASTM F567) and steel palisade (BS 1722-12), barbed or razor topping.\nimport * as THREE from 'three'\nimport type { Kit, MatKey, V3 } from '../lib/kit'\nimport { paint } from '../lib/materials'\nimport { meshGuardMaterial } from '../lib/textures'\n\nexport type FenceStyle = 'chain-link' | 'palisade'\nexport type Topping = 'barbed' | 'razor' | 'none'\n\nexport interface FenceOpts {\n  style: FenceStyle\n  /** fabric or pale height above ground, m */\n  height: number\n  topping: Topping\n  /** coating: galvanised or polyester powder-coat colour */\n  mat?: MatKey\n}\n\nconst GALV: MatKey = 'galvanised'\n/** typical: line post spacing, m */\nexport const POST_SPACING: Record<FenceStyle, number> = { 'chain-link': 3.0, palisade: 2.75 }\n\nconst v = (a: V3) => new THREE.Vector3(...a)\nconst arr = (p: THREE.Vector3) => p.toArray() as V3\n\nfunction frame(a: V3, b: V3) {\n  const A = v(a)\n  const d = v(b).sub(A)\n  d.y = 0\n  const L = d.length()\n  d.normalize()\n  const n = new THREE.Vector3(-d.z, 0, d.x)\n  const q = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(1, 0, 0), d)\n  return { A, d, n, L, q }\n}\n\n/** Concrete footing collar at grade around a post. */\nfunction collar(k: Kit, p: V3, r: number) {\n  k.cyl('concrete', r, 0.06, [p[0], p[1] + 0.03, p[2]], 'y', 14, r * 0.8)\n}\n\n/** One post. Terminal posts are heavier. The topping arm leans to the `out` side. */\nexport function fencePost(k: Kit, p: V3, o: FenceOpts, along: THREE.Vector3, out: THREE.Vector3, terminal = false) {\n  const mat = o.mat ?? GALV\n  const H = o.height\n  if (o.style === 'chain-link') {\n    const r = terminal ? 0.038 : 0.03\n    collar(k, p, r * 4)\n    k.cyl(mat, r, H + 0.05, [p[0], p[1] + (H + 0.05) / 2, p[2]], 'y', 12)\n    k.dome(mat, r * 1.15, [p[0], p[1] + H + 0.05, p[2]], 'y', Math.PI / 2, 0.6, 12)\n  } else {\n    // RSJ-style post 102 x 44, typical\n    collar(k, p, 0.16)\n    const q = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(1, 0, 0), along)\n    k.box(mat, [0.044, H + 0.1, 0.006], [p[0], p[1] + (H + 0.1) / 2, p[2]], q)\n    for (const s of [-1, 1]) k.box(mat, [0.006, H + 0.1, 0.102], arr(v(p).addScaledVector(along, s * 0.019).setY(p[1] + (H + 0.1) / 2)), q)\n    k.box(mat, [0.06, 0.006, 0.12], [p[0], p[1] + H + 0.1, p[2]], q)\n  }\n  topping(k, p, o, out)\n}\n\nfunction armTop(p: V3, o: FenceOpts, out: THREE.Vector3) {\n  const y = p[1] + o.height + (o.style === 'palisade' ? 0.1 : 0.05)\n  return { y, tip: arr(v(p).addScaledVector(out, 0.32).setY(y + 0.32)), base: [p[0], y, p[2]] as V3 }\n}\n\nfunction topping(k: Kit, p: V3, o: FenceOpts, out: THREE.Vector3) {\n  if (o.topping === 'none') return\n  const { base, tip } = armTop(p, o, out)\n  if (o.topping === 'barbed') k.rod(o.mat ?? GALV, base, tip, 0.012, 6)\n  else {\n    k.rod(o.mat ?? GALV, base, [base[0], base[1] + 0.3, base[2]], 0.012, 6)\n    k.rod(o.mat ?? GALV, [base[0], base[1] + 0.3, base[2]], arr(v(base).addScaledVector(out, 0.15).setY(base[1] + 0.3)), 0.01, 6)\n  }\n}\n\n/** Fence run from a to b (ground points), posts at both ends unless `startPost` is false. Returns post count. */\nexport function fenceRun(k: Kit, a: V3, b: V3, o: FenceOpts, startPost = true, out?: V3) {\n  const { A, d, n, L, q } = frame(a, b)\n  if (L < 0.05) return 0\n  const outV = out ? v(out).normalize() : n.clone()\n  const mat = o.mat ?? GALV\n  const bays = Math.max(1, Math.ceil(L / POST_SPACING[o.style]))\n  const bl = L / bays\n  const H = o.height\n  const at = (s: number, y: number) => arr(A.clone().addScaledVector(d, s).setY(a[1] + y))\n  for (let i = startPost ? 0 : 1; i <= bays; i++) fencePost(k, at(i * bl, 0), o, d, outV, i === 0 || i === bays)\n  const mid = (y: number) => at(L / 2, y)\n  if (o.style === 'chain-link') {\n    // top rail, bottom tension wire, fabric (50 mm diamond mesh)\n    k.rod(mat, at(0, H - 0.02), at(L, H - 0.02), 0.021, 8)\n    k.rod(mat, at(0, 0.06), at(L, 0.06), 0.003, 4)\n    k.rod(mat, at(0, H * 0.5), at(L, H * 0.5), 0.003, 4)\n    const geo = new THREE.PlaneGeometry(L, H - 0.04)\n    const uv = geo.getAttribute('uv') as THREE.BufferAttribute\n    for (let i = 0; i < uv.count; i++) uv.setXY(i, (uv.getX(i) * L) / 0.2, (uv.getY(i) * (H - 0.04)) / 0.2)\n    k.add(fabric(mat), geo, new THREE.Matrix4().compose(v(mid((H - 0.04) / 2 + 0.03)).addScaledVector(n, 0.035), q, new THREE.Vector3(1, 1, 1)))\n    // diagonal braces at terminal posts\n    for (const [s0, s1] of [[0, Math.min(bl, 1.6)], [L, L - Math.min(bl, 1.6)]]) k.rod(mat, at(s0, H * 0.8), at(s1, 0.15), 0.017, 6)\n  } else {\n    // two (three above 2.4 m) angle rails, W-section pales with triple-pointed heads\n    const rails = H > 2.3 ? [0.3, H / 2, H - 0.35] : [0.3, H - 0.35]\n    for (const y of rails) {\n      k.box(mat, [L, 0.05, 0.006], mid(y), q)\n      k.box(mat, [L, 0.006, 0.05], arr(v(mid(y + 0.022)).addScaledVector(n, 0.025)), q)\n    }\n    const pitch = 0.155 // typical: 65 mm pale, 90 mm gap\n    const np = Math.max(1, Math.floor((L - 0.05) / pitch))\n    const off = (L - (np - 1) * pitch) / 2\n    const pale = paleGeometry(H)\n    for (let i = 0; i < np; i++) {\n      const p = v(at(off + i * pitch, 0)).addScaledVector(n, 0.05)\n      k.addShared(mat, pale, new THREE.Matrix4().compose(p, q, new THREE.Vector3(1, 1, 1)))\n      k.hex('nut', 0.016, 0.01, arr(p.clone().addScaledVector(n, 0.022).setY(a[1] + rails[rails.length - 1])), arr(n))\n    }\n  }\n  // topping strands or razor coil, carried on the arms\n  if (o.topping !== 'none') {\n    const { y, tip } = armTop(arr(A), o, outV)\n    const off = v(tip).sub(A.clone().setY(y))\n    if (o.topping === 'barbed')\n      for (const t of [0.35, 0.68, 1]) {\n        const p0 = arr(A.clone().setY(y).addScaledVector(off, t))\n        k.rod(mat, p0, arr(v(p0).addScaledVector(d, L)), 0.0025, 4)\n      }\n    else razorCoil(k, arr(A.clone().setY(y + 0.3).addScaledVector(outV, 0.08)), d, L, 0.45)\n  }\n  return bays + (startPost ? 1 : 0)\n}\n\nconst fab = new Map<string, THREE.Material>()\nfunction fabric(mat: MatKey) {\n  const key = typeof mat === 'string' ? mat : mat.uuid\n  let m = fab.get(key)\n  if (!m) fab.set(key, (m = meshGuardMaterial(typeof mat === 'string' ? (mat === 'galvanised' ? '#a9aeb1' : '#2f5a3a') : '#' + (mat as THREE.MeshStandardMaterial).color.getHexString())))\n  return m\n}\n\nconst paleCache = new Map<number, THREE.BufferGeometry>()\n/** W-section pale, 65 mm wide, triple-pointed head, 2.5 mm steel; origin at the foot, face in local XY. */\nfunction paleGeometry(H: number) {\n  let g = paleCache.get(H)\n  if (g) return g\n  const w = 0.065\n  const sh = new THREE.Shape()\n  sh.moveTo(-w / 2, 0)\n  sh.lineTo(w / 2, 0)\n  sh.lineTo(w / 2, H - 0.07)\n  sh.lineTo(w / 2 - 0.004, H - 0.02)\n  sh.lineTo(w / 6, H - 0.06)\n  sh.lineTo(0, H)\n  sh.lineTo(-w / 6, H - 0.06)\n  sh.lineTo(-w / 2 + 0.004, H - 0.02)\n  sh.lineTo(-w / 2, H - 0.07)\n  sh.closePath()\n  const flat = new THREE.ExtrudeGeometry(sh, { depth: 0.0025, bevelEnabled: false })\n  // two folded flanges give the W its depth\n  const fl = new THREE.BoxGeometry(0.0025, H - 0.1, 0.018)\n  const parts: THREE.BufferGeometry[] = [flat]\n  for (const x of [-w / 2, w / 2, -w / 6, w / 6]) {\n    const f = fl.clone()\n    f.translate(x, (H - 0.1) / 2, x === -w / 2 || x === w / 2 ? -0.008 : 0.01)\n    parts.push(f)\n  }\n  g = mergeAll(parts)\n  paleCache.set(H, g)\n  return g\n}\n\nfunction mergeAll(list: THREE.BufferGeometry[]) {\n  const geos = list.map((x) => (x.index ? x.toNonIndexed() : x))\n  let n = 0\n  for (const x of geos) n += x.getAttribute('position').count\n  const pos = new Float32Array(n * 3)\n  const nor = new Float32Array(n * 3)\n  const uv = new Float32Array(n * 2)\n  let o = 0\n  for (const x of geos) {\n    x.computeVertexNormals()\n    const c = x.getAttribute('position').count\n    pos.set(x.getAttribute('position').array as Float32Array, o * 3)\n    nor.set(x.getAttribute('normal').array as Float32Array, o * 3)\n    const u = x.getAttribute('uv')\n    if (u) uv.set(u.array as Float32Array, o * 2)\n    o += c\n  }\n  const g = new THREE.BufferGeometry()\n  g.setAttribute('position', new THREE.BufferAttribute(pos, 3))\n  g.setAttribute('normal', new THREE.BufferAttribute(nor, 3))\n  g.setAttribute('uv', new THREE.BufferAttribute(uv, 2))\n  return g\n}\n\n/** Concertina razor coil along d from p for L, coil diameter D (low-poly helix). */\nexport function razorCoil(k: Kit, p: V3, d: THREE.Vector3, L: number, D: number) {\n  const [u, w] = [new THREE.Vector3(0, 1, 0), new THREE.Vector3().crossVectors(d, new THREE.Vector3(0, 1, 0)).normalize()]\n  const turns = Math.max(2, Math.round(L / 0.2))\n  const per = 10\n  const pts: THREE.Vector3[] = []\n  for (let i = 0; i <= turns * per; i++) {\n    const t = i / (turns * per)\n    const a = t * turns * Math.PI * 2\n    pts.push(v(p).addScaledVector(d, t * L).addScaledVector(u, Math.sin(a) * D / 2).addScaledVector(w, Math.cos(a) * D / 2))\n  }\n  const curve = new THREE.CatmullRomCurve3(pts)\n  k.add(paint('#c9cdd0', { metalness: 0.9, roughness: 0.25, clearcoat: 0 }), new THREE.TubeGeometry(curve, turns * per, 0.004, 3, false))\n}\n\n/** Double-leaf swing gate (chain-link or palisade infill) across a to b, leaves closed. */\nexport function swingGate(k: Kit, a: V3, b: V3, o: FenceOpts) {\n  const { A, d, n, L, q } = frame(a, b)\n  const mat = o.mat ?? GALV\n  const H = o.height\n  for (const s of [0, L]) {\n    const p = arr(A.clone().addScaledVector(d, s))\n    collar(k, p, 0.18)\n    k.cyl(mat, 0.05, H + 0.15, [p[0], a[1] + (H + 0.15) / 2, p[2]], 'y', 14)\n    k.dome(mat, 0.055, [p[0], a[1] + H + 0.15, p[2]], 'y', Math.PI / 2, 0.6, 12)\n    topping(k, p, o, n)\n  }\n  const leaf = (L - 0.16) / 2\n  for (const [s0, dir] of [[0.08, 1], [L - 0.08, -1]] as const) {\n    const c = (t: number, y: number) => arr(A.clone().addScaledVector(d, s0 + dir * t).setY(a[1] + y))\n    for (const y of [0.08, H - 0.05]) k.rod(mat, c(0.03, y), c(leaf - 0.01, y), 0.021, 8)\n    for (const t of [0.03, leaf - 0.01]) k.rod(mat, c(t, 0.08), c(t, H - 0.05), 0.021, 8)\n    k.rod(mat, c(0.03, 0.1), c(leaf - 0.01, H - 0.08), 0.017, 6)\n    for (const y of [0.25, H - 0.3]) k.box('nut', [0.06, 0.03, 0.03], c(0.0, y))\n    if (o.style === 'chain-link') {\n      const geo = new THREE.PlaneGeometry(leaf - 0.06, H - 0.16)\n      const uv = geo.getAttribute('uv') as THREE.BufferAttribute\n      for (let i = 0; i < uv.count; i++) uv.setXY(i, (uv.getX(i) * (leaf - 0.06)) / 0.2, (uv.getY(i) * (H - 0.16)) / 0.2)\n      k.add(fabric(mat), geo, new THREE.Matrix4().compose(v(c(leaf / 2, H / 2)).addScaledVector(n, 0.025), q, new THREE.Vector3(1, 1, 1)))\n    } else {\n      const pale = paleGeometry(H - 0.05)\n      for (let t = 0.1; t < leaf - 0.05; t += 0.155) k.addShared(mat, pale, new THREE.Matrix4().compose(v(c(t, 0.05)).addScaledVector(n, 0.03), q, new THREE.Vector3(1, 1, 1)))\n    }\n  }\n  // drop bolt and padlock hasp at the meeting stiles\n  const m = arr(A.clone().addScaledVector(d, L / 2).setY(a[1] + 1.05).addScaledVector(n, 0.03))\n  k.box('brass', [0.05, 0.08, 0.03], m, q)\n  k.rod(mat, arr(v(m).setY(a[1])), arr(v(m).setY(a[1] + 0.5)), 0.008, 6)\n}\n\n/** Cantilever sliding gate closed across a to b, counterbalance tail running back past a; track wheels on rail posts. */\nexport function slidingGate(k: Kit, a: V3, b: V3, o: FenceOpts) {\n  const { A, d, n, L, q } = frame(a, b)\n  const mat = o.mat ?? GALV\n  const H = o.height\n  const tail = L * 0.45\n  const at = (s: number, y: number) => arr(A.clone().addScaledVector(d, s).setY(a[1] + y).addScaledVector(n, 0.12))\n  // guide posts with roller carriages, receiving post at b\n  for (const s of [-tail * 0.15, -tail * 0.85]) {\n    const p = arr(A.clone().addScaledVector(d, s))\n    collar(k, p, 0.2)\n    k.box(mat, [0.1, 0.25, 0.1], [p[0], a[1] + 0.125, p[2]])\n    k.box('nut', [0.3, 0.1, 0.12], at(s, 0.16))\n    k.box(mat, [0.1, H + 0.1, 0.1], [p[0], a[1] + (H + 0.1) / 2, p[2]])\n  }\n  const pb = arr(A.clone().addScaledVector(d, L + 0.1))\n  collar(k, pb, 0.2)\n  k.box(mat, [0.1, H + 0.1, 0.1], [pb[0], a[1] + (H + 0.1) / 2, pb[2]])\n  // leaf frame: bottom box track beam, top rail, stiles, diagonal bracing in the tail\n  k.box(mat, [L + tail, 0.12, 0.08], at((L - tail) / 2, 0.26), q)\n  k.box(mat, [L + tail, 0.05, 0.05], at((L - tail) / 2, H), q)\n  for (const s of [-tail, 0, L]) k.box(mat, [0.05, H - 0.26, 0.05], at(s, (H + 0.26) / 2), q)\n  k.rod(mat, at(-tail, 0.32), at(0, H), 0.02, 6)\n  if (o.style === 'chain-link') {\n    const geo = new THREE.PlaneGeometry(L, H - 0.32)\n    const uv = geo.getAttribute('uv') as THREE.BufferAttribute\n    for (let i = 0; i < uv.count; i++) uv.setXY(i, (uv.getX(i) * L) / 0.2, (uv.getY(i) * (H - 0.32)) / 0.2)\n    k.add(fabric(mat), geo, new THREE.Matrix4().compose(v(at(L / 2, (H + 0.32) / 2)), q, new THREE.Vector3(1, 1, 1)))\n  } else {\n    const pale = paleGeometry(H)\n    for (let s = 0.08; s < L - 0.05; s += 0.155) k.addShared(mat, pale, new THREE.Matrix4().compose(v(at(s, 0.2)).addScaledVector(n, 0.045), q, new THREE.Vector3(1, 1, 1)))\n  }\n  // motor operator on its plinth beside the first guide post\n  const mp = arr(A.clone().addScaledVector(d, -0.5).addScaledVector(n, 0.55))\n  k.box('concrete', [0.5, 0.15, 0.4], [mp[0], a[1] + 0.075, mp[2]])\n  k.rbox(paint('#6f7478'), [0.3, 0.38, 0.22], 0.03, [mp[0], a[1] + 0.34, mp[2]], q)\n  if (o.topping !== 'none') {\n    const y = a[1] + H + 0.05\n    const start = arr(A.clone().addScaledVector(d, -tail).setY(y))\n    if (o.topping === 'razor') razorCoil(k, [start[0], y + 0.2, start[2]], d, L + tail, 0.4)\n    else for (const t of [0.12, 0.24]) k.rod(mat, arr(v(start).setY(y + t)), arr(v(start).setY(y + t).addScaledVector(d, L + tail)), 0.0025, 4)\n  }\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": "PerimeterFence",
    "anchor": "base",
    "standards": [
      "BS 1722-10",
      "BS 1722-12",
      "ASTM F567",
      "ASTM F1184"
    ]
  }
}