{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "fire-extinguisher",
  "type": "registry:component",
  "title": "Fire extinguishers",
  "description": "EN 3 fire extinguishers with red bodies and agent colour zones (dry powder blue, CO2 black with discharge horn, foam cream), front instruction label, valve head with squeeze lever, safety pin, gauge and hose. unit portable-bracket hangs one on a neck cradle and strap on a post or wall; cabinet stands them in a red GRP cabinet with glazed or solid door; wheeled puts a 25–150 kg unit on a trolley. capacity (kg) sets the cylinder size; an ISO 7010 F001 sign is included.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "safety/fire-extinguisher.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/safety/fire-extinguisher.tsx",
      "content": "// Fire extinguishers: portable units (dry powder, CO2, foam) on a bracket or in a red cabinet, or a wheeled unit on its trolley; EN 3 red bodies with agent colour bands, capacity sets the size. EN 3-7, EN 1866-1, ISO 7010.\nimport * as THREE from 'three'\nimport { Kit, mat4, type V3 } from '../lib/kit'\nimport { paint } from '../lib/materials'\nimport { KitMesh, useKit, type GroupProps } from '../lib/mesh'\nimport { gauge, tagPlate } from '../lib/parts'\nimport { ICON, canvasMat, faceZ, mounted, polePlate, signMat, type Mount } from './_fg'\n\nexport type Agent = 'dry-powder' | 'co2' | 'foam'\nexport type ExtinguisherUnit = 'portable-bracket' | 'cabinet' | 'wheeled'\n\nexport interface FireExtinguisherProps {\n  /** how the extinguisher is kept (named `unit` because `type` is reserved by Object3D) */\n  unit?: ExtinguisherUnit\n  agent?: Agent\n  /** agent charge, kg (foam: litres); portable 1–12, wheeled 25–150 */\n  capacity?: number\n  /** extinguishers in the cabinet */\n  count?: number\n  door?: 'glazed' | 'solid'\n  /** bracket on a free-standing post or a wall */\n  mount?: Mount\n  sign?: boolean\n  tag?: string\n}\n\nexport const defaults = {\n  unit: 'cabinet' as ExtinguisherUnit,\n  agent: 'dry-powder' as Agent,\n  capacity: 9,\n  count: 2,\n  door: 'glazed' as 'glazed' | 'solid',\n  mount: 'pole' as Mount,\n  sign: true,\n  tag: 'EXT-12',\n}\n\nconst BODY = paint('#c1121c', { metalness: 0.2, roughness: 0.35, clearcoat: 0.7, clearcoatRoughness: 0.2 })\nconst HEAD = paint('#1d1f22', { metalness: 0.5, roughness: 0.35 })\nconst RED = 'firewaterRed'\n// EN 3-7 colour zones: dry powder blue, CO2 black, foam cream\nconst AGENT: Record<Agent, { band: string; text: string; ink: string; cls: string }> = {\n  'dry-powder': { band: '#1f4e9a', text: 'DRY POWDER', ink: '#ffffff', cls: 'A B C' },\n  co2: { band: '#141414', text: 'CO2', ink: '#ffffff', cls: 'B' },\n  foam: { band: '#eadcae', text: 'FOAM', ink: '#141414', cls: 'A B' },\n}\nconst BAND: Record<Agent, ReturnType<typeof paint>> = {\n  'dry-powder': paint(AGENT['dry-powder'].band, { metalness: 0.2, roughness: 0.4, clearcoat: 0.6 }),\n  co2: paint(AGENT.co2.band, { metalness: 0.2, roughness: 0.4, clearcoat: 0.6 }),\n  foam: paint(AGENT.foam.band, { metalness: 0.2, roughness: 0.4, clearcoat: 0.6 }),\n}\n\n/** cylinder radius and shell height from the charge (cube-root scaling from a 9 kg powder / 5 kg CO2 unit) */\nexport function extinguisherSize(agent: Agent, kg: number) {\n  if (agent === 'co2') {\n    const s = Math.cbrt(kg / 5)\n    return { r: 0.07 * s, h: 0.6 * s }\n  }\n  const s = Math.cbrt(kg / 9)\n  return { r: 0.085 * s, h: 0.5 * s }\n}\n\nexport function FireExtinguisher(props: FireExtinguisherProps & GroupProps) {\n  const { unit, agent, capacity, count, door, mount, sign, tag, ...group } = { ...defaults, ...props }\n  const n = Math.max(1, Math.min(3, Math.round(count)))\n  const data = useKit(\n    (k) => {\n      if (unit === 'wheeled') wheeled(k, agent, capacity >= 25 ? Math.min(150, capacity) : 50, tag)\n      else {\n        const kg = Math.max(1, Math.min(12, capacity))\n        if (unit === 'cabinet') cabinet(k, agent, kg, n, door, sign, tag)\n        else bracket(k, agent, kg, mount, sign, tag)\n      }\n    },\n    [unit, agent, capacity, n, door, mount, sign, tag],\n  )\n  return (\n    <group {...group}>\n      <KitMesh data={data} />\n    </group>\n  )\n}\n\nexport default FireExtinguisher\n\nfunction labelMat(agent: Agent, kg: number) {\n  const a = AGENT[agent]\n  return canvasMat(`fe-label-${agent}-${kg}`, 256, 384, (g) => {\n    g.fillStyle = '#f4f2ec'\n    g.fillRect(0, 0, 256, 384)\n    g.fillStyle = a.band\n    g.fillRect(0, 0, 256, 70)\n    g.fillStyle = a.ink\n    g.textAlign = 'center'\n    g.textBaseline = 'middle'\n    g.font = `bold ${a.text.length > 4 ? 34 : 46}px Arial, sans-serif`\n    g.fillText(a.text, 128, 36)\n    g.fillStyle = '#111'\n    g.font = 'bold 40px Arial, sans-serif'\n    g.fillText(`${kg} ${agent === 'foam' ? 'L' : 'kg'}`, 128, 108)\n    g.font = 'bold 26px Arial, sans-serif'\n    g.fillText(`CLASS ${a.cls}`, 128, 150)\n    for (let i = 0; i < 3; i++) {\n      g.strokeStyle = '#111'\n      g.lineWidth = 3\n      g.strokeRect(24 + i * 72, 176, 60, 60)\n      g.fillText(['1', '2', '3'][i], 54 + i * 72, 206)\n    }\n    for (let i = 0; i < 6; i++) g.fillRect(24, 256 + i * 18, 208 - (i % 2) * 50, 6)\n  })\n}\n\n/** Curved instruction label centred on +Z (open cylinder strip, UVs 0…1). */\nfunction label(k: Kit, m: THREE.Material, r: number, h: number, c: V3) {\n  k.add(m, new THREE.CylinderGeometry(r, r, h, 16, 1, true, -0.75, 1.5), mat4(c))\n}\n\n/** Portable extinguisher standing on `p` (base centre), front +Z. Returns the top of the head. */\nfunction portable(k: Kit, p: V3, agent: Agent, kg: number) {\n  const co2 = agent === 'co2'\n  const { r, h } = extinguisherSize(agent, kg)\n  const s = r / 0.085\n  const [x, y, z] = p\n  // moulded foot ring, shell, shoulder, colour band, front label\n  k.lathe('rubberBlack', [[r * 0.6, 0], [r + 0.006, 0], [r + 0.006, 0.03], [r - 0.002, 0.034]], [x, y, z], 'y', 28)\n  k.cyl(BODY, r, h - 0.03, [x, y + 0.03 + (h - 0.03) / 2, z], 'y', 32)\n  k.dome(BODY, r, [x, y + h, z], 'y', Math.PI / 2, 0.55, 28)\n  k.cyl(BAND[agent], r + 0.0008, h * 0.09, [x, y + h * 0.83, z], 'y', 32, undefined, true)\n  label(k, labelMat(agent, kg), r + 0.0012, h * 0.46, [x, y + h * 0.51, z])\n  // valve: brass neck, head, carry handle, squeeze lever, safety pin with tamper tie\n  const yv = y + h + r * 0.55\n  k.cyl('brass', 0.017 * s + 0.004, 0.035, [x, yv, z], 'y', 14)\n  k.rbox(HEAD, [0.04 * s + 0.01, 0.045, 0.05 * s + 0.012], 0.006, [x, yv + 0.04, z])\n  k.box(HEAD, [0.02, 0.012, 0.15 * s], [x, yv + 0.03, z - 0.07 * s])\n  k.box(HEAD, [0.02, 0.012, 0.14 * s], [x, yv + 0.07, z - 0.055 * s], [0, 0.97, -0.24])\n  k.torus('safetyYellow', 0.012, 0.0025, [x + 0.028, yv + 0.045, z], 'x', 6, 12)\n  k.rod('safetyYellow', [x - 0.025, yv + 0.045, z], [x + 0.03, yv + 0.045, z], 0.0025, 6)\n  k.cyl('actuatorRed', 0.004, 0.012, [x + 0.032, yv + 0.04, z + 0.008], 'y', 6)\n  if (!co2) gauge(k, [x, yv + 0.04, z + 0.025 * s + 0.006], 'z', 0.034, 'bar', 25)\n  // discharge hose, nozzle (or CO2 horn) parked in a clip on the side\n  const xs = x + r + 0.022\n  k.box('stainless', [0.018, 0.03, 0.014], [x + r + 0.008, y + h * 0.58, z + 0.02])\n  if (co2) {\n    k.tube('rubberBlack', [[x, yv + 0.04, z + 0.028], [x + r * 0.9, yv + 0.01, z + 0.035], [xs, y + h * 0.78, z + 0.02], [xs, y + h * 0.66, z + 0.02]], 0.008, 6, 0.04)\n    k.cyl(HEAD, 0.016, 0.04, [xs, y + h * 0.64, z + 0.02], 'y', 12)\n    // hollow discharge horn\n    k.lathe(HEAD, [[0.014, 0], [0.05 * s, 0.24 * s], [0.046 * s, 0.24 * s], [0.01, 0.004]], [xs, y + h * 0.62, z + 0.02], '-y', 24)\n  } else {\n    k.tube('rubberBlack', [[x, yv + 0.04, z + 0.028], [x + r * 0.9, yv + 0.01, z + 0.035], [xs, y + h * 0.78, z + 0.02], [xs, y + h * 0.66, z + 0.02]], 0.008, 6, 0.04)\n    k.lathe(HEAD, [[0.012, 0], [0.012, 0.09], [0.008, 0.1], [0.005, 0.1], [0.005, 0.07]], [xs, y + h * 0.66, z + 0.02], '-y', 12)\n  }\n  return yv + 0.075\n}\n\n/** ISO 7010 F001 sign plate centred at (0, y), plate face at z. */\nfunction signPlate(k: Kit, y: number, z: number, w = 0.3) {\n  k.box('stainless', [w + 0.006, w / 2 + 0.006, 0.003], [0, y, z + 0.0015])\n  k.plane(signMat(['FIRE', 'EXTINGUISHER'], '#c22a20', '#ffffff', ICON.extinguisher), w, w / 2, [0, y, z + 0.0045])\n}\n\nfunction bracket(k: Kit, agent: Agent, kg: number, mount: Mount, sign: boolean, tag: string) {\n  const { r, h } = extinguisherSize(agent, kg)\n  const yb = 0.35 // cylinder base above the deck\n  const zc = r + 0.03\n  const yc = yb + h * 0.55\n  const ph = h + 0.2\n  mounted(k, mount, yc, [0.12, ph], () => {\n    // neck cradle under the valve (hook) and a quick-release strap round the body\n    const yn = yb + h + r * 0.3 - yc\n    k.box('stainless', [0.07, 0.03, 0.012], [0, yn - 0.02, 0.006])\n    k.box('stainless', [0.07, 0.006, zc + 0.03], [0, yn, (zc + 0.03) / 2])\n    for (const sx of [-1, 1]) k.box('stainless', [0.006, 0.03, 0.03], [sx * 0.032, yn + 0.015, zc])\n    const ys = -h * 0.25\n    k.box('stainless', [0.08, 0.04, 0.012], [0, ys, 0.006])\n    k.lathe('rubberBlack', [[r + 0.002, ys - 0.012], [r + 0.006, ys - 0.012], [r + 0.006, ys + 0.012], [r + 0.002, ys + 0.012], [r + 0.002, ys - 0.012]], [0, 0, zc], 'y', 28, -Math.PI * 0.7, Math.PI * 1.4)\n    k.box('stainless', [0.03, 0.026, 0.01], [0, ys, zc + r + 0.008])\n    for (const sx of [-1, 1]) for (const yy of [yn - 0.02, ys]) k.hex('nut', 0.014, 0.006, [sx * 0.027, yy, 0.015], 'z')\n  }, Math.max(yc + ph / 2 + 0.05, sign ? yb + h + 0.55 : 0))\n  const zf = faceZ(mount)\n  portable(k, [0, yb, zf + zc], agent, kg)\n  if (sign) {\n    const ys = yb + h + 0.38\n    const z = mount === 'pole' ? polePlate(k, ys, 0.16, 0.12) + 0.012 : 0.008\n    if (mount === 'pole') for (const sx of [-1, 1]) k.cyl('stainless', 0.008, 0.012, [sx * 0.06, ys, z - 0.006], 'z', 8)\n    else k.box('stainless', [0.32, 0.17, 0.008], [0, ys, 0.004])\n    signPlate(k, ys, z)\n  }\n  tagPlate(k, tag, [0, yb - 0.06, zf + 0.001], 'z', 0.1)\n}\n\nfunction cabinet(k: Kit, agent: Agent, kg: number, n: number, door: 'glazed' | 'solid', sign: boolean, tag: string) {\n  const { r, h } = extinguisherSize(agent, kg)\n  const pitch = 2 * r + 0.12\n  const W = 0.08 + n * pitch\n  const H = Math.max(0.6, h + r * 0.55 + 0.32)\n  const D = Math.max(0.26, 2 * r + 0.12)\n  const y0 = 0.15\n  const t = 0.004\n  // GRP shell: back, sides, floor, roof with drip lip\n  k.box(RED, [W, H, t], [0, y0 + H / 2, t / 2])\n  for (const s of [-1, 1]) k.box(RED, [t, H, D - t], [s * (W / 2 - t / 2), y0 + H / 2, (D + t) / 2])\n  k.box(RED, [W - 2 * t, t, D - t], [0, y0 + t / 2, (D + t) / 2])\n  k.box(RED, [W - 2 * t, t, D - t], [0, y0 + H - t / 2, (D + t) / 2])\n  k.rbox(RED, [W + 0.04, 0.03, D + 0.05], 0.008, [0, y0 + H + 0.015, D / 2 + 0.01])\n  // plinth stand\n  for (const sx of [-1, 1])\n    for (const sz of [0.03, D - 0.03]) {\n      k.box('galvanised', [0.04, y0 - 0.008, 0.04], [sx * (W / 2 - 0.03), (y0 - 0.008) / 2 + 0.008, sz])\n      k.box('galvanised', [0.08, 0.008, 0.08], [sx * (W / 2 - 0.03), 0.004, sz])\n      k.hex('nut', 0.022, 0.01, [sx * (W / 2 - 0.03) + 0.025, 0.013, sz + 0.025], 'y')\n    }\n  for (const sz of [0.03, D - 0.03]) k.box('galvanised', [W - 0.02, 0.04, 0.04], [0, y0 - 0.02, sz])\n  // door on the front: frame, glazing or solid panel with louvres, hinges, latch, seal\n  const fw = 0.045\n  const yc = y0 + H / 2\n  const zd = D + 0.01\n  k.box(RED, [W, fw, 0.02], [0, y0 + fw / 2, zd])\n  k.box(RED, [W, fw * 2.6, 0.02], [0, y0 + H - fw * 1.3, zd])\n  for (const s of [-1, 1]) k.box(RED, [fw, H - fw * 3.6, 0.02], [s * (W / 2 - fw / 2), y0 + fw + (H - fw * 3.6) / 2, zd])\n  const wy = y0 + fw + (H - fw * 3.6) / 2\n  const wh = H - fw * 3.6\n  if (door === 'glazed') {\n    k.box('glass', [W - 2 * fw, wh, 0.004], [0, wy, zd])\n    k.box('rubberBlack', [W - 2 * fw + 0.006, 0.006, 0.008], [0, wy + wh / 2, zd])\n    k.box('rubberBlack', [W - 2 * fw + 0.006, 0.006, 0.008], [0, wy - wh / 2, zd])\n  } else {\n    k.box(RED, [W - 2 * fw, wh, 0.012], [0, wy, zd - 0.002])\n    for (let i = 0; i < 5; i++) k.box('castIron', [W * 0.5, 0.008, 0.004], [0, y0 + fw + 0.04 + i * 0.022, zd + 0.005])\n  }\n  for (const y of [y0 + 0.1, y0 + H - 0.1]) k.cyl('stainless', 0.008, 0.07, [-W / 2 - 0.006, y, zd], 'y', 10)\n  k.rbox('stainless', [0.022, 0.1, 0.02], 0.004, [W / 2 - fw / 2, yc, zd + 0.02])\n  k.cyl('castIron', 0.008, 0.008, [W / 2 - fw / 2, yc + 0.07, zd + 0.014], 'z', 10)\n  if (sign) {\n    const w = Math.min(W - 0.06, door === 'solid' ? 0.34 : 0.2)\n    const ys = door === 'solid' ? wy + wh * 0.18 : y0 + H - fw * 1.3\n    signPlate(k, ys, zd + (door === 'solid' ? 0.004 : 0.01), w)\n  }\n  for (let i = 0; i < n; i++) portable(k, [-W / 2 + 0.04 + pitch / 2 + i * pitch, y0 + t, D / 2 + 0.01], agent, kg)\n  tagPlate(k, tag, [W / 2 + 0.001, yc + 0.2, D / 2], 'x', 0.09)\n}\n\n/** Wheeled unit, cylinder upright on its trolley, hose on a rack, front +Z. */\nfunction wheeled(k: Kit, agent: Agent, kg: number, tag: string) {\n  const s = Math.cbrt(kg / 50)\n  const r = 0.16 * s\n  const h = 0.82 * s\n  const yb = 0.1\n  const co2 = agent === 'co2'\n  k.cyl(BODY, r, h, [0, yb + h / 2, 0], 'y', 40)\n  k.dome(BODY, r, [0, yb + h, 0], 'y', Math.PI / 2, 0.5, 36)\n  k.dome(BODY, r, [0, yb, 0], '-y', Math.PI / 2, 0.35, 36)\n  k.cyl(BAND[agent], r + 0.0008, h * 0.08, [0, yb + h * 0.86, 0], 'y', 40, undefined, true)\n  label(k, labelMat(agent, kg), r + 0.0012, h * 0.45, [0, yb + h * 0.525, 0])\n  // trolley: axle, wheels with hubs, tubular frame, handle, toe plate, cylinder clamps\n  const wr = 0.2\n  const zA = -r + 0.04\n  const xw = Math.max(0.3, r + 0.14)\n  const xf = xw - 0.1\n  k.rod('stainless', [-xw - 0.03, wr, zA], [xw + 0.03, wr, zA], 0.015, 10)\n  for (const sx of [-1, 1]) {\n    k.lathe('rubberBlack', [[wr * 0.62, -0.035], [wr - 0.01, -0.035], [wr, -0.02], [wr, 0.02], [wr - 0.01, 0.035], [wr * 0.62, 0.035]], [sx * xw, wr, zA], 'x', 32)\n    k.cyl('galvanised', wr * 0.62, 0.06, [sx * xw, wr, zA], 'x', 24)\n    k.cyl('galvanised', 0.035, 0.09, [sx * xw, wr, zA], 'x', 12)\n    k.hex('nut', 0.03, 0.012, [sx * (xw + 0.05), wr, zA], 'x')\n    k.tube(RED, [[sx * xf, wr, zA], [sx * xf, yb + h + 0.25, zA - 0.08]], 0.016, 8)\n    k.tube(RED, [[sx * xf, 0.03, r + 0.04], [sx * xf, 0.03, zA], [sx * xf, wr, zA]], 0.016, 8, 0.05)\n    k.box('rubberBlack', [0.05, 0.03, 0.05], [sx * xf, 0.015, r + 0.04])\n  }\n  for (const y of [yb + h * 0.25, yb + h * 0.8]) {\n    k.lathe(RED, [[r + 0.001, y - 0.015], [r + 0.006, y - 0.015], [r + 0.006, y + 0.015], [r + 0.001, y + 0.015], [r + 0.001, y - 0.015]], [0, 0, 0], 'y', 32, Math.PI * 0.55, Math.PI * 0.9)\n    k.box(RED, [2 * xf, 0.03, 0.02], [0, y, zA - 0.03])\n    k.hex('nut', 0.02, 0.01, [0, y, zA - 0.045], 'z')\n  }\n  k.tube(RED, [[-xf, yb + h + 0.25, zA - 0.08], [xf, yb + h + 0.25, zA - 0.08]], 0.016, 8)\n  k.cyl('rubberBlack', 0.02, 0.2, [0, yb + h + 0.25, zA - 0.08], 'x', 12)\n  k.box(RED, [2 * xf, 0.03, 0.03], [0, 0.03, r + 0.04])\n  // valve head with gauge, hose coiled on a rack, nozzle gun or CO2 horn\n  const yv = yb + h + r * 0.5\n  k.cyl('brass', 0.03, 0.06, [0, yv + 0.03, 0], 'y', 14)\n  k.rbox(HEAD, [0.07, 0.05, 0.07], 0.008, [0, yv + 0.085, 0])\n  k.cyl('stainless', 0.008, 0.12, [0, yv + 0.15, 0], 'y', 8)\n  k.handwheel('handwheel', 0.1, [0, yv + 0.2, 0], 'y', 3)\n  if (!co2) gauge(k, [0.05, yv + 0.09, 0.03], 'z', 0.06, 'bar', 25)\n  const xr = xf + 0.02\n  k.box('galvanised', [0.01, 0.28, 0.18], [xr, yb + h * 0.62, zA + 0.04])\n  // ~5 m discharge hose coiled on the rack\n  for (let i = 0; i < 6; i++) k.torus('rubberBlack', 0.13, 0.013, [xr + 0.02 + i * 0.026, yb + h * 0.62, zA + 0.04], 'x', 8, 32)\n  k.tube('rubberBlack', [[0, yv + 0.085, 0.035], [xr * 0.5, yv + 0.05, 0.08], [xr + 0.02, yb + h * 0.62 + 0.13, zA + 0.08]], 0.013, 8, 0.05)\n  const xn = xr + 0.06\n  if (co2) k.lathe(HEAD, [[0.02, 0], [0.08, 0.3], [0.075, 0.3], [0.016, 0.005]], [xn, yb + h * 0.45, zA + 0.12], '-y', 24)\n  else {\n    k.lathe(HEAD, [[0.02, 0], [0.02, 0.18], [0.012, 0.2], [0.008, 0.2], [0.008, 0.16]], [xn, yb + h * 0.55, zA + 0.12], '-y', 14)\n    k.cyl('safetyYellow', 0.024, 0.04, [xn, yb + h * 0.55 + 0.02, zA + 0.12], 'y', 12)\n  }\n  k.box('stainless', [0.03, 0.03, 0.04], [xr + 0.02, yb + h * 0.48, zA + 0.1])\n  tagPlate(k, tag, [0, yb + h * 0.18, r + 0.002], 'z', 0.09)\n}\n"
    },
    {
      "path": "safety/_fg.ts",
      "type": "registry:file",
      "target": "components/deck-twin/safety/_fg.ts",
      "content": "// Shared fire & gas hardware: galvanised pole stanchion, pole and wall brackets, field cable, LEDs, lenses and canvas faces.\nimport * as THREE from 'three'\nimport { Kit, type MatKey, type V3 } from '../lib/kit'\nimport { paint, weather } from '../lib/materials'\nimport { pipeOD } from '../lib/standards'\n\nexport type Mount = 'pole' | 'wall'\n\nexport const LENS: MatKey = paint('#15202b', { metalness: 0.3, roughness: 0.06 })\nexport const WHITE: MatKey = paint('#eceae4', { metalness: 0.05, roughness: 0.55 })\nexport const LED_GREEN: MatKey = paint('#2bff6e', { emissive: '#2bff6e', emissiveIntensity: 0.9, roughness: 0.3 })\nexport const LED_RED: MatKey = paint('#ff3b30', { emissive: '#ff3b30', emissiveIntensity: 0.9, roughness: 0.3 })\n\n/** 2\" galvanised stanchion pipe */\nexport const POLE_R = pipeOD(2) / 2\nconst PLATE_T = 0.006\n\nconst deg = (d: number) => (d * Math.PI) / 180\nexport { deg }\n\n/** Free-standing stanchion: base plate with four anchor bolts and gussets, 2\" pole to `top`, welded cap. Pole on the origin. */\nexport function poleStanchion(k: Kit, top: number, mat: MatKey = 'galvanised') {\n  const B = 0.26\n  k.box(mat, [B, 0.016, B], [0, 0.008, 0])\n  for (const sx of [-1, 1])\n    for (const sz of [-1, 1]) {\n      const p: V3 = [sx * (B / 2 - 0.035), 0, sz * (B / 2 - 0.035)]\n      k.cyl('galvanised', 0.008, 0.05, [p[0], 0.035, p[2]], 'y', 8)\n      k.hex('nut', 0.03, 0.014, [p[0], 0.023, p[2]], 'y')\n      k.cyl('galvanised', 0.017, 0.003, [p[0], 0.0175, p[2]], 'y', 12)\n    }\n  k.cyl(mat, POLE_R, top - 0.016, [0, 0.016 + (top - 0.016) / 2, 0], 'y', 20)\n  k.cyl(mat, POLE_R + 0.004, 0.008, [0, top + 0.004, 0], 'y', 20)\n  const g = new THREE.Shape()\n  g.moveTo(0, 0).lineTo(0.09, 0).lineTo(0, 0.16).closePath()\n  for (let i = 0; i < 4; i++) {\n    const a = (i * Math.PI) / 2 + Math.PI / 4\n    const q = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), a)\n    const m = new THREE.Matrix4().compose(new THREE.Vector3(0, 0.016, 0).add(new THREE.Vector3(POLE_R, 0, -0.004).applyQuaternion(q)), q, new THREE.Vector3(1, 1, 1))\n    k.extrude(mat, g, 0.008, m)\n  }\n}\n\n/** Plate clamped to the front (+Z) of the pole by two U-bolts. Returns the z of the plate front face. */\nexport function polePlate(k: Kit, y: number, w: number, h: number, mat: MatKey = 'stainless') {\n  const z0 = POLE_R + 0.004\n  k.box(mat, [w, h, PLATE_T], [0, y, z0 + PLATE_T / 2])\n  for (const s of [-1, 1]) {\n    const yy = y + s * (h / 2 - 0.025)\n    k.torus('galvanised', POLE_R + 0.006, 0.005, [0, yy, 0], 'y', 6, 20, Math.PI)\n    k.rod('galvanised', [-(POLE_R + 0.006), yy, 0], [-(POLE_R + 0.006), yy, z0 + PLATE_T + 0.012], 0.005, 6)\n    k.rod('galvanised', [POLE_R + 0.006, yy, 0], [POLE_R + 0.006, yy, z0 + PLATE_T + 0.012], 0.005, 6)\n    for (const sx of [-1, 1]) k.hex('nut', 0.017, 0.008, [sx * (POLE_R + 0.006), yy, z0 + PLATE_T + 0.005], 'z')\n  }\n  // the U-bolt arcs sit behind the pole (torus half facing -Z)\n  return z0 + PLATE_T\n}\n\n/** Stainless wall plate in the XY plane (back face on z = 0), four fixings. Returns the front face z. */\nexport function wallPlate(k: Kit, y: number, w: number, h: number) {\n  k.box('stainless', [w, h, 0.008], [0, y, 0.004])\n  for (const sx of [-1, 1]) for (const sy of [-1, 1]) k.hex('nut', 0.016, 0.008, [sx * (w / 2 - 0.014), y + sy * (h / 2 - 0.014), 0.012], 'z')\n  return 0.008\n}\n\n/** z of the bracket face that mounted() builds on. */\nexport const faceZ = (mount: Mount) => (mount === 'pole' ? POLE_R + 0.004 + PLATE_T : 0.008)\n\n/** Builds the stanchion (pole) or wall plate, then runs `fn` with the origin on the bracket face at `y`, +Z out of the bracket. */\nexport function mounted(k: Kit, mount: Mount, y: number, plate: [number, number], fn: (zFace: number) => void, poleTop = y + plate[1] / 2 + 0.05) {\n  let zf: number\n  if (mount === 'pole') {\n    poleStanchion(k, poleTop)\n    zf = polePlate(k, y, plate[0], plate[1])\n  } else zf = wallPlate(k, y, plate[0], plate[1])\n  k.at([0, y, zf], undefined, () => fn(zf))\n  return zf\n}\n\n/** Armoured field cable (SWA, black sheath). */\nexport function cable(k: Kit, pts: V3[], r = 0.0065) {\n  k.spline('cable', pts, r, 6, Math.max(16, pts.length * 8))\n}\n\n/** Cable from `from` down the left side of the pole (or the wall) to the deck, cleated every ~0.5 m. */\nexport function cableToDeck(k: Kit, mount: Mount, from: V3, r = 0.0065) {\n  const x = mount === 'pole' ? -(POLE_R + r + 0.001) : from[0]\n  const z = mount === 'pole' ? 0 : r + 0.009\n  const yTop = Math.min(from[1] - 0.08, from[1])\n  cable(k, [from, [from[0], from[1] - 0.05, from[2]], [(from[0] + x) / 2, yTop - 0.05, (from[2] + z) / 2], [x, yTop - 0.14, z], [x, 0.2, z]], r)\n  k.tube('cable', [[x, 0.2, z], [x, 0.03, z], mount === 'pole' ? [x - 0.2, 0.03, z] : [x, 0.03, z + 0.2]], r, 6, 0.06)\n  for (let y = 0.4; y < yTop - 0.2; y += 0.5) {\n    if (mount === 'pole') k.torus('stainless', POLE_R + 0.002, 0.003, [0, y, 0], 'y', 4, 18)\n    else k.box('stainless', [0.03, 0.012, 0.004], [x, y, 2 * r + 0.011])\n  }\n}\n\n// ------------------------------------------------------------------ canvas faces (created lazily, browser only)\n\nconst texCache = new Map<string, THREE.MeshStandardMaterial>()\n\nexport function canvasMat(key: string, w: number, h: number, draw: (g: CanvasRenderingContext2D) => void, opts: { emissive?: number; roughness?: number } = {}) {\n  let m = texCache.get(key)\n  if (m) return m\n  const c = document.createElement('canvas')\n  c.width = w\n  c.height = h\n  draw(c.getContext('2d')!)\n  const tex = new THREE.CanvasTexture(c)\n  tex.colorSpace = THREE.SRGBColorSpace\n  tex.anisotropy = 4\n  m = new THREE.MeshStandardMaterial({ map: tex, metalness: 0.1, roughness: opts.roughness ?? 0.45 })\n  if (opts.emissive) {\n    m.emissive = new THREE.Color('#ffffff')\n    m.emissiveMap = tex\n    m.emissiveIntensity = opts.emissive\n  }\n  m.name = key\n  weather(m, 0.3)\n  texCache.set(key, m)\n  return m\n}\n\nfunction fit(g: CanvasRenderingContext2D, text: string, max: number, size: number, weight = 'bold') {\n  g.font = `${weight} ${size}px Arial, sans-serif`\n  while (g.measureText(text).width > max && size > 10) g.font = `${weight} ${(size -= 2)}px Arial, sans-serif`\n}\n\n/** ISO 7010-style sign: coloured field, white border, optional pictogram square on the left, one or two text lines. */\nexport function signMat(lines: string[], bg: string, fg = '#ffffff', icon?: (g: CanvasRenderingContext2D, s: number) => void) {\n  return canvasMat(`fgsign-${lines.join('|')}-${bg}-${fg}-${icon ? 1 : 0}`, 512, 256, (g) => {\n    g.fillStyle = bg\n    g.fillRect(0, 0, 512, 256)\n    g.strokeStyle = fg\n    g.lineWidth = 8\n    g.strokeRect(10, 10, 492, 236)\n    let x0 = 256\n    let w = 460\n    if (icon) {\n      g.save()\n      g.translate(28, 28)\n      icon(g, 200)\n      g.restore()\n      x0 = 370\n      w = 250\n    }\n    g.fillStyle = fg\n    g.textAlign = 'center'\n    g.textBaseline = 'middle'\n    const n = lines.length\n    lines.forEach((t, i) => {\n      fit(g, t, w, n > 2 ? 46 : 60)\n      g.fillText(t, x0, 128 + (i - (n - 1) / 2) * (n > 2 ? 58 : 72))\n    })\n  })\n}\n\n/** Black/yellow hazard-bordered warning plate. */\nexport function warnMat(lines: string[]) {\n  return canvasMat(`fgwarn-${lines.join('|')}`, 512, 256, (g) => {\n    g.fillStyle = '#f2c318'\n    g.fillRect(0, 0, 512, 256)\n    g.save()\n    g.beginPath()\n    g.rect(0, 0, 512, 256)\n    g.rect(22, 22, 468, 212)\n    g.clip('evenodd')\n    g.fillStyle = '#141414'\n    for (let x = -256; x < 768; x += 44) {\n      g.beginPath()\n      g.moveTo(x, 0)\n      g.lineTo(x + 22, 0)\n      g.lineTo(x - 234, 256)\n      g.lineTo(x - 256, 256)\n      g.closePath()\n      g.fill()\n    }\n    g.restore()\n    g.fillStyle = '#141414'\n    g.textAlign = 'center'\n    g.textBaseline = 'middle'\n    lines.forEach((t, i) => {\n      fit(g, t, 440, i === 0 ? 70 : 44)\n      g.fillText(t, 256, lines.length === 1 ? 128 : i === 0 ? 100 : 176)\n    })\n  })\n}\n\n/** White pictogram helpers drawn into an s × s square. */\nexport const ICON = {\n  flame(g: CanvasRenderingContext2D, s: number) {\n    g.fillStyle = '#ffffff'\n    g.beginPath()\n    g.moveTo(s * 0.5, s * 0.08)\n    g.bezierCurveTo(s * 0.85, s * 0.4, s * 0.82, s * 0.7, s * 0.5, s * 0.9)\n    g.bezierCurveTo(s * 0.18, s * 0.7, s * 0.2, s * 0.45, s * 0.38, s * 0.3)\n    g.bezierCurveTo(s * 0.4, s * 0.45, s * 0.48, s * 0.5, s * 0.52, s * 0.42)\n    g.closePath()\n    g.fill()\n  },\n  shower(g: CanvasRenderingContext2D, s: number) {\n    g.fillStyle = '#ffffff'\n    g.strokeStyle = '#ffffff'\n    g.lineWidth = s * 0.04\n    g.beginPath()\n    g.arc(s * 0.62, s * 0.18, s * 0.12, Math.PI, 0)\n    g.fill()\n    for (let i = 0; i < 5; i++) {\n      g.beginPath()\n      g.moveTo(s * (0.52 + i * 0.05), s * 0.24)\n      g.lineTo(s * (0.48 + i * 0.05), s * 0.42)\n      g.stroke()\n    }\n    g.beginPath()\n    g.arc(s * 0.4, s * 0.38, s * 0.07, 0, Math.PI * 2)\n    g.fill()\n    g.fillRect(s * 0.34, s * 0.47, s * 0.12, s * 0.28)\n    g.fillRect(s * 0.34, s * 0.75, s * 0.05, s * 0.18)\n    g.fillRect(s * 0.41, s * 0.75, s * 0.05, s * 0.18)\n  },\n  extinguisher(g: CanvasRenderingContext2D, s: number) {\n    g.fillStyle = '#ffffff'\n    g.fillRect(s * 0.36, s * 0.3, s * 0.28, s * 0.6)\n    g.beginPath()\n    g.arc(s * 0.5, s * 0.3, s * 0.14, Math.PI, 0)\n    g.fill()\n    g.fillRect(s * 0.45, s * 0.1, s * 0.1, s * 0.08)\n    g.lineWidth = s * 0.04\n    g.strokeStyle = '#ffffff'\n    g.beginPath()\n    g.moveTo(s * 0.55, s * 0.14)\n    g.quadraticCurveTo(s * 0.85, s * 0.18, s * 0.8, s * 0.5)\n    g.stroke()\n  },\n  hose(g: CanvasRenderingContext2D, s: number) {\n    g.strokeStyle = '#ffffff'\n    g.lineWidth = s * 0.06\n    for (const r of [0.14, 0.24, 0.34]) {\n      g.beginPath()\n      g.arc(s * 0.45, s * 0.5, s * r, 0, Math.PI * 2)\n      g.stroke()\n    }\n    g.beginPath()\n    g.moveTo(s * 0.79, s * 0.5)\n    g.lineTo(s * 0.92, s * 0.85)\n    g.stroke()\n  },\n}\n\n/** Ex d stopping (blanking) plug in an unused cable entry, axis out of the entry. */\nexport function stopPlug(k: Kit, pos: V3, axis: 'x' | '-x' | 'y' | '-y' | 'z' | '-z', d = 0.026) {\n  k.hex('castIron', d, 0.006, pos, axis)\n  const v = { x: [1, 0, 0], '-x': [-1, 0, 0], y: [0, 1, 0], '-y': [0, -1, 0], z: [0, 0, 1], '-z': [0, 0, -1] }[axis]\n  k.cyl('castIron', d * 0.36, 0.006, [pos[0] + v[0] * 0.006, pos[1] + v[1] * 0.006, pos[2] + v[2] * 0.006], axis, 12)\n}\n\n/**\n * Flameproof (Ex d) cast junction box: body with a wide flamepath flange, bolted lid, earth stud and bottom entries.\n * Back face at `pos`, lid facing +Z. `used` entries get brass glands, the rest stopping plugs. Returns the entry centres (bottom face).\n */\nexport function exdBox(k: Kit, pos: V3, size: V3 = [0.18, 0.18, 0.12], entries = 2, used = 1, mat: MatKey = 'castIron'): V3[] {\n  const [w, h, d] = size\n  const [x, y, z] = pos\n  k.rbox(mat, [w, h, d * 0.62], 0.01, [x, y, z + d * 0.31])\n  k.rbox(mat, [w + 0.024, h + 0.024, d * 0.12], 0.006, [x, y, z + d * 0.68])\n  k.rbox(mat, [w, h, d * 0.26], 0.012, [x, y, z + d * 0.87])\n  // cover bolts round the flamepath flange\n  const nx = Math.max(2, Math.round(w / 0.05))\n  const ny = Math.max(2, Math.round(h / 0.05))\n  for (let i = 0; i <= nx; i++)\n    for (const s of [-1, 1]) k.cyl('stainless', 0.006, 0.006, [x - w / 2 + (w * i) / nx, y + s * (h / 2 + 0.006), z + d * 0.74 + 0.003], 'z', 8)\n  for (let i = 1; i < ny; i++)\n    for (const s of [-1, 1]) k.cyl('stainless', 0.006, 0.006, [x + s * (w / 2 + 0.006), y - h / 2 + (h * i) / ny, z + d * 0.74 + 0.003], 'z', 8)\n  // mounting lugs, earth stud\n  for (const s of [-1, 1]) {\n    k.box(mat, [0.03, 0.03, 0.012], [x + s * (w / 2 + 0.012), y + s * (h / 2 - 0.02), z + 0.006])\n    k.hex('nut', 0.014, 0.007, [x + s * (w / 2 + 0.014), y + s * (h / 2 - 0.02), z + 0.0155], 'z')\n  }\n  k.cyl('brass', 0.004, 0.025, [x + w / 2 + 0.012, y - h * 0.25, z + d * 0.35], 'x', 8)\n  k.hex('brass', 0.012, 0.006, [x + w / 2 + 0.018, y - h * 0.25, z + d * 0.35], 'x')\n  const out: V3[] = []\n  for (let i = 0; i < entries; i++) {\n    const ex = x + (entries === 1 ? 0 : -w * 0.3 + (w * 0.6 * i) / (entries - 1))\n    const p: V3 = [ex, y - h / 2, z + d * 0.31]\n    k.cyl(mat, 0.016, 0.008, [p[0], p[1] - 0.004, p[2]], 'y', 14)\n    if (i < used) {\n      k.hex('brass', 0.03, 0.014, [p[0], p[1] - 0.015, p[2]], 'y')\n      k.cyl('brass', 0.012, 0.02, [p[0], p[1] - 0.032, p[2]], 'y', 12)\n      k.cyl('rubberBlack', 0.0085, 0.012, [p[0], p[1] - 0.046, p[2]], 'y', 10)\n    } else stopPlug(k, [p[0], p[1] - 0.011, p[2]], '-y')\n    out.push([p[0], p[1] - 0.05, p[2]])\n  }\n  return out\n}\n"
    }
  ],
  "categories": [
    "safety"
  ],
  "meta": {
    "export": "FireExtinguisher",
    "anchor": "base",
    "standards": [
      "EN 3-7",
      "EN 1866-1",
      "ISO 7010",
      "NFPA 10"
    ]
  }
}