{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "transformer",
  "type": "registry:component",
  "title": "Power transformer (oil-immersed)",
  "description": "Oil-immersed ONAN / ONAF power transformer on a bunded steel plinth with skids. Pressed-steel radiator banks with isolating valves and side-blow fans, conservator with magnetic level gauge, OLTC sight glass, Buchholz relay and silica-gel breathers, OLTC compartment with motor drive and protective relay, bushing CT boxes on outdoor turrets, cover access ladder from 10 MVA, pressure relief device, marshalling kiosk with OTI / WTI and a rating plate. Tank size follows the MVA rating; terminations are air-filled cable boxes or porcelain bushings on the cover.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "electrical/transformer.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/electrical/transformer.tsx",
      "content": "// Oil-immersed power transformer (ONAN / ONAF) on a bunded steel plinth: radiators, fans, bushings or cable boxes, conservator, Buchholz, OLTC. IEC 60076.\nimport * as THREE from 'three'\nimport { useEffect, useMemo } from 'react'\nimport { Kit, type MatKey, type Orient, type V3 } from '../lib/kit'\nimport { paint } from '../lib/materials'\nimport { KitMesh, type GroupProps } from '../lib/mesh'\nimport { gauge, grating, ladder, tagPlate } from '../lib/parts'\nimport { Rotors, fanRotor, type RotorSpec } from '../lib/rotors'\nimport { fanGuardMaterial, signMaterial } from '../lib/textures'\nimport { ratingPlateMaterial } from './_textures'\n\nexport interface TransformerProps {\n  /** rated power, MVA (sets tank, radiators, conservator) */\n  mva?: number\n  /** HV / LV line voltage, kV */\n  hvKv?: number\n  lvKv?: number\n  cooling?: 'ONAN' | 'ONAF'\n  /** cable boxes on the tank ends, or porcelain bushings on the cover */\n  terminations?: 'cable-box' | 'outdoor'\n  /** on-load tap changer compartment and motor drive */\n  oltc?: boolean\n  tag?: string\n  /** spin the ONAF fans */\n  running?: boolean\n}\n\nexport const defaults = {\n  mva: 20,\n  hvKv: 33,\n  lvKv: 11,\n  cooling: 'ONAF' as 'ONAN' | 'ONAF',\n  terminations: 'cable-box' as 'cable-box' | 'outdoor',\n  oltc: true,\n  tag: 'TR-101',\n  running: true,\n}\n\ntype P = Omit<typeof defaults, 'running'>\n\nconst clamp = (v: number, a: number, b: number) => Math.min(b, Math.max(a, v))\nconst faceQ = (n: V3) => new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 0, 1), new THREE.Vector3(...n).normalize())\n\n/** Tank length (X), height, width (Z) from the rating. */\nfunction tankSize(mva: number): V3 {\n  const m = clamp(mva, 0.5, 100)\n  return [1.1 + 0.63 * Math.sqrt(m), 1.1 + 0.5 * Math.pow(m, 0.42), 0.75 + 0.3 * Math.pow(m, 0.42)] // typical\n}\n\nexport function Transformer({ running = true, ...rest }: TransformerProps & GroupProps) {\n  const { mva, hvKv, lvKv, cooling, terminations, oltc, tag, ...group } = { ...defaults, ...rest }\n  const { built, rotors } = useMemo(() => build({ mva, hvKv, lvKv, cooling, terminations, oltc, tag }), [mva, hvKv, lvKv, cooling, terminations, oltc, tag])\n  useEffect(() => () => [built, ...new Set(rotors.map((r) => r.built))].forEach((b) => b.geometry.dispose()), [built, rotors])\n  return (\n    <group {...group}>\n      <KitMesh data={built} />\n      <Rotors rotors={rotors} running={running} />\n    </group>\n  )\n}\n\nexport default Transformer\n\nfunction annulus(k: Kit, mat: MatKey, rIn: number, rOut: number, y0: number, y1: number, pos: V3, axis: Orient = 'y', seg = 32) {\n  k.lathe(mat, [[rIn, y0], [rOut, y0], [rOut, y1], [rIn, y1], [rIn, y0]], pos, axis, seg)\n}\n\n/** Porcelain bushing with alternating sheds, flange at pos, along axis for L. */\nfunction bushing(k: Kit, porcelain: MatKey, pos: V3, axis: 'y' | 'x' | '-x', L: number, r: number) {\n  const prof: [number, number][] = [[0, 0], [r * 1.6, 0], [r * 1.6, 0.03], [r, 0.03]]\n  const n = Math.max(3, Math.round((L * 0.78) / (r * 0.75)))\n  const pitch = (L * 0.8) / n\n  let y = 0.03\n  for (let i = 0; i < n; i++) {\n    const big = i % 2 === 0 ? 1.85 : 1.55\n    prof.push([r * big, y + pitch * 0.3], [r * (big - 0.05), y + pitch * 0.45], [r, y + pitch * 0.62])\n    y += pitch\n    prof.push([r, y])\n  }\n  prof.push([r * 0.9, L * 0.9], [r * 0.5, L * 0.9], [r * 0.5, L], [0, L])\n  k.lathe(porcelain, prof, pos, axis, 24)\n  k.cyl('flangeSteel', r * 2.1, 0.03, pos, axis, 24)\n  k.boltCircle('boltXylan', 'nut', 8, r * 1.85, pos, axis, Math.max(0.01, r * 0.16), 0.05, [0.02], Math.PI / 8)\n}\n\n/** Wafer radiator valve with lever. */\nfunction radValve(k: Kit, pos: V3, r: number) {\n  k.cyl('valveBody', r * 1.7, 0.05, pos, 'z', 20)\n  k.box('handwheel', [0.015, 0.1, 0.015], [pos[0], pos[1] + r * 1.7 + 0.05, pos[2]])\n  k.box('handwheel', [0.12, 0.012, 0.02], [pos[0] + 0.05, pos[1] + r * 1.7 + 0.1, pos[2]])\n}\n\nfunction build(p: P) {\n  const k = new Kit()\n  const [L, H, W] = tankSize(p.mva)\n  const onaf = p.cooling === 'ONAF'\n  const outdoor = p.terminations === 'outdoor'\n  const sealed = p.mva < 2.5 // small units are hermetically sealed, no conservator\n  const tank = paint('#9aa39c', { metalness: 0.3, roughness: 0.5, clearcoat: 0.3, clearcoatRoughness: 0.45 })\n  const fit = paint('#9aa39c', { metalness: 0.3, roughness: 0.5, clearcoat: 0, weather: 0.6 })\n  const porcelain = paint('#6e3420', { metalness: 0.05, roughness: 0.12, clearcoat: 0.9, clearcoatRoughness: 0.1, weather: 0 })\n  const silica = paint('#d9892b', { metalness: 0, roughness: 0.7, clearcoat: 0, weather: 0 })\n  const bundM = paint('#7f878b', { metalness: 0.3, roughness: 0.6, clearcoat: 0, weather: 1.2 })\n  const cu: MatKey = 'copper'\n\n  // bund: steel drip tray, beams under the skids, grating over the sump, drain valve\n  const Lb = L + 1.0\n  const Wb = W + 1.2\n  const hb = 0.3\n  k.box(bundM, [Lb, 0.02, Wb], [0, 0.01, 0])\n  for (const s of [-1, 1]) {\n    k.box(bundM, [Lb, hb, 0.012], [0, hb / 2, s * (Wb / 2 - 0.006)])\n    k.box(bundM, [0.012, hb, Wb], [s * (Lb / 2 - 0.006), hb / 2, 0])\n    k.box(bundM, [Lb, 0.012, 0.06], [0, hb, s * (Wb / 2 - 0.03)])\n    k.box(bundM, [0.06, 0.012, Wb], [s * (Lb / 2 - 0.03), hb, 0])\n    const n = Math.round(Lb / 0.8)\n    for (let i = 1; i < n; i++) k.box(bundM, [0.01, hb - 0.03, 0.06], [-Lb / 2 + (i * Lb) / n, hb / 2, s * (Wb / 2 + 0.03)])\n  }\n  const zs = Math.min(W * 0.3, W / 2 - 0.15)\n  for (const s of [-1, 1]) k.box(bundM, [L + 0.3, hb - 0.05, 0.2], [0, (hb - 0.05) / 2 + 0.02, s * zs])\n  grating(k, [0, hb - 0.03, 0], Lb - 0.03, Wb - 0.03, bundM)\n  const xd = Lb / 2 - 0.3\n  k.cyl(bundM, 0.03, 0.06, [xd, 0.06, -Wb / 2 - 0.02], 'z', 12)\n  k.box('valveBody', [0.08, 0.08, 0.06], [xd, 0.06, -Wb / 2 - 0.045])\n  k.cyl('handwheel', 0.006, 0.08, [xd, 0.14, -Wb / 2 - 0.045], 'y', 6)\n  k.handwheel('handwheel', 0.08, [xd, 0.18, -Wb / 2 - 0.045], 'y')\n\n  // skids (haul holes), jacking pads, tank, cover with bolted rim, lifting lugs\n  const yS = hb\n  const hSk = 0.16\n  const y0 = yS + hSk\n  const y1 = y0 + H\n  for (const s of [-1, 1]) {\n    const z = s * zs\n    k.box('equipmentPaintDark', [L + 0.2, 0.014, 0.16], [0, yS + 0.007, z])\n    k.box('equipmentPaintDark', [L + 0.2, 0.014, 0.16], [0, y0 - 0.007, z])\n    k.box('equipmentPaintDark', [L + 0.2, hSk - 0.028, 0.014], [0, yS + hSk / 2, z])\n    for (const sx of [-1, 1]) k.torus('nut', 0.04, 0.012, [sx * (L / 2 + 0.06), yS + hSk / 2, z], 'z', 6, 14)\n  }\n  for (const sx of [-1, 1])\n    for (const sz of [-1, 1]) {\n      k.box(fit, [0.22, 0.05, 0.2], [sx * (L / 2 + 0.08), y0 + 0.25, sz * (W / 2 - 0.15)])\n      for (const dz of [-0.08, 0.08]) k.box(fit, [0.18, 0.15, 0.012], [sx * (L / 2 + 0.07), y0 + 0.16, sz * (W / 2 - 0.15) + dz])\n    }\n  k.rbox(tank, [L, H, W], 0.05, [0, y0 + H / 2, 0])\n  k.box(fit, [L + 0.04, 0.06, W + 0.04], [0, y0 + 0.03, 0])\n  k.box(fit, [L + 0.1, 0.04, W + 0.1], [0, y1 - 0.02, 0])\n  k.box(tank, [L + 0.06, 0.03, W + 0.06], [0, y1 + 0.015, 0])\n  k.box(tank, [L - 0.1, 0.02, W - 0.1], [0, y1 + 0.04, 0])\n  {\n    const nx = Math.max(4, Math.round((L + 0.04) / 0.16))\n    const nz = Math.max(3, Math.round((W + 0.04) / 0.16))\n    for (let i = 0; i <= nx; i++) for (const s of [-1, 1]) k.hex('nut', 0.03, 0.02, [-L / 2 - 0.02 + (i * (L + 0.04)) / nx, y1 + 0.04, s * (W / 2 + 0.03)])\n    for (let i = 1; i < nz; i++) for (const s of [-1, 1]) k.hex('nut', 0.03, 0.02, [s * (L / 2 + 0.03), y1 + 0.04, -W / 2 - 0.02 + (i * (W + 0.04)) / nz])\n  }\n  for (const sx of [-1, 1])\n    for (const sz of [-1, 1]) {\n      const lp: V3 = [sx * (L / 2 + 0.05), y1 - 0.12, sz * (W / 2 - 0.12)]\n      k.box(fit, [0.1, 0.22, 0.03], lp)\n      k.torus('nut', 0.035, 0.008, [lp[0], lp[1] + 0.05, lp[2]], 'z', 6, 14)\n    }\n\n  // radiator banks on both long sides (OLTC end of +Z kept clear), ONAF side-blow fans\n  const dp = clamp(0.25 + p.mva * 0.01, 0.28, onaf ? 0.38 : 0.52)\n  const hp = H * 0.72\n  const yRb = y0 + H * 0.12\n  const yRt = yRb + hp\n  const oltcW = clamp(L * 0.22, 0.4, 0.75)\n  const oltcX0 = L / 2 - 0.1 - oltcW\n  const rotors: RotorSpec[] = []\n  const fans = new Map<string, RotorSpec>()\n  for (const s of [-1, 1]) {\n    const xa = -L / 2 + 0.22\n    const xb = s > 0 && p.oltc ? oltcX0 - 0.1 : L / 2 - 0.22\n    const span = xb - xa\n    if (span < 0.4) continue\n    const nB = Math.max(1, Math.round(span / 1.4))\n    const bl = span / nB\n    for (let b = 0; b < nB; b++) {\n      const x0 = xa + b * bl + 0.06\n      const x1 = x0 + bl - 0.12\n      const xc = (x0 + x1) / 2\n      const zH = s * (W / 2 + 0.16)\n      for (const y of [yRt, yRb]) {\n        k.rod(fit, [xc, y, s * (W / 2 - 0.01)], [xc, y, zH], 0.045, 16)\n        radValve(k, [xc, y, s * (W / 2 + 0.06)], 0.045)\n        k.rod(fit, [x0, y, zH], [x1, y, zH], 0.04, 14)\n        k.dome(fit, 0.04, [x1, y, zH], 'x', Math.PI / 2, 1, 12)\n        k.dome(fit, 0.04, [x0, y, zH], '-x', Math.PI / 2, 1, 12)\n      }\n      k.hex('brass', 0.03, 0.02, [x1 - 0.03, yRt + 0.045, zH])\n      k.hex('brass', 0.03, 0.02, [x1 - 0.03, yRb - 0.045, zH])\n      const np = Math.max(3, Math.floor((x1 - x0) / 0.05))\n      for (let i = 0; i < np; i++) {\n        const x = x0 + (i + 0.5) * ((x1 - x0) / np)\n        k.box(fit, [0.012, hp + 0.06, dp], [x, (yRt + yRb) / 2, zH + s * (dp / 2 - 0.02)])\n        k.box(fit, [0.02, hp * 0.9, 0.012], [x, (yRt + yRb) / 2, zH + s * (dp - 0.02)])\n      }\n      for (const y of [yRt - 0.02, yRb + 0.02]) k.box(fit, [x1 - x0, 0.02, 0.012], [xc, y, zH + s * (dp - 0.01)])\n      k.rod('equipmentPaintDark', [xc, yRt + 0.08, (s * W) / 2], [xc, yRt + 0.08, zH + s * (dp - 0.02)], 0.012, 6)\n      if (!onaf) continue\n      const nf = Math.max(1, Math.round((x1 - x0) / 1.1))\n      const rf = Math.max(0.12, Math.min(0.3, (x1 - x0) / nf / 2 - 0.05, hp * 0.22))\n      for (let f = 0; f < nf; f++) {\n        const xf = x0 + ((f + 0.5) * (x1 - x0)) / nf\n        const yf = yRb + hp * 0.35\n        const zf = zH + s * (dp + 0.1)\n        k.cyl('equipmentPaintDark', rf + 0.02, 0.12, [xf, yf, zf], 'z', 32, undefined, true)\n        k.disc(fanGuardMaterial(), rf + 0.015, [xf, yf, zf + s * 0.061], s > 0 ? 'z' : '-z', 32)\n        k.cyl('motorPaint', rf * 0.3, 0.12, [xf, yf, zf + s * 0.02], 'z', 16)\n        // support brackets back to the radiator edge\n        for (const dy of [-1, 1]) k.box('equipmentPaintDark', [0.03, 0.03, 0.16], [xf, yf + dy * (rf + 0.03), zf - s * 0.09])\n        k.box('equipmentPaintDark', [0.1, 0.08, 0.06], [xf + rf + 0.08, yf - rf * 0.6, zf])\n        const key = rf.toFixed(3)\n        if (!fans.has(key)) fans.set(key, fanRotor('stainless', rf * 0.94, [0, 0, 0], 'z', 205, { blades: 5, chord: rf * 0.42, pitch: 0.34, hub: rf * 0.2, hubLen: 0.03 }))\n        rotors.push({ ...fans.get(key)!, pos: [xf, yf, zf - s * 0.045] })\n      }\n    }\n  }\n\n  // OLTC compartment, diverter head, motor-drive cabinet with drive shaft (+Z side, +X end)\n  const xo = (oltcX0 + L / 2 - 0.1) / 2\n  if (p.oltc) {\n    const wo = L / 2 - 0.1 - oltcX0\n    const dO = 0.32\n    const yo0 = y0 + H * 0.3\n    const yo1 = y1 - 0.06\n    k.rbox(tank, [wo, yo1 - yo0, dO], 0.03, [xo, (yo0 + yo1) / 2, W / 2 + dO / 2 - 0.01])\n    k.box(fit, [wo + 0.05, 0.03, dO + 0.04], [xo, yo1 + 0.015, W / 2 + dO / 2])\n    for (let i = 0; i < 5; i++) k.hex('nut', 0.026, 0.018, [xo - wo / 2 + (i + 0.5) * (wo / 5), yo1 + 0.04, W / 2 + dO + 0.005])\n    k.cyl(fit, 0.13, 0.08, [xo, yo1 + 0.07, W / 2 + dO / 2], 'y', 24)\n    const mw = Math.min(0.55, wo)\n    const md: V3 = [xo, y0 + 0.55, W / 2 + dO + 0.16]\n    k.rbox(fit, [mw, 0.7, 0.3], 0.02, md)\n    k.box('glass', [0.16, 0.12, 0.004], [md[0], md[1] + 0.12, md[2] + 0.152])\n    k.box('stainless', [0.02, 0.1, 0.03], [md[0] + mw / 2 - 0.06, md[1], md[2] + 0.16])\n    k.box('equipmentPaintDark', [0.06, 0.5, 0.17], [md[0], md[1], W / 2 + dO / 2 + 0.08])\n    k.rod('stainless', [xo, md[1] + 0.35, md[2] - 0.08], [xo, yo0 - 0.06, md[2] - 0.08], 0.012, 8)\n    k.rbox(fit, [0.1, 0.1, 0.1], 0.015, [xo, yo0 - 0.05, md[2] - 0.08])\n    k.cyl('brass', 0.02, 0.05, [xo + 0.12, md[1] - 0.37, md[2]], 'y', 8)\n    k.spline('cable', [[xo + 0.12, md[1] - 0.4, md[2]], [xo + 0.14, yS + 0.1, md[2] + 0.05], [xo + 0.16, hb - 0.02, md[2] + 0.06]], 0.012, 6, 8)\n    tagPlate(k, 'OLTC', [xo, (yo0 + yo1) / 2 + 0.1, W / 2 + dO + 0.002], 'z', 0.14)\n  }\n\n  // terminations\n  const rHV = clamp(0.03 + p.hvKv * 0.0009, 0.035, 0.09) * clamp(W / 1.6, 0.7, 1)\n  const rLV = clamp(0.03 + p.lvKv * 0.0012, 0.035, 0.07) * clamp(W / 1.6, 0.7, 1)\n  let xHv = L / 2\n  let xLv = -L / 2\n  let hvBottom = y1 - 0.3\n  if (outdoor) {\n    xHv = L / 2 - 0.3 - rHV * 1.85\n    xLv = -L / 2 + 0.3 + rLV * 1.85\n    const LH = 0.35 + p.hvKv * 0.014 // typical creepage-driven length\n    const LL = 0.25 + p.lvKv * 0.012\n    const row = (x: number, n: number, Lb: number, r: number, neutral: boolean) => {\n      const pitch = (W - 0.4) / (n - 1)\n      for (let i = 0; i < n; i++) {\n        const z = -W / 2 + 0.2 + i * pitch\n        const yt = y1 + 0.05\n        k.cyl(fit, r * 2.1, 0.12, [x, yt + 0.06, z], 'y', 24) // turret\n        // bushing CT terminal box on the turret\n        k.rbox(fit, [0.1, 0.08, 0.06], 0.01, [x + (x > 0 ? 1 : -1) * (r * 2.1 + 0.04), yt + 0.06, z])\n        k.rod('tubing', [x + (x > 0 ? 1 : -1) * (r * 2.1 + 0.04), yt + 0.02, z], [x + (x > 0 ? 1 : -1) * (r * 2.1 + 0.04), y1 + 0.02, z], 0.006, 6)\n        bushing(k, porcelain, [x, yt + 0.12, z], 'y', Lb, r)\n        k.cyl('aluminium', r * 0.35, 0.12, [x, yt + 0.12 + Lb + 0.06, z], 'y', 10)\n        k.box('aluminium', [0.08, 0.03, 0.05], [x, yt + 0.12 + Lb + 0.1, z])\n        if (neutral && i === n - 1) k.tube(cu, [[x, yt + 0.12 + Lb + 0.1, z], [x, yt + 0.12 + Lb + 0.2, z], [-L / 2 - 0.05, yt + 0.12 + Lb + 0.2, z], [-L / 2 - 0.05, y0 + 0.12, z]], 0.008, 6, 0.05)\n      }\n    }\n    row(xHv, 3, LH, rHV, false)\n    row(xLv, 4, LL, rLV, true)\n  } else {\n    const cableBox = (end: 1 | -1, kv: number, nPh: number, perPh: number, r: number) => {\n      const depth = clamp(0.32 + kv * 0.006, 0.35, 0.5)\n      const wBox = clamp(0.5 + kv * 0.018 + (nPh - 3) * 0.12, 0.5, W - 0.2)\n      const hBox = clamp(0.55 + kv * 0.012, 0.55, H * 0.45)\n      const yc = y1 - 0.1 - hBox / 2\n      const xw = (end * L) / 2\n      const xc = xw + (end * depth) / 2\n      k.rbox(tank, [depth, hBox, wBox], 0.025, [xc, yc, 0])\n      k.box(fit, [0.02, hBox + 0.04, wBox + 0.04], [xw + end * (depth + 0.005), yc, 0])\n      const nb = Math.max(3, Math.round(wBox / 0.15))\n      for (let i = 0; i <= nb; i++) for (const sy of [-1, 1]) k.hex('nut', 0.024, 0.014, [xw + end * (depth + 0.018), yc + sy * (hBox / 2 + 0.008), -wBox / 2 + (i * wBox) / nb], 'x')\n      k.box('equipmentPaintDark', [depth * 0.8, 0.02, wBox * 0.85], [xc, yc - hBox / 2 - 0.01, 0])\n      k.plane(signMaterial('hv'), 0.24, 0.12, [xw + end * (depth + 0.017), yc + hBox * 0.2, 0], faceQ([end, 0, 0]))\n      for (let i = 0; i < nPh; i++) {\n        const z = -wBox / 2 + ((i + 0.5) * wBox) / nPh\n        bushing(k, porcelain, [xw, yc + hBox * 0.12, z], end > 0 ? 'x' : '-x', Math.min(depth - 0.06, 0.18 + kv * 0.008), r)\n        for (let c = 0; c < perPh; c++) {\n          const zc = z + (c - (perPh - 1) / 2) * 0.07\n          const xg = xw + end * (depth * 0.55)\n          k.cyl('brass', 0.03, 0.05, [xg, yc - hBox / 2 - 0.035, zc], 'y', 12)\n          k.spline('cable', [[xg, yc - hBox / 2 - 0.06, zc], [xg + end * 0.04, (yc - hBox / 2 + yS) / 2, zc], [xg + end * 0.12, yS + 0.08, zc * 1.1], [xg + end * 0.16, hb + 0.03, zc * 1.1]], 0.025, 8, 24)\n        }\n      }\n      return { yc, hBox, wBox }\n    }\n    const hv = cableBox(1, p.hvKv, 3, 1, rHV)\n    hvBottom = hv.yc - hv.hBox / 2\n    const lv = cableBox(-1, p.lvKv, 4, clamp(Math.ceil(p.mva / (p.lvKv * 1.2)), 1, 3), rLV)\n    // LV neutral earth strap down the end wall\n    k.box(cu, [0.006, lv.yc - lv.hBox / 2 - y0 - 0.1, 0.04], [-L / 2 - 0.02, (lv.yc - lv.hBox / 2 + y0 + 0.1) / 2, -lv.wBox / 2 + 0.05])\n  }\n\n  // earth bar + bosses, drain / filter valves, sampling cock\n  k.box(cu, [0.3, 0.05, 0.008], [-L / 2 + 0.35, y0 + 0.12, -W / 2 - 0.02])\n  for (const dx of [-0.1, 0, 0.1]) k.hex('nut', 0.02, 0.012, [-L / 2 + 0.35 + dx, y0 + 0.12, -W / 2 - 0.028], 'z')\n  for (const e of [[-L / 2 + 0.35, -W / 2 - 0.02], [-L / 2 + 0.25, W / 2 + 0.02]] as [number, number][]) {\n    k.cyl('brass', 0.025, 0.04, [e[0], y0 + 0.06, e[1]], 'z', 10)\n    k.spline(cu, [[e[0], y0 + 0.06, e[1]], [e[0], yS + 0.04, e[1] * 1.05], [e[0] + 0.1, hb + 0.01, e[1] * 1.05]], 0.01, 6, 10)\n  }\n  const valveAt = (pos: V3) => {\n    k.rod(fit, [pos[0], pos[1], pos[2] + 0.02], [pos[0], pos[1], pos[2] - 0.1], 0.03, 12)\n    k.box('valveBody', [0.09, 0.09, 0.1], [pos[0], pos[1], pos[2] - 0.15])\n    k.cyl('handwheel', 0.006, 0.1, [pos[0], pos[1] + 0.09, pos[2] - 0.15], 'y', 6)\n    k.handwheel('handwheel', 0.12, [pos[0], pos[1] + 0.14, pos[2] - 0.15], 'y')\n    k.cyl('flangeSteel', 0.06, 0.02, [pos[0], pos[1], pos[2] - 0.21], 'z', 18)\n  }\n  valveAt([L / 2 - 0.25, y0 + 0.14, -W / 2])\n  valveAt([-L / 2 + 0.7, y1 - 0.15, -W / 2])\n  k.cyl('brass', 0.012, 0.06, [L / 2 - 0.45, y0 + 0.14, -W / 2 - 0.03], 'z', 8)\n\n  // wall stiffeners\n  for (const s of [-1, 1]) for (const z of [-W / 2 + 0.12, W / 2 - 0.12]) k.box(fit, [0.06, H * 0.82, 0.08], [s * (L / 2 + 0.03), y0 + H * 0.45, z])\n  {\n    const n = Math.max(2, Math.round(L / 0.9))\n    for (let i = 0; i <= n; i++) {\n      const x = -L / 2 + 0.1 + (i * (L - 0.2)) / n\n      for (const s of [-1, 1]) {\n        if (s > 0 && p.oltc && x > oltcX0 - 0.05) continue\n        k.box(fit, [0.07, H * 0.08, 0.06], [x, y0 + 0.1 + H * 0.04, s * (W / 2 + 0.03)])\n        k.box(fit, [0.07, H * 0.12, 0.06], [x, y1 - 0.06 - H * 0.06, s * (W / 2 + 0.03)])\n      }\n    }\n  }\n\n  // conservator with Buchholz relay, breathers and OLTC protective relay (free-breathing units only)\n  const Dc = clamp(0.22 + 0.008 * p.mva, 0.25, Math.min(0.8, W * 0.45))\n  const c0 = outdoor ? xLv + rLV * 1.85 + 0.35 : -L / 2 + 0.3\n  const c1 = outdoor ? xHv - rHV * 1.85 - 0.75 : L / 2 - 0.8\n  const Lcn = clamp(c1 - c0, 0.6, 3.5)\n  const xC = c1 - c0 >= 0.6 ? (c0 + c1) / 2 : c0 + 0.3\n  const zC = -W / 2 + Math.max(Dc / 2, 0.2)\n  const yC = y1 + 0.45 + Dc / 2\n  const xa = Math.min(xC + Lcn / 2 + 0.4, L / 2 - 0.15)\n  if (!sealed) {\n    const xOl = xC - Lcn / 2 + Lcn * 0.2\n    k.cyl(fit, Dc / 2, Lcn, [xC, yC, zC], 'x', 36)\n    for (const s of [-1, 1]) k.dome(fit, Dc / 2, [xC + (s * Lcn) / 2, yC, zC], s > 0 ? 'x' : '-x', Math.PI / 2, 0.35, 32)\n    annulus(k, fit, Dc / 2, Dc / 2 + 0.012, -0.015, 0.015, [xOl, yC, zC], 'x', 36)\n    for (const s of [-1, 1]) annulus(k, fit, Dc / 2, Dc / 2 + 0.015, -0.02, 0.02, [xC + s * (Lcn / 2 - 0.02), yC, zC], 'x', 36)\n    for (const x of [xC - Lcn * 0.33, xC + Lcn * 0.33]) {\n      for (const dz of [-Dc * 0.3, Dc * 0.3]) k.box('equipmentPaintDark', [0.08, yC - y1 - 0.02, 0.06], [x, (yC + y1) / 2, zC + dz])\n      k.torus('equipmentPaintDark', Dc / 2 + 0.01, 0.015, [x, yC, zC], 'x', 6, 32, Math.PI)\n      k.box('equipmentPaintDark', [0.1, 0.03, Dc * 0.9], [x, y1 + 0.06, zC])\n    }\n    gauge(k, [xC - Lcn / 2 - Dc * 0.18, yC, zC], '-x', Math.min(0.16, Dc * 0.4), 'bar', 10) // magnetic oil-level gauge\n    // OLTC compartment sight glass on the conservator end segment\n    if (p.oltc) {\n      k.cyl('brass', 0.05, 0.02, [xC - Lcn / 2 + Lcn * 0.1, yC, zC + Dc / 2 + 0.005], 'z', 18)\n      k.disc('glass', 0.038, [xC - Lcn / 2 + Lcn * 0.1, yC, zC + Dc / 2 + 0.016], 'z', 18)\n    }\n    k.cyl('brass', 0.04, 0.05, [xC + Lcn * 0.25, yC + Dc / 2 + 0.02, zC], 'y', 14)\n    // Buchholz pipe: riser from the cover, rising run with the relay between isolating valves\n    const xIn = xC + Lcn / 2 - Math.min(0.25, Lcn * 0.2)\n    const yR = y1 + 0.24\n    k.tube(fit, [[xa, y1 + 0.03, zC], [xa, yR, zC], [xIn, yR + 0.025, zC], [xIn, yC - Dc / 2 + 0.02, zC]], 0.04, 14, 0.08)\n    const xr = (xa + xIn) / 2\n    const yr = yR + 0.012\n    k.rbox('castIron', [0.2, 0.18, 0.16], 0.02, [xr, yr, zC])\n    for (const s of [-1, 1]) {\n      k.cyl('castIron', 0.075, 0.02, [xr + s * 0.11, yr, zC], 'x', 20)\n      k.disc('glass', 0.045, [xr, yr, zC + s * 0.081], s > 0 ? 'z' : '-z', 20)\n    }\n    k.rbox('castIron', [0.12, 0.08, 0.1], 0.015, [xr, yr + 0.13, zC])\n    k.cyl('brass', 0.008, 0.05, [xr - 0.06, yr + 0.12, zC + 0.04], 'y', 6)\n    for (const [xv, yv] of [[xa, y1 + 0.13], [xIn, yC - Dc / 2 - 0.12]]) {\n      k.box('valveBody', [0.11, 0.08, 0.11], [xv, yv, zC])\n      k.handwheel('handwheel', 0.1, [xv, yv, zC + 0.1], 'z')\n      k.rod('handwheel', [xv, yv, zC + 0.05], [xv, yv, zC + 0.1], 0.006, 6)\n    }\n    // OLTC pipe: up from the diverter head, outboard of the tank to the conservator end, across, up into its compartment\n    if (p.oltc) {\n      const zo = W / 2 + 0.16\n      const yTopO = y1 + 0.12\n      const xEnd = xOl - Lcn * 0.1\n      const yRun = yTopO + 0.18\n      k.tube(fit, [[xo, yTopO, zo], [xo, yRun, zo], [xEnd, yRun, zo], [xEnd, yRun, zC + Dc * 0.35], [xEnd, yC - Dc * 0.3, zC + Dc * 0.35]], 0.025, 10, 0.06)\n      k.rbox('castIron', [0.14, 0.12, 0.1], 0.015, [(xo + xEnd) / 2, yRun, zo])\n    }\n    // silica-gel breathers piped down the -X end wall\n    const breather = (xTop: number, zb: number, yb: number, sz: number) => {\n      const xw = -L / 2 - 0.1\n      k.tube('tubing', [[xTop, yC + Dc / 2 - 0.01, zC], [xTop, yC + Dc / 2 + 0.08, zC], [xw, yC + Dc / 2 + 0.08, zC], [xw, yC + Dc / 2 + 0.08, zb], [xw, yb + sz * 0.25, zb]], 0.016, 8, 0.05)\n      k.cyl(fit, sz * 0.32, 0.03, [xw, yb + sz * 0.22, zb], 'y', 20)\n      k.cyl('glass', sz * 0.27, sz * 0.4, [xw, yb, zb], 'y', 20)\n      k.cyl(silica, sz * 0.24, sz * 0.36, [xw, yb, zb], 'y', 16)\n      k.cyl(fit, sz * 0.32, 0.03, [xw, yb - sz * 0.22, zb], 'y', 20)\n      k.cyl('glass', sz * 0.2, sz * 0.14, [xw, yb - sz * 0.32, zb], 'y', 16)\n      k.box('equipmentPaintDark', [0.1, 0.06, 0.08], [xw + 0.06, yb + sz * 0.22, zb])\n    }\n    const yBr = y0 + Math.min(1.25, H * 0.5)\n    breather(xC - Lcn / 2 + Lcn * 0.45, -W / 2 + 0.12, yBr, 0.45)\n    if (p.oltc) breather(xOl - 0.15, -W / 2 + 0.32, yBr + 0.1, 0.3)\n  }\n\n  // pressure relief device with directed-discharge hood piped to the bund\n  {\n    const s = clamp(W / 1.8, 0.6, 1)\n    const pr: V3 = [sealed ? 0 : xa, y1 + 0.05, W / 2 - 0.3 * s]\n    annulus(k, fit, 0, 0.14 * s, pr[1], pr[1] + 0.03, [pr[0], 0, pr[2]], 'y', 28)\n    k.cyl('castIron', 0.11 * s, 0.1, [pr[0], pr[1] + 0.08, pr[2]], 'y', 24)\n    k.dome('castIron', 0.11 * s, [pr[0], pr[1] + 0.13, pr[2]], 'y', Math.PI / 2, 0.35, 24)\n    k.cyl('actuatorYellow', 0.012, 0.08, [pr[0], pr[1] + 0.2, pr[2]], 'y', 8)\n    k.cyl(fit, 0.17 * s, 0.22, [pr[0], pr[1] + 0.11, pr[2]], 'y', 28, undefined, true)\n    const xOut = L / 2 + 0.12\n    k.tube(fit, [[pr[0] + 0.15 * s, pr[1] + 0.08, pr[2]], [xOut, pr[1] + 0.08, pr[2]], [xOut, pr[1] + 0.08, W / 2 + 0.07], [xOut, hb + 0.15, W / 2 + 0.07]], 0.05 * s, 12, 0.1)\n  }\n\n  // marshalling kiosk with OTI / WTI on the +X end, capillaries to cover pockets, rating plate\n  {\n    const xw = L / 2\n    const hK = clamp(hvBottom - (y0 + 0.2) - 0.08, 0.25, 0.6)\n    const yK = y0 + 0.2 + hK / 2\n    const zK = W * 0.25\n    const wK = Math.min(0.38, W * 0.4)\n    const xK = xw + 0.1\n    k.rbox(fit, [0.2, hK, wK], 0.015, [xK, yK, zK])\n    k.box('stainless', [0.03, 0.08, 0.02], [xK + 0.11, yK, zK + wK / 2 - 0.04])\n    const d = Math.min(0.11, hK * 0.4, wK * 0.4)\n    gauge(k, [xK + 0.105, yK + hK * 0.18, zK - wK * 0.22], 'x', d, '°C', 150)\n    gauge(k, [xK + 0.105, yK + hK * 0.18, zK + wK * 0.18], 'x', d, '°C', 150)\n    for (const dz of [-wK * 0.22, wK * 0.18]) {\n      k.tube('tubing', [[xK + 0.05, yK + hK / 2, zK + dz], [xK + 0.05, y1 + 0.02, zK + dz], [xw - 0.12, y1 + 0.06, zK + dz]], 0.004, 6, 0.04)\n      k.cyl('brass', 0.02, 0.06, [xw - 0.12, y1 + 0.08, zK + dz], 'y', 10)\n    }\n    k.cyl('brass', 0.02, 0.05, [xK, yK - hK / 2 - 0.03, zK], 'y', 8)\n    k.spline('cable', [[xK, yK - hK / 2 - 0.05, zK], [xK + 0.04, yS + 0.08, zK + 0.05], [xK + 0.08, hb + 0.02, zK + 0.06]], 0.012, 6, 12)\n    const plate = ratingPlateMaterial('POWER TRANSFORMER', [\n      `Tag  ${p.tag}`,\n      `Rating  ${p.mva} MVA   ${onaf ? 'ONAN / ONAF' : 'ONAN'}`,\n      `Voltage  ${p.hvKv} / ${p.lvKv} kV`,\n      'Vector group  Dyn11   50 Hz',\n      p.oltc ? 'Tap changer  OLTC ±10 %, 17 pos' : 'Tap changer  off-circuit ±5 %',\n      'Oil  mineral, IEC 60296',\n    ])\n    const wp = Math.min(0.36, W * 0.36)\n    const zP = -W * 0.25\n    k.box('stainless', [0.004, wp * 0.67 + 0.01, wp + 0.01], [xw + 0.002, y0 + 0.2 + wp * 0.34, zP])\n    k.plane(plate, wp, wp * 0.667, [xw + 0.0045, y0 + 0.2 + wp * 0.34, zP], faceQ([1, 0, 0]))\n  }\n\n  // fixed ladder to the cover (Buchholz / breather access) on the -X end, larger units\n  if (p.mva >= 10) {\n    const zl = W / 2 - 0.3\n    ladder(k, [-L / 2 - 0.22, hb, zl], y1 + 0.05, [-1, 0, 0], { cage: false, extend: 1.0 })\n    for (const y of [y0 + 0.4, y1 - 0.25]) for (const s of [-1, 1]) k.box('safetyYellow', [0.22, 0.04, 0.04], [-L / 2 - 0.11, y, zl + s * 0.225])\n  }\n  tagPlate(k, p.tag, [0, y0 + H * 0.88, W / 2 + 0.003], 'z', 0.2)\n  tagPlate(k, p.tag, [0, y0 + H * 0.88, -W / 2 - 0.003], '-z', 0.2)\n\n  return { built: k.build(), rotors }\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": "Transformer",
    "anchor": "base",
    "standards": [
      "IEC 60076-1",
      "IEC 60076-2",
      "IEC 60137",
      "IEC 60214-1",
      "IEC 60296"
    ]
  }
}