# IR3 flame detector

Triple-IR flame detector in a red Ex d housing with three sensor windows, optical-integrity/BIT lamp and weather hood (hollow, closed edges), on a stainless ball-swivel mount. Default aim 20° down, as manufacturers advise 10–20° so rain runs off the window. One cable gland and one stopping plug, earth stud. Free-standing stanchion or wall bracket.

**Standards:** EN 54-10, FM 3260, IEC 60079-1

## Installation

```bash
npx -y https://decktwin.com/cli/deck-twin-0.2.2.tgz add flame-detector
```

Copies `safety/flame-detector.tsx`, any helper files it imports and the shared `lib/` into `src/components/deck-twin/`. Needs `three`, `@react-three/fiber` and `@react-three/drei`.

## Usage

```tsx
import { Canvas } from '@react-three/fiber'
import { OrbitControls } from '@react-three/drei'
import { Stage } from './components/deck-twin/lib/stage'
import { FlameDetector } from './components/deck-twin/safety/flame-detector'

export default function Scene() {
  return (
    <Canvas shadows camera={{ position: [8, 5, 9], fov: 35 }}>
      <Stage>
        <FlameDetector />
      </Stage>
      <OrbitControls makeDefault />
    </Canvas>
  )
}
```

## Props

| Prop | Type | Default | Range |
| --- | --- | --- | --- |
| `mount` | `"pole" \| "wall"` | `"pole"` |  |
| `height` | `number (m)` | `2.5` | 0.8 – 5 |
| `tilt` | `number (°)` | `20` | -10 – 70 |
| `pan` | `number (°)` | `0` | -180 – 180 |
| `hood` | `boolean` | `true` |  |
| `tag` | `string` | `"FD-2105"` |  |
| `...group` | position, rotation, scale… |  |  |

## P&ID

A detector symbol (circle with F or FD, or a small triangle) with a wedge or cone drawn from it showing the field of view, aimed into the area it covers; the tilt angle is often written beside it.

tag FD · tag IR3 · tag UV/IR · ISA BE · ISA BT · F&G layout convention (NFPA 170 / ISO 7240-style plan symbols; legends vary by company); cone of vision per NORSOK Z-001 / ISA TR84.00.07 mapping practice

The cone of vision is the giveaway. A pair of symbols joined by a beam line is an open-path gas detector. ISA uses B (burner / combustion) for flame: BE / BT / BSH on some P&IDs.

Flame detector datasheet (EN 54-10 / FM 3260) fields and the props they set:

| Field | Prop |
| --- | --- |
| Mounting | `mount` |
| Mounting height | `height` |
| Tilt (down) | `tilt` |
| Pan | `pan` |
| Weather hood | `hood` |
| Tag number | `tag` |

## Typical suppliers

Det-Tronics · Honeywell · Spectrex · MSA. The geometry is generic and sized from public standards. It is not vendor CAD.

## Source

`safety/flame-detector.tsx`. This is the component file only: it imports shared code, so install it with the command above rather than pasting it alone. https://decktwin.com/r/flame-detector.json has every file.

```tsx
// IR3 flame detector: Ex d housing with three IR windows and BIT lamp, weather hood, on a ball-swivel mount aimed by tilt and pan, on a stanchion or wall bracket. EN 54-10, FM 3260.
import * as THREE from 'three'
import { Kit, type V3 } from '../lib/kit'
import { KitMesh, useKit, type GroupProps } from '../lib/mesh'
import { tagPlate } from '../lib/parts'
import { LED_GREEN, LENS, cableToDeck, deg, mounted, stopPlug, type Mount } from './_fg'

export interface FlameDetectorProps {
  mount?: Mount
  /** swivel pivot above the deck, m */
  height?: number
  /** aim below horizontal, degrees */
  tilt?: number
  /** aim about the vertical, degrees (0 = +Z, positive turns towards +X) */
  pan?: number
  hood?: boolean
  tag?: string
}

export const defaults = { mount: 'pole' as Mount, height: 2.5, tilt: 20, pan: 0, hood: true, tag: 'FD-2105' }

export function FlameDetector(props: FlameDetectorProps & GroupProps) {
  const { mount, height, tilt, pan, hood, tag, ...group } = { ...defaults, ...props }
  const data = useKit((k) => build(k, mount, Math.max(0.8, height), tilt, mount === 'wall' ? Math.max(-70, Math.min(70, pan)) : pan, hood, tag), [mount, height, tilt, pan, hood, tag])
  return (
    <group {...group}>
      <KitMesh data={data} />
    </group>
  )
}

export default FlameDetector

function build(k: Kit, mount: Mount, height: number, tilt: number, pan: number, hood: boolean, tag: string) {
  const ballZ = 0.06
  let gland = new THREE.Vector3()
  const zf = mounted(k, mount, height, [0.1, 0.18], () => {
    // swivel arm, clamp block and ball joint
    k.box('stainless', [0.05, 0.05, 0.014], [0, 0, 0.007])
    for (const sx of [-1, 1]) for (const sy of [-1, 1]) k.hex('nut', 0.012, 0.006, [sx * 0.018, sy * 0.018, 0.017], 'z')
    k.rod('stainless', [0, 0, 0.014], [0, 0, ballZ], 0.013, 12)
    k.sphere('stainless', 0.021, [0, 0, ballZ], undefined, 14)
    k.cyl('stainless', 0.026, 0.012, [0, 0, ballZ - 0.012], 'z', 12)
    k.hex('nut', 0.022, 0.012, [0.028, 0, ballZ - 0.012], 'x')
    tagPlate(k, tag, [0, -0.07, 0.001], 'z', 0.08)
    const F = new THREE.Matrix4()
      .makeTranslation(0, 0, ballZ)
      .multiply(new THREE.Matrix4().makeRotationY(deg(pan)))
      .multiply(new THREE.Matrix4().makeRotationX(deg(tilt)))
    k.with(F, () => {
      k.rod('stainless', [0, 0, 0], [0, 0.024, 0], 0.01, 10)
      k.hex('nut', 0.03, 0.012, [0, 0.01, 0], 'y')
      k.box('stainless', [0.05, 0.012, 0.08], [0, 0.026, 0.015])
      k.with(new THREE.Matrix4().makeTranslation(0, 0.097, -0.06), () => housing(k, hood))
    })
    gland = new THREE.Vector3(0, 0.097 - 0.082, -0.02).applyMatrix4(F)
  })
  const g: V3 = [gland.x, gland.y + height, gland.z + zf]
  cableToDeck(k, mount, g)
}

/** Housing along +Z from z = 0 (rear) to the window at z ≈ 0.15. */
function housing(k: Kit, hood: boolean) {
  k.cyl('actuatorRed', 0.058, 0.02, [0, 0, 0.01], 'z', 24)
  k.cyl('actuatorRed', 0.065, 0.11, [0, 0, 0.075], 'z', 28)
  k.torus('actuatorRed', 0.065, 0.004, [0, 0, 0.03], 'z', 6, 28)
  k.cyl('stainless', 0.068, 0.018, [0, 0, 0.139], 'z', 28)
  k.disc(LENS, 0.058, [0, 0, 0.1485], 'z', 28)
  for (let i = 0; i < 3; i++) {
    const a = Math.PI / 2 + (i * 2 * Math.PI) / 3
    const p: V3 = [Math.cos(a) * 0.03, Math.sin(a) * 0.03, 0.149]
    k.cyl('stainless', 0.015, 0.004, p, 'z', 16)
    k.dome(LENS, 0.012, [p[0], p[1], 0.151], 'z', Math.PI / 2, 0.55, 14)
  }
  k.sphere(LED_GREEN, 0.006, [0, 0, 0.151], undefined, 10)
  k.dome('glass', 0.054, [0, 0, 0.149], 'z', Math.PI / 2, 0.22, 24)
  for (let i = 0; i < 4; i++) {
    const a = Math.PI / 4 + (i * Math.PI) / 2
    k.hex('nut', 0.01, 0.005, [Math.cos(a) * 0.06, Math.sin(a) * 0.06, 0.1505], 'z')
  }
  // weather hood over the top of the window (sunshade)
  if (hood) {
    k.lathe('actuatorRed', [[0.07, 0.1], [0.075, 0.1], [0.075, 0.205], [0.07, 0.205], [0.07, 0.1]], [0, 0, 0], 'z', 20, 0.4 * Math.PI, 1.2 * Math.PI)
    // close the two cut edges of the hood shell
    for (const phi of [0.4 * Math.PI, 1.6 * Math.PI]) {
      const q = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), Math.atan2(-Math.cos(phi), Math.sin(phi)))
      k.box('actuatorRed', [0.005, 0.0016, 0.105], [0.0725 * Math.sin(phi), -0.0725 * Math.cos(phi), 0.1525], q)
    }
  }
  k.box('nameplate', [0.002, 0.03, 0.05], [0.066, 0, 0.075])
  k.hex('brass', 0.03, 0.02, [0, -0.072, 0.04], 'y')
  stopPlug(k, [0, -0.068, 0.1], '-y')
  k.hex('brass', 0.012, 0.006, [-0.068, 0, 0.03], 'x')
}
```
