{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "fire-monitor",
  "type": "registry:component",
  "title": "Fire monitor",
  "description": "Fixed firewater monitor on a red riser: flanged side inlet with open bore, long-radius elbow on a dummy-leg foot plate, low-point drain, locked-open OS&Y isolation valve, optional inline water-powered oscillator (flanged both ends, drive housing with speed and arc controls, manual override), slewing turret with lock handwheels, elevating gooseneck (80° up to 15° down) and hollow fog nozzle with bumper and tiller.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "safety/fire-monitor.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/safety/fire-monitor.tsx",
      "content": "// Fixed fire monitor on a firewater riser: flanged inlet and long-radius elbow on a dummy-leg foot plate, OS&Y isolation valve, slewing turret, elevating gooseneck, fog nozzle with tiller and optional water-powered oscillator. EN 15767, NFPA 15, ASME B16.5.\nimport * as THREE from 'three'\nimport { Kit, type V3 } from '../lib/kit'\nimport { KitMesh, useKit, type GroupProps } from '../lib/mesh'\nimport { flange, pipe, tagPlate, weldBead } from '../lib/parts'\nimport { port, type Port } from '../lib/ports'\nimport { GASKET_SPIRAL_WOUND, elbow90, faceToFace, flange as flangeDims, pipeOD, type Rating } from '../lib/standards'\nimport { deg } from './_fg'\nimport { boltUp, osyGate, portBore } from './_valves'\n\nexport interface FireMonitorProps {\n  /** riser size, inches */\n  nps?: number\n  rating?: Rating\n  /** monitor base flange above the deck, m */\n  height?: number\n  /** turret rotation about the vertical, degrees (0 = nozzle to +Z) */\n  slew?: number\n  /** nozzle elevation above horizontal, degrees */\n  elevation?: number\n  oscillating?: boolean\n  tag?: string\n}\n\nexport const defaults = { nps: 4, rating: 150 as Rating, height: 1.5, slew: 0, elevation: 20, oscillating: true, tag: 'FM-7101' }\n\nconst RED = 'firewaterRed'\nconst G = GASKET_SPIRAL_WOUND\nconst OSC_H = 0.2 // typical inline water-powered oscillator height, m\n\nfunction layout(nps: number, rating: Rating, height: number) {\n  const D = pipeOD(nps)\n  const f = flangeDims(nps, rating)\n  const R = elbow90(nps)\n  const yi = Math.max(0.35, f.od / 2 + 0.15)\n  const xi = -R - Math.max(0.25, D * 2)\n  const Lg = faceToFace('gate', nps, rating)\n  const yv0 = yi + R + 0.2 // valve bottom face\n  const yTop = Math.max(height, yv0 + Lg + G + 0.35)\n  return { D, f, R, yi, xi, Lg, yv0, yTop }\n}\n\nexport function ports(props: FireMonitorProps = {}): Port[] {\n  const p = { ...defaults, ...props }\n  const { yi, xi } = layout(p.nps, p.rating, p.height)\n  return [port('inlet', 'Firewater inlet', [xi, yi, 0], [-1, 0, 0], p.nps, p.rating)]\n}\n\nexport function FireMonitor(props: FireMonitorProps & GroupProps) {\n  const { nps, rating, height, slew, elevation, oscillating, tag, ...group } = { ...defaults, ...props }\n  const L = layout(nps, rating, height)\n  const Dw = pipeOD(Math.min(nps, 4))\n  const y0 = L.yTop + G + 0.1 + (oscillating ? OSC_H : 0)\n  const fixed = useKit((k) => build(k, nps, rating, height, oscillating, tag), [nps, rating, height, oscillating, tag])\n  const turret = useKit((k) => buildTurret(k, Dw), [Dw])\n  const nozzle = useKit((k) => buildNozzle(k, Dw), [Dw])\n  const { y1, Rw, xs } = turretDims(Dw)\n  return (\n    <group {...group}>\n      <KitMesh data={fixed} />\n      <group position={[0, y0, 0]} rotation-y={deg(slew)}>\n        <KitMesh data={turret} />\n        <group position={[xs, y1 + Rw, 0]} rotation-x={-deg(Math.max(-15, Math.min(80, elevation)))}>\n          <KitMesh data={nozzle} />\n        </group>\n      </group>\n    </group>\n  )\n}\n\nexport default FireMonitor\n\nfunction build(k: Kit, nps: number, rating: Rating, height: number, osc: boolean, tag: string) {\n  const { D, f, R, yi, xi, Lg, yv0, yTop } = layout(nps, rating, height)\n  // inlet spool, LR elbow, dummy leg on a foot plate\n  const a = flange(k, nps, rating, [xi, yi, 0], '-x', true)\n  portBore(k, nps, rating, [xi, yi, 0], '-x')\n  pipe(k, [xi + a, yi, 0], [-R, yi, 0], nps, RED)\n  weldBead(k, nps, [xi + a, yi, 0], 'x')\n  k.bend(RED, [-R, yi, 0], [1, 0, 0], [0, 1, 0], R, D)\n  weldBead(k, nps, [-R, yi, 0], 'x')\n  weldBead(k, nps, [0, yi + R, 0], 'y')\n  const xl = -R * 0.3\n  const yl = yi + 0.28 * R - D * 0.42\n  pipe(k, [xl, 0.02, 0], [xl, yl, 0], nps, RED)\n  k.cyl(RED, D / 2 + 0.002, 0.006, [xl, yl, 0], 'y', 24)\n  k.box('galvanised', [D + 0.2, 0.02, D + 0.2], [xl, 0.01, 0])\n  for (const sx of [-1, 1]) for (const sz of [-1, 1]) {\n    k.cyl('galvanised', 0.009, 0.05, [xl + sx * (D / 2 + 0.06), 0.035, sz * (D / 2 + 0.06)], 'y', 8)\n    k.hex('nut', 0.032, 0.015, [xl + sx * (D / 2 + 0.06), 0.028, sz * (D / 2 + 0.06)], 'y')\n  }\n  // low-point drain valve under the inlet spool\n  const xd = (xi + a - R) / 2\n  k.cyl(RED, 0.016, 0.06, [xd, yi - D / 2 - 0.02, 0], 'y', 10)\n  k.box('brass', [0.045, 0.04, 0.04], [xd, yi - D / 2 - 0.07, 0])\n  k.box(RED, [0.008, 0.012, 0.1], [xd, yi - D / 2 - 0.07, 0.05])\n  // riser: elbow → valve → spool → monitor flange\n  const b = flange(k, nps, rating, [0, yv0 - G / 2, 0], 'y')\n  pipe(k, [0, yi + R, 0], [0, yv0 - G / 2 - b, 0], nps, RED)\n  boltUp(k, nps, rating, [0, yv0, 0], 'y')\n  const yv = yv0 + G / 2 + Lg / 2\n  // valve along +Y, stem towards +Z\n  k.with(new THREE.Matrix4().makeBasis(new THREE.Vector3(0, 1, 0), new THREE.Vector3(0, 0, 1), new THREE.Vector3(1, 0, 0)).setPosition(0, yv, 0), () => osyGate(k, nps, rating))\n  const y2 = yv + Lg / 2 + G / 2\n  boltUp(k, nps, rating, [0, y2, 0], 'y')\n  const c0 = flange(k, nps, rating, [0, y2 + G / 2, 0], '-y')\n  const c1 = flange(k, nps, rating, [0, yTop, 0], 'y')\n  pipe(k, [0, y2 + G / 2 + c0, 0], [0, yTop - c1, 0], nps, RED)\n  weldBead(k, nps, [0, yTop - c1, 0], 'y')\n  boltUp(k, nps, rating, [0, yTop + G / 2, 0], 'y')\n  tagPlate(k, tag, [0, (y2 + yTop) / 2, D / 2 + 0.004], 'z', 0.09)\n  // water-powered oscillator between the riser flange and the monitor (flanged both ends), then the monitor base\n  const Dw = pipeOD(Math.min(nps, 4))\n  const ft = f.thickness + f.raisedFaceHeight\n  let yb = yTop + G\n  if (osc) {\n    k.cyl('stainless', f.od / 2, ft, [0, yb + ft / 2, 0], 'y', 32)\n    k.cyl('stainless', Dw * 0.72, OSC_H - 2 * ft, [0, yb + OSC_H / 2, 0], 'y', 28)\n    k.cyl('stainless', f.od / 2, ft, [0, yb + OSC_H - ft / 2, 0], 'y', 32)\n    k.around(f.bolts, f.boltCircle / 2, [0, yb + OSC_H, 0], 'y', (q) => k.hex('nut', f.boltDia * 1.85, f.boltDia * 0.9, [q[0], q[1] + f.boltDia * 0.45, q[2]], 'y'), Math.PI / f.bolts)\n    // drive housing with speed and arc controls, manual override lever\n    k.rbox('stainless', [0.11, OSC_H - 2 * ft - 0.01, 0.12], 0.012, [-Dw * 0.72 - 0.04, yb + OSC_H / 2, 0])\n    for (const dz of [-0.03, 0.03]) k.cyl('castIron', 0.012, 0.02, [-Dw * 0.72 - 0.1, yb + OSC_H / 2, dz], 'x', 12)\n    k.rod('stainless', [Dw * 0.72, yb + OSC_H / 2, 0], [Dw * 0.72 + 0.1, yb + OSC_H / 2 + 0.04, 0], 0.006, 8)\n    k.sphere('rubberBlack', 0.014, [Dw * 0.72 + 0.1, yb + OSC_H / 2 + 0.04, 0], undefined, 10)\n    yb += OSC_H\n  }\n  k.cyl(RED, f.od / 2, ft, [0, yb + ft / 2, 0], 'y', 32)\n  k.cyl(RED, Dw * 0.62, 0.1 - f.thickness, [0, yb + f.thickness + (0.1 - f.thickness) / 2, 0], 'y', 28)\n  k.torus('castIron', Dw * 0.64, 0.006, [0, yb + 0.085, 0], 'y', 6, 28)\n  k.cyl('brass', 0.005, 0.02, [Dw * 0.62 + 0.008, yb + 0.06, 0], 'x', 8)\n}\n\nfunction turretDims(Dw: number) {\n  const y1 = 0.13\n  const Rw = Dw * 1.5\n  const xs = Rw + 0.06\n  return { y1, Rw, xs }\n}\n\n/** Slewing body, origin on the turret axis at the slew bearing. */\nfunction buildTurret(k: Kit, Dw: number) {\n  const { y1, Rw, xs } = turretDims(Dw)\n  k.cyl(RED, Dw * 0.66, 0.05, [0, 0.025, 0], 'y', 28)\n  for (let i = 0; i < 8; i++) {\n    const a = (i / 8) * Math.PI * 2\n    k.hex('nut', 0.016, 0.008, [Math.cos(a) * Dw * 0.56, 0.054, Math.sin(a) * Dw * 0.56], 'y')\n  }\n  k.cyl(RED, Dw * 0.5, y1 - 0.05, [0, 0.05 + (y1 - 0.05) / 2, 0], 'y', 24)\n  k.bend(RED, [0, y1, 0], [0, 1, 0], [1, 0, 0], Rw, Dw)\n  k.pipe(RED, [Rw, y1 + Rw, 0], [xs - 0.02, y1 + Rw, 0], Dw)\n  k.cyl(RED, Dw * 0.62, 0.04, [xs - 0.02, y1 + Rw, 0], 'x', 24)\n  // slew lock handwheel\n  k.rod('stainless', [0, 0.03, Dw * 0.66], [0, 0.03, Dw * 0.66 + 0.06], 0.006, 8)\n  k.handwheel('handwheel', 0.11, [0, 0.03, Dw * 0.66 + 0.06], 'z', 3)\n  // elevation lock handwheel on the far side of the swivel\n  k.rod('stainless', [xs, y1 + Rw, -Dw * 0.62], [xs, y1 + Rw, -Dw * 0.62 - 0.05], 0.006, 8)\n  k.handwheel('handwheel', 0.11, [xs, y1 + Rw, -Dw * 0.62 - 0.05], '-z', 3)\n}\n\n/** Elevating barrel and nozzle; origin at the elevation swivel, nozzle along +Z at zero elevation. */\nfunction buildNozzle(k: Kit, Dw: number) {\n  const Rw = Dw * 1.5\n  k.cyl(RED, Dw * 0.62, 0.04, [0.02, 0, 0], 'x', 24)\n  k.torus('castIron', Dw * 0.63, 0.005, [0.0, 0, 0], 'x', 6, 24)\n  k.pipe(RED, [0.04, 0, 0], [0.06, 0, 0], Dw)\n  k.bend(RED, [0.06, 0, 0], [1, 0, 0], [0, 0, 1], Rw, Dw)\n  const xn = 0.06 + Rw\n  const z0 = Rw\n  k.cyl(RED, Dw / 2, 0.06, [xn, 0, z0 + 0.03], 'z', 24)\n  k.cyl(RED, Dw * 0.6, 0.025, [xn, 0, z0 + 0.07], 'z', 24)\n  k.lathe('stainless', [[Dw * 0.5, 0], [Dw * 0.5, 0.08], [Dw * 0.4, 0.26], [Dw * 0.42, 0.27], [Dw * 0.42, 0.29]], [xn, 0, z0 + 0.08], 'z', 28)\n  // fog nozzle: pattern ring and rubber bumper\n  k.lathe('brass', [[Dw * 0.42, 0], [Dw * 0.6, 0.02], [Dw * 0.6, 0.12], [Dw * 0.5, 0.14], [Dw * 0.3, 0.14]], [xn, 0, z0 + 0.37], 'z', 28)\n  for (let i = 0; i < 6; i++) {\n    const a = (i / 6) * Math.PI * 2\n    k.box('brass', [0.012, 0.012, 0.08], [xn + Math.cos(a) * Dw * 0.62, Math.sin(a) * Dw * 0.62, z0 + 0.44])\n  }\n  k.torus('rubberBlack', Dw * 0.56, 0.018, [xn, 0, z0 + 0.51], 'z', 10, 28)\n  k.lathe('pipeBore', [[Dw * 0.3, 0.14], [Dw * 0.3, -0.3]], [xn, 0, z0 + 0.37], 'z', 20)\n  // tiller handle with grip\n  const g: V3 = [xn, 0.2, -0.32]\n  k.rod('stainless', [xn, Dw * 0.5, z0 + 0.12], [xn, 0.16, z0 - 0.05], 0.012, 10)\n  k.rod('stainless', [xn, 0.16, z0 - 0.05], g, 0.012, 10)\n  k.cyl('rubberBlack', 0.017, 0.12, [g[0], g[1] + 0.012, g[2] - 0.045], [0, 0.25, -1], 10)\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"
    },
    {
      "path": "safety/_valves.ts",
      "type": "registry:file",
      "target": "components/deck-twin/safety/_valves.ts",
      "content": "// Firewater isolation valve built into a Kit: flanged OS&Y gate valve (rising stem, handwheel, locked-open chain) along local X, stem up local Y.\nimport * as THREE from 'three'\nimport { Kit, axisVec, mat4, type Axis, type MatKey, type V3 } from '../lib/kit'\nimport { gasket } from '../lib/parts'\nimport { GASKET_SPIRAL_WOUND, faceToFace, flange as flangeDims, handwheelDia, pipeOD, stemHeight, type Rating } from '../lib/standards'\n\n/** OS&Y gate valve centred on the origin of the current Kit frame; ends at x = ±faceToFace/2. */\nexport function osyGate(k: Kit, nps: number, rating: Rating, body: MatKey = 'firewaterRed') {\n  const D = pipeOD(nps)\n  const f = flangeDims(nps, rating)\n  const L = faceToFace('gate', nps, rating)\n  const neck = (D / 2) * 1.12\n  for (const s of [-1, 1]) {\n    const x = (s * L) / 2\n    k.at([x, 0, 0], s > 0 ? 'x' : '-x', () => {\n      const yb = -f.raisedFaceHeight - f.thickness\n      k.lathe(body, [[neck, yb - 0.003], [neck + 0.003, yb], [f.od / 2, yb], [f.od / 2, -f.raisedFaceHeight], [f.raisedFace / 2, -f.raisedFaceHeight]], [0, 0, 0], 'y', 32)\n      k.lathe('machined', [[f.raisedFace / 2, -f.raisedFaceHeight], [f.raisedFace / 2, 0], [D / 2 * 0.85, 0]], [0, 0, 0], 'y', 32)\n    })\n  }\n  k.cyl(body, neck, L - 2 * (f.raisedFaceHeight + f.thickness) + 0.004, [0, 0, 0], 'x', 28)\n  const rx = Math.min(D * 0.55 + 0.012, L / 2 - f.raisedFaceHeight - f.thickness)\n  const rz = D * 0.66 + 0.014\n  const yTop = D * 0.85 + 0.02\n  const yBot = -D * 0.5\n  k.add(body, new THREE.CylinderGeometry(1, 1, yTop - yBot, 32), mat4([0, (yTop + yBot) / 2, 0], 'y', [rx, 1, rz]))\n  k.sphere(body, 1, [0, yBot, 0], [rx, D * 0.25, rz], 24)\n  // bonnet flange and studs\n  const t = Math.max(0.01, D * 0.11)\n  const bd = Math.max(0.01, Math.min(0.032, f.boltDia * 0.9))\n  const brx = rx + f.boltDia * 2.4\n  const brz = rz + f.boltDia * 2.4\n  k.add(body, new THREE.CylinderGeometry(1, 1, 2 * t, 32), mat4([0, yTop, 0], 'y', [brx, 1, brz]))\n  const n = nps >= 8 ? 12 : 8\n  for (let i = 0; i < n; i++) {\n    const a = ((i + 0.5) / n) * Math.PI * 2\n    const x = Math.cos(a) * (brx - bd * 1.6)\n    const z = Math.sin(a) * (brz - bd * 1.6)\n    k.cyl('boltXylan', bd / 2, 2 * t + bd * 2.2, [x, yTop, z], 'y', 6)\n    k.hex('nut', bd * 1.85, bd * 0.9, [x, yTop + t + bd * 0.45, z], 'y')\n  }\n  // bonnet, yoke, stem, handwheel\n  const H = stemHeight('gate', nps, rating)\n  const hB = Math.max(0.06, D * 1.0 + 0.03)\n  const stemR = Math.max(0.007, D * 0.085)\n  const y0 = yTop + t\n  k.add(body, new THREE.CylinderGeometry(1, 1, hB, 28), mat4([0, y0 + hB / 2, 0], 'y', [Math.max(stemR * 2.2, rx * 0.7), 1, Math.max(stemR * 2.2, rz * 0.62)]))\n  const yB = y0 + hB\n  const yT = Math.max(yB + 0.1, H * 0.78)\n  const legX = stemR * 2.4 + Math.max(0.02, D * 0.22)\n  const legW = Math.max(0.014, D * 0.16)\n  const legT = Math.max(0.01, D * 0.09)\n  k.cyl(body, stemR * 2.6, 0.02, [0, yB + 0.01, 0], 'y', 16)\n  for (const s of [-1, 1]) k.box(body, [legW, yT - yB, legT], [s * legX, (yB + yT) / 2, 0])\n  k.box(body, [legX * 2 + legW, legT * 1.6, legT * 2.2], [0, yT, 0])\n  k.cyl(body, stemR * 2.5, legT * 3, [0, yT, 0], 'y', 16)\n  k.cyl('stainless', stemR, H - yB, [0, (H + yB) / 2, 0], 'y', 12)\n  const hw = Math.min(0.75, handwheelDia('gate', nps, rating))\n  k.handwheel('handwheel', hw, [0, yT + legT * 1.5 + 0.01, 0], 'y', hw > 0.35 ? 6 : 4)\n  // locked-open chain and padlock through the rim\n  const r = hw / 2\n  const yh = yT + legT * 1.5 + 0.01\n  k.tube('galvanised', [[r * 0.7, yh, r * 0.7], [legX + legW, (yh + yB) / 2, legT], [legX + legW / 2, yB + 0.04, legT / 2]], 0.004, 4)\n  k.box('brass', [0.035, 0.04, 0.015], [legX + legW + 0.01, (yh + yB) / 2 - 0.03, legT])\n  return L\n}\n\n/** Gasket and stud bolts across two mating flanges whose faces meet at `pos` (flanges drawn separately). */\nexport function boltUp(k: Kit, nps: number, rating: Rating, pos: V3, axis: Axis) {\n  const f = flangeDims(nps, rating)\n  const v = axisVec(axis)\n  gasket(k, nps, rating, pos, axis)\n  const L = 2 * (f.thickness + f.raisedFaceHeight) + GASKET_SPIRAL_WOUND + f.boltDia * 3\n  const start = new THREE.Vector3(...pos).addScaledVector(v, -L / 2).toArray() as V3\n  k.boltCircle('boltXylan', 'nut', f.bolts, f.boltCircle / 2, start, axis, f.boltDia, L, [f.boltDia * 0.95, L - f.boltDia * 0.95])\n}\n\n/** Bore lining inside a port flange (lib flanges have no inner hub surface), face at `face`, `axis` pointing out. */\nexport function portBore(k: Kit, nps: number, rating: Rating, face: V3, axis: Axis) {\n  const f = flangeDims(nps, rating)\n  const ri = (pipeOD(nps) / 2) * 0.88\n  k.lathe('pipeBore', [[ri, 0.0005], [ri, -(f.hubLength + f.raisedFaceHeight + 0.01)]], face, axis, 24)\n}\n"
    }
  ],
  "categories": [
    "safety"
  ],
  "meta": {
    "export": "FireMonitor",
    "anchor": "base",
    "standards": [
      "EN 15767",
      "NFPA 15",
      "ASME B16.5",
      "ASME B16.9"
    ]
  }
}