{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "flare-stack",
  "type": "registry:component",
  "title": "Flare stack",
  "description": "Onshore elevated flare: riser on a concrete plinth, self-supported (flared base shell with anchor chairs), derrick-supported (three-leg lattice tower with riser guides) or guyed. Molecular (or internal velocity) purge seal, flare tip with windshield and pilots, pilot and ignition lines from a grade-level ignition panel, caged ladders in ISO 14122 flights with rest platforms, top platform, aviation lights and day-marking bands. Optional animated flame.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "thermal/flare-stack.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/thermal/flare-stack.tsx",
      "content": "// Onshore elevated flare stack: self-supported, derrick-supported or guyed riser, molecular seal, flare tip with pilots, ignition panel, caged ladders with rest platforms. API 521, API 537.\nimport * as THREE from 'three'\nimport { Kit, type V3 } from '../lib/kit'\nimport { KitMesh, useKit, type GroupProps } from '../lib/mesh'\nimport { paint } from '../lib/materials'\nimport { flange, junctionBox, ladder, nozzle, pipeRun, ringPlatform, tagPlate } from '../lib/parts'\nimport { port, type Port } from '../lib/ports'\nimport { ACCESS, elbow90, flange as flangeDims, nozzleStandout, pipeOD, type Rating } from '../lib/standards'\nimport { Flame } from './_flame'\nimport { flareTip, tipLength } from './_flare'\nimport { basePlate, triTower } from './_lattice'\nimport { clamp } from './_thermal'\n\nexport interface FlareStackProps {\n  /** tip outlet above grade, m */\n  height?: number\n  support?: 'self' | 'derrick' | 'guyed'\n  headerNps?: number\n  rating?: Rating\n  pilots?: number\n  /** purge-reduction seal below the tip */\n  seal?: 'molecular' | 'velocity'\n  lit?: boolean\n  flameHeight?: number\n  tag?: string\n}\n\nexport const defaults = {\n  height: 40,\n  support: 'derrick' as 'self' | 'derrick' | 'guyed',\n  headerNps: 24,\n  rating: 150 as Rating,\n  pilots: 3,\n  seal: 'molecular' as 'molecular' | 'velocity',\n  lit: false,\n  flameHeight: 12,\n  tag: 'FS-100',\n}\n\ntype P = typeof defaults\n\nconst GASKET = 0.0045\n\nfunction dims(p: P) {\n  const R = pipeOD(p.headerNps) / 2\n  const Lt = tipLength(p.headerNps)\n  const H = Math.max(12, p.height)\n  const yTipJoint = H - Lt - 0.12\n  const mol = p.seal === 'molecular'\n  const Rseal = mol ? R * 1.9 + 0.1 : R // typical molecular seal shell; a velocity seal sits inside the tip\n  const sealH = mol ? clamp(2.5 + R * 3, 3, 6) : 0\n  const ySeal1 = mol ? yTipJoint - 0.6 : yTipJoint - 0.31\n  const ySeal0 = ySeal1 - sealH\n  const self = p.support === 'self'\n  const Rb = self ? Math.max(R * 1.8, 0.7) : R // base shell radius\n  const yIn = Math.max(1.6, R + 1.0)\n  const so = nozzleStandout(p.headerNps, p.rating) + 0.25\n  return { mol, R, Lt, H, yTipJoint, Rseal, sealH, ySeal0, ySeal1, self, Rb, yIn, so }\n}\n\nexport function ports(props: FlareStackProps = {}): Port[] {\n  const p = { ...defaults, ...props } as P\n  const d = dims(p)\n  return [port('inlet', 'Flare header inlet', [-d.Rb - d.so, d.yIn, 0], [-1, 0, 0], p.headerNps, p.rating)]\n}\n\nexport function FlareStack(props: FlareStackProps & GroupProps) {\n  const { height, support, headerNps, rating, pilots, seal, lit, flameHeight, tag, ...group } = { ...defaults, ...props }\n  const p = { height, support, headerNps, rating, pilots, seal, lit, flameHeight, tag } as P\n  const d = dims(p)\n  const data = useKit((k) => build(k, p), [height, support, headerNps, rating, pilots, seal, tag])\n  return (\n    <group {...group}>\n      <KitMesh data={data} />\n      {lit && <Flame position={[0, d.H, 0]} height={flameHeight} />}\n    </group>\n  )\n}\n\nexport default FlareStack\n\nfunction build(k: Kit, p: P) {\n  const d = dims(p)\n  const { R, Rb, H } = d\n  const riser = paint('#a7abad', { metalness: 0.55, roughness: 0.45, clearcoat: 0, weather: 1 }) // heat-resistant aluminium paint\n  const red = paint('#c0281e', { weather: 1 })\n  const white = paint('#ecebe6', { weather: 1 })\n  const od = 2 * R\n  // concrete plinth and base\n  k.cyl('concrete', Math.max(Rb + 0.5, 1.0), 0.3, [0, 0.15, 0], 'y', 8)\n  const y0 = 0.3\n  if (d.self) {\n    // flared base shell with anchor chairs, tapering to the riser\n    const yTaper = H * 0.3\n    k.lathe(riser, [[Rb + 0.15, y0], [Rb + 0.15, y0 + 0.04], [Rb, y0 + 0.04], [Rb, y0 + 3], [R + 0.01, yTaper]], [0, 0, 0], 'y', 48)\n    for (let i = 0; i < 12; i++) {\n      const a = ((i + 0.5) / 12) * Math.PI * 2\n      const q = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), -a)\n      const at = (r: number, y: number): V3 => [Math.cos(a) * r, y, Math.sin(a) * r]\n      for (const da of [-0.06, 0.06]) {\n        const qa = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), -(a + da))\n        k.box(riser, [0.2, 0.42, 0.02], [Math.cos(a + da) * (Rb + 0.08), y0 + 0.25, Math.sin(a + da) * (Rb + 0.08)], qa)\n      }\n      k.box(riser, [0.2, 0.025, 0.2], at(Rb + 0.09, y0 + 0.45), q)\n      k.rod('nut', at(Rb + 0.09, y0), at(Rb + 0.09, y0 + 0.52), 0.02, 8)\n      k.hex('nut', 0.06, 0.035, at(Rb + 0.09, y0 + 0.48))\n    }\n  } else {\n    for (let y = 6; y < d.ySeal0 - 1; y += 6) k.cyl(riser, R + 0.02, 0.06, [0, y, 0], 'y', 32) // spool flanges\n  }\n  // molecular seal and tip\n  if (d.mol) {\n    k.lathe(riser, [[R, d.ySeal0 - 0.01], [d.Rseal, d.ySeal0 + d.Rseal * 0.6], [d.Rseal, d.ySeal1 - d.Rseal * 0.5], [R, d.ySeal1 - 0.12], [R, d.yTipJoint - 0.3]], [0, 0, 0], 'y', 40)\n    nozzle(k, { base: [0, d.ySeal0 + d.Rseal * 0.3, -d.Rseal * 0.8], dir: [0, -0.5, -0.87], nps: 2, rating: p.rating, standout: 0.25, mat: riser, blind: true })\n    k.lathe('vesselInner', [[R, d.ySeal0 - 0.01], [d.Rseal, d.ySeal0 + d.Rseal * 0.6], [d.Rseal, d.ySeal1 - d.Rseal * 0.5], [R, d.ySeal1 - 0.12]], [0, 0, 0], 'y', 40)\n  }\n  const tf = flangeDims(p.headerNps, p.rating)\n  k.pipe(riser, [0, d.yTipJoint - 0.31, 0], [0, d.yTipJoint - GASKET / 2 - tf.hubLength - tf.raisedFaceHeight + 0.002, 0], od, 0.012, 32)\n  const tip = flareTip(k, [0, d.yTipJoint, 0], p.headerNps, p.rating, Math.max(1, Math.round(p.pilots)))\n  // day marking: red / white bands on the upper seventh\n  if (p.support !== 'derrick') {\n    const yb0 = Math.max(d.ySeal0 - H / 7, H * 0.55)\n    const n = 5\n    const bh = (d.ySeal0 - yb0) / n\n    for (let i = 0; i < n; i++) k.cyl(i % 2 ? white : red, R + 0.004, bh, [0, yb0 + (i + 0.5) * bh, 0], 'y', 32, undefined, true)\n  }\n  // inlet: flanged horizontal run turning up into the riser through a long-radius elbow (open bore, no tee wall)\n  const fd = flangeDims(p.headerNps, p.rating)\n  const Lf = fd.hubLength + fd.raisedFaceHeight\n  const xFace = -Rb - d.so\n  flange(k, p.headerNps, p.rating, [xFace, d.yIn, 0], [-1, 0, 0], true)\n  pipeRun(k, [[xFace + Lf, d.yIn, 0], [0, d.yIn, 0], [0, d.ySeal0, 0]], p.headerNps, { mat: riser })\n  const Relb = elbow90(p.headerNps)\n  k.box('galvanised', [0.3, d.yIn - R - y0, 0.4], [-Relb - 0.25, (d.yIn - R + y0) / 2, 0])\n  k.box('galvanised', [0.45, 0.02, 0.55], [-Relb - 0.25, y0 + 0.01, 0])\n  nozzle(k, { base: [xFace + Lf + 0.35, d.yIn - R, 0], dir: [0, -1, 0], nps: 2, rating: p.rating, standout: Math.min(0.25, d.yIn - R - y0 - 0.05), mat: riser, blind: true })\n  // support structure\n  if (p.support === 'derrick') {\n    const r0 = Math.max(2.5, H / 9)\n    const r1 = Math.max(1.0, R + 0.7)\n    const top = d.ySeal0 - 0.3\n    const t = triTower(k, [0, 0, 0], 0.03, top, r0, r1, { legR: clamp(H * 0.0035, 0.08, 0.2), braceR: 0.045, panel: Math.max(3, r0 * 1.1), mat: 'galvanised', phase: Math.PI / 2 + Math.PI / 3, guides: R + 0.05 })\n    for (let i = 0; i < 3; i++) {\n      const c = t.at(0.03, i)\n      basePlate(k, c[0], c[2], 0.7, 'galvanised')\n    }\n  } else if (p.support === 'guyed') {\n    const rA = H * 0.45\n    const levels = H > 45 ? [0.4, 0.75] : [0.6]\n    for (let i = 0; i < 3; i++) {\n      const a = Math.PI / 2 + Math.PI / 3 + (i * 2 * Math.PI) / 3\n      const ax = Math.cos(a) * rA\n      const az = Math.sin(a) * rA\n      k.box('concrete', [1.2, 0.5, 1.2], [ax, 0.25, az])\n      k.box('galvanised', [0.3, 0.2, 0.06], [ax, 0.6, az])\n      for (const f of levels) {\n        const y = Math.min(H * f, d.ySeal0 - 0.8)\n        k.torus('galvanised', R + 0.06, 0.04, [0, y, 0], 'y', 6, 32)\n        const ex = Math.cos(a) * (R + 0.1)\n        const ez = Math.sin(a) * (R + 0.1)\n        k.rod('cable', [ex, y, ez], [ax, 0.65, az], 0.012, 6)\n        const tb: V3 = [ax + (ex - ax) * 0.04, 0.65 + (y - 0.65) * 0.04, az + (ez - az) * 0.04]\n        k.rod('galvanised', [ax, 0.65, az], tb, 0.025, 8)\n      }\n    }\n  }\n  // ladders and rest platforms on the +Z face\n  const flight = ACCESS.ladder.maxFlight\n  const yTop = d.yTipJoint - 1.1\n  const stops: number[] = []\n  for (let y = flight; y < yTop - 1.5; y += flight) stops.push(y)\n  stops.push(yTop)\n  const z0 = (y: number) => (d.self && y < H * 0.3 ? Rb - (Rb - R) * clamp((y - 3) / (H * 0.3 - 3), 0, 1) : R) + 0.25\n  let yPrev = y0\n  stops.forEach((y, i) => {\n    const sx = i % 2 ? 0.35 : -0.35\n    const zf = z0(yPrev) + 0.0\n    ladder(k, [sx, yPrev, Math.max(zf, z0(y))], y, [0, 0, 1])\n    const last = i === stops.length - 1\n    const rIn = (y > d.ySeal0 ? d.Rseal : z0(y) - 0.25) + 0.05\n    if (last) ringPlatform(k, y, Math.max(rIn, d.Rseal + 0.05), 1.0, 0, Math.PI * 2 - 0.001)\n    else ringPlatform(k, y, rIn, 1.0, -Math.PI / 2 - 0.75, -Math.PI / 2 + 0.75)\n    yPrev = y\n  })\n  // aviation lights at the top platform and mid-height\n  const lamp = paint('#ff2a1a', { emissive: '#ff1a0a', emissiveIntensity: 1.5, clearcoat: 0 })\n  for (const y of [yTop + 1.2, H * 0.5])\n    for (const a of [0, Math.PI]) {\n      const r = (y > d.ySeal0 ? d.Rseal : R) + 0.12\n      k.box('galvanised', [0.12, 0.04, 0.04], [Math.cos(a) * (r - 0.06), y - 0.06, Math.sin(a) * (r - 0.06)])\n      k.sphere(lamp, 0.07, [Math.cos(a) * r, y, Math.sin(a) * r], undefined, 12)\n    }\n  // ignition panel at grade with pilot gas and ignition lines up the riser\n  const px = Rb + 3\n  k.box('galvanised', [0.08, 1.2, 0.08], [px, 0.6, 0])\n  k.box('galvanised', [0.5, 0.02, 0.5], [px, 0.01, 0])\n  junctionBox(k, [px, 1.25, 0.04], 'z', [0.6, 0.8, 0.3], paint('#c9ccc8'))\n  tagPlate(k, p.tag, [px, 1.75, 0.2], 'z', 0.25)\n  tip.pilots.forEach((pp, i) => {\n    const a = Math.PI * 0.15 + i * 0.12\n    const x = Math.cos(a) * (R + 0.08)\n    const z = Math.sin(a) * (R + 0.08)\n    const xb = Math.cos(a) * (Rb + 0.08)\n    const zb = Math.sin(a) * (Rb + 0.08)\n    const seal = d.Rseal + 0.08\n    k.tube('stainless', [\n      [px - 0.1 + i * 0.05, 0.9, 0.18],\n      [px - 0.1 + i * 0.05, 0.6, 0.18],\n      [xb + 0.1, 0.6, zb],\n      [xb + 0.02, 3.2, zb],\n      [x, d.self ? H * 0.32 : 3.4, z],\n      [x, d.ySeal0 - 0.2, z],\n      [Math.cos(a) * seal, d.ySeal0 + d.Rseal * 0.7, Math.sin(a) * seal],\n      [Math.cos(a) * seal, d.ySeal1 - d.Rseal * 0.6, Math.sin(a) * seal],\n      [pp[0], d.ySeal1, pp[2]],\n      pp,\n    ], 0.0167, 6, 0.25)\n  })\n}\n"
    },
    {
      "path": "thermal/_flame.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/thermal/_flame.tsx",
      "content": "// Animated flare flame: noise-driven camera-facing billboard plus a soft glow, sized by flame height in metres.\nimport { useEffect, useMemo, useRef } from 'react'\nimport * as THREE from 'three'\nimport { useFrame } from '@react-three/fiber'\nimport type { V3 } from '../lib/kit'\n\nconst vert = /* glsl */ `\nvarying vec2 vUv;\nvoid main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`\n\nconst frag = /* glsl */ `\nuniform float uTime;\nvarying vec2 vUv;\nfloat hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }\nfloat noise(vec2 p) { vec2 i = floor(p), f = fract(p); vec2 u = f * f * (3.0 - 2.0 * f);\n  return mix(mix(hash(i), hash(i + vec2(1, 0)), u.x), mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), u.x), u.y); }\nfloat fbm(vec2 p) { float v = 0.0, a = 0.5; for (int i = 0; i < 5; i++) { v += a * noise(p); p = p * 2.03 + 17.1; a *= 0.5; } return v; }\nvoid main() {\n  float y = vUv.y;\n  float x = (vUv.x - 0.5) - y * y * 0.18;\n  float n = fbm(vec2(x * 3.0, y * 2.2 - uTime * 2.6));\n  float n2 = fbm(vec2(x * 6.0 + 3.0, y * 4.0 - uTime * 4.0));\n  float width = mix(0.14, 0.3, smoothstep(0.0, 0.45, y)) * (1.0 - smoothstep(0.55, 1.0, y) * 0.75);\n  float d = abs(x) / max(width, 1e-3);\n  float body = 1.0 - smoothstep(0.35, 1.0, d + (n - 0.5) * 1.1);\n  body *= smoothstep(0.0, 0.05, y) * (1.0 - smoothstep(0.55 + n * 0.35, 0.98, y));\n  float core = (1.0 - smoothstep(0.0, 0.55, d + (n2 - 0.5) * 0.6)) * (1.0 - smoothstep(0.1, 0.45, y));\n  vec3 col = mix(vec3(0.95, 0.22, 0.03), vec3(1.0, 0.5, 0.1), n);\n  col = mix(col, vec3(1.0, 0.88, 0.6), core);\n  float a = clamp(body * (0.65 + 0.5 * n2) + core * 0.6, 0.0, 1.0);\n  gl_FragColor = vec4(col * a * 1.3, a);\n}`\n\nconst glowFrag = /* glsl */ `\nuniform float uTime;\nvarying vec2 vUv;\nvoid main() { float r = length(vUv - 0.5) * 2.0; float a = pow(max(1.0 - r, 0.0), 2.2) * (0.3 + 0.05 * sin(uTime * 13.0));\n  gl_FragColor = vec4(vec3(1.0, 0.55, 0.2) * a, a); }`\n\nconst tmp = new THREE.Vector3()\n\nexport function Flame({ position, height = 8 }: { position: V3; height?: number }) {\n  const flame = useRef<THREE.Mesh>(null)\n  const glow = useRef<THREE.Mesh>(null)\n  const { mats, geo, glowGeo } = useMemo(() => {\n    const uTime = { value: 0 }\n    const common = { transparent: true, depthWrite: false, toneMapped: false, side: THREE.DoubleSide }\n    return {\n      mats: {\n        uTime,\n        flame: new THREE.ShaderMaterial({ uniforms: { uTime }, vertexShader: vert, fragmentShader: frag, ...common, blending: THREE.CustomBlending, blendSrc: THREE.OneFactor, blendDst: THREE.OneMinusSrcAlphaFactor }),\n        glow: new THREE.ShaderMaterial({ uniforms: { uTime }, vertexShader: vert, fragmentShader: glowFrag, ...common, blending: THREE.AdditiveBlending }),\n      },\n      geo: new THREE.PlaneGeometry(1, 1).translate(0, 0.5, 0),\n      glowGeo: new THREE.PlaneGeometry(1, 1),\n    }\n  }, [])\n  useEffect(() => () => [mats.flame, mats.glow, geo, glowGeo].forEach((o) => o.dispose()), [mats, geo, glowGeo])\n  useFrame((state) => {\n    mats.uTime.value = state.clock.elapsedTime\n    const cam = state.camera\n    if (flame.current) {\n      const wp = flame.current.getWorldPosition(tmp)\n      flame.current.rotation.y = Math.atan2(cam.position.x - wp.x, cam.position.z - wp.z)\n    }\n    if (glow.current) glow.current.quaternion.copy(cam.quaternion)\n  })\n  return (\n    <group position={position}>\n      <mesh ref={flame} geometry={geo} material={mats.flame} scale={[height * 0.55, height, 1]} renderOrder={5} frustumCulled={false} raycast={() => null} />\n      <mesh ref={glow} geometry={glowGeo} material={mats.glow} position={[0, height * 0.35, 0]} scale={height * 2} renderOrder={4} frustumCulled={false} raycast={() => null} />\n      <pointLight position={[0, height * 0.3, 0]} color=\"#ff8a3a\" intensity={height * height * 30} distance={height * 8} decay={2} />\n    </group>\n  )\n}\n"
    },
    {
      "path": "thermal/_flare.ts",
      "type": "registry:file",
      "target": "components/deck-twin/thermal/_flare.ts",
      "content": "// Vertical flare tip assembly: flanged inlet, heat-resistant barrel, windshield, retention ring, pilots with gas and ignition lines. API 537.\nimport * as THREE from 'three'\nimport type { Kit, V3 } from '../lib/kit'\nimport { paint } from '../lib/materials'\nimport { flangedJoint } from '../lib/parts'\nimport { pipeOD, type Rating } from '../lib/standards'\n\nexport const tipMetal = () => paint('#5e5955', { metalness: 0.65, roughness: 0.55, clearcoat: 0, weather: 1.2 })\n\n/** typical: tip barrel length for a header size. */\nexport const tipLength = (nps: number) => Math.min(5, Math.max(2.5, 2.2 + 0.06 * nps))\n\n/** Builds the tip from its inlet flange joint at `base` (centre of the joint) upwards. Returns the outlet height and pilot positions. */\nexport function flareTip(k: Kit, base: V3, nps: number, rating: Rating, pilots = 3) {\n  const M = tipMetal()\n  const od = pipeOD(nps)\n  const R = od / 2\n  const Lt = tipLength(nps)\n  const [x, y0, z] = base\n  flangedJoint(k, nps, rating, base, 'y')\n  const yb = y0 + 0.12\n  const yt = yb + Lt\n  k.pipe(M, [x, yb, z], [x, yt, z], od, 0.012, 40)\n  // muffler / spool flange below the windshield and the retention ring at the outlet\n  k.cyl(M, R + 0.05, 0.05, [x, yb + Lt * 0.35, z], 'y', 40)\n  k.torus(M, R + 0.012, 0.02, [x, yt, z], 'y', 8, 48)\n  for (let i = 0; i < 12; i++) {\n    const a = (i / 12) * Math.PI * 2\n    k.box(M, [0.012, 0.06, 0.05], [x + Math.cos(a) * (R - 0.006), yt + 0.03, z + Math.sin(a) * (R - 0.006)], new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), -a))\n  }\n  // windshield: open sleeve around the upper barrel on standoff lugs\n  const Rw = R + 0.14 + 0.06 * R\n  const yw0 = yt - Lt * 0.42\n  k.lathe(M, [[Rw + 0.006, yw0], [Rw + 0.006, yt + 0.06], [Rw, yt + 0.06], [Rw, yw0], [Rw + 0.006, yw0]], [x, 0, z], 'y', 48)\n  for (let i = 0; i < 6; i++) {\n    const a = (i / 6) * Math.PI * 2 + 0.3\n    const q = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), -a)\n    for (const yy of [yw0 + 0.15, yt - 0.15]) k.box(M, [Rw - R, 0.08, 0.012], [x + (Math.cos(a) * (R + Rw)) / 2, yy, z + (Math.sin(a) * (R + Rw)) / 2], q)\n  }\n  // pilots: riser, pilot head with wind hood, thermocouple conduit\n  const pos: V3[] = []\n  for (let i = 0; i < pilots; i++) {\n    const a = (i / pilots) * Math.PI * 2 + Math.PI / 6\n    const r = Rw + 0.12\n    const px = x + Math.cos(a) * r\n    const pz = z + Math.sin(a) * r\n    k.rod('stainless', [px, y0 - 0.6, pz], [px, yt - 0.12, pz], 0.0167, 8)\n    k.cyl(M, 0.045, 0.32, [px, yt + 0.02, pz], 'y', 14, 0.035)\n    k.cyl(M, 0.06, 0.06, [px, yt + 0.2, pz], 'y', 14, 0.045, true)\n    k.rod('stainless', [px + Math.cos(a) * 0.05, y0 - 0.6, pz + Math.sin(a) * 0.05], [px + Math.cos(a) * 0.05, yt - 0.05, pz + Math.sin(a) * 0.05], 0.008, 6)\n    for (const yy of [yb + 0.4, yb + Lt * 0.5, yt - 0.3]) k.rod(M, [x + Math.cos(a) * (R + 0.005), yy, z + Math.sin(a) * (R + 0.005)], [px, yy, pz], 0.012, 6)\n    pos.push([px, y0 - 0.6, pz])\n  }\n  return { top: yt, pilots: pos, Rw }\n}\n"
    },
    {
      "path": "thermal/_lattice.ts",
      "type": "registry:file",
      "target": "components/deck-twin/thermal/_lattice.ts",
      "content": "// Tubular lattice steel for booms, derricks and towers: tapered box trusses and three-leg towers built from solid rods.\nimport * as THREE from 'three'\nimport type { Kit, MatKey, V3 } from '../lib/kit'\n\nexport interface BoxTrussOpts {\n  /** width (across, along `side`) and depth (along `up`) at a and b */\n  w0: number\n  h0: number\n  w1?: number\n  h1?: number\n  /** approximate up direction for the depth axis */\n  up?: V3\n  chordR: number\n  braceR: number\n  panel?: number\n  mat?: MatKey\n}\n\nconst vec = (v: THREE.Vector3) => v.toArray() as V3\n\n/** Tapered rectangular Warren truss from a to b. Returns a function giving chord corner i (0..3) at fraction t. */\nexport function boxTruss(k: Kit, a: V3, b: V3, o: BoxTrussOpts) {\n  const A = new THREE.Vector3(...a)\n  const B = new THREE.Vector3(...b)\n  const d = B.clone().sub(A)\n  const L = d.length()\n  d.normalize()\n  const upRef = new THREE.Vector3(...(o.up ?? [0, 1, 0]))\n  const side = new THREE.Vector3().crossVectors(d, upRef).normalize()\n  const up = new THREE.Vector3().crossVectors(side, d).normalize()\n  const mat = o.mat ?? 'structSteel'\n  const w1 = o.w1 ?? o.w0\n  const h1 = o.h1 ?? o.h0\n  const S = [[-1, -1], [1, -1], [1, 1], [-1, 1]]\n  const at = (t: number, i: number): V3 => {\n    const w = o.w0 + (w1 - o.w0) * t\n    const h = o.h0 + (h1 - o.h0) * t\n    const [ss, su] = S[((i % 4) + 4) % 4]\n    return vec(A.clone().addScaledVector(d, L * t).addScaledVector(side, (ss * w) / 2).addScaledVector(up, (su * h) / 2))\n  }\n  for (let i = 0; i < 4; i++) k.rod(mat, at(0, i), at(1, i), o.chordR, 10)\n  const n = Math.max(2, Math.round(L / (o.panel ?? Math.max(1.5, o.w0 * 0.9))))\n  for (let s = 0; s <= n; s++) {\n    const t = s / n\n    for (let i = 0; i < 4; i++) k.rod(mat, at(t, i), at(t, i + 1), o.braceR, 6)\n    if (s % 2 === 0) k.rod(mat, at(t, 0), at(t, 2), o.braceR * 0.8, 6)\n    if (s === n) break\n    const t1 = (s + 1) / n\n    for (let i = 0; i < 4; i++) {\n      if (s % 2) k.rod(mat, at(t, i), at(t1, i + 1), o.braceR, 6)\n      else k.rod(mat, at(t, i + 1), at(t1, i), o.braceR, 6)\n    }\n  }\n  return { at, dir: vec(d), side: vec(side), up: vec(up), L, n }\n}\n\n/** Three-leg tapered tower about +Y from y0 to y1; radius r0 at the bottom, r1 at the top. Returns the leg position helper. */\nexport function triTower(k: Kit, centre: V3, y0: number, y1: number, r0: number, r1: number, o: { legR: number; braceR: number; panel?: number; mat?: MatKey; phase?: number; guides?: number }) {\n  const mat = o.mat ?? 'galvanised'\n  const ph = o.phase ?? Math.PI / 2\n  const at = (y: number, i: number): V3 => {\n    const u = (y - y0) / (y1 - y0)\n    const r = r0 + (r1 - r0) * u\n    const a = ph + (i * 2 * Math.PI) / 3\n    return [centre[0] + Math.cos(a) * r, y, centre[2] + Math.sin(a) * r]\n  }\n  for (let i = 0; i < 3; i++) k.rod(mat, at(y0, i), at(y1, i), o.legR, 10)\n  const n = Math.max(2, Math.round((y1 - y0) / (o.panel ?? 3)))\n  for (let s = 0; s <= n; s++) {\n    const y = y0 + ((y1 - y0) * s) / n\n    const ya = y0 + ((y1 - y0) * (s + 1)) / n\n    for (let i = 0; i < 3; i++) {\n      k.rod(mat, at(y, i), at(y, (i + 1) % 3), o.braceR, 6)\n      if (s < n) {\n        k.rod(mat, at(y, i), at(ya, (i + 1) % 3), o.braceR * 0.85, 6)\n        k.rod(mat, at(y, (i + 1) % 3), at(ya, i), o.braceR * 0.85, 6)\n      }\n      // guide brackets to a central riser\n      if (o.guides && s > 0) {\n        const p = at(y, i)\n        const dx = p[0] - centre[0]\n        const dz = p[2] - centre[2]\n        const f = o.guides / Math.hypot(dx, dz)\n        k.rod(mat, p, [centre[0] + dx * f, y, centre[2] + dz * f], o.braceR * 0.8, 6)\n      }\n    }\n  }\n  return { at, n }\n}\n\n/** Square base plate with four anchor bolts and nuts at deck level. */\nexport function basePlate(k: Kit, x: number, z: number, size: number, mat: MatKey = 'galvanised', y = 0) {\n  const t = Math.max(0.02, size * 0.06)\n  k.box(mat, [size, t, size], [x, y + t / 2, z])\n  const o = size / 2 - Math.max(0.04, size * 0.15)\n  for (const sx of [-1, 1])\n    for (const sz of [-1, 1]) {\n      k.rod('nut', [x + sx * o, y, z + sz * o], [x + sx * o, y + t + 0.06, z + sz * o], 0.012, 8)\n      k.hex('nut', 0.04, 0.02, [x + sx * o, y + t + 0.012, z + sz * o])\n    }\n}\n"
    },
    {
      "path": "thermal/_thermal.ts",
      "type": "registry:file",
      "target": "components/deck-twin/thermal/_thermal.ts",
      "content": "// Shared geometry for thermal equipment: body (girth) flanges, formed heads, eccentric cones, nozzle-to-nozzle bolting.\nimport * as THREE from 'three'\nimport type { Kit, MatKey, V3 } from '../lib/kit'\nimport { axisVec } from '../lib/kit'\nimport { gasket, nozzle, type NozzleOpts } from '../lib/parts'\nimport { flange as flangeDims, pipeOD, pipeWall, type Rating } from '../lib/standards'\n\nexport const clamp = (v: number, a: number, b: number) => Math.min(b, Math.max(a, v))\n\nexport interface Girth {\n  Rf: number\n  tf: number\n  hubL: number\n  hubT: number\n  Rbc: number\n  nb: number\n  db: number\n}\n\n/** typical: custom body flange for a shell of outside radius Ro (not B16.5/B16.47; sized like TEMA girth flanges). */\nexport function girthDims(Ro: number): Girth {\n  const D = 2 * Ro\n  const db = D < 0.6 ? 0.024 : D < 1.2 ? 0.03 : 0.036\n  const Rbc = Ro + 0.04 + 1.6 * db\n  const Rf = Rbc + 1.4 * db + 0.012\n  const tf = clamp(0.05 + 0.06 * D, 0.06, 0.16)\n  const nb = Math.max(12, 4 * Math.round((2 * Math.PI * Rbc) / 0.11 / 4))\n  return { Rf, tf, hubL: clamp(0.06 + 0.04 * D, 0.07, 0.14), hubT: clamp(0.012 + 0.012 * D, 0.016, 0.04), Rbc, nb, db }\n}\n\n/** Girth flange about the X axis at y/z = 0: raised face at `x`, hub running towards `dir`. */\nexport function girthFlange(k: Kit, mat: MatKey, x: number, Ro: number, G: Girth, dir: 1 | -1, seg = 64) {\n  const Ri = Ro * 0.97\n  const { Rf, tf, hubL, hubT } = G\n  if (dir > 0)\n    k.lathe(mat, [[Ri, x], [Rf, x], [Rf, x + tf], [Ro + hubT, x + tf], [Ro, x + tf + hubL]], [0, 0, 0], 'x', seg)\n  else k.lathe(mat, [[Ro, x - tf - hubL], [Ro + hubT, x - tf], [Rf, x - tf], [Rf, x], [Ri, x]], [0, 0, 0], 'x', seg)\n}\n\n/** Through-studs with a nut at each end, between flange back faces xa < xb, about the X axis. */\nexport function girthStuds(k: Kit, xa: number, xb: number, G: Girth, R = G.Rbc, n = G.nb, d = G.db, mat: MatKey = 'boltXylan') {\n  const nh = d * 0.95\n  const len = xb - xa + 2 * nh + 1.2 * d\n  k.boltCircle(mat, 'nut', n, R, [xa - nh - 0.6 * d, 0, 0], 'x', d, len, [0.6 * d + nh / 2, len - 0.6 * d - nh / 2], Math.PI / n)\n}\n\n/** 2:1 ellipsoidal (or any depth) head about X: tangent line at x0, crown towards dir. */\nexport function headProfile(R: number, depth: number, x0: number, dir: 1 | -1, n = 18): [number, number][] {\n  const pts: [number, number][] = []\n  for (let i = 0; i <= n; i++) {\n    const t = (i / n) * (Math.PI / 2)\n    pts.push([R * Math.cos(t), x0 + dir * depth * Math.sin(t)])\n  }\n  return dir > 0 ? pts : pts.reverse()\n}\n\nexport function head(k: Kit, mat: MatKey, R: number, depth: number, x0: number, dir: 1 | -1, seg = 64) {\n  k.lathe(mat, headProfile(R, depth, x0, dir), [0, 0, 0], 'x', seg)\n}\n\n/** Shell cylinder about X between x0 < x1. */\nexport function shellX(k: Kit, mat: MatKey, R: number, x0: number, x1: number, seg = 64) {\n  if (x1 - x0 > 1e-4) k.lathe(mat, [[R, x0], [R, x1]], [0, 0, 0], 'x', seg)\n}\n\n/** Circumferential weld seam bead about X. */\nexport function seamX(k: Kit, mat: MatKey, R: number, x: number) {\n  k.torus(mat, R + 0.001, 0.005, [x, 0, 0], 'x', 6, 64)\n}\n\n/** Eccentric cone about X, circle (r0, centre y0) at x0 to (r1, y1) at x1 (flat-bottom kettle reducer). */\nexport function eccentricCone(k: Kit, mat: MatKey, x0: number, r0: number, y0: number, x1: number, r1: number, y1: number, seg = 64) {\n  const pos: number[] = []\n  const idx: number[] = []\n  for (let i = 0; i <= seg; i++) {\n    const a = (i / seg) * Math.PI * 2\n    const c = Math.cos(a)\n    const s = Math.sin(a)\n    pos.push(x0, y0 + r0 * s, r0 * c, x1, y1 + r1 * s, r1 * c)\n  }\n  for (let i = 0; i < seg; i++) {\n    const a = i * 2\n    idx.push(a, a + 1, a + 2, a + 1, a + 3, a + 2)\n  }\n  const g = new THREE.BufferGeometry()\n  g.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3))\n  g.setIndex(idx)\n  g.computeVertexNormals()\n  k.add(mat, g)\n}\n\n/** Gasket + through-studs for two B16.5 flanges meeting face to face at `mid` along `axis`. */\nexport function jointBolting(k: Kit, nps: number, rating: Rating, mid: V3, axis: V3) {\n  const f = flangeDims(nps, rating)\n  const v = axisVec(axis)\n  const g = 0.0045\n  gasket(k, nps, rating, mid, axis, g)\n  const span = 2 * (f.thickness + f.raisedFaceHeight) + g\n  const studL = span + f.boltDia * 3\n  const start = new THREE.Vector3(...mid).addScaledVector(v, -studL / 2).toArray() as V3\n  k.boltCircle('boltXylan', 'nut', f.bolts, f.boltCircle / 2, start, axis, f.boltDia, studL, [f.boltDia * 0.95, studL - f.boltDia * 0.95])\n}\n\n/** Earthing boss: small stainless pad with a bolt, on a surface facing +normal. */\nexport function earthBoss(k: Kit, pos: V3, normal: 'x' | 'z' | '-x' | '-z' | 'y') {\n  k.box('stainless', [0.05, 0.05, 0.05], pos)\n  const v = axisVec(normal)\n  k.hex('nut', 0.022, 0.012, [pos[0] + v.x * 0.03, pos[1] + v.y * 0.03, pos[2] + v.z * 0.03], normal)\n}\n\n/** nozzle() with a dark bore end just above the shell (and pad) surface, so an open nozzle reads hollow instead of showing the painted shell. */\nexport function openNozzle(k: Kit, o: NozzleOpts & { shellR?: number }): V3 {\n  const face = nozzle(k, o)\n  if (!o.blind) {\n    const ri = pipeOD(o.nps) / 2 - pipeWall(o.nps, '80')\n    const sag = o.shellR ? o.shellR - Math.sqrt(Math.max(0, o.shellR ** 2 - ri ** 2)) : 0\n    const d = new THREE.Vector3(...o.dir).normalize()\n    const c = new THREE.Vector3(...o.base).addScaledVector(d, Math.max(sag + 0.004, o.pad ? 0.0095 : 0.004))\n    k.disc('pipeBore', ri * 0.995, c.toArray() as V3, d.toArray() as V3, 28)\n  }\n  return face\n}\n"
    }
  ],
  "categories": [
    "thermal"
  ],
  "meta": {
    "export": "FlareStack",
    "anchor": "base",
    "standards": [
      "API 521",
      "API 537",
      "ASME B16.5",
      "ISO 14122",
      "ICAO Annex 14"
    ]
  }
}