{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "small-bore-fitting",
  "type": "registry:component",
  "title": "Small-bore forged fitting",
  "description": "ASME B16.11 forged steel small-bore fittings, Class 3000 or 6000, socket-weld or NPT threaded: 90° and 45° elbows, tee, coupling, half coupling, cap and hex-head plug. Also an MSS SP-83 ground-joint union with hex nut and an MSS SP-95 concentric swage nipple. Socket bore, depth, wall and centre-to-bottom-of-socket follow B16.11. Sockets have the pipe-stop shoulder and drilled bores meeting in a radiused passage (the tee branch is cut into the run bore). Female threads are tapered NPT. `stub` inserts pipe with the 1.6 mm socket gap and fillet weld, or makes it up into the thread.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "piping/small-bore-fitting.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/piping/small-bore-fitting.tsx",
      "content": "// Small-bore forged fittings: ASME B16.11 socket-weld / threaded elbows, tees, couplings, half couplings, caps and plugs, MSS SP-83 unions and MSS SP-95 swage nipples.\nimport * as THREE from 'three'\nimport { mat4, type Kit, type V3 } from '../lib/kit'\nimport { KitMesh, useKit, type GroupProps } from '../lib/mesh'\nimport { port, type Port } from '../lib/ports'\nimport { inch, pipeOD, pipeWall, type Schedule } from '../lib/standards'\nimport { add, filletWeld, hexRing, nptTpi, pipeStub, thread } from './_flanges'\nimport { cutCylinder, gridMesh, saddleTube, socket, threaded, type Klass } from './_flanges-b1611'\n\nexport type SmallBoreKind = 'elbow90' | 'elbow45' | 'tee' | 'coupling' | 'half-coupling' | 'union' | 'cap' | 'plug' | 'swage'\nexport type SmallBoreEnd = 'socket' | 'thread'\n\nexport interface SmallBoreFittingProps {\n  kind?: SmallBoreKind\n  /** socket-weld or NPT threaded ends (swage: socket = plain ends, thread = threaded both ends) */\n  end?: SmallBoreEnd\n  /** nominal size, inches (1/2–4) */\n  nps?: number\n  /** swage small end, inches */\n  smallNps?: number\n  pressureClass?: Klass\n  /** pipe schedule for inserted stubs and swage walls */\n  schedule?: Schedule\n  /** pipe inserted at each open end, m (0 = bare fitting) */\n  stub?: number\n}\n\nexport const defaults = { kind: 'elbow90' as SmallBoreKind, end: 'socket' as SmallBoreEnd, nps: 1, smallNps: 0.5, pressureClass: 3000 as Klass, schedule: '80' as Schedule, stub: 0 }\n\nconst S45 = Math.SQRT1_2\nconst SWAGE_L: Record<number, number> = { 0.5: 2.5, 0.75: 3, 1: 3.5, 1.25: 4, 1.5: 4.5, 2: 6.5, 2.5: 7, 3: 8, 4: 9 }\n// typical: MSS SP-83 Class 3000 union lengths and B16.11 threaded coupling W (inches)\nconst UNION_L: Record<number, number> = { 0.5: 1.9, 0.75: 2.05, 1: 2.35, 1.25: 2.65, 1.5: 2.9, 2: 3.4, 2.5: 4, 3: 4.4, 4: 5 }\nconst COUPLING_W: Record<number, number> = { 0.5: 1.88, 0.75: 2, 1: 2.38, 1.25: 2.62, 1.5: 3.12, 2: 3.38, 2.5: 3.62, 3: 4.25, 4: 4.75 }\nconst pick = (t: Record<number, number>, n: number) => {\n  const keys = Object.keys(t).map(Number)\n  return t[keys.reduce((b, k) => (Math.abs(k - n) < Math.abs(b - n) ? k : b), keys[0])]\n}\n\n/** End geometry data for one size, class and end type. */\nfunction endData(end: SmallBoreEnd, nps: number, cls: Klass) {\n  const sk = socket(nps, cls)\n  const th = threaded(nps, cls)\n  const pitch = 0.0254 / nptTpi(nps)\n  const ro = pipeOD(nps) / 2\n  if (end === 'socket') return { Ro: sk.bore / 2 + sk.wall, depth: sk.depth, rS: sk.bore / 2, rD: sk.d / 2, a90: sk.a90 + sk.depth, a45: sk.a45 + sk.depth, pitch }\n  return { Ro: th.od / 2, depth: th.thread, rS: ro - pitch * 0.56, rD: Math.min(sk.d / 2, ro - pitch * 0.56 - th.thread / 32 - 0.001), a90: th.a90, a45: th.a45, pitch }\n}\n\ninterface Leg {\n  dir: V3\n  /** centre to end face */\n  L: number\n}\n\nfunction layout(p: SmallBoreFittingProps) {\n  const o = { ...defaults, ...p }\n  const nps = Math.min(Math.max(o.nps, 0.5), 4)\n  const cls: Klass = o.pressureClass === 6000 ? 6000 : 3000\n  const e = endData(o.end, nps, cls)\n  const legs: Leg[] = []\n  let extra = 0\n  switch (o.kind) {\n    case 'elbow90':\n      legs.push({ dir: [-1, 0, 0], L: e.a90 }, { dir: [0, 1, 0], L: e.a90 })\n      break\n    case 'elbow45':\n      legs.push({ dir: [-1, 0, 0], L: e.a45 }, { dir: [S45, S45, 0], L: e.a45 })\n      break\n    case 'tee':\n      legs.push({ dir: [-1, 0, 0], L: e.a90 }, { dir: [1, 0, 0], L: e.a90 }, { dir: [0, 1, 0], L: e.a90 })\n      break\n    case 'coupling': {\n      // B16.11: socket coupling laying length E between socket bottoms; threaded coupling overall W\n      const E = nps <= 0.75 ? 0.0095 : nps <= 1.5 ? 0.013 : 0.019\n      const half = o.end === 'socket' ? e.depth + E / 2 : inch(pick(COUPLING_W, nps)) / 2\n      legs.push({ dir: [-1, 0, 0], L: half }, { dir: [1, 0, 0], L: half })\n      break\n    }\n    case 'half-coupling':\n      legs.push({ dir: [0, 1, 0], L: o.end === 'socket' ? e.depth + 0.008 : inch(pick(COUPLING_W, nps)) * 0.55 })\n      break\n    case 'union':\n      legs.push({ dir: [-1, 0, 0], L: inch(pick(UNION_L, nps)) / 2 }, { dir: [1, 0, 0], L: inch(pick(UNION_L, nps)) / 2 })\n      break\n    case 'cap':\n      legs.push({ dir: [-1, 0, 0], L: e.depth })\n      extra = Math.max(0.006, e.Ro - e.rS) * 1.3 // typical end thickness\n      break\n    case 'plug':\n      legs.push({ dir: [-1, 0, 0], L: 0 })\n      break\n    case 'swage': {\n      const L = inch(pick(SWAGE_L, nps))\n      legs.push({ dir: [-1, 0, 0], L: L / 2 }, { dir: [1, 0, 0], L: L / 2 })\n      break\n    }\n  }\n  return { ...o, nps, cls, e, legs, extra, small: Math.min(o.smallNps, nps) }\n}\n\nconst portId = (kind: SmallBoreKind, i: number) => (kind === 'tee' && i === 2 ? 'branch' : i === 0 ? 'A' : 'B')\n\n// Ports at each end face (or at the inserted pipe's weld end when stub > 0).\nexport function ports(p: SmallBoreFittingProps = {}): Port[] {\n  const g = layout(p)\n  const st = Math.max(0, g.stub)\n  return g.legs.map((l, i) => {\n    const n = g.kind === 'swage' && i === 1 ? g.small : g.nps\n    const pos = add([0, 0, 0], l.dir, l.L + (g.kind === 'plug' || g.kind === 'swage' ? 0 : st))\n    return port(portId(g.kind, i), g.kind === 'plug' ? 'Thread' : `End ${portId(g.kind, i)}`, pos, l.dir, n)\n  })\n}\n\nexport function SmallBoreFitting({ kind = defaults.kind, end = defaults.end, nps = defaults.nps, smallNps = defaults.smallNps, pressureClass = defaults.pressureClass, schedule = defaults.schedule, stub = defaults.stub, ...group }: SmallBoreFittingProps & GroupProps) {\n  const data = useKit((k) => build(k, { kind, end, nps, smallNps, pressureClass, schedule, stub }), [kind, end, nps, smallNps, pressureClass, schedule, stub])\n  return (\n    <group {...group}>\n      <KitMesh data={data} />\n    </group>\n  )\n}\n\nexport default SmallBoreFitting\n\nconst M = 'flangeSteel'\n\n/** One leg about its axis from the body centre to the end face: outer, end face, socket or female thread, then bore down to yb. */\nfunction leg(k: Kit, l: Leg, end: SmallBoreEnd, e: ReturnType<typeof endData>, yb: number, Ro = e.Ro, n = 32) {\n  const c = Math.min(0.0015, Ro * 0.05)\n  const L = l.L\n  k.lathe(M, [[Ro, 0], [Ro, L - c], [Ro - c, L], [e.rS + c, L]], [0, 0, 0], l.dir, n)\n  if (end === 'socket') {\n    k.lathe('machined', [[e.rS + c, L], [e.rS, L - c], [e.rS, L - e.depth], [e.rD, L - e.depth]], [0, 0, 0], l.dir, n)\n    if (L - e.depth > yb) k.lathe('pipeBore', [[e.rD, L - e.depth], [e.rD, yb]], [0, 0, 0], l.dir, n)\n  } else {\n    const r1 = e.rS - e.depth / 32\n    k.lathe('machined', [[e.rS + c, L], ...thread(e.rS, r1, L - c, L - e.depth, e.pitch, true), [e.rD, L - e.depth - 0.001]], [0, 0, 0], l.dir, n)\n    if (L - e.depth - 0.001 > yb) k.lathe('pipeBore', [[e.rD, L - e.depth - 0.001], [e.rD, yb]], [0, 0, 0], l.dir, n)\n  }\n}\n\n/** Bore passage bend between leg A (along -X) and a leg along `d`, through angle th, centreline radius Rb. */\nfunction boreBend(k: Kit, rD: number, Rb: number, th: number) {\n  const T = Rb * Math.tan(th / 2)\n  const cb = new THREE.Vector3(-T, Rb, 0)\n  const P: V3[][] = []\n  const N: V3[][] = []\n  const nT = 32\n  for (let i = 0; i <= 10; i++) {\n    const a = (th * i) / 10\n    const n1 = new THREE.Vector3(Math.sin(a), -Math.cos(a), 0)\n    const c = cb.clone().addScaledVector(n1, Rb)\n    P.push([])\n    N.push([])\n    for (let j = 0; j <= nT; j++) {\n      const f = (j / nT) * Math.PI * 2\n      const r = n1.clone().multiplyScalar(Math.cos(f)).add(new THREE.Vector3(0, 0, Math.sin(f)))\n      P[i].push(c.clone().addScaledVector(r, rD).toArray() as V3)\n      N[i].push(r.negate().toArray() as V3)\n    }\n  }\n  gridMesh(k, 'pipeBore', P, { N })\n  return T\n}\n\n/** Pipe inserted into a socket (1.6 mm off the bottom, fillet weld) or made up into a female thread. */\nfunction insert(k: Kit, l: Leg, end: SmallBoreEnd, e: ReturnType<typeof endData>, nps: number, schedule: Schedule, stub: number) {\n  const mouth = add([0, 0, 0], l.dir, l.L)\n  const engage = end === 'socket' ? e.depth - 0.0016 : e.depth - 2 * e.pitch\n  pipeStub(k, add(mouth, l.dir, -engage), l.dir, engage + stub, nps, schedule, end === 'socket' ? 'square' : 'thread', 'bevel')\n  if (end === 'socket') filletWeld(k, mouth, [-l.dir[0], -l.dir[1], -l.dir[2]], pipeOD(nps) / 2, 0, Math.max(0.003, Math.min(pipeWall(nps, schedule) * 1.25, 0.008)))\n}\n\nfunction build(k: Kit, p: SmallBoreFittingProps) {\n  const g = layout(p)\n  const { e, legs, end, nps, kind, schedule } = g\n  const stub = Math.max(0, g.stub)\n  if (kind === 'swage') return swage(k, g.nps, g.small, end, schedule, legs[1].L)\n  if (kind === 'plug') return plug(k, nps, e)\n  if (kind === 'elbow90' || kind === 'elbow45') {\n    const th = kind === 'elbow90' ? Math.PI / 2 : Math.PI / 4\n    const T = boreBend(k, e.rD, Math.max(e.rD * 1.1, 0.004), th)\n    legs.forEach((l) => leg(k, l, end, e, T))\n    // forged back corner: spherical lune between the two leg end planes\n    k.add(M, new THREE.SphereGeometry(e.Ro, 32, 16, Math.PI / 2, th), mat4([0, 0, 0], 'z'))\n  } else if (kind === 'tee') {\n    const x1 = legs[0].L - e.depth - (end === 'thread' ? 0.001 : 0)\n    leg(k, legs[0], end, e, x1)\n    leg(k, legs[1], end, e, x1)\n    cutCylinder(k, 'pipeBore', e.rD, -x1, x1, e.rD * 0.95, true)\n    const rb = Math.min(e.rD, e.rD * 0.95)\n    leg(k, legs[2], end, { ...e, rD: rb }, e.rD + 0.0005)\n    saddleTube(k, 'pipeBore', rb, e.rD, e.rD + 0.0005, true)\n  } else if (kind === 'cap') {\n    leg(k, legs[0], end, e, 0)\n    const yb = legs[0].L - e.depth\n    k.lathe('machined', [[e.rD, yb], [0, yb]], [0, 0, 0], [-1, 0, 0], 32)\n    const r = Math.min(g.extra * 0.6, e.Ro * 0.3)\n    k.lathe(M, [[e.Ro, 0], [e.Ro, g.extra - r], [e.Ro - r * 0.3, g.extra - r * 0.3], [e.Ro - r, g.extra], [0, g.extra]], [0, 0, 0], [1, 0, 0], 32)\n  } else if (kind === 'half-coupling') {\n    leg(k, legs[0], end, e, 0)\n    k.lathe(M, [[e.rD, 0], [e.Ro, 0]], [0, 0, 0], [0, 1, 0], 32)\n  } else {\n    legs.forEach((l) => leg(k, l, end, e, 0))\n    if (kind === 'union') union(k, nps, e, legs[0].L)\n  }\n  if (stub > 0 && kind !== 'cap') legs.forEach((l) => insert(k, l, end, e, nps, schedule, stub))\n  else if (stub > 0) insert(k, legs[0], end, e, nps, schedule, stub)\n}\n\n/** MSS SP-83 ground-joint union: tail piece collar, hex union nut, threaded head piece. */\nfunction union(k: Kit, nps: number, e: ReturnType<typeof endData>, half: number) {\n  const Rc = e.Ro * 1.12\n  // tail collar under the nut, ground ball seat line at the joint\n  k.lathe(M, [[e.Ro, -half * 0.42], [Rc, -half * 0.38], [Rc, 0.002], [e.Ro, 0.004]], [0, 0, 0], [1, 0, 0], 32)\n  k.torus('machined', e.Ro * 0.98, 0.0006, [0.004, 0, 0], 'x', 4, 32)\n  // head thread showing beyond the nut\n  const pitch = 0.0254 / (nptTpi(nps) + 2)\n  k.lathe('machined', thread(Rc * 1.02, Rc * 1.02, half * 0.18, half * 0.42, pitch, false), [0, 0, 0], [1, 0, 0], 32)\n  k.lathe(M, [[Rc * 1.02, half * 0.42], [Rc * 1.02, half * 0.46], [e.Ro, half * 0.5]], [0, 0, 0], [1, 0, 0], 32)\n  const af = Math.max(Rc * 2.3, e.Ro * 2 + 0.01)\n  hexRing(k, af, e.Ro + 0.0005, [-half * 0.52, 0, 0], [1, 0, 0], half * 0.72)\n}\n\n/** B16.11 hex-head plug: male NPT shank from the shoulder (origin) along -X, hex head along +X. */\nfunction plug(k: Kit, nps: number, e: ReturnType<typeof endData>) {\n  const ro = pipeOD(nps) / 2\n  const Lt = e.depth\n  k.lathe('machined', [[ro * 0.92, 0], [ro, 0.001], ...thread(ro, ro - Lt / 32, 0.002, Lt - 0.0003, e.pitch, false), [ro - Lt / 32 - e.pitch * 0.4, Lt], [0, Lt]], [0, 0, 0], [-1, 0, 0], 32)\n  // typical head: across flats and height scale with the pipe OD\n  const af = ro * 1.5 + 0.006\n  const hh = Math.max(0.008, ro * 0.6)\n  hexRing(k, af, 0, [0, 0, 0], [1, 0, 0], hh)\n  k.lathe(M, [[af * 0.5, hh], [af * 0.47, hh + 0.001], [0, hh + 0.001]], [0, 0, 0], 'x', 24)\n}\n\n/** MSS SP-95 concentric swage nipple: large end at -X, small end at +X; bevelled, plain or threaded ends. */\nfunction swage(k: Kit, big: number, small: number, end: SmallBoreEnd, schedule: Schedule, half: number) {\n  const R1 = pipeOD(big) / 2\n  const R2 = pipeOD(small) / 2\n  const r1 = R1 - pipeWall(big, schedule)\n  const r2 = R2 - pipeWall(small, schedule)\n  const L = half * 2\n  const a = L * 0.3\n  const b = L * 0.7\n  const n = 32\n  const base: V3 = [-half, 0, 0]\n  const ends = (R: number, r: number, nn: number, y: number, top: boolean) => {\n    const pitch = 0.0254 / nptTpi(nn)\n    if (end === 'thread') {\n      const Lt = Math.min(a * 0.9, inch((0.8 * (R * 2) / 0.0254 + 6.8) / nptTpi(nn)))\n      if (top) k.lathe('machined', [...thread(R, R - Lt / 32, y - Lt, y - 0.0003, pitch, false), [R - Lt / 32 - pitch * 0.4, y], [r, y]], base, 'x', n)\n      else k.lathe('machined', [[r, y], [R - Lt / 32 - pitch * 0.4, y], ...thread(R - Lt / 32, R, y + 0.0003, y + Lt, pitch, false)], base, 'x', n)\n      return Lt\n    }\n    // socket-weld use: plain square ends\n    if (top) k.lathe(M, [[R, y], [r, y]], base, 'x', n)\n    else k.lathe(M, [[r, y], [R, y]], base, 'x', n)\n    return 0\n  }\n  const t1 = ends(R1, r1, big, 0, false)\n  const t2 = ends(R2, r2, small, L, true)\n  k.lathe(M, [[R1, t1], [R1, a], [R2, b], [R2, L - t2]], base, 'x', n)\n  k.lathe('pipeBore', [[r2, L], [r2, b], [r1, a], [r1, 0]], base, 'x', n)\n}\n"
    },
    {
      "path": "piping/_flanges.ts",
      "type": "registry:file",
      "target": "components/deck-twin/piping/_flanges.ts",
      "content": "// Flange-type geometry: ASME B16.5 weld-neck, slip-on, socket-weld, threaded and lap-joint bodies, drilled plates, RF / FF / RTJ facings, NPT threads, fillet welds and pipe stubs.\nimport * as THREE from 'three'\nimport { latheCrisp, mat4, perp, uvScale, type Kit, type MatKey, type Profile, type V3 } from '../lib/kit'\nimport { textMaterial } from '../lib/textures'\nimport { GASKET_SPIRAL_WOUND, flange as flangeDims, inch, mm, nearest, pipeOD, pipeWall, stubEnd, studLength, tableKeys, type Rating, type Schedule } from '../lib/standards'\n\nexport type Facing = 'RF' | 'FF' | 'RTJ'\nexport type FlangeKind = 'WN' | 'SO' | 'SW' | 'THD' | 'LJ'\nexport const GAP = GASKET_SPIRAL_WOUND\nexport const LAND = 0.0016\nexport const BEVEL_TAN = Math.tan((37.5 * Math.PI) / 180)\nconst BEVEL = BEVEL_TAN\n\nexport const add = (a: V3, d: V3, s = 1): V3 => [a[0] + d[0] * s, a[1] + d[1] * s, a[2] + d[2] * s]\nexport const seg = (d: number) => (d > 0.5 ? 64 : d > 0.25 ? 48 : d > 0.1 ? 40 : 32)\nconst look = <T>(t: Record<number, T>, nps: number): T => t[nearest(tableKeys(t), nps)]\nconst has = (t: Record<number, unknown> | undefined, nps: number) => !!t && t[nearest(tableKeys(t), nps)] !== undefined && Math.abs(nearest(tableKeys(t), nps) - nps) < 0.01\n\n// ------------------------------------------------------------------ B16.5 tables (mm)\n\n// Slip-on / threaded / socket-weld length through hub Y, raised face excluded.\nconst SO_Y: Partial<Record<Rating, Record<number, number>>> = {\n  150: { 0.5: 14, 0.75: 14, 1: 16, 1.25: 19, 1.5: 21, 2: 24, 2.5: 27, 3: 29, 3.5: 30, 4: 32, 5: 35, 6: 38, 8: 43, 10: 48, 12: 54, 14: 56, 16: 62, 18: 67, 20: 71, 24: 81 },\n  300: { 0.5: 21, 0.75: 24, 1: 25, 1.25: 25, 1.5: 29, 2: 32, 2.5: 37, 3: 41, 3.5: 43, 4: 46, 5: 49, 6: 51, 8: 60, 10: 65, 12: 71, 14: 75, 16: 81, 18: 87, 20: 94, 24: 105 },\n  600: { 0.5: 22, 0.75: 25, 1: 27, 1.25: 29, 1.5: 32, 2: 37, 2.5: 41, 3: 46, 4: 54, 5: 60, 6: 67, 8: 76, 10: 86, 12: 92, 14: 94, 16: 106, 18: 117, 20: 127, 24: 140 },\n  900: { 0.5: 32, 0.75: 35, 1: 41, 1.25: 41, 1.5: 44, 2: 57, 2.5: 64, 3: 54, 4: 70, 5: 79, 6: 86, 8: 102, 10: 108, 12: 117, 14: 130, 16: 133, 18: 152, 20: 159, 24: 203 },\n  1500: { 0.5: 32, 0.75: 35, 1: 41, 1.25: 41, 1.5: 44, 2: 57, 2.5: 64 },\n}\n// Slip-on bore B (also the socket bore of socket-weld flanges).\nconst SO_B: Record<number, number> = {\n  0.5: 22.2, 0.75: 27.7, 1: 34.5, 1.25: 43.2, 1.5: 49.5, 2: 61.9, 2.5: 74.6, 3: 90.7, 3.5: 103.4, 4: 116.1, 5: 143.8,\n  6: 170.7, 8: 221.5, 10: 276.2, 12: 327, 14: 359.2, 16: 410.5, 18: 461.8, 20: 513.1, 24: 616,\n}\n// Socket-weld flange socket depth D.\nconst SW_D: Record<number, number> = { 0.5: 9.5, 0.75: 11.2, 1: 12.7, 1.25: 14.2, 1.5: 15.7, 2: 17.5, 2.5: 19.1, 3: 20.6 }\n\n/** True when B16.5 defines this flange type at this size and class. */\nexport function defined(kind: FlangeKind, nps: number, rating: Rating) {\n  if (kind === 'WN') return true\n  if (rating === 2500 && kind !== 'THD' && kind !== 'LJ') return false\n  if (kind === 'SW') return nps <= (rating >= 900 ? 2.5 : 3)\n  if (rating === 1500) return nps <= 2.5\n  if (rating === 2500) return nps <= 12\n  return nps <= 24\n}\n\n/** The requested class if B16.5 defines this flange type there, else the next lower class that does. */\nexport function definedRating(kind: FlangeKind, nps: number, rating: Rating): Rating {\n  const all: Rating[] = [2500, 1500, 900, 600, 300, 150]\n  return all.find((r) => r <= rating && defined(kind, nps, r)) ?? 150\n}\n\n/** Slip-on / threaded / socket-weld / lap-joint length through hub, raised face excluded. */\nexport function hubLength(kind: FlangeKind, nps: number, rating: Rating) {\n  const f = flangeDims(nps, rating)\n  if (kind === 'WN') return f.hubLength\n  const t = SO_Y[rating]\n  // approx: outside the B16.5 slip-on table, 0.58 x the weld-neck hub\n  let y = has(t, nps) ? mm(look(t!, nps)) : f.hubLength * 0.58\n  // approx: B16.5 lap-joint hubs are longer than slip-on from NPS 14 in Class 150 / 300\n  if (kind === 'LJ' && nps >= 14 && rating <= 300) y *= 1.35\n  return Math.max(y, f.thickness + 0.004)\n}\nexport const soBore = (nps: number) => (nps <= 24 ? mm(look(SO_B, nps)) : pipeOD(nps) + 0.006)\nexport const swDepth = (nps: number) => mm(look(SW_D, nps))\n/** B16.5 bolt hole: bolt + 1/8\" up to 1-3/4\" bolts, + 1/4\" above. */\nexport const boltHole = (bd: number) => bd + inch(bd <= inch(1.75) + 1e-6 ? 0.125 : 0.25)\n\n// Octagonal ring grooves by ring number [P pitch dia, E depth, F width], mm (B16.5 / B16.20).\nconst RING: Record<number, [number, number, number]> = {\n  11: [34.14, 5.54, 7.14], 12: [39.67, 6.35, 8.74], 13: [42.88, 6.35, 8.74], 14: [44.45, 6.35, 8.74], 15: [47.63, 6.35, 8.74],\n  16: [50.8, 6.35, 8.74], 17: [57.15, 6.35, 8.74], 18: [60.33, 6.35, 8.74], 19: [65.07, 6.35, 8.74], 20: [68.27, 6.35, 8.74],\n  21: [72.23, 7.92, 11.91], 22: [82.55, 6.35, 8.74], 23: [82.55, 7.92, 11.91], 24: [95.25, 7.92, 11.91], 25: [101.6, 6.35, 8.74],\n  26: [101.6, 7.92, 11.91], 27: [107.95, 7.92, 11.91], 28: [111.13, 9.53, 13.49], 29: [114.3, 6.35, 8.74], 31: [123.83, 7.92, 11.91],\n  32: [127, 9.53, 13.49], 33: [131.78, 6.35, 8.74], 34: [131.78, 7.92, 11.91], 35: [136.53, 7.92, 11.91], 36: [149.23, 6.35, 8.74],\n  37: [149.23, 7.92, 11.91], 38: [157.18, 11.13, 16.66], 39: [161.93, 7.92, 11.91], 40: [171.45, 6.35, 8.74], 41: [180.98, 7.92, 11.91],\n  42: [190.5, 12.7, 19.84], 43: [193.68, 6.35, 8.74], 44: [193.68, 7.92, 11.91], 45: [211.12, 7.92, 11.91], 46: [211.14, 9.53, 13.49],\n  47: [228.6, 12.7, 19.84], 48: [247.65, 6.35, 8.74], 49: [269.88, 7.92, 11.91], 50: [269.88, 11.13, 16.66], 51: [279.4, 14.27, 23.01],\n  52: [304.8, 6.35, 8.74], 53: [323.85, 7.92, 11.91], 54: [323.85, 11.13, 16.66], 55: [342.9, 17.48, 30.18], 56: [381, 6.35, 8.74],\n  57: [381, 7.92, 11.91], 58: [381, 14.27, 23.01], 59: [396.88, 6.35, 8.74], 60: [406.4, 17.48, 33.32], 61: [419.1, 7.92, 11.91],\n  62: [419.1, 11.13, 16.66], 63: [419.1, 15.88, 26.97], 64: [454.03, 6.35, 8.74], 65: [469.9, 7.92, 11.91], 66: [469.9, 11.13, 16.66],\n  67: [469.9, 17.48, 30.18], 68: [517.53, 6.35, 8.74], 69: [533.4, 7.92, 11.91], 70: [533.4, 12.7, 19.84], 71: [533.4, 17.48, 30.18],\n  72: [558.8, 6.35, 8.74], 73: [584.2, 9.53, 13.49], 74: [584.2, 12.7, 19.84], 75: [584.2, 17.48, 33.32], 76: [673.1, 6.35, 8.74],\n  77: [692.15, 11.13, 16.66], 78: [692.15, 15.88, 26.97], 79: [692.15, 20.62, 36.53],\n}\nconst RN_300 = { 0.5: 11, 0.75: 13, 1: 16, 1.25: 18, 1.5: 20, 2: 23, 2.5: 26, 3: 31, 3.5: 34, 4: 37, 5: 41, 6: 45, 8: 49, 10: 53, 12: 57, 14: 61, 16: 65, 18: 69, 20: 73, 24: 77 }\nconst RN_900 = { 0.5: 12, 0.75: 14, 1: 16, 1.25: 18, 1.5: 20, 2: 24, 2.5: 27, 3: 31, 4: 37, 5: 41, 6: 45, 8: 49, 10: 53, 12: 57, 14: 62, 16: 66, 18: 70, 20: 74, 24: 78 }\n// B16.5 ring number per class and NPS.\nconst RING_NO: Record<Rating, Record<number, number>> = {\n  150: { 1: 15, 1.25: 17, 1.5: 19, 2: 22, 2.5: 25, 3: 29, 3.5: 33, 4: 36, 5: 40, 6: 43, 8: 48, 10: 52, 12: 56, 14: 59, 16: 64, 18: 68, 20: 72, 24: 76 },\n  300: RN_300,\n  600: RN_300,\n  900: RN_900,\n  1500: { ...RN_900, 3: 35, 4: 39, 5: 44, 6: 46, 8: 50, 10: 54, 12: 58, 14: 63, 16: 67, 18: 71, 20: 75, 24: 79 },\n  2500: { 0.5: 13, 0.75: 16, 1: 18, 1.25: 21, 1.5: 23, 2: 26, 2.5: 28, 3: 32, 4: 38, 5: 42, 6: 47, 8: 51, 10: 55, 12: 60 },\n}\n\n/** RTJ groove for a flange: ring number, pitch dia, depth, top width and raised-portion dia K (m). */\nexport function ringGroove(nps: number, rating: Rating) {\n  const f0 = flangeDims(nps, rating)\n  // approx: B16.47 Series A ring grooves scaled from the raised-face diameter\n  if (nps > 24) return { ring: 'R', pitch: f0.raisedFace * 0.9, depth: mm(rating <= 300 ? 11.13 : 15.88), width: mm(rating <= 300 ? 16.66 : 26.97), K: f0.raisedFace * 0.9 + mm(rating <= 300 ? 29 : 39) }\n  const t = has(RING_NO[rating], nps) ? RING_NO[rating] : RING_NO[300]\n  const no = look(t, nps)\n  const [p, e, w] = RING[no]\n  const f = flangeDims(nps, rating)\n  // approx: K = P + F + 12 mm (fits the B16.5 K column within a few mm), kept inside the bolt holes\n  const K = Math.min(mm(p + w + 12), f.boltCircle - boltHole(f.boltDia) - 0.004)\n  return { ring: `R${no}`, pitch: mm(p), depth: mm(e), width: mm(w), K }\n}\n\n/** Facing height above the flange plate (port offset). */\nexport function facingHeight(facing: Facing, nps: number, rating: Rating) {\n  if (facing === 'FF') return 0\n  if (facing === 'RTJ') return ringGroove(nps, rating).depth\n  return flangeDims(nps, rating).raisedFaceHeight\n}\n\n/** NPT threads per inch. */\nexport const nptTpi = (nps: number) => (nps <= 0.375 ? 18 : nps <= 0.75 ? 14 : nps <= 2 ? 11.5 : 8)\n\n// ------------------------------------------------------------------ primitives\n\n/** Orthonormal frame (u, w, v) about dir, matching Kit.boltCircle / perp. */\nexport function frame(dir: V3) {\n  const v = new THREE.Vector3(...dir).normalize()\n  const [u, w] = perp(dir)\n  return { u, w, v }\n}\n\nconst circle = (cx: number, cy: number, r: number, n: number, cw: boolean) => {\n  const pts: THREE.Vector2[] = []\n  for (let i = 0; i < n; i++) {\n    const a = ((cw ? -i : i) / n) * Math.PI * 2\n    pts.push(new THREE.Vector2(cx + Math.cos(a) * r, cy + Math.sin(a) * r))\n  }\n  return pts\n}\n\nexport interface Drill {\n  n: number\n  bc: number\n  hole: number\n}\n\n/**\n * Flat plate with real bolt holes (and a central opening rIn) from `back` along `dir`, thickness t. Hole phase matches\n * Kit.boltCircle. `bore` draws the central opening wall; the front face can take its own material.\n */\nexport function drilledPlate(k: Kit, mat: MatKey, frontMat: MatKey, back: V3, dir: V3, t: number, rIn: number, rOut: number, d: Drill | null, n = seg(rOut * 2), bore = false) {\n  const { u, w, v } = frame(dir)\n  const M = new THREE.Matrix4().makeBasis(u, w, v).setPosition(...back)\n  const contour = circle(0, 0, rOut, n, false)\n  const holes: { c: [number, number]; r: number; pts: THREE.Vector2[] }[] = []\n  if (rIn > 1e-5) holes.push({ c: [0, 0], r: rIn, pts: circle(0, 0, rIn, n, true) })\n  if (d)\n    for (let i = 0; i < d.n; i++) {\n      const a = Math.PI / d.n + (i / d.n) * Math.PI * 2\n      const c: [number, number] = [Math.cos(a) * d.bc / 2, Math.sin(a) * d.bc / 2]\n      holes.push({ c, r: d.hole / 2, pts: circle(c[0], c[1], d.hole / 2, d.hole > 0.04 ? 20 : 14, true) })\n    }\n  const tris = THREE.ShapeUtils.triangulateShape(contour, holes.map((h) => h.pts))\n  const all = [...contour, ...holes.flatMap((h) => h.pts)]\n  const face = (z: number, nz: number) => {\n    const pos: number[] = []\n    const nor: number[] = []\n    for (const p of all) pos.push(p.x, p.y, z), nor.push(0, 0, nz)\n    const idx: number[] = []\n    for (const [a, b, c] of tris) nz > 0 ? idx.push(a, b, c) : idx.push(a, c, b)\n    const g = new THREE.BufferGeometry()\n    g.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3))\n    g.setAttribute('normal', new THREE.Float32BufferAttribute(nor, 3))\n    g.setIndex(idx)\n    return g\n  }\n  k.add(frontMat, face(t, 1), M)\n  k.add(mat, face(0, -1), M.clone())\n  // walls: outer (normals out), holes (normals into the hole)\n  const pos: number[] = []\n  const nor: number[] = []\n  const idx: number[] = []\n  const wall = (cx: number, cy: number, r: number, m: number, inward: boolean) => {\n    const b = pos.length / 3\n    for (let j = 0; j <= m; j++) {\n      const a = ((inward ? -j : j) / m) * Math.PI * 2\n      const c = Math.cos(a)\n      const s = Math.sin(a)\n      const sg = inward ? -1 : 1\n      pos.push(cx + c * r, cy + s * r, 0, cx + c * r, cy + s * r, t)\n      nor.push(c * sg, s * sg, 0, c * sg, s * sg, 0)\n    }\n    for (let j = 0; j < m; j++) {\n      const a0 = b + j * 2\n      if (inward) idx.push(a0, a0 + 1, a0 + 2, a0 + 1, a0 + 3, a0 + 2)\n      else idx.push(a0, a0 + 2, a0 + 1, a0 + 1, a0 + 2, a0 + 3)\n    }\n  }\n  wall(0, 0, rOut, n, false)\n  holes.forEach((h, i) => (i > 0 || bore || rIn <= 1e-5 ? wall(h.c[0], h.c[1], h.r, h.pts.length, true) : null))\n  const g = new THREE.BufferGeometry()\n  g.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3))\n  g.setAttribute('normal', new THREE.Float32BufferAttribute(nor, 3))\n  g.setIndex(idx)\n  k.add(mat, g, M.clone())\n}\n\n/** Quarter-ish arc points from angle a0 to a1 about (cr, cy) in the (r, y) plane. */\nexport function arc(cr: number, cy: number, r: number, a0: number, a1: number, n = 5): Profile {\n  return Array.from({ length: n + 1 }, (_, i) => {\n    const a = a0 + ((a1 - a0) * i) / n\n    return [cr + Math.cos(a) * r, cy + Math.sin(a) * r] as [number, number]\n  })\n}\n\n/** Sawtooth thread profile between y0 and y1 (walks y0 → y1). External: crest at r, root at r - h. Internal: crest at r, root at r + h. */\nexport function thread(r0: number, r1: number, y0: number, y1: number, pitch: number, internal: boolean): Profile {\n  const n = Math.max(1, Math.round(Math.abs(y1 - y0) / pitch))\n  const h = pitch * 0.8 * 0.7\n  const p: Profile = []\n  for (let i = 0; i <= n; i++) {\n    const s = i / n\n    const r = r0 + (r1 - r0) * s\n    const y = y0 + (y1 - y0) * s\n    p.push([r, y])\n    if (i < n) p.push([r + (internal ? h : -h), y + (y1 - y0) / n / 2])\n  }\n  return p\n}\n\n/** Hollow pipe stub from `start` along `dir`. Ends: 'bevel' (B16.25 37.5° with land), 'square', 'open', 'thread' (NPT). */\nexport type End = 'bevel' | 'square' | 'open' | 'thread'\nexport function pipeStub(k: Kit, start: V3, dir: V3, L: number, nps: number, schedule: Schedule, endA: End, endB: End, mat: MatKey = 'pipePaint', ro = pipeOD(nps) / 2, ri = ro - pipeWall(nps, schedule)) {\n  if (L <= 1e-5) return\n  const n = seg(ro * 2)\n  const wall = ro - ri\n  const bev = Math.min(Math.max(0, wall - LAND) * BEVEL, L / 3)\n  const pitch = 0.0254 / nptTpi(nps)\n  // NPT effective thread length L2 = (0.8 D + 6.8) / tpi\n  const thr = Math.min(L * 0.6, inch((0.8 * (ro * 2) / 0.0254 + 6.8) / nptTpi(nps)))\n  let y0 = 0\n  let y1 = L\n  if (endA === 'bevel') k.lathe('machined', [[ri, 0], [ri + LAND, 0], [ro, bev]], start, dir, n), (y0 = bev)\n  else if (endA === 'square') k.lathe(mat, [[ri, 0], [ro, 0]], start, dir, n)\n  else if (endA === 'thread') {\n    k.lathe('machined', [[ri, 0], [ro - thr / 32 - pitch * 0.4, 0], ...thread(ro - thr / 32, ro, 0.0003, thr, pitch, false)], start, dir, n)\n    y0 = thr\n  }\n  if (endB === 'bevel') k.lathe('machined', [[ro, L - bev], [ri + LAND, L], [ri, L]], start, dir, n), (y1 = L - bev)\n  else if (endB === 'square') k.lathe(mat, [[ro, L], [ri, L]], start, dir, n)\n  else if (endB === 'thread') {\n    k.lathe('machined', [...thread(ro, ro - thr / 32, L - thr, L - 0.0003, pitch, false), [ro - thr / 32 - pitch * 0.4, L], [ri, L]], start, dir, n)\n    y1 = L - thr\n  }\n  if (y1 > y0) k.lathe(mat, [[ro, y0], [ro, y1]], start, dir, n)\n  k.lathe('pipeBore', [[ri, L], [ri, 0]], start, dir, n)\n}\n\n/** Fillet weld ring: legs along the pipe (-axis from y) and on a face at y; profile slightly convex. */\nexport function filletWeld(k: Kit, base: V3, dir: V3, r: number, y: number, leg: number, n = seg(r * 2)) {\n  const p: Profile = []\n  for (let i = 0; i <= 6; i++) {\n    const s = i / 6\n    const bulge = Math.sin(s * Math.PI) * leg * 0.12\n    p.push([r + leg * s + bulge * 0.7, y - leg * (1 - s) + bulge * 0.7])\n  }\n  k.lathe('flangeSteel', p, base, dir, n)\n}\n\n/** Circumferential butt weld cap. */\nexport function buttWeld(k: Kit, pos: V3, dir: V3, r: number, wall: number) {\n  const w = Math.max(0.005, Math.min(0.02, wall * 1.6))\n  k.torus('flangeSteel', r + 0.0006, w / 2, pos, dir, 6, seg(r * 2))\n}\n\n/** Stamped marking on a cylindrical rim: a thin band of text centred on angle `at` (radians from the frame's u axis). */\nexport function stamp(k: Kit, text: string, base: V3, dir: V3, r: number, y0: number, y1: number, at = 0) {\n  const h = Math.abs(y1 - y0)\n  if (h < 0.004 || !text) return\n  const phi = Math.min(h * 4, r * 2.2) / r\n  const g = uvScale(latheCrisp([[r + 0.0004, Math.min(y0, y1)], [r + 0.0004, Math.max(y0, y1)]], 12, at - phi / 2, phi), 1, 1 / h)\n  k.add(textMaterial(text, '#878d91', '#2b3035', 512, 128), g, mat4(base, dir))\n}\n\n// ------------------------------------------------------------------ loose flange body\n\n/**\n * Gasket facing on a plate whose front is at `base` (axis dir): RF with serrations, RTJ raised portion with the octagonal\n * groove, FF flat ring, or (lapBore > 0) the radiused lap-joint bore edge. Returns facing height, outer radius and the\n * axial position where the bore wall starts.\n */\nexport function facingLathe(k: Kit, fc: Facing, nps: number, rating: Rating, rb: number, base: V3, dir: V3, n: number, M: MatKey = 'flangeSteel', lapBore = 0) {\n  const f = flangeDims(nps, rating)\n  if (lapBore > 0) {\n    const r = Math.min(inch(0.12 + nps * 0.025), inch(0.5))\n    k.lathe('machined', arc(lapBore + r, -r, r, Math.PI / 2, Math.PI, 5), base, dir, n)\n    return { h: 0, rFace: lapBore + r, yb: -r }\n  }\n  if (fc === 'RF') {\n    const h = f.raisedFaceHeight\n    const rFace = f.raisedFace / 2\n    // serrated finish suggested by a few fine concentric grooves\n    const p: Profile = [[rFace, 0], [rFace, h]]\n    const step = (rFace - rb) / 7\n    for (let i = 1; i < 7; i++) {\n      const r = rFace - step * i\n      p.push([r + 0.0006, h], [r, h - 0.0003], [r - 0.0006, h])\n    }\n    p.push([rb + 0.001, h], [rb, h - 0.001])\n    k.lathe('machined', p, base, dir, n)\n    return { h, rFace, yb: h - 0.001 }\n  }\n  if (fc === 'RTJ') {\n    const g = ringGroove(nps, rating)\n    const h = g.depth\n    const rFace = g.K / 2\n    const bw = Math.max(0.0008, g.width / 2 - h * Math.tan((23 * Math.PI) / 180))\n    k.lathe(M, [[rFace, 0], [rFace, h]], base, dir, n)\n    k.lathe('machined', [[rFace, h], [g.pitch / 2 + g.width / 2, h], [g.pitch / 2 + bw, 0.0003], [g.pitch / 2 - bw, 0.0003], [g.pitch / 2 - g.width / 2, h], [rb + 0.001, h], [rb, h - 0.001]], base, dir, n)\n    return { h, rFace, yb: h - 0.001 }\n  }\n  const rFace = (rb + f.boltCircle / 2 - boltHole(f.boltDia)) / 2\n  k.lathe('machined', [[rFace, 0], [rb + 0.001, 0], [rb, -0.001]], base, dir, n)\n  return { h: 0, rFace, yb: -0.001 }\n}\n\nexport interface FlangeOpts {\n  kind: FlangeKind\n  nps: number\n  rating: Rating\n  facing: Facing\n  /** pipe schedule for the WN / SW bore */\n  schedule: Schedule\n  /** bolt holes drilled (true) or a plain disc */\n  drilled?: boolean\n  marking?: string\n  mat?: MatKey\n}\n\nexport interface FlangeInfo {\n  /** facing height above the plate front */\n  h: number\n  /** plate thickness */\n  t: number\n  /** plate front to hub end */\n  Y: number\n  /** bore radius the line sees */\n  bore: number\n  /** slip-on / socket / lap bore radius */\n  B: number\n  /** socket depth from the hub end (SW) */\n  D: number\n  od: number\n}\n\n/**\n * One loose B16.5 flange: plate front face at `base`, facing pointing along `dir`, hub running back along -dir.\n * Bolt holes are real. Returns the key dimensions.\n */\nexport function looseFlange(k: Kit, base: V3, dir: V3, o: FlangeOpts): FlangeInfo {\n  const { kind, nps, rating, facing, schedule } = o\n  const f = flangeDims(nps, rating)\n  const M: MatKey = o.mat ?? 'flangeSteel'\n  const ro = pipeOD(nps) / 2\n  const n = seg(f.od)\n  const lj = kind === 'LJ'\n  const fc: Facing = lj ? 'FF' : facing\n  // lap-joint flanges are flat-faced; 150 / 300 tf includes the 1.6 mm that other types spend on the raised face\n  const t = f.thickness + (lj && rating <= 300 ? f.raisedFaceHeight : 0)\n  const Y = Math.max(hubLength(kind, nps, rating), t + 0.004)\n  const hubR = Math.max(f.hubDia / 2, ro * 1.05)\n  const riPipe = ro - pipeWall(nps, schedule)\n  const B = kind === 'WN' ? riPipe : soBore(nps) / 2\n  const pitch = 0.0254 / nptTpi(nps)\n  const hth = pitch * 0.56\n  const D = kind === 'SW' ? swDepth(nps) : 0\n  // radius of the bore where it meets the face\n  const rb = kind === 'WN' || kind === 'SW' ? riPipe : kind === 'THD' ? ro - hth - (Y - 0.003) / 32 : B\n  const bore = rb\n  const { h, rFace, yb } = facingLathe(k, fc, nps, rating, rb, base, dir, n, M, lj ? B : 0)\n  // drilled plate between rFace and the OD\n  drilledPlate(k, M, fc === 'FF' && !lj ? 'machined' : M, add(base, dir, -t), dir, t, rFace, f.od / 2, o.drilled === false ? null : { n: f.bolts, bc: f.boltCircle, hole: boltHole(f.boltDia) }, n)\n  // hub: outer surface from the hub end up to the back face\n  const fil = Math.min(inch(0.25 + nps * 0.012), (Y - t) * 0.35)\n  const y0 = -Y\n  const hubTop: Profile = [...arc(hubR + fil, -t - fil, fil, Math.PI, Math.PI / 2, 5), ...(rFace > hubR + fil ? [[rFace, -t] as [number, number]] : [])]\n  if (kind === 'WN') {\n    const bev = Math.max(0, ro - riPipe - LAND) * BEVEL\n    const neck = Math.min((Y - t) * 0.12, (ro - riPipe) * 1.5)\n    k.lathe('machined', [[riPipe, y0], [riPipe + LAND, y0], [ro, y0 + bev]], base, dir, n)\n    k.lathe(M, [[ro, y0 + bev], [ro, y0 + bev + neck], [hubR, -t - fil], ...hubTop], base, dir, n)\n    k.lathe('pipeBore', [[riPipe, yb], [riPipe, y0]], base, dir, n)\n  } else {\n    // slip-on style hubs taper slightly to the hub end\n    const rHe = Math.max(B + (ro - riPipe) * 1.6 + 0.002, hubR - (Y - t) * 0.1)\n    const hub: Profile = [[rHe, y0], [hubR, -t - fil], ...hubTop]\n    let rIn: number\n    if (kind === 'SO' || kind === 'LJ') {\n      k.lathe('machined', [[B, yb], [B, y0 + 0.002], [B + 0.002, y0]], base, dir, n)\n      rIn = B + 0.002\n    } else if (kind === 'SW') {\n      const yS = y0 + D\n      k.lathe('pipeBore', [[riPipe, yb], [riPipe, yS]], base, dir, n)\n      k.lathe('machined', [[riPipe, yS], [B, yS], [B, y0 + 0.0015], [B + 0.0015, y0]], base, dir, n)\n      rIn = B + 0.0015\n    } else {\n      // NPT internal thread, largest at the hub end, tapering 1:16 on diameter towards the face; 45° entry chamfer\n      const r1 = rb + (Y - 0.003) / 32\n      const ch = pitch * 1.5\n      k.lathe('machined', [[rb, yb], ...thread(rb, r1, yb - 0.001, y0 + ch, pitch, true), [r1 + ch, y0]], base, dir, n)\n      rIn = r1 + ch\n    }\n    k.lathe(M, [[rIn, y0], ...hub], base, dir, n)\n  }\n  if (o.marking) stamp(k, o.marking, base, dir, f.od / 2, -t * 0.82, -t * 0.18)\n  return { h, t, Y, bore, B, D, od: f.od }\n}\n\n/** Default stamped marking. */\nexport const marking = (kind: string, nps: number, rating: Rating, facing: Facing, extra = '') =>\n  `${nps}\" ${rating}# ${kind} ${facing} ${extra} A105N`.replace(/\\s+/g, ' ')\n\n// ------------------------------------------------------------------ inline end flanges with half joint studs\n\n/** Weld-neck RF end flange whose face is a port at `face` pointing along `dir`, plus the back half of the joint studs. Returns face to weld end. */\nexport function endFlange(k: Kit, nps: number, rating: Rating, face: V3, dir: V3, schedule: Schedule = 'STD', studs = true, mat?: MatKey) {\n  const f = flangeDims(nps, rating)\n  const base = add(face, dir, -f.raisedFaceHeight)\n  const i = looseFlange(k, base, dir, { kind: 'WN', nps, rating, facing: 'RF', schedule, mat })\n  if (studs) {\n    const half = studLength(nps, rating) / 2\n    k.boltCircle('boltXylan', 'nut', f.bolts, f.boltCircle / 2, add(face, dir, GAP / 2 - half), dir, f.boltDia, half, [f.boltDia * 0.6])\n  }\n  return i.Y + f.raisedFaceHeight\n}\n\n/** Lap-joint stub end (B16.9 long or MSS SP-43 short pattern): lap face at `face` pointing along dir. Returns length, lap radius, wall. */\nexport function stubEndGeo(k: Kit, face: V3, dir: V3, nps: number, schedule: Schedule, pattern: 'long' | 'short', mat: MatKey = 'flangeSteel') {\n  const s = stubEnd(nps)\n  // typical: MSS SP-43 short-pattern lengths\n  const short = inch(nps <= 1.5 ? 2 : nps <= 3.5 ? 2.5 : nps <= 5 ? 3 : nps <= 12 ? 4 : 6)\n  const L = pattern === 'long' ? s.length : short\n  const ro = pipeOD(nps) / 2\n  const wt = pipeWall(nps, schedule)\n  const ri = ro - wt\n  const G = s.lapDia / 2\n  const n = seg(G * 2)\n  const fr = Math.max(wt * 1.2, 0.003)\n  const bev = Math.max(0, wt - LAND) * BEVEL\n  k.lathe('machined', [[G, -0.002], [G - 0.002, 0], [ri + 0.0008, 0], [ri, -0.0008]], face, dir, n)\n  k.lathe(mat, [[ro, -L + bev], [ro, -wt - fr], ...arc(ro + fr, -wt - fr, fr, Math.PI, Math.PI / 2, 4), [G - 0.001, -wt], [G, -wt + 0.001], [G, -0.002]], face, dir, n)\n  k.lathe('pipeBore', [[ri, -0.0008], [ri, -L]], face, dir, n)\n  k.lathe('machined', [[ri, -L], [ri + LAND, -L], [ro, -L + bev]], face, dir, n)\n  return { L, G, wt }\n}\n\n// ------------------------------------------------------------------ inline assemblies\n\n/** Weld end to raised face of a WN RF flange. */\nexport const flangeLen = (nps: number, rating: Rating) => {\n  const f = flangeDims(nps, rating)\n  return f.hubLength + f.raisedFaceHeight\n}\n\n/** Inline flanged ends: WN RF flanges at x = ±L/2 facing out with half joint studs, plus pipe nipples of length `neck` inward. Returns the nipple inner ends. */\nexport function inlineEnds(k: Kit, nps: number, rating: Rating, L: number, neck: number, schedule: Schedule = 'STD', mat: MatKey = 'pipePaint') {\n  const fl = endFlange(k, nps, rating, [-L / 2, 0, 0], [-1, 0, 0], schedule)\n  endFlange(k, nps, rating, [L / 2, 0, 0], [1, 0, 0], schedule)\n  const ro = pipeOD(nps) / 2\n  const wt = pipeWall(nps, schedule)\n  const xa = -L / 2 + fl\n  const xb = L / 2 - fl\n  if (neck > 0) {\n    pipeStub(k, [xa, 0, 0], [1, 0, 0], neck, nps, schedule, 'open', 'square', mat)\n    pipeStub(k, [xb - neck, 0, 0], [1, 0, 0], neck, nps, schedule, 'square', 'open', mat)\n    buttWeld(k, [xa, 0, 0], [1, 0, 0], ro, wt)\n    buttWeld(k, [xb, 0, 0], [1, 0, 0], ro, wt)\n  }\n  return { xa: xa + neck, xb: xb - neck, fl }\n}\n\n/** Sinusoidal-convolution metal bellows along +X from x0: n convolutions of pitch q, root radius rr, height h, wall t, with cuffs. Returns its length. */\nexport function bellows(k: Kit, x0: number, n: number, q: number, rr: number, h: number, t: number, cuff: number, mat: MatKey = 'stainless') {\n  const out: Profile = [[rr, 0]]\n  const L = n * q + 2 * cuff\n  const m = 14\n  for (let i = 0; i <= n * m; i++) {\n    const y = cuff + (i / m) * q\n    out.push([rr + h * (0.5 - 0.5 * Math.cos((2 * Math.PI * i) / m)), y])\n  }\n  out.push([rr, L])\n  const segs = seg((rr + h) * 2)\n  k.lathe(mat, out, [x0, 0, 0], 'x', segs)\n  k.lathe(mat, out.map(([r, y]) => [r - t, y] as [number, number]).reverse(), [x0, 0, 0], 'x', segs)\n  return L\n}\n\n/** Wafer clamped between two WN RF flanges: flanges with faces at ±(w/2 + gasket), spiral-wound gaskets and full studs spanning. */\nexport function waferFlanges(k: Kit, nps: number, rating: Rating, w: number, schedule: Schedule = 'STD', stub = 0.12) {\n  const f = flangeDims(nps, rating)\n  const xf = w / 2 + GAP\n  for (const s of [-1, 1]) {\n    const face: V3 = [s * xf, 0, 0]\n    const dir: V3 = [s, 0, 0]\n    const base = add(face, dir, f.raisedFaceHeight)\n    const i = looseFlange(k, base, [-s, 0, 0], { kind: 'WN', nps, rating, facing: 'RF', schedule })\n    const we = s * (xf + f.raisedFaceHeight + i.Y)\n    if (stub > 0) {\n      pipeStub(k, [we, 0, 0], dir, stub, nps, schedule, 'open', 'bevel')\n      buttWeld(k, [we, 0, 0], dir, pipeOD(nps) / 2, pipeWall(nps, schedule))\n    }\n    k.lathe('gasket', [[pipeOD(nps) / 2 + 0.003, s * (w / 2) + (s < 0 ? -GAP : 0)], [f.raisedFace / 2 - 0.002, s * (w / 2) + (s < 0 ? -GAP : 0)], [f.raisedFace / 2 - 0.002, s * (w / 2) + (s < 0 ? 0 : GAP)], [pipeOD(nps) / 2 + 0.003, s * (w / 2) + (s < 0 ? 0 : GAP)]], [0, 0, 0], 'x', seg(f.raisedFace))\n  }\n  const span = 2 * (xf + f.raisedFaceHeight + f.thickness) + f.boltDia * 2.2\n  k.boltCircle('boltXylan', 'nut', f.bolts, f.boltCircle / 2, [-span / 2, 0, 0], 'x', f.boltDia, span, [f.boltDia * 0.5, span - f.boltDia * 0.5])\n  return { xf, f }\n}\n\n/** Fig 1502 union ends relative to the seal plane (typical proportions). */\nexport function hammerSpan(nps: number) {\n  const od = pipeOD(nps)\n  const nutL = od * 1.08\n  return { xm: -nutL * 0.72 - od * 1.25, xf: nutL * 0.28 + od * 1.35 * 0.55 }\n}\n\n/**\n * typical Fig 1502 hammer union about the X axis, seal plane at x = jx: male sub with nose and red three-lug wing nut\n * (-X side), female sub with ACME thread and lip seal (+X side). Returns the outer ends of both subs.\n */\nexport function hammerUnion(k: Kit, jx: number, nps: number, mat: MatKey = 'valveBody', label = '1502') {\n  const od = pipeOD(nps)\n  const ri = od / 2 - pipeWall(nps, '160')\n  const ringR = od * 0.98\n  const nutL = od * 1.08\n  const rBody = od * 0.62\n  const rFem = ringR * 0.84\n  const n = seg(ringR * 2)\n  const { xm: dm, xf: df } = hammerSpan(nps)\n  const xm = jx + dm\n  const xf = jx + df\n  // male sub: body, nose shoulder under the nut, seal face at jx\n  k.lathe(mat, [[ri, xm], [rBody, xm], [rBody, jx - nutL * 0.8], [rFem * 0.92, jx - nutL * 0.62], [rFem * 0.92, jx - 0.002], [ri + 0.004, jx]], [0, 0, 0], 'x', n)\n  // female sub: ACME thread band showing beyond the nut, shoulder, body\n  const pitch = 0.0254 / 3\n  k.lathe('machined', thread(rFem, rFem, jx + nutL * 0.2, jx + nutL * 0.28 + pitch * 2, pitch, false), [0, 0, 0], 'x', n)\n  k.lathe(mat, [[ri + 0.004, jx], [rFem, jx + 0.003], [rFem, jx + nutL * 0.2], [rFem, jx + nutL * 0.28 + pitch * 2], [rFem * 1.04, jx + nutL * 0.34], [rFem * 1.04, jx + nutL * 0.42], [rBody, jx + nutL * 0.5], [rBody, xf], [ri, xf]], [0, 0, 0], 'x', n)\n  k.lathe('pipeBore', [[ri, xf], [ri, xm]], [0, 0, 0], 'x', n)\n  // lip seal ring proud of the bore at the joint\n  k.lathe('rubberBlack', [[ri, jx + 0.006], [ri - 0.002, jx + 0.003], [ri - 0.002, jx - 0.001], [ri, jx - 0.003]], [0, 0, 0], 'x', n)\n  // wing nut: ring with three hammer lugs\n  const x0 = jx - nutL * 0.72\n  const x1 = jx + nutL * 0.28\n  k.lathe('actuatorRed', [[rFem * 1.01, x0], [ringR * 0.96, x0], [ringR, x0 + 0.004], [ringR, x1 - 0.004], [ringR * 0.96, x1], [rFem * 1.01, x1]], [0, 0, 0], 'x', n)\n  k.lathe('actuatorRed', [[rFem * 1.01, x1], [rFem * 1.01, x0]], [0, 0, 0], 'x', n)\n  const lugH = od * 0.5\n  for (let i = 0; i < 3; i++) {\n    const a = Math.PI / 2 + (i / 3) * Math.PI * 2\n    const r = ringR + lugH / 2 - 0.004\n    const c = new THREE.Vector3(0, Math.sin(a), Math.cos(a))\n    // hammer lug with a striking face\n    k.rbox('actuatorRed', [nutL * 0.78, lugH, od * 0.3], od * 0.03, [(x0 + x1) / 2, c.y * r, c.z * r], [0, c.y, c.z])\n  }\n  stamp(k, label, [0, 0, 0], [1, 0, 0], ringR, x0 + nutL * 0.2, x1 - nutL * 0.2, Math.PI / 3)\n  return { xm, xf, ri, rBody, od }\n}\n\n/** Hex prism (across flats af) with a round through-hole, from `from` along unit `dir` for len. */\nexport function hexRing(k: Kit, af: number, rIn: number, from: V3, dir: V3, len: number, mat: MatKey = 'flangeSteel') {\n  const R = af / Math.sqrt(3)\n  const sh = new THREE.Shape()\n  for (let i = 0; i < 6; i++) {\n    const a = (i / 6) * Math.PI * 2 + Math.PI / 6\n    if (i) sh.lineTo(R * Math.cos(a), R * Math.sin(a))\n    else sh.moveTo(R * Math.cos(a), R * Math.sin(a))\n  }\n  sh.closePath()\n  if (rIn > 0) {\n    const h = new THREE.Path()\n    h.absarc(0, 0, rIn, 0, Math.PI * 2, true)\n    sh.holes.push(h)\n  }\n  const q = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 0, 1), new THREE.Vector3(...dir))\n  k.extrude(mat, sh, len, new THREE.Matrix4().compose(new THREE.Vector3(...from), q, new THREE.Vector3(1, 1, 1)), 24)\n}\n"
    },
    {
      "path": "piping/_flanges-b1611.ts",
      "type": "registry:file",
      "target": "components/deck-twin/piping/_flanges-b1611.ts",
      "content": "// ASME B16.11 forged socket-weld / threaded fitting and MSS SP-97 branch-outlet dimension tables (mm), plus grid-mesh and ray-surface helpers.\nimport * as THREE from 'three'\nimport type { Kit, MatKey, V3 } from '../lib/kit'\nimport { mm, nearest, tableKeys } from '../lib/standards'\n\nexport type Klass = 3000 | 6000\nconst at = <T>(t: Record<number, T>, nps: number): T => t[nearest(tableKeys(t), nps)]\n\n// B16.11 socket-weld: [socket bore B, socket depth J, socket wall C 3000, C 6000, bore D 3000, D 6000, A90 3000, A90 6000, A45 3000, A45 6000]\nconst SW: Record<number, number[]> = {\n  0.5: [21.95, 10, 4.65, 5.95, 16.6, 12.5, 15.5, 19.5, 11.5, 12.5],\n  0.75: [27.3, 13, 4.9, 6.95, 21.7, 16.3, 19.5, 22.5, 12.5, 14.5],\n  1: [34.05, 13, 5.7, 7.9, 27.4, 21.5, 22, 27, 14, 17],\n  1.25: [42.8, 13, 6.05, 7.9, 35.8, 30.2, 27, 32, 17, 21],\n  1.5: [48.9, 13, 6.35, 8.9, 41.7, 34.7, 32, 38, 21, 25],\n  2: [61.35, 16, 6.95, 10.9, 53.5, 43.6, 38, 41, 25, 29],\n  // approx: 6000 above NPS 2 is not in B16.11; extrapolated\n  2.5: [74.2, 16, 8.75, 12.5, 64.2, 52, 41.5, 50, 29, 33],\n  3: [90.15, 16, 9.5, 14, 79.5, 64, 57.5, 60, 31.5, 37],\n  4: [115.8, 19, 10.7, 16, 103.8, 86, 66.5, 72, 41.5, 46],\n}\nexport function socket(nps: number, cls: Klass) {\n  const r = at(SW, nps)\n  const s = cls === 6000 ? 1 : 0\n  return { bore: mm(r[0]), depth: mm(r[1]), wall: mm(r[2 + s]), d: mm(r[4 + s]), a90: mm(r[6 + s]), a45: mm(r[8 + s]) }\n}\n\n// approx: B16.11 threaded fittings [A90, A45, band OD H, min thread length] for Class 3000 / 6000, from the B16.11 tables (threaded 3000 pairs with SW 6000 wall).\nconst THD: Record<number, [number[], number[]]> = {\n  0.5: [[33.5, 25, 38, 10.9], [38, 28.5, 46, 10.9]],\n  0.75: [[38, 28.5, 46, 12.7], [44.5, 33.5, 56, 12.7]],\n  1: [[44.5, 33.5, 56, 14.7], [51, 35, 62, 14.7]],\n  1.25: [[51, 35, 62, 17], [60.5, 43.5, 75, 17]],\n  1.5: [[60.5, 43.5, 75, 17.8], [64, 44.5, 84, 17.8]],\n  2: [[64, 44.5, 84, 19], [83, 52, 102, 19]],\n  2.5: [[83, 52, 102, 23.6], [95, 64, 121, 23.6]],\n  3: [[95, 64, 121, 25.9], [106, 79, 146, 25.9]],\n  4: [[114, 79, 152, 27.7], [114, 79, 152, 27.7]],\n}\nexport function threaded(nps: number, cls: Klass) {\n  const r = at(THD, nps)[cls === 6000 ? 1 : 0]\n  return { a90: mm(r[0]), a45: mm(r[1]), od: mm(r[2]), thread: mm(r[3]) }\n}\n\n// MSS SP-97 Weldolet (standard weight): [A height, B base OD, C base bore].\nconst WELDOLET: Record<number, number[]> = {\n  0.25: [14.3, 25.4, 15.9], 0.375: [19.05, 31.75, 19.05], 0.5: [19.05, 34.93, 23.81], 0.75: [22.23, 44.45, 30.16], 1: [26.99, 53.98, 36.51],\n  1.25: [31.75, 65.09, 44.45], 1.5: [33.34, 73.03, 50.8], 2: [38.1, 88.9, 65.09], 2.5: [41.28, 103.19, 76.2], 3: [44.45, 122.24, 93.66],\n  3.5: [47.63, 142.88, 112.71], 4: [50.8, 152.4, 120.65], 5: [57.15, 179.3, 141.29], 6: [60.33, 215.9, 169.86], 8: [69.85, 263.53, 220.66],\n  10: [77.79, 322.26, 274.64], 12: [85.73, 377.83, 325.44], 14: [88.9, 409.58, 357.19], 16: [93.6, 463.55, 407.99], 18: [96.84, 520.7, 458.79],\n  20: [101.6, 571.5, 508], 24: [144.5, 717.5, 609.6],\n}\n// MSS SP-97 Sockolet [A, B body OD, D bore] for Class 3000 and 6000 (Thredolet shares the envelope, approx).\nconst SOCKOLET: Record<Klass, Record<number, number[]>> = {\n  3000: {\n    0.25: [19.05, 25.4, 9.25], 0.375: [20.64, 31.75, 12.52], 0.5: [25.4, 35.72, 15.8], 0.75: [26.99, 43.66, 20.93], 1: [33.34, 50.8, 26.64],\n    1.25: [33.34, 65.09, 35.05], 1.5: [34.93, 72.23, 40.89], 2: [38.1, 88.11, 52.5], 2.5: [39.69, 103.19, 62.71], 3: [44.45, 122.24, 77.93],\n    4: [47.63, 151.61, 102.26],\n  },\n  6000: { 0.5: [31.75, 43.66, 11.84], 0.75: [36.51, 49.61, 15.6], 1: [39.69, 61.91, 20.7], 1.25: [41.28, 69.06, 29.46], 1.5: [42.86, 82.55, 33.99], 2: [52.39, 102.39, 42.9] },\n}\nexport const weldolet = (nps: number) => at(WELDOLET, nps).map(mm)\nexport const sockolet = (nps: number, cls: Klass) => at(SOCKOLET[cls], nps).map(mm)\n\n// ------------------------------------------------------------------ meshing\n\n/** Quad grid P[i][j] with optional explicit normals; otherwise normals from central differences, oriented by `ref`. `wrap` joins the last column to the first. */\nexport function gridMesh(k: Kit, mat: MatKey, P: V3[][], opts: { N?: V3[][]; ref?: (p: V3, i: number, j: number) => V3; wrap?: boolean } = {}) {\n  const nr = P.length\n  const nc = P[0].length\n  const V = (q: V3) => new THREE.Vector3(...q)\n  const pos: number[] = []\n  const nor: number[] = []\n  const N: THREE.Vector3[][] = []\n  for (let i = 0; i < nr; i++) {\n    N.push([])\n    for (let j = 0; j < nc; j++) {\n      let n: THREE.Vector3\n      if (opts.N) n = V(opts.N[i][j])\n      else {\n        const jm = j > 0 ? j - 1 : opts.wrap ? nc - 2 : 0\n        const jp = j < nc - 1 ? j + 1 : opts.wrap ? 1 : nc - 1\n        const im = Math.max(0, i - 1)\n        const ip = Math.min(nr - 1, i + 1)\n        n = V(P[i][jp]).sub(V(P[i][jm])).cross(V(P[ip][j]).sub(V(P[im][j])))\n        if (n.lengthSq() < 1e-20) n.set(0, 1, 0)\n        n.normalize()\n        if (opts.ref && n.dot(V(opts.ref(P[i][j], i, j))) < 0) n.negate()\n      }\n      N[i].push(n)\n      pos.push(...P[i][j])\n      nor.push(n.x, n.y, n.z)\n    }\n  }\n  let sign = 0\n  for (let i = 0; i < nr - 1 && !sign; i++)\n    for (let j = 0; j < nc - 1 && !sign; j++) {\n      const a = V(P[i][j])\n      const g = V(P[i + 1][j]).sub(a).cross(V(P[i][j + 1]).sub(a))\n      if (g.lengthSq() > 1e-16) sign = g.dot(N[i][j]) >= 0 ? 1 : -1\n    }\n  const idx: number[] = []\n  for (let i = 0; i < nr - 1; i++)\n    for (let j = 0; j < nc - 1; j++) {\n      const a = i * nc + j\n      const b = a + nc\n      if (sign >= 0) idx.push(a, b, a + 1, b, b + 1, a + 1)\n      else idx.push(a, a + 1, b, b, a + 1, b + 1)\n    }\n  const g = new THREE.BufferGeometry()\n  g.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3))\n  g.setAttribute('normal', new THREE.Float32BufferAttribute(nor, 3))\n  g.setIndex(idx)\n  k.add(mat, g)\n}\n\n/** Bisection along q + s·a for where f (distance-minus-radius, < 0 inside) crosses zero between s0 (inside) and s1 (outside). */\nexport function rayHit(f: (p: THREE.Vector3) => number, q: THREE.Vector3, a: THREE.Vector3, s0: number, s1: number) {\n  const p = new THREE.Vector3()\n  let lo = s0\n  let hi = s1\n  for (let it = 0; it < 48; it++) {\n    const m = (lo + hi) / 2\n    if (f(p.copy(q).addScaledVector(a, m)) < 0) lo = m\n    else hi = m\n  }\n  return (lo + hi) / 2\n}\n\n/**\n * Cylinder about X from x0 to x1 (radius r) with a round hole of radius h on its +Y side centred at x = 0, so a\n * perpendicular branch bore can open into it. Shares one angular sampling across the patch, strip and ends.\n */\nexport function cutCylinder(k: Kit, mat: MatKey, r: number, x0: number, x1: number, h: number, inward: boolean, nv = 48) {\n  h = Math.min(h, r * 0.95)\n  const step = (Math.PI * 2) / nv\n  const du = Math.min(h * 1.3 + 0.0015, Math.min(-x0, x1) * 0.95)\n  const kv = Math.min(Math.ceil((Math.asin(h / r) * 1.2 + 0.05) / step), nv / 2 - 2)\n  const dv = kv * step\n  const S = (u: number, v: number): V3 => [u, r * Math.cos(v), r * Math.sin(v)]\n  const Nn = (v: number): V3 => (inward ? [0, -Math.cos(v), -Math.sin(v)] : [0, Math.cos(v), Math.sin(v)])\n  const nuS = 6\n  const rect: [number, number][] = []\n  for (let m = 0; m <= 2 * kv; m++) rect.push([du, -dv + step * m])\n  for (let i = 1; i <= nuS; i++) rect.push([du - (2 * du * i) / nuS, dv])\n  for (let m = 1; m <= 2 * kv; m++) rect.push([-du, dv - step * m])\n  for (let i = 1; i <= nuS; i++) rect.push([-du + (2 * du * i) / nuS, -dv])\n  const P: V3[][] = []\n  const N: V3[][] = []\n  for (let i = 0; i <= 4; i++) {\n    P.push([])\n    N.push([])\n    for (const [qu, qv] of rect) {\n      const t = Math.atan2(qv / dv, qu / du)\n      const hu = h * Math.cos(t)\n      const hv = Math.atan2(h * Math.sin(t), Math.sqrt(r * r - (h * Math.sin(t)) ** 2))\n      const u = hu + (qu - hu) * (i / 4)\n      const v = hv + (qv - hv) * (i / 4)\n      P[i].push(S(u, v))\n      N[i].push(Nn(v))\n    }\n  }\n  gridMesh(k, mat, P, { N })\n  const band = (u0: number, u1: number, v0: number, v1: number, nu: number, n: number) => {\n    const Q: V3[][] = []\n    const M: V3[][] = []\n    for (let i = 0; i <= nu; i++) {\n      Q.push([])\n      M.push([])\n      for (let j = 0; j <= n; j++) {\n        const v = v0 + ((v1 - v0) * j) / n\n        Q[i].push(S(u0 + ((u1 - u0) * i) / nu, v))\n        M[i].push(Nn(v))\n      }\n    }\n    gridMesh(k, mat, Q, { N: M })\n  }\n  band(-du, du, dv, Math.PI * 2 - dv, nuS, nv - 2 * kv)\n  band(x0, -du, 0, Math.PI * 2, 1, nv)\n  band(du, x1, 0, Math.PI * 2, 1, nv)\n}\n\n/** Branch tube of radius rb along +Y from its saddle on an X-axis cylinder of radius rr up to y1. */\nexport function saddleTube(k: Kit, mat: MatKey, rb: number, rr: number, y1: number, inward: boolean, nT = 48) {\n  rb = Math.min(rb, rr * 0.95)\n  const P: V3[][] = []\n  const N: V3[][] = []\n  for (let i = 0; i <= 3; i++) {\n    P.push([])\n    N.push([])\n    for (let j = 0; j <= nT; j++) {\n      const t = (j / nT) * Math.PI * 2\n      const ys = Math.sqrt(rr * rr - (rb * Math.sin(t)) ** 2)\n      P[i].push([rb * Math.cos(t), ys + ((y1 - ys) * i) / 3, rb * Math.sin(t)])\n      N[i].push(inward ? [-Math.cos(t), 0, -Math.sin(t)] : [Math.cos(t), 0, Math.sin(t)])\n    }\n  }\n  gridMesh(k, mat, P, { N })\n}\n"
    }
  ],
  "categories": [
    "piping"
  ],
  "meta": {
    "export": "SmallBoreFitting",
    "anchor": "fitting",
    "standards": [
      "ASME B16.11",
      "ASME B1.20.1",
      "MSS SP-83",
      "MSS SP-95",
      "ASME B31.3"
    ]
  }
}