{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "printed-circuit-heat-exchanger",
  "type": "registry:component",
  "title": "Printed-circuit heat exchanger (PCHE)",
  "description": "Compact diffusion-bonded heat exchanger: stacked chemically etched plates bonded into solid stainless core blocks (bond lines visible), blocks welded side by side, half-pipe headers welded over the channel openings with flanged nozzles (hot side on the ends, cold side on the face), blinded maintenance nozzles, lifting lugs and a bolted support frame. Typical for high-pressure gas cooling, dew-pointing and compression offshore.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "thermal/printed-circuit-heat-exchanger.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/thermal/printed-circuit-heat-exchanger.tsx",
      "content": "// Printed-circuit (diffusion-bonded) heat exchanger: stacked etched-plate core blocks, welded half-pipe headers with flanged nozzles, support frame. ASME VIII-1 (Div. 1 / 2), ASME B16.5.\nimport { Kit, type MatKey, type V3 } from '../lib/kit'\nimport { KitMesh, useKit, type GroupProps } from '../lib/mesh'\nimport { liftingLug, member, tagPlate } from '../lib/parts'\nimport { port, type Port } from '../lib/ports'\nimport { nozzleStandout, pipeOD, type Rating } from '../lib/standards'\nimport { basePlate } from './_lattice'\nimport { clamp, openNozzle } from './_thermal'\n\nexport interface PrintedCircuitHeatExchangerProps {\n  /** core blocks welded side by side */\n  blocks?: number\n  /** block length along the hot-side flow, m */\n  length?: number\n  /** block width, m */\n  width?: number\n  /** block height (plate stack), m */\n  height?: number\n  hotNps?: number\n  coldNps?: number\n  rating?: Rating\n  /** block underside above deck, m */\n  elevation?: number\n  tag?: string\n}\n\nexport const defaults = {\n  blocks: 2,\n  length: 1.6,\n  width: 0.6,\n  height: 0.6,\n  hotNps: 6,\n  coldNps: 4,\n  rating: 900 as Rating,\n  elevation: 0.6,\n  tag: 'E-500',\n}\n\ntype P = typeof defaults\n\nfunction dims(p: P) {\n  const nb = Math.max(1, Math.round(p.blocks))\n  const L = p.length\n  const W = nb * p.width\n  const H = p.height\n  const y0 = p.elevation\n  const yc = y0 + H / 2\n  const rh = clamp(pipeOD(p.hotNps) * 0.75, 0.08, H / 2 - 0.02) // end header half-pipe\n  const rc = clamp(pipeOD(p.coldNps) * 0.75, 0.06, H / 2 - 0.02) // side header half-pipe\n  const soH = nozzleStandout(p.hotNps, p.rating)\n  const soC = nozzleStandout(p.coldNps, p.rating)\n  const xc = L / 2 - rc - 0.08 // side header centres, near the block ends\n  return { nb, L, W, H, y0, yc, rh, rc, soH, soC, xc }\n}\n\nexport function ports(props: PrintedCircuitHeatExchangerProps = {}): Port[] {\n  const p = { ...defaults, ...props } as P\n  const d = dims(p)\n  return [\n    port('hot-in', 'Hot side inlet', [-d.L / 2 - d.rh - d.soH, d.yc, 0], [-1, 0, 0], p.hotNps, p.rating),\n    port('hot-out', 'Hot side outlet', [d.L / 2 + d.rh + d.soH, d.yc, 0], [1, 0, 0], p.hotNps, p.rating),\n    port('cold-in', 'Cold side inlet', [d.xc, d.yc, d.W / 2 + d.rc + d.soC], [0, 0, 1], p.coldNps, p.rating),\n    port('cold-out', 'Cold side outlet', [-d.xc, d.yc, d.W / 2 + d.rc + d.soC], [0, 0, 1], p.coldNps, p.rating),\n  ]\n}\n\nexport function PrintedCircuitHeatExchanger(props: PrintedCircuitHeatExchangerProps & GroupProps) {\n  const { blocks, length, width, height, hotNps, coldNps, rating, elevation, tag, ...group } = { ...defaults, ...props }\n  const p = { blocks, length, width, height, hotNps, coldNps, rating, elevation, tag } 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 PrintedCircuitHeatExchanger\n\n// Half-pipe header welded to a block face: axis along +Y, bulging towards phi range, closed by half-disc end plates.\nfunction halfPipe(k: Kit, mat: MatKey, c: V3, r: number, h: number, phi0: number) {\n  const y0 = -h / 2\n  const y1 = h / 2\n  const t = 0.012\n  k.lathe(mat, [[r, y0], [r, y1]], c, 'y', 32, phi0, Math.PI)\n  k.lathe(mat, [[0, y0 - t], [r, y0 - t], [r, y0]], c, 'y', 32, phi0, Math.PI)\n  k.lathe(mat, [[r, y1], [r, y1 + t], [0, y1 + t]], c, 'y', 32, phi0, Math.PI)\n}\n\nfunction build(k: Kit, p: P) {\n  const d = dims(p)\n  const { L, W, H, y0, yc, rh, rc } = d\n  const steel: MatKey = 'stainless'\n  // core blocks: bonded plate stack (fine bond lines), side plates and weld seams between blocks\n  for (let i = 0; i < d.nb; i++) {\n    const z = -W / 2 + (i + 0.5) * p.width\n    k.box(steel, [L, H, p.width - 0.004], [0, yc, z])\n    for (let y = y0 + 0.03; y < y0 + H - 0.02; y += 0.03) k.box('flangeSteel', [L + 0.002, 0.0015, p.width - 0.002], [0, y, z])\n    if (i > 0) k.box('machined', [L, H, 0.008], [0, yc, -W / 2 + i * p.width])\n  }\n  for (const y of [y0 - 0.015, y0 + H + 0.015]) k.box(steel, [L + 0.02, 0.03, W + 0.02], [0, y, 0])\n  // hot-side end headers spanning the block faces, cold-side headers on the +Z face near the ends\n  const hh = H - 0.06\n  for (const s of [-1, 1]) {\n    for (let i = 0; i < d.nb; i++) {\n      const z = -W / 2 + (i + 0.5) * p.width\n      halfPipe(k, steel, [s * (L / 2), yc, z], rh, hh, s < 0 ? Math.PI : 0)\n    }\n    // end manifold joining the block headers, carrying the hot nozzle\n    if (d.nb > 1) k.cyl(steel, rh * 0.7, W - p.width, [s * (L / 2 + rh * 0.55), yc, 0], 'z', 24)\n    openNozzle(k, { base: [s * (L / 2 + rh), yc, 0], dir: [s, 0, 0], nps: p.hotNps, rating: p.rating, standout: d.soH, mat: steel, shellR: rh })\n    halfPipe(k, steel, [s * d.xc, yc, W / 2], rc, hh, -Math.PI / 2)\n    openNozzle(k, { base: [s * d.xc, yc, W / 2 + rc], dir: [0, 0, 1], nps: p.coldNps, rating: p.rating, standout: d.soC, mat: steel, shellR: rc })\n  }\n  // maintenance (cleaning) nozzles, blinded, on the -Z face headers\n  for (const s of [-1, 1]) {\n    halfPipe(k, steel, [s * d.xc, yc, -W / 2], rc * 0.7, hh * 0.6, Math.PI / 2)\n    openNozzle(k, { base: [s * d.xc, yc, -W / 2 - rc * 0.7], dir: [0, 0, -1], nps: 2, rating: p.rating, standout: 0.18, mat: steel, blind: true })\n  }\n  // support frame: two cross beams on legs with base plates\n  for (const x of [-L * 0.3, L * 0.3]) {\n    member(k, [x, y0 - 0.03 - 0.06, -W / 2 - 0.1], [x, y0 - 0.03 - 0.06, W / 2 + 0.1], 'HEB 120', 'galvanised', [0, 1, 0])\n    for (const s of [-1, 1]) {\n      member(k, [x, 0.02, s * (W / 2 + 0.04)], [x, y0 - 0.15, s * (W / 2 + 0.04)], 'HEB 120', 'galvanised', [0, 0, 1])\n      basePlate(k, x, s * (W / 2 + 0.04), 0.26, 'galvanised')\n    }\n  }\n  for (const sx of [-1, 1]) for (const sz of [-1, 1]) liftingLug(k, [sx * (L / 2 - 0.15), y0 + H + 0.03, sz * (W / 2 - 0.1)], 'x', 0.8, steel)\n  tagPlate(k, p.tag, [0, yc, -W / 2 - 0.001], [0, 0, -1], 0.25)\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": "PrintedCircuitHeatExchanger",
    "anchor": "base",
    "standards": [
      "ASME BPVC VIII-1",
      "ASME B16.5"
    ]
  }
}