{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "neutral-earthing-resistor",
  "type": "registry:component",
  "title": "Neutral earthing resistor",
  "description": "Outdoor neutral earthing resistor: stacks of stainless grid elements on post insulators with mica plates and series links, in a ventilated 316 stainless enclosure (louvred sides, roof with overhang) on galvanised legs. Neutral entry by a cable box with ring CT and gland or by a roof bushing; LV earth bushing to the earth bar, two enclosure earth bosses, rating plate with the resistance from kV and amps, HV warning and tag. Energy (kV, A, s) sets the number of stacks; `open` removes a side panel.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "electrical/neutral-earthing-resistor.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/electrical/neutral-earthing-resistor.tsx",
      "content": "// Neutral earthing resistor: stainless grid elements on insulators in a ventilated 316 enclosure on legs, HV entry bushing, CT cable box, earth bushing. IEEE 32, IEC 60529.\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 { tagPlate } from '../lib/parts'\nimport { signMaterial } from '../lib/textures'\nimport { cableGland, earthBoss, louvrePanel } from './_parts'\nimport { ratingPlateMaterial } from './_textures'\n\nexport interface NeutralEarthingResistorProps {\n  /** system line voltage, kV */\n  kv?: number\n  /** let-through current, A */\n  amps?: number\n  /** rated time, s */\n  seconds?: number\n  /** one side panel removed to show the resistor grids */\n  open?: boolean\n  /** neutral connection: cable box with ring CT, or roof bushing for an overhead / busbar connection */\n  entry?: 'cable' | 'bushing'\n  tag?: string\n}\n\nexport const defaults = {\n  kv: 11,\n  amps: 400,\n  seconds: 10,\n  open: false,\n  entry: 'cable' as 'cable' | 'bushing',\n  tag: 'NER-8201',\n}\n\nexport function NeutralEarthingResistor(props: NeutralEarthingResistorProps & GroupProps) {\n  const { kv, amps, seconds, open, entry, tag, ...group } = { ...defaults, ...props }\n  const data = useKit((k) => build(k, { kv, amps, seconds, open, entry, tag }), [kv, amps, seconds, open, entry, tag])\n  return (\n    <group {...group}>\n      <KitMesh data={data} />\n    </group>\n  )\n}\n\nexport default NeutralEarthingResistor\n\nfunction build(k: Kit, p: typeof defaults) {\n  const S: MatKey = 'stainless'\n  // energy sets the grid count (stacked frames of punched stainless grids)\n  const V = (p.kv * 1000) / Math.sqrt(3)\n  const MJ = (V * p.amps * p.seconds) / 1e6\n  const stacks = Math.max(1, Math.min(6, Math.ceil(MJ / 40))) // typical ~40 MJ per stack\n  const tiers = Math.max(2, Math.min(5, Math.ceil(p.kv / 6)))\n  const L = 0.4 + stacks * 0.55\n  const Wd = 0.9\n  const leg = 0.5\n  const Hb = 0.35 + tiers * 0.32\n  const y0 = leg\n  const y1 = leg + Hb\n  // legs and base channels\n  for (const sx of [-1, 1])\n    for (const sz of [-1, 1]) {\n      k.box('galvanised', [0.08, leg, 0.08], [sx * (L / 2 - 0.06), leg / 2, sz * (Wd / 2 - 0.06)])\n      k.box('galvanised', [0.2, 0.012, 0.2], [sx * (L / 2 - 0.06), 0.006, sz * (Wd / 2 - 0.06)])\n    }\n  k.box('galvanised', [L, 0.06, Wd], [0, y0 + 0.03, 0])\n  // enclosure frame (hollow): corner posts, roof with overhang, mesh / louvred side panels\n  for (const sx of [-1, 1]) for (const sz of [-1, 1]) k.box(S, [0.04, Hb, 0.04], [sx * (L / 2 - 0.02), y0 + Hb / 2, sz * (Wd / 2 - 0.02)])\n  k.box(S, [L + 0.12, 0.03, Wd + 0.12], [0, y1 + 0.015, 0])\n  k.box(S, [L + 0.12, 0.08, 0.01], [0, y1 - 0.01, Wd / 2 + 0.055])\n  const qz = new THREE.Quaternion()\n  const qzb = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI)\n  const qx = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2)\n  const qxb = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), -Math.PI / 2)\n  // louvred panels on both long sides (front one removable), plain end panels with the bushings\n  const sides: [V3, THREE.Quaternion, number][] = [[[0, y0 + Hb / 2, Wd / 2], qz, L - 0.08], [[0, y0 + Hb / 2, -Wd / 2], qzb, L - 0.08]]\n  for (const [i, [c, q, w]] of sides.entries()) {\n    if (i === 0 && p.open) continue\n    louvrePanel(k, w, Hb - 0.08, c, q, '#c3c7cb')\n    louvrePanel(k, w, Hb - 0.08, [c[0], c[1], c[2] * 0.98], q.clone().multiply(qzb), '#c3c7cb')\n  }\n  for (const s of [-1, 1]) k.box(S, [0.004, Hb - 0.04, Wd - 0.08], [s * (L / 2 - 0.002), y0 + Hb / 2, 0])\n  // grid stacks on post insulators, series links, tie rods\n  const porcelain = paint('#6e3420', { metalness: 0.05, roughness: 0.15, clearcoat: 0.8 })\n  const grid = paint('#9aa0a3', { metalness: 0.7, roughness: 0.5, weather: 0.3 })\n  for (let s = 0; s < stacks; s++) {\n    const x = -L / 2 + 0.2 + 0.275 + s * 0.55\n    for (const dz of [-0.25, 0.25]) for (const dx of [-0.18, 0.18]) k.cyl(porcelain, 0.035, 0.18, [x + dx, y0 + 0.15, dz], 'y', 12)\n    for (let t = 0; t < tiers; t++) {\n      const yt = y0 + 0.3 + t * 0.32\n      for (let g = 0; g < 7; g++) k.box(grid, [0.42, 0.012, 0.55], [x, yt + g * 0.035, 0])\n      for (const dz of [-0.3, 0.3]) for (const dx of [-0.2, 0.2]) k.rod(S, [x + dx, yt - 0.02, dz], [x + dx, yt + 0.25, dz], 0.008, 6)\n      k.box(paint('#d9d3c0', { roughness: 0.6 }), [0.46, 0.02, 0.6], [x, yt + 0.26, 0]) // mica insulating plate\n      k.box('copper', [0.03, 0.3, 0.01], [x + 0.22, yt + 0.12, 0.29])\n    }\n  }\n  // HV neutral entry: porcelain bushing on the roof, cable box with CT underneath on the +X end\n  const xb = L / 2 - 0.25\n  const LB = 0.2 + p.kv * 0.012\n  const prof: [number, number][] = [[0.06, 0], [0.04, 0]]\n  for (let i = 0; i < 4; i++) prof.push([0.04, i * (LB / 4) + 0.02], [0.075, i * (LB / 4) + 0.04], [0.04, i * (LB / 4) + 0.07])\n  prof.push([0.03, LB], [0, LB])\n  if (p.entry === 'bushing') {\n    k.cyl(S, 0.09, 0.02, [xb, y1 + 0.04, 0], 'y', 20)\n    k.boltCircle('boltXylan', 'nut', 6, 0.075, [xb, y1 + 0.03, 0], 'y', 0.01, 0.04, [0.025])\n    k.lathe(porcelain, prof, [xb, y1 + 0.05, 0], 'y', 20)\n    k.cyl('aluminium', 0.012, 0.08, [xb, y1 + 0.05 + LB + 0.04, 0], 'y', 8)\n    k.box('copper', [0.03, 0.12, 0.01], [xb, y1 - 0.08, 0])\n  } else {\n    const cb: V3 = [L / 2 + 0.2, y0 + Hb * 0.55, 0]\n    k.rbox(S, [0.4, 0.5, 0.5], 0.02, cb)\n    k.box('nut', [0.14, 0.04, 0.14], [cb[0], cb[1] + 0.05, 0]) // CT window (ring CT behind)\n    k.plane(signMaterial('hv'), 0.24, 0.12, [cb[0] + 0.201, cb[1] + 0.12, 0], qx)\n    const g = cableGland(k, [cb[0], cb[1] - 0.25, 0], '-y', 0.045)\n    k.spline('cable', [g, [g[0], 0.3, 0.05], [g[0] + 0.1, 0.05, 0.15], [g[0] + 0.6, 0.03, 0.2]], 0.024, 8, 20)\n  }\n  // LV earth bushing on the -X end to the earth bar, two enclosure earth bosses\n  k.cyl(porcelain, 0.03, 0.1, [-L / 2 - 0.05, y0 + 0.2, 0.2], 'x', 12)\n  k.tube('copper', [[-L / 2 - 0.1, y0 + 0.2, 0.2], [-L / 2 - 0.2, y0 + 0.2, 0.2], [-L / 2 - 0.2, 0.02, 0.2], [-L / 2 - 0.5, 0.02, 0.2]], 0.01, 6, 0.05)\n  earthBoss(k, [L / 2 - 0.06, leg * 0.6, Wd / 2 - 0.02], 'z')\n  earthBoss(k, [-L / 2 + 0.06, leg * 0.6, -Wd / 2 + 0.02], '-z')\n  // rating plate, warning, tag\n  k.plane(ratingPlateMaterial('NEUTRAL EARTHING RESISTOR', [`Tag  ${p.tag}`, `System  ${p.kv} kV`, `Current  ${p.amps} A for ${p.seconds} s`, `Resistance  ${(V / p.amps).toFixed(1)} Ω`, 'Element  AISI 304 grid', 'Enclosure  IP23, AISI 316']), 0.3, 0.2, [-L / 2 - 0.003, y0 + Hb * 0.55, -0.15], qxb)\n  k.plane(signMaterial('hv'), 0.3, 0.15, [-L / 2 - 0.003, y0 + Hb * 0.85, -0.15], qxb)\n  tagPlate(k, p.tag, [-L / 2 - 0.003, y0 + Hb * 0.3, -0.15], '-x', 0.16)\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": "NeutralEarthingResistor",
    "anchor": "base",
    "standards": [
      "IEEE 32",
      "IEC 60529 (IP23)",
      "ENA / utility NER specs"
    ]
  }
}