{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "hairpin-exchanger",
  "type": "registry:component",
  "title": "Hairpin (double-pipe) exchanger",
  "description": "Double-pipe hairpin exchanger: two shell legs stacked one above the other, bolted return-bend housing enclosing the inner U-bend, front nozzle heads carrying the shell-side nozzles, closure blinds with packed glands where the inner pipe leaves, tube-side flanged ends, shell inlet and outlet nozzles, vent and drain, and clamp stands with U-bolts.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "thermal/hairpin-exchanger.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/thermal/hairpin-exchanger.tsx",
      "content": "// Double-pipe hairpin exchanger: two shell legs, bolted return-bend housing, closure flanges with tube-side ends, shell nozzles, clamp supports. TEMA / ASME VIII-1, ASME B16.5.\nimport { Kit, type MatKey } from '../lib/kit'\nimport { KitMesh, useKit, type GroupProps } from '../lib/mesh'\nimport { blindFlange, flange, gasket, member, tagPlate } from '../lib/parts'\nimport { port, type Port } from '../lib/ports'\nimport { flange as flangeDims, nozzleStandout, pipeOD, pipeWall, type Rating } from '../lib/standards'\nimport { basePlate } from './_lattice'\nimport { openNozzle } from './_thermal'\n\nexport interface HairpinExchangerProps {\n  /** outer (shell) pipe size */\n  shellNps?: number\n  /** inner (tube) pipe size */\n  tubeNps?: number\n  /** leg length, closure flange to return-bend housing, m */\n  length?: number\n  rating?: Rating\n  /** lower leg bottom above deck, m */\n  elevation?: number\n  tag?: string\n  paint?: MatKey\n}\n\nexport const defaults = {\n  shellNps: 6,\n  tubeNps: 3,\n  length: 6,\n  rating: 300 as Rating,\n  elevation: 0.7,\n  tag: 'E-400',\n  paint: 'equipmentPaint' as MatKey,\n}\n\ntype P = typeof defaults\n\nfunction dims(p: P) {\n  const tubeNps = Math.min(p.tubeNps, p.shellNps / 2)\n  const Rs = pipeOD(p.shellNps) / 2\n  const fs = flangeDims(p.shellNps, p.rating)\n  const gap = Math.max(fs.od + 0.06, 2 * Rs + 0.2) // leg centre spacing clears the closure flanges\n  const y1 = p.elevation + Rs\n  const y2 = y1 + gap\n  const L = p.length\n  const x0 = -L / 2 // closure flange face (shell side)\n  const x1 = L / 2 // return-end flange face\n  const ft = flangeDims(tubeNps, p.rating)\n  const tb = fs.thickness * 1.15 + 0.0045 // closure blind\n  const tubeOut = 0.3 + ft.hubLength // inner pipe projection beyond the closure\n  const xT = x0 - tb - tubeOut // tube-side flange face\n  const so = nozzleStandout(p.shellNps, p.rating)\n  // front nozzle head: short enlarged body behind the closure carrying the shell-side nozzle\n  const Rh = Rs * 1.3 + 0.02\n  const L0 = fs.hubLength + fs.raisedFaceHeight\n  const hx0 = x0 + L0 - 0.005\n  const hLen = 2 * Rs + 0.3\n  const xs = hx0 + hLen / 2\n  const hb = gap / 2 + Rs + 0.12 // return housing half height\n  return { Rh, hx0, hLen, tubeNps, Rs, fs, gap, y1, y2, L, x0, x1, ft, tb, xT, so, xs, hb }\n}\n\nexport function ports(props: HairpinExchangerProps = {}): Port[] {\n  const p = { ...defaults, ...props } as P\n  const d = dims(p)\n  return [\n    port('shell-in', 'Shell inlet', [d.xs, d.y2 + d.Rh + d.so, 0], [0, 1, 0], p.shellNps, p.rating),\n    port('shell-out', 'Shell outlet', [d.xs, d.y1 - d.Rh - d.so, 0], [0, -1, 0], p.shellNps, p.rating),\n    port('tube-in', 'Tube inlet', [d.xT, d.y1, 0], [-1, 0, 0], d.tubeNps, p.rating),\n    port('tube-out', 'Tube outlet', [d.xT, d.y2, 0], [-1, 0, 0], d.tubeNps, p.rating),\n  ]\n}\n\nexport function HairpinExchanger(props: HairpinExchangerProps & GroupProps) {\n  const { shellNps, tubeNps, length, rating, elevation, tag, paint, ...group } = { ...defaults, ...props }\n  const p = { shellNps, tubeNps, length, rating, elevation, tag, paint } as P\n  const data = useKit((k) => build(k, p), Object.values(p))\n  return (\n    <group {...group}>\n      <KitMesh data={data} />\n    </group>\n  )\n}\n\nexport default HairpinExchanger\n\nfunction build(k: Kit, p: P) {\n  const d = dims(p)\n  const P = p.paint\n  const { Rs, fs, x0, x1, y1, y2 } = d\n  const sh = pipeOD(p.shellNps)\n  for (const y of [y1, y2]) {\n    // shell leg with weld-neck flanges both ends\n    const L0 = fs.hubLength + fs.raisedFaceHeight\n    const xh1 = d.hx0 + d.hLen\n    k.cyl(P, d.Rh, d.hLen, [d.hx0 + d.hLen / 2, y, 0], 'x', 40)\n    k.cyl(P, Rs, 0.12, [xh1 + 0.06, y, 0], 'x', 40, d.Rh)\n    k.pipe(P, [xh1 + 0.11, y, 0], [x1 - L0, y, 0], sh, pipeWall(p.shellNps, '40'), 40)\n    flange(k, p.shellNps, p.rating, [x0, y, 0], [-1, 0, 0])\n    flange(k, p.shellNps, p.rating, [x1, y, 0], [1, 0, 0])\n    // closure: blind with the inner pipe through a packed gland, tube-side flange\n    blindFlange(k, p.shellNps, p.rating, [x0, y, 0], [-1, 0, 0])\n    const xb = x0 - d.tb\n    k.cyl('flangeSteel', pipeOD(d.tubeNps) / 2 + 0.03, 0.08, [xb - 0.04, y, 0], 'x', 24)\n    k.boltCircle('boltXylan', 'nut', 2, pipeOD(d.tubeNps) / 2 + 0.06, [xb - 0.1, y, 0], 'x', 0.012, 0.12, [0.02], 0)\n    const Lt = d.ft.hubLength + d.ft.raisedFaceHeight\n    k.pipe('pipePaint', [xb + 0.02, y, 0], [d.xT + Lt, y, 0], pipeOD(d.tubeNps), pipeWall(d.tubeNps, '80'), 24)\n    flange(k, d.tubeNps, p.rating, [d.xT, y, 0], [-1, 0, 0], true)\n    k.disc('pipeBore', pipeOD(d.tubeNps) / 2 - pipeWall(d.tubeNps, '80'), [xb + 0.01, y, 0], '-x', 24)\n  }\n  // return-bend housing: flanged bonnet enclosing the U-bend, bolted to both leg flanges\n  const hb = d.hb\n  const xr = x1 + 0.0045\n  const tp = fs.thickness * 1.2\n  const W = fs.od + 0.04\n  const H = d.gap + fs.od + 0.04\n  const ym = (y1 + y2) / 2\n  k.rbox(P, [tp, H, W], 0.04, [xr + tp / 2, ym, 0])\n  const bonL = Math.max(0.3, Rs * 2.2)\n  k.rbox(P, [bonL, 2 * hb - 0.1, sh + 0.06], (sh + 0.06) / 2 - 0.001, [xr + tp + bonL / 2 - 0.05, ym, 0], undefined, 4)\n  for (const y of [y1, y2]) {\n    gasket(k, p.shellNps, p.rating, [x1 + 0.00225, y, 0], 'x')\n    const studL = fs.thickness + fs.raisedFaceHeight + tp + 0.0045 + fs.boltDia * 2.6\n    k.boltCircle('boltXylan', 'nut', fs.bolts, fs.boltCircle / 2, [x1 - fs.thickness - fs.raisedFaceHeight - fs.boltDia * 1.3, y, 0], 'x', fs.boltDia, studL, [fs.boltDia * 0.75, studL - fs.boltDia * 0.75])\n  }\n  k.cyl(P, 0.04, 0.12, [xr + tp + bonL - 0.05, ym + hb - 0.15, 0], 'x', 12) // lifting eye boss\n  openNozzle(k, { base: [xr + tp + bonL * 0.5, ym - hb + 0.05, 0], dir: [0, -1, 0], nps: 1, rating: p.rating, standout: 0.15, mat: P, blind: true })\n  // shell nozzles near the closure end: inlet on the upper leg top, outlet on the lower leg bottom\n  openNozzle(k, { base: [d.xs, y2 + d.Rh, 0], dir: [0, 1, 0], nps: p.shellNps, rating: p.rating, standout: d.so, mat: P, shellR: d.Rh })\n  openNozzle(k, { base: [d.xs, y1 - d.Rh, 0], dir: [0, -1, 0], nps: p.shellNps, rating: p.rating, standout: d.so, mat: P, shellR: d.Rh })\n  // vent and drain at the return end\n  openNozzle(k, { base: [x1 - 0.5, y2 + Rs, 0], dir: [0, 1, 0], nps: 1, rating: p.rating, standout: 0.15, mat: P, blind: true })\n  // clamp supports: two stands with U-bolts around both legs\n  const pw = Math.max(0.3, sh + 0.12)\n  for (const x of [x0 + Math.max(0.9, d.L * 0.2) + Rs, x1 - Math.max(0.6, d.L * 0.15)]) {\n    for (const s of [-1, 1]) member(k, [x, 0.025, s * (pw / 2)], [x, y2 + Rs + 0.06, s * (pw / 2)], 'UPN 100', 'galvanised', [0, 0, s])\n    basePlate(k, x, 0, pw + 0.18, 'galvanised')\n    for (const y of [y1, y2]) {\n      k.box('galvanised', [0.1, 0.012, pw + 0.06], [x, y - Rs - 0.006, 0])\n      k.torus('galvanised', Rs + 0.008, 0.008, [x, y, 0], 'x', 6, 28, Math.PI)\n      for (const s of [-1, 1]) k.hex('nut', 0.03, 0.016, [x, y - Rs + 0.002, s * (Rs + 0.008)])\n    }\n    k.box('galvanised', [0.1, 0.012, pw + 0.06], [x, y2 + Rs + 0.06, 0])\n  }\n  tagPlate(k, p.tag, [d.L * 0.1, ym, 0.04], 'z', 0.2)\n  k.box(P, [0.24, d.gap - 2 * Rs - 0.02, 0.06], [d.L * 0.1, ym, 0])\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": "HairpinExchanger",
    "anchor": "base",
    "standards": [
      "ASME BPVC VIII-1",
      "TEMA",
      "ASME B16.5"
    ]
  }
}