{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "aspirating-smoke-detector",
  "type": "registry:component",
  "title": "Aspirating smoke detector",
  "description": "Very early warning aspirating smoke detector for E-houses, LERs and switchrooms. Wall-mounted unit with status LEDs, smoke bar graph and airflow display, four pipe inlets (unused ones plugged), exhaust stub and field cable. Red 25 mm ABS sampling pipe rises to pipeHeight, sweeps into a horizontal run of length per branch with labelled sampling holes every spacing, push-fit sockets every 3 m, wall clips and a drilled end cap. Pipes are hollow. branches 2 adds a run the other way.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "safety/aspirating-smoke-detector.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/safety/aspirating-smoke-detector.tsx",
      "content": "// Aspirating smoke detector (ASD) for E-houses, LERs and switchrooms: wall-mounted detector unit with status display, red ABS sampling pipe network with labelled sampling holes, wall clips, sockets and drilled end caps, exhaust and field cable. EN 54-20, NFPA 72, BS 5839-1.\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 { tagPlate } from '../lib/parts'\nimport { canvasMat, cableToDeck, stopPlug } from './_fg'\n\nexport interface AspiratingSmokeDetectorProps {\n  /** detector unit centre above the floor, m */\n  height?: number\n  /** sampling pipe height under the ceiling, m */\n  pipeHeight?: number\n  /** horizontal pipe run per branch, m */\n  length?: number\n  /** sampling hole spacing, m */\n  spacing?: number\n  /** 1: one run to +X; 2: runs to +X and -X */\n  branches?: number\n  tag?: string\n}\n\nexport const defaults = { height: 1.5, pipeHeight: 3, length: 8, spacing: 2, branches: 1, tag: 'ASD-9101' }\n\nconst PIPE = paint('#c22a1f', { metalness: 0.05, roughness: 0.38, clearcoat: 0.4 })\nconst CASE = paint('#e7e7e2', { metalness: 0.05, roughness: 0.5 })\nconst OD = 0.025 // ABS sampling pipe, m\nconst WALL = 0.002\nconst RB = 0.06 // swept bend radius, m\nconst W = 0.35 // typical industrial ASD unit\nconst HU = 0.225 // typical\nconst DU = 0.13 // typical\nconst ZP = DU / 2 // pipe centreline off the wall, m\n\nexport function AspiratingSmokeDetector(props: AspiratingSmokeDetectorProps & GroupProps) {\n  const { height, pipeHeight, length, spacing, branches, tag, ...group } = { ...defaults, ...props }\n  const h = Math.max(0.8, height)\n  const c = Math.max(h + HU / 2 + 0.3, pipeHeight)\n  const data = useKit((k) => build(k, h, c, Math.max(0.5, length), Math.max(0.3, spacing), branches >= 2 ? 2 : 1, tag), [h, c, length, spacing, branches, tag])\n  return (\n    <group {...group}>\n      <KitMesh data={data} />\n    </group>\n  )\n}\n\nexport default AspiratingSmokeDetector\n\nfunction panelMat() {\n  return canvasMat('asd-panel', 512, 320, (g) => {\n    g.fillStyle = '#2b2f33'\n    g.fillRect(0, 0, 512, 320)\n    g.fillStyle = '#e7e7e2'\n    g.font = 'bold 26px Arial, sans-serif'\n    g.textBaseline = 'middle'\n    g.fillText('ASPIRATING SMOKE DETECTOR', 24, 30)\n    const leds: [string, string][] = [['FIRE 2', '#ff3b30'], ['FIRE 1', '#ff3b30'], ['ACTION', '#ffae1a'], ['ALERT', '#ffae1a'], ['FAULT', '#ffae1a'], ['POWER', '#2bff6e']]\n    leds.forEach(([t, col], i) => {\n      const y = 78 + i * 38\n      g.fillStyle = i === 5 ? col : '#4a4f55'\n      g.beginPath()\n      g.arc(40, y, 10, 0, Math.PI * 2)\n      g.fill()\n      g.fillStyle = '#d6d6d0'\n      g.font = '20px Arial, sans-serif'\n      g.fillText(t, 62, y)\n    })\n    // smoke level bar graph with alarm thresholds\n    for (let i = 0; i < 20; i++) {\n      g.fillStyle = i < 3 ? '#2bff6e' : '#3a3f45'\n      g.fillRect(200 + i * 14, 270 - i * 8, 10, 12 + i * 8)\n    }\n    g.fillStyle = '#d6d6d0'\n    g.font = '18px Arial, sans-serif'\n    g.fillText('SMOKE  %obs/m', 200, 300)\n    g.fillText('AIRFLOW  OK', 380, 300)\n  }, { emissive: 0.3 })\n}\n\nfunction labelMat() {\n  return canvasMat('asd-hole', 64, 256, (g) => {\n    g.fillStyle = '#ffffff'\n    g.fillRect(0, 0, 64, 256)\n    g.fillStyle = '#c22a1f'\n    g.fillRect(0, 0, 64, 14)\n    g.fillRect(0, 242, 64, 14)\n    g.translate(32, 128)\n    g.rotate(Math.PI / 2)\n    g.font = 'bold 26px Arial, sans-serif'\n    g.textAlign = 'center'\n    g.textBaseline = 'middle'\n    g.fillText('SAMPLING HOLE', 0, 0)\n  })\n}\n\n/** Wall clip: stand-off saddle from the wall (z = 0) to a pipe at (p) running along `along`. */\nfunction clip(k: Kit, p: V3, along: 'x' | 'y') {\n  const zb = p[2] - OD / 2 - 0.004\n  k.box('plasticGrey', along === 'x' ? [0.03, 0.03, zb] : [0.03, 0.03, zb], [p[0], p[1], zb / 2])\n  k.torus('plasticGrey', OD / 2 + 0.0025, 0.003, p, along, 6, 20)\n  k.cyl('nut', 0.005, 0.004, [p[0], p[1], zb + 0.0005], 'z', 8)\n}\n\nfunction socket(k: Kit, p: V3, axis: 'x' | 'y') {\n  k.cyl(PIPE, OD / 2 + 0.003, 0.05, p, axis, 24)\n}\n\nfunction build(k: Kit, h: number, c: number, L: number, s: number, branches: number, tag: string) {\n  // detector unit: wall bracket, case, front cover with display, inlets on top, exhaust under\n  k.box('stainless', [W - 0.04, HU + 0.06, 0.006], [0, h, 0.003])\n  for (const sx of [-1, 1]) for (const sy of [-1, 1]) k.hex('nut', 0.016, 0.007, [sx * (W / 2 - 0.035), h + sy * (HU / 2 + 0.018), 0.0095], 'z')\n  k.rbox(CASE, [W, HU, DU - 0.02], 0.012, [0, h, 0.006 + (DU - 0.02) / 2])\n  k.rbox(CASE, [W - 0.01, HU - 0.01, 0.02], 0.008, [0, h, DU - 0.004])\n  k.add(panelMat(), new THREE.PlaneGeometry(0.2, 0.125), mat4([-0.05, h + 0.02, DU + 0.0065]))\n  k.box('glass', [0.21, 0.135, 0.002], [-0.05, h + 0.02, DU + 0.007])\n  for (let i = 0; i < 4; i++) k.cyl('nut', 0.005, 0.004, [(i < 2 ? -1 : 1) * (W / 2 - 0.02), h + (i % 2 ? 1 : -1) * (HU / 2 - 0.02), DU + 0.007], 'z', 10)\n  tagPlate(k, tag, [0.11, h + 0.06, DU + 0.0065], 'z', 0.09)\n  k.box('nameplate', [0.08, 0.04, 0.002], [0.11, h - 0.04, DU + 0.006])\n  const inlets = [-0.12, -0.04, 0.04, 0.12]\n  const used = branches === 2 ? [0.12, -0.12] : [0.12]\n  const yt = h + HU / 2\n  for (const x of inlets) {\n    k.cyl(CASE, OD / 2 + 0.006, 0.02, [x, yt + 0.01, ZP], 'y', 20)\n    if (!used.includes(x)) k.cyl('rubberBlack', OD / 2 + 0.002, 0.012, [x, yt + 0.026, ZP], 'y', 16)\n  }\n  // exhaust port: short open pipe stub pointing down\n  k.cyl(CASE, OD / 2 + 0.006, 0.02, [0.1, h - HU / 2 - 0.01, ZP], 'y', 20)\n  k.pipe(PIPE, [0.1, h - HU / 2 - 0.005, ZP], [0.1, h - HU / 2 - 0.12, ZP], OD, WALL, 24)\n  // field cable entry\n  k.hex('brass', 0.026, 0.014, [-0.1, h - HU / 2 - 0.007, ZP], 'y')\n  stopPlug(k, [-0.04, h - HU / 2 - 0.004, ZP], '-y', 0.024)\n  cableToDeck(k, 'wall', [-0.1, h - HU / 2 - 0.014, ZP])\n\n  const lab = labelMat()\n  for (const x0 of used) {\n    const dir = x0 > 0 ? 1 : -1\n    const yb = c - RB\n    // riser to the ceiling run, swept bend, horizontal run with sockets every 3 m\n    k.pipe(PIPE, [x0, yt + 0.005, ZP], [x0, yb, ZP], OD, WALL, 24)\n    socket(k, [x0, yt + 0.045, ZP], 'y')\n    k.bend(PIPE, [x0, yb, ZP], [0, 1, 0], [dir, 0, 0], RB, OD, Math.PI / 2, WALL)\n    for (let y = yt + 0.4; y < yb - 0.1; y += 1.5) clip(k, [x0, y, ZP], 'y')\n    const xs = x0 + dir * RB\n    const xe = xs + dir * L\n    k.pipe(PIPE, [xs, c, ZP], [xe, c, ZP], OD, WALL, 24)\n    for (let d = 3; d < L - 0.1; d += 3) socket(k, [xs + dir * d, c, ZP], 'x')\n    for (let d = 0.75; d < L - 0.1; d += 1.5) clip(k, [xs + dir * d, c, ZP], 'x')\n    // sampling holes on the underside, each with a wrap-round label\n    for (let d = s; d < L - 0.05; d += s) {\n      const x = xs + dir * d\n      k.disc('pipeBore', 0.0018, [x, c - OD / 2 - 0.0002, ZP], '-y', 10)\n      k.add(lab, new THREE.CylinderGeometry(OD / 2 + 0.0004, OD / 2 + 0.0004, 0.06, 24, 1, true, -1.3, 2.6), mat4([x + dir * 0.04, c, ZP], 'x'))\n    }\n    // drilled end cap\n    k.cyl(PIPE, OD / 2 + 0.003, 0.03, [xe + dir * 0.01, c, ZP], 'x', 24)\n    k.disc(PIPE, OD / 2 + 0.003, [xe + dir * 0.025, c, ZP], dir > 0 ? 'x' : '-x', 24)\n    k.disc('pipeBore', 0.003, [xe + dir * 0.0252, c, ZP], dir > 0 ? 'x' : '-x', 12)\n  }\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": "AspiratingSmokeDetector",
    "anchor": "base",
    "standards": [
      "EN 54-20",
      "NFPA 72",
      "BS 5839-1"
    ]
  }
}