{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "junction-box",
  "type": "registry:component",
  "title": "Junction box (Ex e)",
  "description": "Increased-safety field junction box in 316 stainless or GRP on a galvanised UPN floor stand. Hollow body with mounting plate, DIN-rail terminals, trunking and cable cores (shown with `open`); gasketed lid with captive screws and hinges, Ex marking and warning label, breather drain, external earth stud and tag plate. A bottom gland plate carries a row of cable glands with shrouds; the cables drop and run out along a deck-level ladder tray. Size sets the enclosure and the stand (one or two legs).",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "electrical/junction-box.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/electrical/junction-box.tsx",
      "content": "// Ex e increased-safety junction box on a floor stand: bolted lid, gland plate with cable glands, cables to a deck tray, earth stud, breather. IEC 60079-7, IEC 62444.\nimport * as THREE from 'three'\nimport { Kit, type MatKey, type V3 } from '../lib/kit'\nimport { paint } from '../lib/materials'\nimport { KitMesh, useKit, type GroupProps } from '../lib/mesh'\nimport { member, tagPlate } from '../lib/parts'\nimport { cableGland, earthBoss } from './_parts'\nimport { legendMaterial } from './_textures'\n\nexport interface JunctionBoxProps {\n  size?: 'small' | 'medium' | 'large'\n  enclosure?: 'stainless' | 'grp'\n  /** number of cable glands on the bottom gland plate */\n  glands?: number\n  /** box centre above deck, m */\n  height?: number\n  /** lid swung open, showing the DIN rail terminals and cable cores inside */\n  open?: boolean\n  tag?: string\n}\n\nexport const defaults = {\n  size: 'medium' as 'small' | 'medium' | 'large',\n  enclosure: 'stainless' as 'stainless' | 'grp',\n  glands: 6,\n  height: 1.2,\n  open: false,\n  tag: 'JB-4101',\n}\n\nconst SIZES: Record<string, V3> = { small: [0.3, 0.3, 0.15], medium: [0.5, 0.4, 0.2], large: [0.8, 0.6, 0.25] } // typical\n\nexport function JunctionBox(props: JunctionBoxProps & GroupProps) {\n  const { size, enclosure, glands, height, open, tag, ...group } = { ...defaults, ...props }\n  const data = useKit((k) => build(k, { size, enclosure, glands, height, open, tag }), [size, enclosure, glands, height, open, tag])\n  return (\n    <group {...group}>\n      <KitMesh data={data} />\n    </group>\n  )\n}\n\nexport default JunctionBox\n\nfunction build(k: Kit, p: typeof defaults) {\n  const [W, H, D] = SIZES[p.size] ?? SIZES.medium\n  const box: MatKey = p.enclosure === 'grp' ? 'plasticGrey' : 'stainless'\n  const yc = Math.max(H / 2 + 0.45, p.height)\n  const zp = 0.04 // stand front face\n  const zf = zp + 0.008 + D // lid front face\n  const legs = W > 0.6 ? [-W * 0.35, W * 0.35] : [0]\n\n  // stand: UPN 100 legs on base plates with anchors, horizontal rails carrying the box\n  for (const x of legs) {\n    member(k, [x, 0.015, -0.01], [x, yc + H / 2 + 0.05, -0.01], 'UPN 100', 'galvanised', [0, 0, 1])\n    k.box('galvanised', [0.22, 0.015, 0.22], [x, 0.0075, -0.01])\n    for (const sx of [-1, 1]) for (const sz of [-1, 1]) k.hex('nut', 0.026, 0.014, [x + sx * 0.08, 0.022, -0.01 + sz * 0.08])\n  }\n  for (const y of [yc - H / 2 + 0.06, yc + H / 2 - 0.06]) k.box('galvanised', [W + 0.12, 0.04, 0.03], [0, y, zp - 0.01])\n  // enclosure: hollow body (back, four walls), lid with captive screws, hinges, earth stud, breather / drain\n  const t = 0.004\n  const Db = D - 0.035\n  const zc = zp + Db / 2\n  k.box(box, [W, H, t], [0, yc, zp + t / 2])\n  for (const s of [-1, 1]) {\n    k.box(box, [W, t, Db], [0, yc + s * (H / 2 - t / 2), zc])\n    k.box(box, [t, H, Db], [s * (W / 2 - t / 2), yc, zc])\n  }\n  for (const s of [-1, 1]) {\n    k.box('rubberBlack', [W, 0.012, 0.004], [0, yc + s * (H / 2 - 0.006), zp + Db - 0.002]) // lid gasket\n    k.box('rubberBlack', [0.012, H, 0.004], [s * (W / 2 - 0.006), yc, zp + Db - 0.002])\n  }\n  // inside: mounting plate, DIN rail with terminals, cores from the glands\n  k.box('galvanised', [W - 0.04, H - 0.04, 0.003], [0, yc, zp + t + 0.0015])\n  const nT = Math.max(4, Math.floor((W - 0.1) / 0.0125))\n  k.box('aluminium', [W - 0.08, 0.035, 0.008], [0, yc + 0.02, zp + 0.012])\n  for (let i = 0; i < nT; i++) {\n    const tx = -W / 2 + 0.05 + i * 0.0125\n    k.box(i % 6 === 5 ? paint('#d8c21a', { roughness: 0.5, metalness: 0, clearcoat: 0, weather: 0 }) : paint('#3b6ea8', { roughness: 0.5, metalness: 0, clearcoat: 0, weather: 0 }), [0.01, 0.06, 0.045], [tx, yc + 0.02, zp + 0.038])\n  }\n  k.box('rubberBlack', [W - 0.08, 0.025, 0.03], [0, yc + H * 0.32, zp + 0.02]) // trunking\n  k.box('rubberBlack', [W - 0.08, 0.025, 0.03], [0, yc - H * 0.25, zp + 0.02])\n  // cable cores from each gland up the trunking to the terminals\n  {\n    const ng = Math.max(1, Math.min(20, Math.round(p.glands)))\n    for (let i = 0; i < ng; i++) {\n      const gx = -W * 0.38 + (ng === 1 ? W * 0.38 : (i * W * 0.76) / (ng - 1))\n      for (const [c, col] of (['#2b5fae', '#8a5a2b', '#3a3a3a'] as const).entries()) {\n        const tx = Math.max(-W / 2 + 0.06, Math.min(W / 2 - 0.06, gx + (c - 1) * 0.012))\n        k.spline(paint(col, { roughness: 0.6, metalness: 0, clearcoat: 0, weather: 0 }), [[gx + (c - 1) * 0.006, yc - H / 2 + t, zp + Db * 0.5], [gx, yc - H * 0.25, zp + 0.04], [tx, yc - 0.01, zp + 0.05]], 0.0025, 4, 10)\n      }\n    }\n  }\n  // lid (hinged on the left edge), drawn in a hinge frame so it can swing open\n  const lidQ = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), p.open ? -1.9 : 0)\n  const hinge = new THREE.Vector3(-W / 2 - 0.008, yc, zf - 0.0175)\n  const L = (x: number, y: number, z: number): V3 => new THREE.Vector3(x + W / 2 + 0.008, y - yc, z - (zf - 0.0175)).applyQuaternion(lidQ).add(hinge).toArray() as V3\n  k.at(hinge.toArray() as V3, lidQ, () => {\n    k.rbox(box, [W + 0.01, H + 0.01, 0.035], 0.012, [W / 2 + 0.008, 0, 0])\n  })\n  const nx = Math.max(2, Math.round(W / 0.2))\n  const ny = Math.max(2, Math.round(H / 0.2))\n  for (let i = 0; i <= nx; i++) for (const s of [-1, 1]) k.cyl('stainless', 0.007, 0.01, L(-W / 2 + 0.02 + (i * (W - 0.04)) / nx, yc + s * (H / 2 - 0.012), zf + 0.004), lidQ.clone().multiply(new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), new THREE.Vector3(0, 0, 1))), 8)\n  for (let i = 1; i < ny; i++) for (const s of [-1, 1]) k.cyl('stainless', 0.007, 0.01, L(s * (W / 2 - 0.012), yc - H / 2 + (i * H) / ny, zf + 0.004), lidQ.clone().multiply(new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), new THREE.Vector3(0, 0, 1))), 8)\n  for (const y of [yc - H * 0.3, yc + H * 0.3]) k.cyl('stainless', 0.01, 0.06, [-W / 2 - 0.008, y, zf - 0.0175], 'y', 10)\n  k.cyl('brass', 0.012, 0.03, [W * 0.35, yc - H / 2 - 0.015, zp + D * 0.8], 'y', 10) // breather / drain\n  earthBoss(k, [W / 2, yc - H * 0.3, zp + D / 2], 'x')\n  k.add(legendMaterial('Ex eb IIC T6 Gb  IP66'), new THREE.PlaneGeometry(Math.min(0.2, W * 0.5), 0.022), new THREE.Matrix4().compose(new THREE.Vector3(...L(0, yc + H * 0.25, zf + 0.001)), lidQ, new THREE.Vector3(1, 1, 1)))\n  k.at(L(0, yc - H * 0.15, zf + 0.001), lidQ, () => k.box('actuatorYellow', [Math.min(0.16, W * 0.4), 0.06, 0.002], [0, 0, 0]))\n  k.add(legendMaterial('ISOLATE ELSEWHERE BEFORE OPENING', true), new THREE.PlaneGeometry(Math.min(0.15, W * 0.38), 0.03), new THREE.Matrix4().compose(new THREE.Vector3(...L(0, yc - H * 0.15, zf + 0.0025)), lidQ, new THREE.Vector3(1, 1, 1)))\n  tagPlate(k, p.tag, [-W / 2 - 0.002, yc - H * 0.15, zc], '-x', Math.min(0.12, D * 0.6))\n\n  // gland plate underneath with glands in a row, cables down and away to a deck tray\n  const n = Math.max(1, Math.min(20, Math.round(p.glands)))\n  const gz = zp + D / 2 - 0.015\n  k.box(box, [W * 0.85, 0.008, D * 0.6], [0, yc - H / 2 - 0.004, gz])\n  const yTray = 0.12\n  const xT = W / 2 + 0.25\n  for (let i = 0; i < n; i++) {\n    const x = -W * 0.38 + (n === 1 ? W * 0.38 : (i * W * 0.76) / (n - 1))\n    const od = i % 3 === 0 ? 0.03 : 0.022\n    const end = cableGland(k, [x, yc - H / 2 - 0.008, gz], '-y', od)\n    const zc = gz + 0.25 + (i % 3) * 0.035\n    k.spline('cable', [end, [x, end[1] - 0.2, gz], [x, yTray + 0.3, zc - 0.05], [x + 0.1, yTray + 0.04, zc], [xT + 0.6, yTray + 0.04, zc]], od / 2, 6, 40)\n  }\n  // ladder cable tray at deck level running out along +X\n  const zt0 = gz + 0.2\n  const zt1 = gz + 0.42\n  for (const z of [zt0, zt1]) k.box('cableTray', [W + 1.2, 0.08, 0.006], [W * 0.0 + 0.35, yTray, z])\n  for (let x = -W / 2 - 0.2; x < W / 2 + 0.9; x += 0.3) k.box('cableTray', [0.03, 0.015, zt1 - zt0], [x, yTray - 0.03, (zt0 + zt1) / 2])\n  for (const x of [-W / 2 - 0.15, W / 2 + 0.85]) k.box('galvanised', [0.05, yTray - 0.04, zt1 - zt0 + 0.04], [x, (yTray - 0.04) / 2, (zt0 + zt1) / 2])\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"
    },
    {
      "path": "electrical/_textures.ts",
      "type": "registry:file",
      "target": "components/deck-twin/electrical/_textures.ts",
      "content": "// Canvas materials for electrical equipment: rating plates, switchgear / MCC fronts, room finishes, indicator lamps.\nimport * as THREE from 'three'\nimport { paint } from '../lib/materials'\n\ntype G = CanvasRenderingContext2D\nconst cache = new Map<string, THREE.MeshStandardMaterial>()\n\nfunction make(key: string, w: number, h: number, draw: (g: G, w: number, h: number) => void, o: { repeat?: boolean; roughness?: number; metalness?: number; emissive?: number } = {}) {\n  const hit = cache.get(key)\n  if (hit) return hit\n  const c = document.createElement('canvas')\n  c.width = w\n  c.height = h\n  draw(c.getContext('2d')!, w, h)\n  const tex = new THREE.CanvasTexture(c)\n  tex.colorSpace = THREE.SRGBColorSpace\n  tex.anisotropy = 8\n  if (o.repeat) tex.wrapS = tex.wrapT = THREE.RepeatWrapping\n  const m = new THREE.MeshStandardMaterial({ map: tex, metalness: o.metalness ?? 0.1, roughness: o.roughness ?? 0.55 })\n  if (o.emissive) {\n    m.emissive = new THREE.Color('#ffffff')\n    m.emissiveMap = tex\n    m.emissiveIntensity = o.emissive\n  }\n  m.name = `el-${key}`\n  cache.set(key, m)\n  return m\n}\n\nfunction rng(seed: number) {\n  let s = seed >>> 0\n  return () => (s = (s * 1664525 + 1013904223) >>> 0) / 4294967296\n}\nfunction speckle(g: G, w: number, h: number, base: string, n: number, dark: string, light: string, seed = 1) {\n  g.fillStyle = base\n  g.fillRect(0, 0, w, h)\n  const r = rng(seed)\n  for (let i = 0; i < n; i++) {\n    g.fillStyle = r() < 0.5 ? dark : light\n    g.fillRect(r() * w, r() * h, 1 + r() * 2, 1 + r() * 2)\n  }\n}\nfunction label(g: G, x: number, y: number, w: number, h: number, text = '', bg = '#f4f4ef', fg = '#111') {\n  g.fillStyle = bg\n  g.fillRect(x, y, w, h)\n  g.strokeStyle = '#333'\n  g.lineWidth = 1\n  g.strokeRect(x, y, w, h)\n  if (!text) return\n  g.fillStyle = fg\n  let size = Math.round(h * 0.62)\n  g.font = `bold ${size}px Arial, sans-serif`\n  while (g.measureText(text).width > w * 0.92 && size > 6) g.font = `bold ${(size -= 1)}px Arial, sans-serif`\n  g.textAlign = 'center'\n  g.textBaseline = 'middle'\n  g.fillText(text, x + w / 2, y + h / 2 + 1)\n}\nfunction seam(g: G, x: number, y: number, w: number, h: number) {\n  g.strokeStyle = 'rgba(30,34,38,0.85)'\n  g.lineWidth = 3\n  g.strokeRect(x, y, w, h)\n  g.strokeStyle = 'rgba(255,255,255,0.45)'\n  g.lineWidth = 1\n  g.strokeRect(x + 3, y + 3, w - 6, h - 6)\n}\nfunction vents(g: G, x: number, y: number, w: number, n: number) {\n  g.fillStyle = 'rgba(28,30,32,0.8)'\n  for (let i = 0; i < n; i++) g.fillRect(x, y + i * 8, w, 4)\n}\nfunction bolt(g: G, x: number, y: number) {\n  g.fillStyle = '#7a8086'\n  g.beginPath()\n  g.arc(x, y, 5, 0, Math.PI * 2)\n  g.fill()\n}\n\n/** Stamped rating plate: a title then key/value lines. */\nexport function ratingPlateMaterial(title: string, lines: string[]) {\n  return make(`plate-${title}-${lines.join('|')}`, 384, 256, (g, w, h) => {\n    const grd = g.createLinearGradient(0, 0, w, h)\n    grd.addColorStop(0, '#d6dadd')\n    grd.addColorStop(1, '#b9bfc4')\n    g.fillStyle = grd\n    g.fillRect(0, 0, w, h)\n    g.strokeStyle = '#555b60'\n    g.lineWidth = 5\n    g.strokeRect(4, 4, w - 8, h - 8)\n    g.fillStyle = '#1b1e21'\n    g.textAlign = 'left'\n    g.font = 'bold 26px Arial, sans-serif'\n    g.fillText(title, 18, 40)\n    g.font = '20px Arial, sans-serif'\n    lines.forEach((t, i) => g.fillText(t, 18, 76 + i * 29))\n    for (const [x, y] of [[14, 14], [w - 14, 14], [14, h - 14], [w - 14, h - 14]]) bolt(g, x, y)\n  }, { metalness: 0.6, roughness: 0.35 })\n}\n\n/** MV metal-clad panel front (UV 0..1 over the whole door face): LV / relay, breaker and cable compartments with mimic. */\nexport function mvFrontMaterial(feeder: string, kv: number, incomer: boolean) {\n  return make(`mv-${feeder}-${kv}-${incomer}`, 256, 736, (g, w, h) => {\n    g.fillStyle = '#cfd2cd'\n    g.fillRect(0, 0, w, h)\n    seam(g, 6, 6, w - 12, 214)\n    seam(g, 6, 228, w - 12, 290)\n    seam(g, 6, 526, w - 12, h - 532)\n    label(g, 40, 18, 176, 26, feeder)\n    // busbar + breaker mimic\n    g.strokeStyle = '#b52a20'\n    g.lineWidth = 7\n    g.beginPath()\n    g.moveTo(0, 252)\n    g.lineTo(w, 252)\n    g.moveTo(128, 252)\n    g.lineTo(128, 300)\n    g.moveTo(128, 340)\n    g.lineTo(128, 500)\n    g.stroke()\n    g.strokeRect(108, 300, 40, 40)\n    if (incomer) {\n      g.beginPath()\n      g.arc(128, 470, 14, 0, Math.PI * 2)\n      g.arc(128, 448, 14, 0, Math.PI * 2)\n      g.stroke()\n    }\n    vents(g, 46, 560, 164, 9)\n    label(g, 46, 650, 164, 24, `DANGER ${kv} kV`, '#f4c414')\n    g.fillStyle = '#111'\n    g.beginPath()\n    g.moveTo(128, 684)\n    g.lineTo(112, 716)\n    g.lineTo(144, 716)\n    g.closePath()\n    g.fill()\n    g.fillStyle = '#f4c414'\n    g.beginPath()\n    g.moveTo(128, 692)\n    g.lineTo(119, 711)\n    g.lineTo(137, 711)\n    g.closePath()\n    g.fill()\n  }, { metalness: 0.2, roughness: 0.5 })\n}\n\n/** LV MCC front: tiers of withdrawable starter drawers with labels. */\nexport function mccFrontMaterial(seed: number, tiers: number) {\n  return make(`mcc-${seed}-${tiers}`, 256, 736, (g, w, h) => {\n    g.fillStyle = '#cfd2cd'\n    g.fillRect(0, 0, w, h)\n    vents(g, 40, 10, 176, 3)\n    const th = (h - 60) / tiers\n    for (let i = 0; i < tiers; i++) {\n      const y = 40 + i * th\n      seam(g, 8, y, w - 16, th - 6)\n      label(g, 120, y + 10, 120, Math.min(20, th * 0.22), `M-${(seed * 10 + i + 101).toString()}`)\n    }\n  }, { metalness: 0.2, roughness: 0.5 })\n}\n\nexport const liningMaterial = () =>\n  make('lining', 128, 256, (g, w, h) => {\n    speckle(g, w, h, '#e1e3df', 600, 'rgba(150,155,150,0.25)', 'rgba(255,255,255,0.3)', 11)\n    g.fillStyle = '#9da29f'\n    g.fillRect(0, 0, 2, h)\n  }, { repeat: true, roughness: 0.65 })\n\nexport const raisedFloorMaterial = () =>\n  make('raisedFloor', 256, 256, (g, w, h) => {\n    speckle(g, w, h, '#a9aeb0', 2600, 'rgba(70,75,80,0.35)', 'rgba(230,232,235,0.35)', 3)\n    g.strokeStyle = '#5c6266'\n    g.lineWidth = 4\n    g.strokeRect(2, 2, w - 4, h - 4)\n  }, { repeat: true, roughness: 0.55 })\n\nexport const ceilingMaterial = () =>\n  make('ceiling', 128, 128, (g, w, h) => {\n    speckle(g, w, h, '#eceeea', 1400, 'rgba(160,165,160,0.35)', 'rgba(255,255,255,0.4)', 13)\n    g.strokeStyle = '#b4b8b4'\n    g.lineWidth = 3\n    g.strokeRect(0, 0, w, h)\n  }, { repeat: true, roughness: 0.85 })\n\n/** GRP panel with a faint woven gel-coat texture. */\nexport const grpMaterial = (color = '#e9e4d4') =>\n  make(`grp-${color}`, 128, 128, (g, w, h) => speckle(g, w, h, color, 900, 'rgba(120,110,90,0.12)', 'rgba(255,255,255,0.25)', 21), { repeat: true, roughness: 0.4, metalness: 0 })\n\n/** Engraved legend plate (white on black or black on white). */\nexport const legendMaterial = (text: string, dark = false) =>\n  make(`legend-${text}-${dark}`, 256, 64, (g, w, h) => label(g, 0, 0, w, h, text, dark ? '#1b1d1f' : '#f2f2ee', dark ? '#f2f2ee' : '#111'), { roughness: 0.5 })\n\n/** Protection relay HMI: small LCD with text + LED column. */\nexport const relayFaceMaterial = () =>\n  make('relayFace', 128, 160, (g, w, h) => {\n    g.fillStyle = '#26292c'\n    g.fillRect(0, 0, w, h)\n    g.fillStyle = '#9ec7d8'\n    g.fillRect(14, 14, 80, 56)\n    g.fillStyle = '#1d3440'\n    g.font = 'bold 11px monospace'\n    for (const [i, t] of ['I 412 A', 'P 7.1 MW', 'CB CLOSED'].entries()) g.fillText(t, 18, 30 + i * 15)\n    const leds = ['#2fbf5f', '#d8a21a', '#c33', '#c33', '#555', '#555']\n    leds.forEach((c, i) => {\n      g.fillStyle = c\n      g.beginPath()\n      g.arc(110, 20 + i * 14, 4, 0, Math.PI * 2)\n      g.fill()\n    })\n    g.fillStyle = '#4a4e52'\n    for (let r = 0; r < 3; r++) for (let q = 0; q < 4; q++) g.fillRect(16 + q * 20, 86 + r * 22, 14, 14)\n    g.fillStyle = '#ddd'\n    g.font = '9px Arial'\n    g.fillText('SEL / ABB REF', 16, h - 6)\n  }, { emissive: 0.25, roughness: 0.3 })\n\n/** Emissive indicator lamp lenses (IEC 60073 colours). */\nexport const lamp = (c: 'red' | 'green' | 'amber' | 'white' | 'blue') => {\n  const hex = { red: '#ff3b2e', green: '#35e06a', amber: '#ffb31a', white: '#fff4dc', blue: '#4aa3ff' }[c]\n  return paint(hex, { emissive: hex, emissiveIntensity: 1.6, roughness: 0.25, metalness: 0, clearcoat: 0.8, weather: 0 })\n}\n\n/** Moving-iron ammeter face (square, 0..max A with 2x overload scale). */\nexport const ammeterMaterial = (max = 100) =>\n  make(`ammeter-${max}`, 128, 128, (g, w, h) => {\n    g.fillStyle = '#f4f4ef'\n    g.fillRect(0, 0, w, h)\n    g.strokeStyle = '#222'\n    g.lineWidth = 2\n    g.strokeRect(2, 2, w - 4, h - 4)\n    const cx = w / 2\n    const cy = h * 0.82\n    const R = h * 0.6\n    g.lineWidth = 3\n    g.beginPath()\n    g.arc(cx, cy, R, Math.PI * 1.25, Math.PI * 1.75)\n    g.stroke()\n    g.strokeStyle = '#c33'\n    g.beginPath()\n    g.arc(cx, cy, R, Math.PI * 1.62, Math.PI * 1.75)\n    g.stroke()\n    g.strokeStyle = '#222'\n    g.font = 'bold 11px Arial'\n    g.fillStyle = '#222'\n    g.textAlign = 'center'\n    for (let i = 0; i <= 4; i++) {\n      const a = Math.PI * (1.25 + (i / 4) * 0.5)\n      g.beginPath()\n      g.moveTo(cx + Math.cos(a) * R, cy + Math.sin(a) * R)\n      g.lineTo(cx + Math.cos(a) * (R - 9), cy + Math.sin(a) * (R - 9))\n      g.stroke()\n      g.fillText(String(Math.round((i * max * 2) / 4)), cx + Math.cos(a) * (R - 18), cy + Math.sin(a) * (R - 18))\n    }\n    g.lineWidth = 2\n    g.beginPath()\n    g.moveTo(cx, cy)\n    const a = Math.PI * 1.43\n    g.lineTo(cx + Math.cos(a) * (R + 4), cy + Math.sin(a) * (R + 4))\n    g.stroke()\n    g.font = 'bold 16px Arial'\n    g.fillText('A', cx, h * 0.62)\n  }, { roughness: 0.3 })\n"
    }
  ],
  "categories": [
    "electrical"
  ],
  "meta": {
    "export": "JunctionBox",
    "anchor": "base",
    "standards": [
      "IEC 60079-0 / -7",
      "IEC 60079-14",
      "IEC 62444",
      "IEC 60529 (IP66)"
    ]
  }
}