{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "safety-shower",
  "type": "registry:component",
  "title": "Safety shower and eyewash",
  "description": "Combination emergency shower and eyewash in safety green: column on a floor flange with flanged potable-water inlet, stay-open shower valve with triangular pull rod, perforated shower head, eyewash bowl with twin capped heads and push flag, foot treadle, flow switch, green alarm lamp and ISO 7010 E012 sign. headHeight is limited to the ANSI Z358.1 range.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "safety/safety-shower.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/safety/safety-shower.tsx",
      "content": "// Combination emergency shower and eyewash: green column on a floor flange, stay-open shower valve with pull rod, eyewash bowl with push flag, foot treadle, flow switch, alarm lamp and ISO 7010 sign. ANSI Z358.1, EN 15154.\nimport * as THREE from 'three'\nimport { Kit, type V3 } from '../lib/kit'\nimport { paint } from '../lib/materials'\nimport { KitMesh, useKit, type GroupProps } from '../lib/mesh'\nimport { flange, junctionBox, pipeRun, tagPlate } from '../lib/parts'\nimport { port, type Port } from '../lib/ports'\nimport { elbow90, pipeOD, type Rating } from '../lib/standards'\nimport { ICON, cable, signMat } from './_fg'\nimport { portBore } from './_valves'\n\nexport interface SafetyShowerProps {\n  /** supply size, inches */\n  nps?: number\n  rating?: Rating\n  /** shower head (spray) height above the deck, m; ANSI Z358.1 82–96 in */\n  headHeight?: number\n  foot?: boolean\n  alarm?: boolean\n  sign?: boolean\n  tag?: string\n}\n\nexport const defaults = { nps: 1.25, rating: 150 as Rating, headHeight: 2.2, foot: true, alarm: true, sign: true, tag: 'SS-9001' }\n\nconst GREEN = 'safetyGreen'\nconst LAMP = paint('#3cff7a', { emissive: '#22ff66', emissiveIntensity: 0.7, roughness: 0.2, transparent: true, opacity: 0.9 })\nconst YI = 0.35 // supply branch height, m\nconst XI = -0.3 // inlet flange face, m\n\nexport function ports(props: SafetyShowerProps = {}): Port[] {\n  const p = { ...defaults, ...props }\n  return [port('inlet', 'Potable water inlet', [XI, YI, 0], [-1, 0, 0], p.nps, p.rating, 'utility')]\n}\n\nexport function SafetyShower(props: SafetyShowerProps & GroupProps) {\n  const { nps, rating, headHeight, foot, alarm, sign, tag, ...group } = { ...defaults, ...props }\n  const h = Math.max(2.08, Math.min(2.44, headHeight))\n  const data = useKit((k) => build(k, nps, rating, h, foot, alarm, sign, tag), [nps, rating, h, foot, alarm, sign, tag])\n  return (\n    <group {...group}>\n      <KitMesh data={data} />\n    </group>\n  )\n}\n\nexport default SafetyShower\n\nfunction ballValve(k: Kit, p: V3, d: number, along: 'x' | 'z') {\n  k.cyl('stainless', d * 0.75, d * 2.2, p, along, 16)\n  k.sphere('stainless', d * 0.95, p, undefined, 16)\n}\n\nfunction build(k: Kit, nps: number, rating: Rating, h: number, foot: boolean, alarm: boolean, sign: boolean, tag: string) {\n  const d = pipeOD(nps)\n  const R = elbow90(nps)\n  const top = h + 0.14 // column top, arm level\n  const zh = 0.6 // shower head out from the column\n  // floor flange and column\n  k.cyl(GREEN, 0.13, 0.014, [0, 0.007, 0], 'y', 32)\n  k.cyl(GREEN, d / 2 + 0.02, 0.06, [0, 0.044, 0], 'y', 20)\n  k.around(4, 0.1, [0, 0.014, 0], 'y', (q) => k.hex('nut', 0.026, 0.012, [q[0], 0.02, q[2]], 'y'), Math.PI / 4)\n  k.pipe(GREEN, [0, 0.014, 0], [0, top - R, 0], d)\n  // supply branch: tee on the column, flanged inlet\n  k.cyl(GREEN, d / 2 + 0.006, d * 1.6, [0, YI, 0], 'y', 16)\n  const L = flange(k, nps, rating, [XI, YI, 0], '-x', true)\n  portBore(k, nps, rating, [XI, YI, 0], '-x')\n  k.pipe(GREEN, [XI + L, YI, 0], [-d / 2, YI, 0], d)\n  // top arm to the shower head with stay-open valve and pull rod\n  pipeRun(k, [[0, top - R - 0.001, 0], [0, top, 0], [0, top, zh], [0, top - 0.1, zh]], nps, { mat: GREEN })\n  ballValve(k, [0, top, 0.2], d, 'z')\n  k.box('stainless', [0.012, 0.012, 0.16], [0, top + d, 0.27])\n  const rodX = 0\n  k.rod('stainless', [rodX, top + d, 0.35], [rodX, 1.75, 0.35], 0.004, 6)\n  // triangular pull handle\n  k.tube('stainless', [[rodX, 1.75, 0.35], [rodX - 0.08, 1.62, 0.35], [rodX + 0.08, 1.62, 0.35], [rodX, 1.75, 0.35]], 0.007, 6, 0.01)\n  // shower head: bowl facing down with a perforated rose\n  const yH = top - 0.1\n  k.cyl(GREEN, d / 2 + 0.008, 0.03, [0, yH - 0.015, zh], 'y', 16)\n  k.lathe(GREEN, [[0.02, 0], [0.05, -0.01], [0.12, -0.06], [0.135, -0.08], [0.13, -0.085]], [0, yH - 0.02, zh], 'y', 36)\n  k.disc('stainless', 0.128, [0, yH - 0.104, zh], '-y', 36)\n  for (let i = 0; i < 18; i++) {\n    const a = (i / 18) * Math.PI * 2\n    for (const r of [0.04, 0.085]) k.disc('pipeBore', 0.005, [Math.cos(a) * r, yH - 0.1045, zh + Math.sin(a) * r], '-y', 6)\n  }\n\n  // eyewash: arm with push-flag valve, bowl, twin heads with dust caps, drain\n  const ye = 0.95\n  const ze = 0.36\n  pipeRun(k, [[0, ye - 0.15, 0], [0, ye - 0.15, ze], [0, ye - 0.02, ze]], 1, { mat: GREEN })\n  k.cyl(GREEN, d / 2 + 0.006, d * 1.6, [0, ye - 0.15, 0], 'y', 16)\n  ballValve(k, [0, ye - 0.15, 0.14], pipeOD(1), 'z')\n  // push flag on the valve stem\n  k.rod('stainless', [0, ye - 0.15, 0.14], [0.12, ye - 0.15, 0.14], 0.005, 6)\n  k.box(GREEN, [0.12, 0.09, 0.006], [0.18, ye - 0.12, 0.14])\n  k.plane(signMat(['PUSH'], '#237f52'), 0.11, 0.055, [0.18, ye - 0.12, 0.1435])\n  k.lathe('stainless', [[0.03, 0], [0.06, 0.005], [0.15, 0.06], [0.16, 0.08], [0.155, 0.082], [0.145, 0.062], [0.05, 0.012], [0.02, 0.01]], [0, ye - 0.08, ze], 'y', 40)\n  for (const s of [-1, 1]) {\n    k.rod('stainless', [0, ye - 0.02, ze], [s * 0.055, ye + 0.02, ze], 0.008, 8)\n    k.cyl(GREEN, 0.022, 0.03, [s * 0.055, ye + 0.035, ze], 'y', 16)\n    k.dome('safetyYellow', 0.024, [s * 0.055, ye + 0.05, ze], 'y', Math.PI / 2, 0.45, 16)\n  }\n  k.pipe(GREEN, [0, ye - 0.08, ze], [0, 0.0, ze], pipeOD(1.25))\n  k.cyl('castIron', 0.06, 0.01, [0, 0.005, ze], 'y', 20)\n\n  if (foot) {\n    // foot treadle on a pivot frame, linkage rod to the eyewash valve\n    k.box('galvanised', [0.08, 0.06, 0.04], [0.15, 0.03, 0.22])\n    k.rod('galvanised', [0.15, 0.05, 0.22], [0.15, 0.05, 0.42], 0.006, 8)\n    k.box(GREEN, [0.18, 0.012, 0.22], [0.15, 0.07, 0.42], new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), 0.12))\n    k.box('safetyYellow', [0.16, 0.002, 0.04], [0.15, 0.085, 0.5])\n    k.rod('stainless', [0.15, 0.08, 0.32], [0.15, ye - 0.15, 0.2], 0.005, 6)\n    k.rod('stainless', [0.15, ye - 0.15, 0.2], [0.12, ye - 0.15, 0.14], 0.005, 6)\n  }\n\n  // flow switch on the column, cable down; alarm lamp on the column cap\n  const fy = top - R - 0.25\n  junctionBox(k, [0, fy, d / 2], 'z', [0.12, 0.12, 0.08], 'stainless')\n  cable(k, [[0.034, fy - 0.08, d / 2 + 0.04], [0.04, fy - 0.2, d / 2 + 0.02], [d / 2 + 0.008, fy - 0.4, 0], [d / 2 + 0.008, 0.2, 0], [d / 2 + 0.2, 0.02, 0]])\n  k.cyl(GREEN, d / 2, 0.36, [0, top + 0.18, 0], 'y', 16)\n  k.cyl(GREEN, d / 2 + 0.006, 0.01, [0, top + 0.365, 0], 'y', 16)\n  if (alarm) {\n    k.cyl('castIron', 0.05, 0.05, [0, top + 0.395, 0], 'y', 20)\n    k.cyl(LAMP, 0.04, 0.08, [0, top + 0.46, 0], 'y', 20)\n    k.dome(LAMP, 0.04, [0, top + 0.5, 0], 'y', Math.PI / 2, 0.5, 20)\n    for (let i = 0; i < 4; i++) {\n      const a = (i / 4) * Math.PI * 2\n      k.rod('stainless', [Math.cos(a) * 0.046, top + 0.42, Math.sin(a) * 0.046], [Math.cos(a) * 0.046, top + 0.52, Math.sin(a) * 0.046], 0.003, 6)\n    }\n  }\n  if (sign) {\n    const ys = top + 0.22\n    k.box('stainless', [0.5, 0.25, 0.004], [0, ys, d / 2 + 0.012])\n    for (const s of [-1, 1]) k.box('stainless', [0.03, 0.02, 0.012], [s * 0.025, ys + s * 0.08, d / 2 + 0.004])\n    k.plane(signMat(['EMERGENCY', 'SHOWER &', 'EYEWASH'], '#237f52', '#ffffff', ICON.shower), 0.49, 0.245, [0, ys, d / 2 + 0.0155])\n  }\n  tagPlate(k, tag, [0, 0.6, d / 2 + 0.002], 'z', 0.08)\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": "SafetyShower",
    "anchor": "base",
    "standards": [
      "ANSI Z358.1",
      "EN 15154-1",
      "EN 15154-2",
      "ISO 7010"
    ]
  }
}