{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "local-control-station",
  "type": "registry:component",
  "title": "Local control station",
  "description": "Ex e push-button station for a motor or valve, on a galvanised UPN post with base plate and anchors. Start button and a shrouded lock-off stop with padlock, L-O-R selector, moving-iron ammeter with run lamp and a latching E-stop, each with an engraved legend. Rain hood, bottom cable gland with armoured cable cleated down the post, earth stud and tag plate. Rows appear only for the devices switched on.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "electrical/local-control-station.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/electrical/local-control-station.tsx",
      "content": "// Local control station: Ex e push-button enclosure on a post with start / stop, L-O-R selector, ammeter and E-stop. IEC 60079-7, IEC 60947-5-1, IEC 60204-1.\nimport * as THREE from 'three'\nimport { Kit, type MatKey, type V3 } from '../lib/kit'\nimport { KitMesh, useKit, type GroupProps } from '../lib/mesh'\nimport { member, tagPlate } from '../lib/parts'\nimport { cableGland, earthBoss } from './_parts'\nimport { ammeterMaterial, lamp, legendMaterial } from './_textures'\n\nexport interface LocalControlStationProps {\n  /** enclosure centre above deck, m */\n  height?: number\n  enclosure?: 'stainless' | 'grp'\n  ammeter?: boolean\n  selector?: boolean\n  estop?: boolean\n  /** motor full-load current shown on the ammeter scale, A */\n  amps?: number\n  tag?: string\n}\n\nexport const defaults = {\n  height: 1.3,\n  enclosure: 'stainless' as 'stainless' | 'grp',\n  ammeter: true,\n  selector: true,\n  estop: true,\n  amps: 100,\n  tag: 'HS-1501A',\n}\n\nexport function LocalControlStation(props: LocalControlStationProps & GroupProps) {\n  const { height, enclosure, ammeter, selector, estop, amps, tag, ...group } = { ...defaults, ...props }\n  const data = useKit((k) => build(k, { height, enclosure, ammeter, selector, estop, amps, tag }), [height, enclosure, ammeter, selector, estop, amps, tag])\n  return (\n    <group {...group}>\n      <KitMesh data={data} />\n    </group>\n  )\n}\n\nexport default LocalControlStation\n\nconst front = new THREE.Quaternion()\n\nfunction build(k: Kit, p: typeof defaults) {\n  const box: MatKey = p.enclosure === 'grp' ? 'plasticGrey' : 'stainless'\n  const rows = [p.ammeter && 'A', p.selector && 'S', 'B', p.estop && 'E'].filter(Boolean) as string[]\n  const W = 0.24\n  const H = 0.08 + rows.length * 0.115\n  const D = 0.14\n  const yc = Math.max(H / 2 + 0.5, p.height)\n  const zb = 0.025 // post front face\n  const zf = zb + 0.02 + D // enclosure front face\n\n  // post: UPN 100 on a base plate with four anchor bolts, cap plate\n  const top = yc + H / 2 + 0.12\n  member(k, [0, 0.015, -0.025], [0, top, -0.025], 'UPN 100', 'galvanised', [0, 0, 1])\n  k.box('galvanised', [0.25, 0.015, 0.25], [0, 0.0075, -0.025])\n  for (const sx of [-1, 1])\n    for (const sz of [-1, 1]) {\n      k.cyl('boltXylan', 0.008, 0.05, [sx * 0.09, 0.035, -0.025 + sz * 0.09], 'y', 8)\n      k.hex('nut', 0.026, 0.012, [sx * 0.09, 0.021, -0.025 + sz * 0.09])\n    }\n  k.box('galvanised', [0.12, 0.008, 0.07], [0, top + 0.004, -0.025])\n  // mounting plate and enclosure with a rain hood\n  k.box('galvanised', [W + 0.06, H + 0.1, 0.008], [0, yc, zb + 0.004])\n  k.rbox(box, [W, H, D], 0.012, [0, yc, zb + 0.008 + D / 2])\n  k.rbox(box, [W - 0.01, H - 0.01, 0.012], 0.006, [0, yc, zf + 0.004])\n  for (const sx of [-1, 1]) for (const sy of [-1, 1]) k.cyl('stainless', 0.008, 0.008, [sx * (W / 2 - 0.018), yc + sy * (H / 2 - 0.018), zf + 0.014], 'z', 10)\n  k.box(box, [W + 0.08, 0.006, D + 0.1], [0, yc + H / 2 + 0.04, zb + D / 2 + 0.05], new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), 0.18))\n  for (const s of [-1, 1]) k.box(box, [0.006, 0.05, D + 0.08], [s * (W / 2 + 0.03), yc + H / 2 + 0.02, zb + D / 2 + 0.04])\n\n  const z = zf + 0.01\n  let y = yc + H / 2 - 0.04 - 0.0575\n  for (const r of rows) {\n    if (r === 'A') {\n      k.box('rubberBlack', [0.1, 0.1, 0.02], [0, y + 0.005, z + 0.01])\n      k.plane(ammeterMaterial(p.amps), 0.088, 0.088, [0, y + 0.005, z + 0.0205], front)\n      k.box('glass', [0.09, 0.09, 0.004], [0, y + 0.005, z + 0.024])\n      k.cyl('nut', 0.012, 0.012, [0.085, y + 0.03, z + 0.006], 'z', 12)\n      k.cyl(lamp('green'), 0.009, 0.01, [0.085, y + 0.03, z + 0.016], 'z', 12)\n    } else if (r === 'S') {\n      k.cyl('nut', 0.024, 0.02, [0, y, z + 0.01], 'z', 16)\n      k.box('rubberBlack', [0.012, 0.045, 0.024], [0, y, z + 0.032], new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), 0.5))\n      for (const [t, dx] of [['L', -0.05], ['O', 0], ['R', 0.05]] as [string, number][]) k.plane(legendMaterial(t, true), 0.022, 0.014, [dx, y + 0.038, z + 0.001], front)\n      k.plane(legendMaterial('LOCAL - OFF - REMOTE'), 0.15, 0.018, [0, y - 0.042, z + 0.001], front)\n    } else if (r === 'B') {\n      for (const [dx, mat, txt] of [[-0.055, 'safetyGreen', 'START'], [0.055, 'actuatorRed', 'STOP']] as [number, MatKey, string][]) {\n        k.cyl('nut', 0.026, 0.015, [dx, y + 0.008, z + 0.0075], 'z', 18)\n        k.cyl(mat, 0.019, 0.018, [dx, y + 0.008, z + 0.024], 'z', 18)\n        if (txt === 'STOP') {\n          k.cyl('nut', 0.027, 0.02, [dx, y + 0.008, z + 0.016], 'z', 18, undefined, true) // shroud\n          // lock-off stop: padlock hasp with a padlock fitted\n          k.box('stainless', [0.012, 0.03, 0.025], [dx + 0.034, y + 0.008, z + 0.0125])\n          k.box('safetyYellow', [0.024, 0.026, 0.012], [dx + 0.034, y - 0.018, z + 0.03])\n          k.torus('stainless', 0.008, 0.002, [dx + 0.034, y - 0.002, z + 0.03], 'z', 4, 12, Math.PI)\n        }\n        k.plane(legendMaterial(txt), 0.07, 0.018, [dx, y - 0.04, z + 0.001], front)\n      }\n    } else {\n      k.cyl('actuatorYellow', 0.038, 0.006, [0, y, z + 0.003], 'z', 24)\n      k.cyl('actuatorRed', 0.013, 0.025, [0, y, z + 0.018], 'z', 12)\n      k.dome('actuatorRed', 0.026, [0, y, z + 0.03], 'z', Math.PI / 2, 0.55, 20)\n      k.plane(legendMaterial('EMERGENCY STOP', true), 0.12, 0.016, [0, y - 0.048, z + 0.001], front)\n    }\n    y -= 0.115\n  }\n  // cable gland underneath, armoured cable down the post, Ex label, earth stud, tag plate\n  const gz = zb + 0.008 + D / 2\n  const end = cableGland(k, [0, yc - H / 2, gz], '-y', 0.022)\n  k.spline('cable', [end, [0, end[1] - 0.15, gz - 0.03], [0, 0.4, 0.035], [0, 0.12, 0.04], [0.02, 0.02, 0.12]] as V3[], 0.011, 6, 30)\n  for (let yy = 0.3; yy < yc - H / 2 - 0.15; yy += 0.35) k.box('rubberBlack', [0.06, 0.012, 0.03], [0, yy, 0.04])\n  k.plane(legendMaterial('Ex eb IIC T6 Gb'), 0.08, 0.016, [0, yc - H / 2 + 0.018, zf + 0.011], front)\n  earthBoss(k, [W / 2, yc - H / 2 + 0.03, zb + 0.06], 'x')\n  // lid hinges, stainless breather / drain plug\n  for (const yy of [yc - H * 0.3, yc + H * 0.3]) k.cyl('stainless', 0.008, 0.05, [-W / 2 - 0.006, yy, zf - 0.005], 'y', 8)\n  k.cyl('stainless', 0.01, 0.02, [W * 0.3, yc - H / 2 - 0.01, zb + 0.008 + D * 0.7], 'y', 10)\n  tagPlate(k, p.tag, [0, yc + H / 2 + 0.11, zb + 0.0015], 'z', 0.14)\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": "LocalControlStation",
    "anchor": "base",
    "standards": [
      "IEC 60079-0 / -7",
      "IEC 60947-5-1",
      "IEC 60204-1",
      "ISO 13850"
    ]
  }
}