{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ups-battery-cabinet",
  "type": "registry:component",
  "title": "UPS and battery cabinets",
  "description": "Industrial UPS line-up on a base frame: rectifier / charger and inverter with static bypass (each with mimic panel, lamps and louvred doors), an AC distribution section with meters and MCB window, and hollow VRLA battery cabinets with shelves of 12 V blocks, inter-block links, string labels and a DC breaker. doorOpen swings the battery doors. Top gland plates, hinges, locks, earth boss and tag plate. kVA adds an inverter section above 120 kVA.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "electrical/ups-battery-cabinet.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/electrical/ups-battery-cabinet.tsx",
      "content": "// Industrial UPS line-up: rectifier / charger, inverter with static bypass, distribution and VRLA battery cabinets on a base frame. IEC 62040-1/3, IEC 62485-2.\nimport * as THREE from 'three'\nimport { Kit, type MatKey } from '../lib/kit'\nimport { paint } from '../lib/materials'\nimport { KitMesh, useKit, type GroupProps } from '../lib/mesh'\nimport { gauge, tagPlate } from '../lib/parts'\nimport { textMaterial } from '../lib/textures'\nimport { cableGland, earthBoss, louvrePanel } from './_parts'\nimport { lamp, legendMaterial, relayFaceMaterial } from './_textures'\n\nexport interface UpsBatteryCabinetProps {\n  /** UPS rating, kVA (sets the number of power sections) */\n  kva?: number\n  /** number of battery cabinets */\n  batteryCabinets?: number\n  /** battery cabinet doors open: shelves of VRLA blocks with links and the DC breaker */\n  doorOpen?: boolean\n  tag?: string\n}\n\nexport const defaults = {\n  kva: 60,\n  batteryCabinets: 2,\n  doorOpen: false,\n  tag: 'UPS-8701',\n}\n\nconst CW = 0.8 // section width, typical\nconst CH = 2.0\nconst CD = 0.8\nconst front = new THREE.Quaternion()\n\nexport function UpsBatteryCabinet(props: UpsBatteryCabinetProps & GroupProps) {\n  const { kva, batteryCabinets, doorOpen, tag, ...group } = { ...defaults, ...props }\n  const data = useKit((k) => build(k, { kva, batteryCabinets, doorOpen, tag }), [kva, batteryCabinets, doorOpen, tag])\n  return (\n    <group {...group}>\n      <KitMesh data={data} />\n    </group>\n  )\n}\n\nexport default UpsBatteryCabinet\n\nfunction build(k: Kit, p: typeof defaults) {\n  const body = paint('#d5d7d3', { metalness: 0.2, roughness: 0.45, clearcoat: 0.2, weather: 0 })\n  const dark = paint('#3c4146', { metalness: 0.3, roughness: 0.5, clearcoat: 0, weather: 0 })\n  const kinds: ('rect' | 'inv' | 'dist' | 'bat')[] = ['rect', 'inv']\n  if (p.kva > 120) kinds.push('inv')\n  kinds.push('dist')\n  const nb = Math.max(1, Math.min(6, Math.round(p.batteryCabinets)))\n  for (let i = 0; i < nb; i++) kinds.push('bat')\n  const n = kinds.length\n  const W = n * CW\n  const yb = 0.1\n  // base frame, end covers\n  k.box('structSteelDark', [W + 0.04, yb, CD], [0, yb / 2, 0])\n  for (const s of [-1, 1]) k.box(body, [0.03, CH, CD], [s * (W / 2 + 0.015), yb + CH / 2, 0])\n  earthBoss(k, [W / 2 + 0.03, yb + 0.15, -CD * 0.3], 'x')\n\n  kinds.forEach((kind, i) => {\n    const xc = -W / 2 + CW * (i + 0.5)\n    const zf = CD / 2\n    const open = kind === 'bat' && p.doorOpen\n    // carcass: hollow for battery cabinets (back, sides, roof, floor), solid for the power sections\n    if (kind === 'bat') {\n      const t = 0.015\n      k.box(body, [CW - 0.004, CH, t], [xc, yb + CH / 2, -CD / 2 + t / 2])\n      k.box(body, [CW - 0.004, t, CD], [xc, yb + CH - t / 2, 0])\n      k.box(body, [CW - 0.004, t, CD], [xc, yb + t / 2, 0])\n      for (const s of [-1, 1]) k.box(body, [t, CH, CD], [xc + s * (CW / 2 - t / 2 - 0.002), yb + CH / 2, 0])\n      // shelves of 12 V VRLA blocks with inter-block links, DC breaker on top\n      const shelves = 5\n      for (let sh = 0; sh < shelves; sh++) {\n        const ys = yb + 0.12 + sh * 0.34\n        k.box('galvanised', [CW - 0.04, 0.012, CD - 0.06], [xc, ys, 0])\n        for (let b = 0; b < 3; b++) {\n          const bx = xc - 0.25 + b * 0.25\n          k.box(dark, [0.17, 0.22, 0.48], [bx, ys + 0.116, -0.04])\n          k.box(paint('#2a2c2e', { roughness: 0.6 }), [0.175, 0.03, 0.485], [bx, ys + 0.236, -0.04])\n          for (const [tz, c] of [[0.17, 'actuatorRed'], [-0.25, 'rubberBlack']] as [number, MatKey][]) k.cyl(c, 0.012, 0.025, [bx, ys + 0.26, tz], 'y', 10)\n          if (b < 2) k.box('copper', [0.25, 0.006, 0.02], [bx + 0.125, ys + 0.27, 0.17])\n        }\n        k.plane(legendMaterial(`STRING A  ${sh * 3 + 1}-${sh * 3 + 3}`), 0.2, 0.025, [xc, ys - 0.01, CD / 2 - 0.029], front)\n      }\n      k.rbox(dark, [0.18, 0.16, 0.12], 0.01, [xc, yb + CH - 0.15, 0.2])\n      k.box('actuatorRed', [0.03, 0.06, 0.02], [xc, yb + CH - 0.15, 0.27])\n      // louvred door, hinged on the left, swung open about 100°\n      const hinge = new THREE.Vector3(xc - CW / 2 + 0.01, 0, zf + 0.012)\n      const q = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), open ? -1.5 : 0)\n      k.at([hinge.x, 0, hinge.z], q, () => {\n        k.box(body, [CW - 0.02, CH - 0.02, 0.02], [CW / 2 - 0.01, yb + CH / 2, 0])\n        for (const y of [yb + 0.35, yb + CH - 0.35]) louvrePanel(k, CW * 0.6, 0.4, [CW / 2, y, 0.0105], front, '#d5d7d3')\n        k.box('stainless', [0.03, 0.18, 0.03], [CW - 0.08, yb + CH * 0.5, 0.025])\n        k.plane(textMaterial('BATTERY  DC 220 V', '#f4c414', '#111111', 256, 48), 0.3, 0.056, [CW / 2, yb + CH * 0.62, 0.0105], front)\n        for (const y of [0.3, CH - 0.3]) k.cyl('nut', 0.012, 0.08, [0.0, yb + y, 0], 'y', 8)\n      })\n    } else {\n      k.box(body, [CW - 0.004, CH, CD - 0.02], [xc, yb + CH / 2, -0.01])\n      // door with louvres at the bottom (air in) and fan grilles on the roof (air out)\n      k.box(body, [CW - 0.02, CH - 0.02, 0.02], [xc, yb + CH / 2, zf + 0.01])\n      louvrePanel(k, CW * 0.65, 0.45, [xc, yb + 0.35, zf + 0.0205], front, '#d5d7d3')\n      k.box('stainless', [0.03, 0.18, 0.03], [xc + CW / 2 - 0.08, yb + CH * 0.5, zf + 0.035])\n      for (const y of [0.3, CH - 0.3]) k.cyl('nut', 0.012, 0.08, [xc - CW / 2 + 0.012, yb + y, zf + 0.012], 'y', 8)\n      k.box(dark, [CW * 0.5, 0.06, CD * 0.5], [xc, yb + CH + 0.03, -0.05])\n      if (kind === 'inv' || kind === 'rect') {\n        // touch-screen mimic panel, status lamps, emergency power off under a flap\n        k.box(dark, [0.26, 0.2, 0.02], [xc, yb + 1.55, zf + 0.03])\n        k.plane(relayFaceMaterial(), 0.22, 0.16, [xc, yb + 1.55, zf + 0.0405], front)\n        ;(['green', 'amber', 'red'] as const).forEach((c, j) => k.cyl(lamp(c), 0.012, 0.012, [xc - 0.05 + j * 0.05, yb + 1.38, zf + 0.026], 'z', 12))\n        k.plane(legendMaterial(kind === 'rect' ? 'RECTIFIER / CHARGER' : 'INVERTER + STATIC BYPASS', true), 0.36, 0.04, [xc, yb + 1.78, zf + 0.0205], front)\n      } else {\n        // distribution: meters and a window over the outgoing MCBs\n        gauge(k, [xc - 0.15, yb + 1.6, zf + 0.02], 'z', 0.09, 'bar', 250)\n        gauge(k, [xc + 0.15, yb + 1.6, zf + 0.02], 'z', 0.09, 'bar', 250)\n        k.box('glass', [0.5, 0.4, 0.008], [xc, yb + 1.15, zf + 0.024])\n        k.box(dark, [0.48, 0.38, 0.004], [xc, yb + 1.15, zf + 0.0205])\n        k.plane(legendMaterial('UPS DISTRIBUTION 230 V AC', true), 0.36, 0.04, [xc, yb + 1.85, zf + 0.0205], front)\n      }\n    }\n  })\n  // top cable entry gland plates\n  for (let i = 0; i < 3; i++) {\n    const x = -W / 2 + CW * (i + 0.5)\n    k.box('galvanised', [0.4, 0.006, 0.2], [x, yb + CH + 0.003, -CD / 2 + 0.15])\n    for (let c = 0; c < 3; c++) {\n      cableGland(k, [x - 0.1 + c * 0.1, yb + CH + 0.006, -CD / 2 + 0.15], 'y', 0.03)\n    }\n  }\n  tagPlate(k, p.tag, [-W / 2 + CW / 2, yb + CH - 0.08, CD / 2 + 0.0215], 'z', 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": "UpsBatteryCabinet",
    "anchor": "base",
    "standards": [
      "IEC 62040-1",
      "IEC 62040-3",
      "IEC 62485-2",
      "IEC 60896-21"
    ]
  }
}