{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "navigation-aid",
  "type": "registry:component",
  "title": "Navigation aid (lantern / fog signal)",
  "description": "Offshore platform marking unit to IALA R0139 on a galvanised pedestal: marine lantern with a Fresnel prism drum (size from the nominal range, white / yellow / red, emissive when lit), bird spikes and lightning finial, and an electronic fog signal of stacked horn emitters with its driver box. Control / monitoring box with tag plate and cable to deck, earth boss.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "electrical/navigation-aid.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/electrical/navigation-aid.tsx",
      "content": "// Offshore platform aid to navigation: marine lantern (Fresnel drum) and electronic fog signal on a pedestal with control box. IALA R0139, IALA E-200.\nimport * as THREE from 'three'\nimport { Kit, type MatKey } from '../lib/kit'\nimport { paint } from '../lib/materials'\nimport { KitMesh, useKit, type GroupProps } from '../lib/mesh'\nimport { junctionBox, tagPlate } from '../lib/parts'\nimport { textMaterial } from '../lib/textures'\nimport { cableGland, earthBoss } from './_parts'\n\nexport interface NavigationAidProps {\n  kind?: 'lantern' | 'foghorn' | 'combined'\n  /** lantern colour: white main light, yellow secondary / significant peripheral structure, red obstruction */\n  color?: 'white' | 'yellow' | 'red'\n  /** nominal range, nautical miles (sets the lantern size) */\n  range?: number\n  /** pedestal height, m */\n  height?: number\n  lit?: boolean\n  tag?: string\n}\n\nexport const defaults = {\n  kind: 'combined' as 'lantern' | 'foghorn' | 'combined',\n  color: 'white' as 'white' | 'yellow' | 'red',\n  range: 10,\n  height: 1.2,\n  lit: true,\n  tag: 'NAV-01',\n}\n\nexport function NavigationAid(props: NavigationAidProps & GroupProps) {\n  const { kind, color, range, height, lit, tag, ...group } = { ...defaults, ...props }\n  const data = useKit((k) => build(k, { kind, color, range, height, lit, tag }), [kind, color, range, height, lit, tag])\n  return (\n    <group {...group}>\n      <KitMesh data={data} />\n    </group>\n  )\n}\n\nexport default NavigationAid\n\nfunction build(k: Kit, p: typeof defaults) {\n  const G: MatKey = 'galvanised'\n  const yellowBody = paint('#e5b114', { roughness: 0.4, clearcoat: 0.4 })\n  const H = Math.min(4, Math.max(0.6, p.height))\n  // pedestal: tube on a base plate with gussets and anchors, top plate\n  k.box(G, [0.4, 0.02, 0.4], [0, 0.01, 0])\n  for (const sx of [-1, 1]) for (const sz of [-1, 1]) k.hex('nut', 0.03, 0.016, [sx * 0.15, 0.028, sz * 0.15])\n  for (let i = 0; i < 4; i++) {\n    const a = (i * Math.PI) / 2\n    k.box(G, [0.012, 0.14, 0.1], [Math.cos(a) * 0.12, 0.09, Math.sin(a) * 0.12], new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), -a + Math.PI / 2))\n  }\n  k.cyl(G, 0.07, H - 0.02, [0, 0.02 + (H - 0.02) / 2, 0], 'y', 20)\n  k.cyl(G, 0.2, 0.02, [0, H + 0.01, 0], 'y', 28)\n  earthBoss(k, [0.07, 0.25, 0], 'x')\n  // control / monitoring box (flash sync, remote alarm) on the pedestal, cable to deck\n  junctionBox(k, [0, 0.75, 0.07], 'z', [0.26, 0.3, 0.14], 'stainless')\n  k.plane(textMaterial('AtoN CONTROL', '#1f4e9a', '#ffffff', 256, 48), 0.18, 0.034, [0, 0.82, 0.212], new THREE.Quaternion())\n  k.spline('cable', [[0, 0.58, 0.14], [0, 0.3, 0.16], [0.05, 0.04, 0.3], [0.4, 0.02, 0.45]], 0.01, 6, 16)\n  tagPlate(k, p.tag, [0, 0.45, 0.071], 'z', 0.12)\n\n  const y = H + 0.02\n  if (p.kind !== 'foghorn') {\n    // marine lantern: base, Fresnel drum (stacked prism rings), lamp, cap with bird spikes and lightning finial\n    const R = Math.min(0.3, 0.1 + p.range * 0.012)\n    const hL = R * 1.6\n    const xL = p.kind === 'combined' ? -0.08 : 0\n    const hex = { white: '#fff4dc', yellow: '#ffd23a', red: '#ff3b2e' }[p.color]\n    const lens: MatKey = p.lit ? paint(hex, { emissive: hex, emissiveIntensity: 1.8, roughness: 0.1, metalness: 0, clearcoat: 1, weather: 0 }) : paint(hex, { roughness: 0.08, metalness: 0, clearcoat: 1, transparent: true, opacity: 0.6, weather: 0 })\n    k.cyl(yellowBody, R * 0.9, 0.14, [xL, y + 0.07, 0], 'y', 32)\n    k.cyl('nut', R * 0.95, 0.02, [xL, y + 0.15, 0], 'y', 32)\n    const n = 7\n    for (let i = 0; i < n; i++) {\n      const yi = y + 0.16 + (i + 0.5) * (hL / n)\n      const bulge = 1 - Math.abs(i - (n - 1) / 2) * 0.06\n      k.cyl(lens, R * bulge, hL / n - 0.004, [xL, yi, 0], 'y', 40, R * bulge * 0.97)\n    }\n    k.cyl('nut', R * 0.95, 0.02, [xL, y + 0.17 + hL, 0], 'y', 32)\n    k.dome(yellowBody, R * 0.95, [xL, y + 0.18 + hL, 0], 'y', Math.PI / 2, 0.35, 32)\n    for (let i = 0; i < 8; i++) {\n      const a = (i / 8) * Math.PI * 2\n      k.rod('stainless', [xL + Math.cos(a) * R * 0.5, y + 0.2 + hL + R * 0.2, Math.sin(a) * R * 0.5], [xL + Math.cos(a) * R * 0.75, y + 0.36 + hL + R * 0.2, Math.sin(a) * R * 0.75], 0.003, 4)\n    }\n    k.rod('stainless', [xL, y + 0.2 + hL + R * 0.3, 0], [xL, y + 0.6 + hL, 0], 0.006, 6)\n    cableGland(k, [xL + R * 0.9, y + 0.07, 0], 'x', 0.018)\n  }\n  if (p.kind !== 'lantern') {\n    // electronic fog signal: stacked horn emitters facing outward on a frame, driver box beneath\n    const x0 = p.kind === 'combined' ? 0.45 : 0\n    k.box(G, [0.5, 0.02, 0.2], [x0 / 2 + 0.05, y + 0.01, 0])\n    k.rbox(paint('#d9dbd6', { roughness: 0.4 }), [0.3, 0.25, 0.22], 0.02, [x0, y + 0.145, 0])\n    const horn = paint('#2c2f33', { roughness: 0.5 })\n    for (let r = 0; r < 2; r++)\n      for (const s of [-1, 1]) {\n        const yy = y + 0.36 + r * 0.2\n        k.cyl(horn, 0.05, 0.12, [x0, yy, s * 0.08], s > 0 ? 'z' : '-z', 16, 0.09, true)\n        k.cyl(horn, 0.05, 0.08, [x0, yy, s * 0.02], 'z', 14)\n        k.disc('rubberBlack', 0.088, [x0, yy, s * 0.14 - s * 0.005], s > 0 ? '-z' : 'z', 18)\n      }\n    // open frame: four corner angles and a top plate\n    for (const sx of [-1, 1]) for (const sz of [-1, 1]) k.box(G, [0.025, 0.48, 0.025], [x0 + sx * 0.11, y + 0.51, sz * 0.03])\n    k.box(G, [0.25, 0.012, 0.09], [x0, y + 0.756, 0])\n    cableGland(k, [x0, y + 0.02, 0.11], 'z', 0.016)\n  }\n}\n"
    },
    {
      "path": "electrical/_parts.ts",
      "type": "registry:file",
      "target": "components/deck-twin/electrical/_parts.ts",
      "content": "// Shared electrical details: profiled cladding sheet, cable glands, earth bosses, louvres, multi-cable transits, beacons.\nimport * as THREE from 'three'\nimport { eul, type Kit, type MatKey, type V3 } from '../lib/kit'\nimport { louvreMaterial } from '../lib/textures'\n\n/** Trapezoidal (vertical-rib) profiled sheet: x ∈ [-w/2, w/2], y ∈ [0, h], ribs stand out to +Z by `depth`. */\nexport function ribbedPanel(w: number, h: number, pitch = 0.25, depth = 0.035, back = true): THREE.BufferGeometry {\n  const n = Math.max(1, Math.round(w / pitch))\n  const p = w / n\n  const prof: [number, number][] = [[0, 0], [0.3, 0], [0.4, depth], [0.6, depth], [0.7, 0], [1, 0]]\n  const xs: [number, number][] = []\n  for (let i = 0; i < n; i++)\n    for (let j = 0; j < prof.length - (i < n - 1 ? 1 : 0); j++) {\n      if (i > 0 && j === 0) continue\n      xs.push([-w / 2 + (i + prof[j][0]) * p, prof[j][1]])\n    }\n  const pos: number[] = []\n  const nor: number[] = []\n  const uv: number[] = []\n  const push = (q: number[][], nx: number, nz: number) => {\n    for (const v of q) {\n      pos.push(v[0], v[1], v[2])\n      nor.push(nx, 0, nz)\n      uv.push((v[0] + w / 2) / w, v[1] / h)\n    }\n  }\n  for (let i = 0; i < xs.length - 1; i++) {\n    const [x0, z0] = xs[i]\n    const [x1, z1] = xs[i + 1]\n    const l = Math.hypot(x1 - x0, z1 - z0) || 1\n    push([[x0, 0, z0], [x1, 0, z1], [x1, h, z1], [x0, 0, z0], [x1, h, z1], [x0, h, z0]], -(z1 - z0) / l, (x1 - x0) / l)\n  }\n  if (back) push([[w / 2, 0, -0.002], [-w / 2, 0, -0.002], [-w / 2, h, -0.002], [w / 2, 0, -0.002], [-w / 2, h, -0.002], [w / 2, h, -0.002]], 0, -1)\n  const g = new THREE.BufferGeometry()\n  g.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3))\n  g.setAttribute('normal', new THREE.Float32BufferAttribute(nor, 3))\n  g.setAttribute('uv', new THREE.Float32BufferAttribute(uv, 2))\n  return g\n}\n\n/** Ex e / Ex d cable gland with a shroud, axis along `axis` (gland body from pos outwards), cable size by OD. */\nexport function cableGland(k: Kit, pos: V3, axis: 'y' | '-y' | 'x' | '-x' | 'z' | '-z', cableOD = 0.025) {\n  const d: V3 = axis === 'y' ? [0, 1, 0] : axis === '-y' ? [0, -1, 0] : axis === 'x' ? [1, 0, 0] : axis === '-x' ? [-1, 0, 0] : axis === 'z' ? [0, 0, 1] : [0, 0, -1]\n  const at = (t: number): V3 => [pos[0] + d[0] * t, pos[1] + d[1] * t, pos[2] + d[2] * t]\n  const r = cableOD / 2\n  k.hex('brass', r * 3.4, 0.012, at(0.006), axis)\n  k.cyl('brass', r * 1.55, 0.03, at(0.027), axis, 12)\n  k.hex('brass', r * 3.0, 0.014, at(0.049), axis)\n  k.cyl('rubberBlack', r * 1.45, 0.035, at(0.073), axis, 12, r * 1.1)\n  return at(0.09)\n}\n\n/** Brass earthing boss on a face (normal axis) with a bolted green/yellow bond lug. */\nexport function earthBoss(k: Kit, pos: V3, axis: 'x' | '-x' | 'z' | '-z') {\n  const s = axis.startsWith('-') ? -1 : 1\n  const off = (t: number): V3 => (axis.endsWith('x') ? [pos[0] + s * t, pos[1], pos[2]] : [pos[0], pos[1], pos[2] + s * t])\n  k.cyl('brass', 0.022, 0.03, off(0.015), axis, 12)\n  k.hex('nut', 0.028, 0.012, off(0.036), axis)\n  k.box('copper', axis.endsWith('x') ? [0.006, 0.05, 0.02] : [0.02, 0.05, 0.006], [off(0.032)[0], pos[1] - 0.025, off(0.032)[2]])\n}\n\n/** Weather louvre in a wall frame (outer face at z = 0, +Z out): frame, angled blades, bird mesh behind. */\nexport function louvre(k: Kit, u: number, y: number, w: number, h: number, frame: MatKey = 'structSteelDark', blade: MatKey = 'cladding') {\n  k.box(frame, [w + 0.08, 0.04, 0.08], [u, y + h / 2 + 0.02, 0.03])\n  k.box(frame, [w + 0.08, 0.04, 0.08], [u, y - h / 2 - 0.02, 0.03])\n  for (const s of [-1, 1]) k.box(frame, [0.04, h + 0.08, 0.08], [u + s * (w / 2 + 0.02), y, 0.03])\n  k.box('rubberBlack', [w, h, 0.01], [u, y, 0.004])\n  const n = Math.max(3, Math.round(h / 0.07))\n  for (let i = 0; i < n; i++) k.box(blade, [w, 0.075, 0.006], [u, y - h / 2 + (i + 0.5) * (h / n), 0.04], eul(0.7, 0, 0))\n}\n\n/** Flat louvre panel (texture only), facing +normal, for small enclosures. */\nexport function louvrePanel(k: Kit, w: number, h: number, pos: V3, q: THREE.Quaternion, color = '#c9ccc8') {\n  const g = new THREE.PlaneGeometry(w, h)\n  const uv = g.getAttribute('uv') as THREE.BufferAttribute\n  for (let i = 0; i < uv.count; i++) uv.setY(i, uv.getY(i) * Math.max(1, h / 0.08))\n  k.add(louvreMaterial(color), g, new THREE.Matrix4().compose(new THREE.Vector3(...pos), q, new THREE.Vector3(1, 1, 1)))\n}\n\n/** Multi-cable transit frame (MCT) on a wall face at z = 0, with packed rubber modules. */\nexport function cableTransit(k: Kit, u: number, y: number, cols = 6, rows = 2, frame: MatKey = 'structSteelDark') {\n  const w = cols * 0.075 + 0.08\n  const h = rows * 0.09 + 0.05\n  k.box(frame, [w, h, 0.12], [u, y, 0.06])\n  for (let r = 0; r < rows; r++)\n    for (let c = 0; c < cols; c++) k.box(c % 2 === r % 2 ? 'rubberBlack' : 'gasket', [0.07, 0.08, 0.02], [u - ((cols - 1) * 0.075) / 2 + c * 0.075, y - ((rows - 1) * 0.09) / 2 + r * 0.09, 0.125])\n  k.box('stainless', [w - 0.04, 0.02, 0.03], [u, y + h / 2 - 0.01, 0.125])\n  return { w, h }\n}\n\n/** Status beacon (sounder base + coloured lens), standing on +Y at pos. */\nexport function beacon(k: Kit, pos: V3, lens: MatKey) {\n  k.cyl('plasticGrey', 0.06, 0.06, [pos[0], pos[1] + 0.03, pos[2]], 'y', 14)\n  k.cyl(lens, 0.05, 0.11, [pos[0], pos[1] + 0.115, pos[2]], 'y', 16)\n  k.dome(lens, 0.05, [pos[0], pos[1] + 0.17, pos[2]], 'y', Math.PI / 2, 0.6, 16)\n}\n"
    }
  ],
  "categories": [
    "electrical"
  ],
  "meta": {
    "export": "NavigationAid",
    "anchor": "base",
    "standards": [
      "IALA R0139 (O-139)",
      "IALA E-200",
      "IEC 60529"
    ]
  }
}