{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "riser",
  "type": "registry:component",
  "title": "Riser top section",
  "description": "Export or import riser where it reaches the topsides. Below the deck (y < 0, down to `length`) the coated riser runs through split jacket clamps on brackets and splash-zone sheathing placed from `airGap`. At the deck a forged hang-off collar sits on beams; above it come a monolithic insulating joint, the riser ESDV (full-bore ball valve with a fail-close spring-return actuator) and a riser pressure gauge and transmitter, and a 5D piggable bend to a horizontal B16.5 topside flange.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "wellhead/riser.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/wellhead/riser.tsx",
      "content": "// Export / import riser top section: riser pipe below deck with jacket clamps and splash-zone sheathing, deck hang-off, insulating joint, riser ESDV, 5D bend to a topside flange. API RP 2RD / API 6D / ASME B31.4-B31.8.\nimport type { Kit, V3 } from '../lib/kit'\nimport { paint } from '../lib/materials'\nimport { KitMesh, useKit, type GroupProps } from '../lib/mesh'\nimport { flange, gasket, gauge, member, tagPlate, weldBead } from '../lib/parts'\nimport { port, type Port } from '../lib/ports'\nimport { faceToFace, flange as flangeDims, pipeOD, pipeWall, type Rating } from '../lib/standards'\nimport { ballValve, flangeBore, frame } from './_wellparts'\n\nexport interface RiserProps {\n  nps?: number\n  rating?: Rating\n  /** riser length below the deck, m (geometry runs down to y = -length) */\n  length?: number\n  /** deck height above mean sea level, m (places the splash-zone sheathing) */\n  airGap?: number\n  /** riser emergency shutdown valve above the hang-off */\n  esdv?: boolean\n  /** monolithic insulating joint */\n  insulatingJoint?: boolean\n  /** horizontal topside run after the bend, m */\n  run?: number\n  tag?: string\n}\n\nexport const defaults = {\n  nps: 12,\n  rating: 600 as Rating,\n  length: 20,\n  airGap: 18,\n  esdv: true,\n  insulatingJoint: true,\n  run: 1.2,\n  tag: 'ESDV-R101',\n}\n\ntype P = typeof defaults\n\nconst COAT = paint('#4a4f45', { metalness: 0.2, roughness: 0.7 }) // FBE / 3LPE riser coating\nconst IJ_BAND = paint('#2f5fa8', { metalness: 0.2, roughness: 0.5 })\n\nfunction layout(p: P) {\n  const D = pipeOD(p.nps)\n  const f = flangeDims(p.nps, p.rating)\n  const fl = f.hubLength + f.raisedFaceHeight\n  const Lv = faceToFace('ball-full', p.nps, p.rating)\n  const yIJ0 = 0.45\n  const Lij = p.insulatingJoint ? Math.max(0.8, 2.6 * D) : 0\n  const yIJ1 = yIJ0 + Lij\n  const yV0 = yIJ1 + (p.insulatingJoint ? 0.15 : 0.2) + fl\n  const yV1 = p.esdv ? yV0 + Lv : yV0\n  const R = 5 * D // 5D piggable bend, typical\n  const yB = yV1 + fl + 0.15\n  const yH = yB + R\n  const xP = R + p.run + fl\n  return { D, f, fl, Lv, yIJ0, yIJ1, Lij, yV0, yV1, R, yB, yH, xP }\n}\n\nexport function ports(props: RiserProps = {}): Port[] {\n  const p = { ...defaults, ...props } as P\n  const L = layout(p)\n  return [port('topside', 'Topside flange', [L.xP, L.yH, 0], [1, 0, 0], p.nps, p.rating)]\n}\n\nexport function Riser(props: RiserProps & GroupProps) {\n  const { nps, rating, length, airGap, esdv, insulatingJoint, run, tag, ...group } = { ...defaults, ...props }\n  const p = { nps, rating, length, airGap, esdv, insulatingJoint, run, 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 Riser\n\nfunction clamp(k: Kit, y: number, D: number, jx: number) {\n  const r = D / 2\n  const h = Math.max(0.3, D * 1.2)\n  k.cyl('rubberBlack', r + 0.012, h + 0.04, [0, y, 0], 'y', 40)\n  k.cyl('structSteelDark', r + 0.035, h, [0, y, 0], 'y', 40)\n  // split-clamp bolting lugs on ±Z\n  for (const s of [-1, 1]) {\n    k.box('structSteelDark', [0.03, h * 0.9, 0.16], [-0.02, y, s * (r + 0.1)])\n    k.box('structSteelDark', [0.03, h * 0.9, 0.16], [0.02, y, s * (r + 0.1)])\n    for (const dy of [-0.3, 0, 0.3]) {\n      k.cyl('boltXylan', 0.012, 0.14, [0, y + dy * h, s * (r + 0.12)], 'x', 8)\n      for (const sx of [-1, 1]) k.hex('nut', 0.04, 0.022, [sx * 0.05, y + dy * h, s * (r + 0.12)], 'x')\n    }\n  }\n  // bracket arm back to the jacket member stub (-X)\n  k.box('structSteelDark', [jx - r - 0.03, h * 0.6, 0.03], [-(r + jx) / 2 - 0.015, y, 0])\n  k.box('structSteelDark', [jx - r - 0.03, 0.03, 0.3], [-(r + jx) / 2 - 0.015, y + h * 0.3, 0])\n  k.box('structSteelDark', [jx - r - 0.03, 0.03, 0.3], [-(r + jx) / 2 - 0.015, y - h * 0.3, 0])\n  k.cyl('structSteel', 0.36, h + 2.4, [-jx - 0.33, y, 0], 'y', 32)\n}\n\nfunction build(k: Kit, p: P) {\n  const L = layout(p)\n  const { D, R } = L\n  const wall = pipeWall(p.nps, 'XS')\n  const nps = p.nps\n  const jx = D / 2 + 0.55\n\n  // riser below deck: coated pipe, clamps, splash-zone sheathing\n  const y0 = -Math.max(1, p.length)\n  k.pipe(COAT, [0, y0, 0], [0, -0.35, 0], D, wall, 40)\n  for (let y = -3; y > y0 + 1; y -= 6) clamp(k, y, D, jx)\n  const sz0 = Math.max(y0, -p.airGap - 3)\n  const sz1 = Math.min(-0.6, -p.airGap + 5)\n  if (sz1 > sz0 + 0.5) {\n    k.cyl('rubberBlack', D / 2 + 0.016, sz1 - sz0, [0, (sz0 + sz1) / 2, 0], 'y', 40)\n    for (const y of [sz0, sz1]) k.cyl('stainless', D / 2 + 0.02, 0.06, [0, y, 0], 'y', 40)\n  }\n\n  // hang-off: deck plate and beams, forged collar, split clamp with bolts\n  k.box('deckPlate', [1.4, 0.025, 1.4], [0, -0.0125, 0])\n  for (const s of [-1, 1]) member(k, [-0.9, -0.15, s * (D / 2 + 0.18)], [0.9, -0.15, s * (D / 2 + 0.18)], 'HEB 240', 'structSteelDark')\n  k.pipe(COAT, [0, -0.36, 0], [0, L.yIJ0 + 0.002, 0], D, wall, 40)\n  k.cyl('flangeSteel', D / 2 + 0.06, 0.14, [0, 0.07, 0], 'y', 40)\n  k.cyl('flangeSteel', D / 2 + 0.11, 0.035, [0, 0.0175, 0], 'y', 40)\n  k.around(12, D / 2 + 0.085, [0, 0.035, 0], 'y', (q) => {\n    k.cyl('boltXylan', 0.012, 0.09, [q[0], 0.06, q[2]], 'y', 8)\n    k.hex('nut', 0.04, 0.022, [q[0], 0.046, q[2]])\n  })\n  for (const s of [-1, 1]) {\n    k.box('flangeSteel', [0.16, 0.12, 0.03], [s * (D / 2 + 0.1), 0.2, 0.025])\n    k.box('flangeSteel', [0.16, 0.12, 0.03], [s * (D / 2 + 0.1), 0.2, -0.025])\n    k.cyl('boltXylan', 0.014, 0.14, [s * (D / 2 + 0.12), 0.2, 0], 'z', 8)\n  }\n  k.cyl('flangeSteel', D / 2 + 0.03, 0.16, [0, 0.21, 0], 'y', 40)\n  // riser ID plate\n  tagPlate(k, `${nps}\" RISER`, [0, 0.21, D / 2 + 0.031], 'z', 0.2)\n\n  // monolithic insulating joint: two forged hubs with the insulation band between\n  if (p.insulatingJoint) {\n    const ro = D / 2 + Math.max(0.04, D * 0.18)\n    weldBead(k, nps, [0, L.yIJ0, 0], 'y')\n    weldBead(k, nps, [0, L.yIJ1, 0], 'y')\n    k.lathe('equipmentPaint', [[D / 2 - wall, L.yIJ0], [D / 2, L.yIJ0], [ro, L.yIJ0 + L.Lij * 0.18], [ro, L.yIJ0 + L.Lij * 0.44], [D / 2 - wall, L.yIJ0 + L.Lij * 0.44]], [0, 0, 0], 'y', 48)\n    k.lathe('equipmentPaint', [[D / 2 - wall, L.yIJ0 + L.Lij * 0.56], [ro, L.yIJ0 + L.Lij * 0.56], [ro, L.yIJ0 + L.Lij * 0.82], [D / 2, L.yIJ1], [D / 2 - wall, L.yIJ1]], [0, 0, 0], 'y', 48)\n    k.cyl(IJ_BAND, ro + 0.006, L.Lij * 0.14, [0, L.yIJ0 + L.Lij * 0.5, 0], 'y', 48)\n    k.cyl('cable', 0.006, 0.12, [ro + 0.02, L.yIJ0 + L.Lij * 0.5, 0], 'y', 6)\n    k.box('nameplate', [0.16, 0.1, 0.003], [0, L.yIJ0 + L.Lij * 0.3, ro + 0.002])\n  }\n  // spool up to the ESDV\n  k.pipe('pipePaint', [0, L.yIJ1 - 0.002, 0], [0, L.yV0 - L.fl + 0.002, 0], D, wall, 40)\n  flange(k, nps, p.rating, [0, L.yV0 - 0.0045, 0], 'y')\n  gasket(k, nps, p.rating, [0, L.yV0 - 0.00225, 0], 'y')\n  if (p.esdv) {\n    k.with(frame([0, L.yV0, 0], [0, 1, 0], [0, 0, 1]), () => ballValve(k, nps, p.rating, L.Lv, 'actuator', true, true, p.tag))\n    flange(k, nps, p.rating, [0, L.yV1 + 0.0045, 0], '-y')\n    gasket(k, nps, p.rating, [0, L.yV1 + 0.00225, 0], 'y')\n  } else {\n    flange(k, nps, p.rating, [0, L.yV0 + 0.0045, 0], '-y', true)\n  }\n  // riser pressure gauge and transmitter on the topside spool, operator side (+Z)\n  {\n    const yg = L.yV1 + L.fl + 0.08\n    for (const [a, isPT] of [[0.35, false], [-0.35, true]] as const) {\n      const n: V3 = [Math.sin(a), 0, Math.cos(a)]\n      const b: V3 = [n[0] * D / 2, yg, n[2] * D / 2]\n      const e: V3 = [n[0] * (D / 2 + 0.12), yg, n[2] * (D / 2 + 0.12)]\n      k.rod('flangeSteel', [b[0] * 0.95, yg, b[2] * 0.95], e, 0.02, 12)\n      k.box('valveBody', [0.06, 0.06, 0.06], [n[0] * (D / 2 + 0.15), yg, n[2] * (D / 2 + 0.15)])\n      const t: V3 = [n[0] * (D / 2 + 0.15), yg + 0.06, n[2] * (D / 2 + 0.15)]\n      if (isPT) {\n        k.cyl('actuatorBlue', 0.035, 0.1, [t[0], t[1] + 0.05, t[2]], 'y', 16)\n        k.rbox('actuatorBlue', [0.1, 0.09, 0.09], 0.012, [t[0], t[1] + 0.14, t[2]])\n      } else {\n        k.rod('stainless', t, [t[0], t[1] + 0.08, t[2]], 0.008, 8)\n        gauge(k, [t[0], t[1] + 0.14, t[2]], 'z', 0.12, 'bar', p.rating >= 900 ? 250 : 160)\n      }\n    }\n  }\n  // 5D bend and topside run to the flange\n  k.pipe('pipePaint', [0, L.yV1 + L.fl - 0.002, 0], [0, L.yB + 0.002, 0], D, wall, 40)\n  weldBead(k, nps, [0, L.yB, 0], 'y')\n  k.bend('pipePaint', [0, L.yB, 0], [0, 1, 0], [1, 0, 0], R, D, Math.PI / 2, wall)\n  weldBead(k, nps, [R, L.yH, 0], 'x')\n  k.pipe('pipePaint', [R - 0.002, L.yH, 0], [L.xP - L.fl + 0.002, L.yH, 0], D, wall, 40)\n  flange(k, nps, p.rating, [L.xP, L.yH, 0], 'x', true)\n  flangeBore(k, nps, p.rating, [L.xP, L.yH, 0], 'x', 'XS')\n  // pipe support under the topside run\n  {\n    const x = R + p.run * 0.45\n    const yb = L.yH - D / 2\n    k.pipe('structSteel', [x, 0.02, 0], [x, yb - 0.02, 0], 0.219, 0.008, 24)\n    k.box('structSteel', [0.35, 0.02, 0.35], [x, 0.01, 0])\n    k.box('structSteel', [0.3, 0.02, D * 0.8], [x, yb - 0.01, 0])\n    k.torus('boltXylan', D / 2 + 0.01, 0.009, [x, L.yH, 0], 'x', 6, 28, Math.PI)\n  }\n}\n"
    },
    {
      "path": "wellhead/_wellparts.ts",
      "type": "registry:file",
      "target": "components/deck-twin/wellhead/_wellparts.ts",
      "content": "// Shared API 6A wellhead parts: RTJ joints, studded faces, slab gate valves (handwheel / hydraulic), angle chokes, crossovers, control-line tubing.\nimport * as THREE from 'three'\nimport { eul, type Kit, type MatKey, type V3 } from '../lib/kit'\nimport { paint } from '../lib/materials'\nimport { flange as asmeFlange, tagPlate } from '../lib/parts'\nimport { flange as flangeDims, pipeOD, pipeWall, type Rating, type Schedule } from '../lib/standards'\nimport type { ApiFlange } from './_api6a'\n\nexport const BODY: MatKey = 'valveBody'\nexport const SPOOL: MatKey = paint('#5d646b', { metalness: 0.4, roughness: 0.5 })\nexport const ACT: MatKey = 'actuatorYellow'\nexport const HW: MatKey = 'actuatorRed'\nexport const STUD: MatKey = 'boltXylan'\nexport const NUT: MatKey = 'nut'\nexport const SS: MatKey = 'stainless'\n/** half the ring-gasket gap at each RTJ joint */\nexport const G = 0.003\n\n/** Frame with local +Y = flow, +Z = stem / bonnet, +X = flow × stem, origin at o. */\nexport function frame(o: V3, flow: V3, stem: V3) {\n  const y = new THREE.Vector3(...flow).normalize()\n  const z = new THREE.Vector3(...stem).normalize()\n  const x = new THREE.Vector3().crossVectors(y, z).normalize()\n  return new THREE.Matrix4().makeBasis(x, y, z).setPosition(...o)\n}\nexport const at = (M: THREE.Matrix4, p: V3) => new THREE.Vector3(...p).applyMatrix4(M).toArray() as V3\n\n/** 1/4\"–1/2\" stainless control-line tubing along a polyline with small bends. */\nexport function tubing(k: Kit, pts: V3[], r = 0.0048) {\n  k.tube('tubing', pts, r, 6, Math.max(0.025, r * 6))\n}\n\n/** Tube (hollow cylinder) along local Y from y0 to y1, outer radius ro, bore ri, with a dark bore liner. */\nexport function ring(k: Kit, mat: MatKey, ro: number, ri: number, y0: number, y1: number, seg = 28, liner = true) {\n  k.lathe(mat, [[ri, y0], [ro, y0], [ro, y1], [ri, y1]], [0, 0, 0], 'y', seg)\n  if (liner) bore(k, ri, y0, y1)\n}\n\n/** Dark inward-facing bore liner of radius r between y0 and y1 (local Y). */\nexport function bore(k: Kit, r: number, y0: number, y1: number, seg = 24) {\n  k.lathe('pipeBore', [[r, Math.max(y0, y1)], [r, Math.min(y0, y1)]], [0, 0, 0], 'y', seg)\n}\n\n/** Forged block (rounded in plan) w × h × d centred at c, with a through bore of radius r along local Y. */\nexport function boredBlock(k: Kit, mat: MatKey, size: V3, r: number, c: V3, round = 0.02, liner = true) {\n  const [w, h, d] = size\n  const rr = Math.min(round, w / 2 - 1e-3, d / 2 - 1e-3)\n  const sh = new THREE.Shape()\n  sh.moveTo(-w / 2 + rr, -d / 2)\n  sh.lineTo(w / 2 - rr, -d / 2)\n  sh.quadraticCurveTo(w / 2, -d / 2, w / 2, -d / 2 + rr)\n  sh.lineTo(w / 2, d / 2 - rr)\n  sh.quadraticCurveTo(w / 2, d / 2, w / 2 - rr, d / 2)\n  sh.lineTo(-w / 2 + rr, d / 2)\n  sh.quadraticCurveTo(-w / 2, d / 2, -w / 2, d / 2 - rr)\n  sh.lineTo(-w / 2, -d / 2 + rr)\n  sh.quadraticCurveTo(-w / 2, -d / 2, -w / 2 + rr, -d / 2)\n  const hole = new THREE.Path()\n  hole.absarc(0, 0, r, 0, Math.PI * 2, true)\n  sh.holes.push(hole)\n  // extrude along +Z, then turn Z onto +Y (shape y maps to -Z)\n  const m = new THREE.Matrix4().makeRotationX(-Math.PI / 2).setPosition(c[0], c[1] - h / 2, c[2])\n  k.extrude(mat, sh, h, m, 16)\n  if (liner) k.at([c[0], 0, c[2]], undefined, () => bore(k, r, c[1] - h / 2, c[1] + h / 2))\n}\n\n/** Hollow ellipsoidal ball-valve body about local Y centred at y = yc: radius R, half-height R*sy, bore r. */\nexport function hollowBall(k: Kit, mat: MatKey, R: number, sy: number, r: number, yc: number) {\n  const prof: [number, number][] = []\n  const a0 = -Math.acos(Math.min(1, r / R))\n  for (let i = 0; i <= 20; i++) {\n    const a = a0 + ((-2 * a0) * i) / 20\n    prof.push([Math.max(r, R * Math.cos(a)), yc + R * sy * Math.sin(a)])\n  }\n  prof.unshift([r, yc + R * sy * Math.sin(a0)])\n  prof.push([r, yc - R * sy * Math.sin(a0)])\n  k.lathe(mat, prof, [0, 0, 0], 'y', 32)\n}\n\n/** Closes a B16.5 flange bore: machined annulus at the RF face down to the pipe bore, plus a bore liner back through the hub. */\nexport function flangeBore(k: Kit, nps: number, rating: Rating, face: V3, axis: 'x' | '-x' | 'y' | '-y' | 'z' | '-z', schedule: Schedule = '80') {\n  const f = flangeDims(nps, rating)\n  const ro = pipeOD(nps) / 2\n  const ri = ro - pipeWall(nps, schedule)\n  k.at(face, axis, () => {\n    k.lathe('machined', [[ro * 0.88, 0], [ri, 0]], [0, 0, 0], 'y', 28)\n    k.lathe('pipeBore', [[ri, 0], [ri, -(f.hubLength + f.raisedFaceHeight) - 0.004]], [0, 0, 0], 'y', 24)\n  })\n}\n\nconst nutH = (d: ApiFlange) => d.boltDia * 0.95\nconst seg = (d: ApiFlange) => (d.bolts >= 12 ? 40 : 28)\n\n/** Studs and nuts through two equal API flanges meeting at y = yj, with the ring gasket showing in the gap. */\nexport function joint(k: Kit, d: ApiFlange, yj: number) {\n  const so = d.boltDia * 0.4\n  const len = 2 * (G + d.t) + 2 * nutH(d) + 2 * so\n  k.boltCircle(STUD, NUT, d.bolts, d.boltCircle / 2, [0, yj - len / 2, 0], 'y', d.boltDia, len, [so + nutH(d) / 2, len - so - nutH(d) / 2])\n  k.torus(SS, d.ring / 2, Math.max(0.004, d.boltDia * 0.18), [0, yj, 0], 'y', 4, 28)\n}\n\n/** Studs screwed into a studded block face at y = yf (block below), nut on the flange of the part above. */\nexport function studded(k: Kit, d: ApiFlange, yf: number) {\n  const so = d.boltDia * 0.4\n  const inBlock = d.boltDia * 1.2\n  const len = inBlock + G + d.t + nutH(d) + so\n  k.boltCircle(STUD, NUT, d.bolts, d.boltCircle / 2, [0, yf - inBlock, 0], 'y', d.boltDia, len, [inBlock + G + d.t + nutH(d) / 2])\n  k.torus(SS, d.ring / 2, Math.max(0.004, d.boltDia * 0.18), [0, yf, 0], 'y', 4, 28)\n}\n\n/** Integral API flange with its ring-joint face at y = face, body on side s (+1 above, -1 below). */\nexport function flangeAt(k: Kit, d: ApiFlange, face: number, s: 1 | -1, mat: MatKey = BODY) {\n  const y0 = face + s * G\n  const y1 = face + s * (G + d.t)\n  const lo = Math.min(y0, y1)\n  const hi = Math.max(y0, y1)\n  const ch = Math.min(0.006, d.t * 0.12)\n  // small chamfer on the back edge\n  const back = s > 0 ? hi : lo\n  const front = s > 0 ? lo : hi\n  k.lathe(mat, [[d.bore * 0.0127, front], [d.od / 2, front], [d.od / 2, back - s * ch], [d.od / 2 - ch, back], [d.bore * 0.0127, back]], [0, 0, 0], 'y', seg(d))\n}\n\nexport type Operator = { kind: 'handwheel'; mat?: MatKey; D: number } | { kind: 'hydraulic'; r: number; len: number; fusible?: boolean }\n\n/** Slab gate valve between flange faces y = 0 and y = L, bonnet along +Z. Returns the hydraulic supply port (local) when actuated. */\nexport function gateValve(k: Kit, d: ApiFlange, L: number, op: Operator, mat: MatKey = BODY): V3 | null {\n  flangeAt(k, d, 0, 1, mat)\n  flangeAt(k, d, L, -1, mat)\n  const inner = L - 2 * (G + d.t)\n  const rb = d.bore * 0.0127\n  ring(k, mat, d.od * 0.3, rb, G + d.t - 0.002, L - G - d.t + 0.002, 24, false)\n  bore(k, rb, G, L - G)\n  const w = d.od * 0.82\n  const dz = d.od * 0.95\n  const hb = Math.max(0.05, Math.min(d.od * 1.05, inner - 0.02))\n  const yc = L / 2\n  boredBlock(k, mat, [w, hb, dz], rb, [0, yc, 0], Math.min(0.025, w * 0.1), false)\n  // body sealant injection + seat grease fittings\n  k.cyl('brass', 0.008, 0.03, [w / 2 + 0.012, yc + hb * 0.25, 0], 'x', 8)\n  k.hex('brass', 0.017, 0.012, [w / 2 + 0.029, yc + hb * 0.25, 0], 'x')\n  k.hex('brass', 0.015, 0.012, [-w / 2 - 0.006, yc - hb * 0.2, dz * 0.2], 'x')\n  // bonnet flange, nuts, bonnet\n  const rbf = Math.min(w * 0.5, hb * 0.5)\n  let z = dz / 2\n  const tbf = Math.max(0.03, d.t * 0.6)\n  k.cyl(mat, rbf, tbf, [0, yc, z + tbf / 2], 'z', 28)\n  k.around(8, rbf * 0.8, [0, yc, z + tbf + 0.008], 'z', (p) => k.hex(NUT, Math.max(0.022, d.boltDia * 1.3), 0.016, p, 'z'), Math.PI / 8)\n  z += tbf\n  const bl = 0.07 + d.od * 0.18\n  const rbo = rbf * 0.66\n  k.cyl(mat, rbo, bl, [0, yc, z + bl / 2], 'z', 24)\n  z += bl\n  if (op.kind === 'handwheel') {\n    k.cyl(mat, rbo * 0.62, 0.04, [0, yc, z + 0.02], 'z', 18)\n    k.hex('brass', 0.016, 0.01, [rbo * 0.62, yc, z + 0.02], 'x')\n    const sr = Math.max(0.011, d.od * 0.045)\n    k.cyl(SS, sr, 0.12, [0, yc, z + 0.06], 'z', 12)\n    k.handwheel(op.mat ?? HW, op.D, [0, yc, z + 0.1], 'z', 4)\n    k.hex(NUT, sr * 2.4, 0.02, [0, yc, z + 0.12], 'z')\n    return null\n  }\n  // hydraulic piston actuator: adapter, cylinder with caps and tie rods, stem protector with position band\n  const { r, len } = op\n  k.cyl(mat, rbo * 0.85, 0.05, [0, yc, z + 0.025], 'z', 24)\n  z += 0.05\n  k.cyl(ACT, r * 1.08, 0.03, [0, yc, z + 0.015], 'z', 32)\n  k.cyl(ACT, r, len, [0, yc, z + 0.03 + len / 2], 'z', 32)\n  k.cyl(ACT, r * 1.08, 0.035, [0, yc, z + 0.03 + len + 0.0175], 'z', 32)\n  k.around(4, r * 1.04, [0, yc, 0], 'z', (p) => {\n    k.rod(SS, [p[0], p[1], z], [p[0], p[1], z + 0.065 + len + 0.012], 0.007, 8)\n    k.hex(NUT, 0.02, 0.012, [p[0], p[1], z + 0.065 + len + 0.006], 'z')\n  }, Math.PI / 4)\n  const zt = z + 0.065 + len\n  k.cyl(SS, 0.02, 0.1, [0, yc, zt + 0.05], 'z', 14)\n  k.cyl('safetyGreen', 0.023, 0.025, [0, yc, zt + 0.03], 'z', 14)\n  if (op.fusible) {\n    // heat-sensitive lock-open device: collar, segmented lock, fusible plug, manual release lever\n    k.cyl(SS, 0.045, 0.06, [0, yc, zt + 0.13], 'z', 20)\n    k.cyl(HW, 0.047, 0.012, [0, yc, zt + 0.12], 'z', 20)\n    k.around(4, 0.045, [0, yc, zt + 0.14], 'z', (q) => k.box(SS, [0.018, 0.018, 0.02], q), Math.PI / 4)\n    k.cyl('brass', 0.012, 0.03, [0.055, yc, zt + 0.13], 'x', 10)\n    k.box(HW, [0.012, 0.09, 0.012], [0, yc + 0.07, zt + 0.15])\n  } else k.cyl(ACT, 0.03, 0.012, [0, yc, zt + 0.104], 'z', 14)\n  const zp = z + 0.03 + len * 0.22\n  k.cyl('brass', 0.012, 0.04, [r + 0.012, yc, zp], 'x', 10)\n  k.hex('brass', 0.026, 0.016, [r + 0.036, yc, zp], 'x')\n  k.cyl('nameplate', r * 1.002, 0.04, [0, yc, z + 0.03 + len * 0.62], 'z', 32)\n  return [r + 0.046, yc, zp]\n}\n\n/** Angle choke geometry: inlet face distance along -X, outlet face distance along -Y, from the body centre. */\nexport function chokeDims(dIn: ApiFlange, dOut: ApiFlange) {\n  const B = Math.max(dIn.od, dOut.od) * 0.78\n  return { B, inlet: B / 2 + 0.05 + dIn.t + G, outlet: B / 2 + 0.09 + dOut.t + G }\n}\n\nexport type ChokeOp = 'handwheel' | 'actuator' | 'positive'\n\n/**\n * API 6A angle choke, body centre at the origin: inlet flange face at x = -inlet facing -X, outlet face at y = -outlet\n * facing -Y, bonnet / operator along +X. Returns the far end of the operator (x).\n */\nexport function angleChoke(k: Kit, dIn: ApiFlange, dOut: ApiFlange, op: ChokeOp, opening = 0.5): number {\n  const { B, inlet, outlet } = chokeDims(dIn, dOut)\n  // forged block body with inlet and outlet necks\n  k.rbox(BODY, [B, B, B * 0.92], Math.min(0.03, B * 0.12), [0, 0, 0])\n  const ri = dIn.bore * 0.0127\n  const ro = dOut.bore * 0.0127\n  k.at([0, 0, 0], '-x', () => {\n    ring(k, BODY, dIn.od * 0.3, ri, B / 2 - 0.01, inlet - dIn.t - G + 0.002, 24)\n    bore(k, ri, inlet - dIn.t - G, inlet - G)\n  })\n  k.at([0, 0, 0], '-y', () => {\n    ring(k, BODY, dOut.od * 0.32, ro, B / 2 - 0.01, outlet - dOut.t - G + 0.002, 24)\n    bore(k, ro, outlet - dOut.t - G, outlet - G)\n  })\n  // cage trim and seat seen down the bores\n  k.disc('internalsSteel', ri * 1.02, [-B / 2 + 0.012, 0, 0], '-x', 24)\n  k.disc('internalsSteel', ro * 1.02, [0, -B / 2 + 0.012, 0], '-y', 24)\n  k.cyl('pipeBore', ri * 0.35, 0.004, [-B / 2 + 0.014, 0, 0], 'x', 16)\n  k.at([-inlet, 0, 0], '-x', () => flangeAt(k, dIn, 0, -1))\n  k.at([0, -outlet, 0], '-y', () => flangeAt(k, dOut, 0, -1))\n  // wear sleeve body drain / bleed plug\n  k.hex('brass', 0.022, 0.016, [0, B * 0.25, B * 0.46 + 0.008], 'z')\n  // bonnet: hammer-nut with three lugs\n  let x = B / 2\n  const rn = B * 0.42\n  k.cyl(BODY, rn * 0.9, 0.02, [x + 0.01, 0, 0], 'x', 24)\n  x += 0.02\n  k.cyl(BODY, rn, 0.07, [x + 0.035, 0, 0], 'x', 24)\n  for (let i = 0; i < 3; i++) {\n    const a = (i / 3) * Math.PI * 2 + Math.PI / 6\n    k.box(BODY, [0.06, B * 0.2, B * 0.13], [x + 0.035, Math.cos(a) * (rn + B * 0.08), Math.sin(a) * (rn + B * 0.08)], eul(-a, 0, 0))\n  }\n  x += 0.07\n  if (op === 'positive') {\n    // blanking cap over a fixed bean\n    k.cyl(BODY, rn * 0.75, 0.05, [x + 0.025, 0, 0], 'x', 20)\n    k.hex('brass', 0.03, 0.02, [x + 0.06, 0, 0], 'x')\n    return x + 0.07\n  }\n  k.cyl(BODY, rn * 0.55, 0.1, [x + 0.05, 0, 0], 'x', 20)\n  // stem lock (holds the setting when run as a fixed choke)\n  k.rod(SS, [x + 0.05, rn * 0.5, 0], [x + 0.05, rn * 0.5 + 0.05, 0], 0.008, 8)\n  k.rod(HW, [x + 0.05, rn * 0.5 + 0.05, -0.04], [x + 0.05, rn * 0.5 + 0.05, 0.04], 0.007, 8)\n  x += 0.1\n  // position indicator sleeve with brass scale ring; stem travel shown by the opening\n  k.cyl(SS, rn * 0.4, 0.07, [x + 0.035, 0, 0], 'x', 18)\n  k.cyl('brass', rn * 0.42, 0.012, [x + 0.02, 0, 0], 'x', 18)\n  k.box('actuatorRed', [0.05, 0.004, 0.012], [x + 0.035, rn * 0.4 + 0.002, 0])\n  x += 0.07\n  if (op === 'actuator') {\n    // electro-hydraulic stepping actuator\n    k.cyl(BODY, rn * 0.35, 0.05, [x + 0.025, 0, 0], 'x', 14)\n    x += 0.05\n    k.rbox(ACT, [0.17, 0.2, 0.16], 0.02, [x + 0.085, 0, 0])\n    k.cyl(ACT, 0.05, 0.14, [x + 0.085, 0.0, 0.13], 'z', 18)\n    k.cyl(NUT, 0.052, 0.012, [x + 0.085, 0, 0.2], 'z', 18)\n    k.box('nameplate', [0.05, 0.08, 0.002], [x + 0.09, 0, -0.081])\n    k.hex('brass', 0.024, 0.02, [x + 0.05, 0.1 + 0.01, 0], 'y')\n    return x + 0.17\n  }\n  const s = 0.02 + 0.06 * opening\n  k.cyl(SS, 0.014, 0.06 + s, [x + (0.06 + s) / 2, 0, 0], 'x', 10)\n  k.handwheel(HW, Math.max(0.25, B * 1.2), [x + 0.04 + s, 0, 0], 'x', 4)\n  return x + 0.06 + s\n}\n\n/** Crossover: API flange (ring face at y = 0, body above) on a forged neck ending in a B16.5 weld-neck flange whose RF face is at y = L. */\nexport function crossover(k: Kit, d: ApiFlange, nps: number, rating: Rating, L: number) {\n  flangeAt(k, d, 0, 1, 'flangeSteel')\n  const rp = pipeOD(nps) / 2\n  const y0 = G + d.t\n  const fl = asmeFlange(k, nps, rating, [0, L, 0], 'y', true)\n  flangeBore(k, nps, rating, [0, L, 0], 'y', '160')\n  const yN = L - fl\n  if (yN > y0) {\n    k.lathe('flangeSteel', [[rp - pipeWall(nps, '160'), y0], [Math.max(rp * 1.3, d.od * 0.3), y0], [rp * 1.04, y0 + Math.min(0.05, (yN - y0) * 0.6)], [rp, yN + 0.002], [rp - pipeWall(nps, '160'), yN + 0.002]], [0, 0, 0], 'y', 28)\n    k.torus('flangeSteel', rp + 0.0008, Math.max(0.0025, rp * 0.03), [0, yN, 0], 'y', 6, 28)\n  }\n}\n\n/** Minimum crossover length so the API flange, a short neck and the B16.5 weld-neck all fit. */\nexport function crossoverLength(d: ApiFlange, nps: number, rating: Rating, neck = 0.06) {\n  const f = flangeDims(nps, rating)\n  return G + d.t + neck + f.hubLength + f.raisedFaceHeight\n}\n\n/** Plain flanged spacer spool between API faces 0 and L. */\nexport function spacer(k: Kit, d: ApiFlange, L: number, mat: MatKey = BODY) {\n  flangeAt(k, d, 0, 1, mat)\n  flangeAt(k, d, L, -1, mat)\n  ring(k, mat, d.od * 0.3, d.bore * 0.0127, G + d.t - 0.002, L - G - d.t + 0.002, 24, false)\n  bore(k, d.bore * 0.0127, G, L - G)\n}\n\n/** Flanged flow tee between faces 0 and L with an instrument branch (+Z) and needle valve. Returns the branch top (z). */\nexport function flowTee(k: Kit, d: ApiFlange, L: number) {\n  spacer(k, d, L)\n  const r = d.od * 0.3\n  k.cyl(BODY, r * 0.55, r + 0.04, [0, L / 2, (r + 0.04) / 2], 'z', 16)\n  const z = r + 0.04\n  needleValve(k, [0, L / 2, z], 'z')\n  return z + 0.105\n}\n\n/** 1/2\" needle valve on a boss, axis along +axis from base, handle to the side. */\nexport function needleValve(k: Kit, base: V3, axis: 'y' | 'z' | 'x' | '-x', handle: MatKey = HW) {\n  const v = axis === 'y' ? [0, 1, 0] : axis === 'z' ? [0, 0, 1] : axis === 'x' ? [1, 0, 0] : [-1, 0, 0]\n  const p = (t: number): V3 => [base[0] + v[0] * t, base[1] + v[1] * t, base[2] + v[2] * t]\n  k.hex(SS, 0.032, 0.02, p(0.01), axis)\n  k.box(SS, [0.036, 0.036, 0.036], p(0.042))\n  const side: V3 = axis === 'y' ? [1, 0, 0] : [0, 1, 0]\n  const q = p(0.042)\n  k.rod(SS, q, [q[0] + side[0] * 0.05, q[1] + side[1] * 0.05, q[2] + side[2] * 0.05], 0.006, 8)\n  k.box(handle, axis === 'y' ? [0.012, 0.012, 0.06] : [0.06, 0.012, 0.012], [q[0] + side[0] * 0.055, q[1] + side[1] * 0.055, q[2] + side[2] * 0.055])\n  k.rod(SS, p(0.06), p(0.105), 0.008, 8)\n}\n\n/** Exposed API ring-joint face at y = 0 (flange body below): machined face, ring groove, studs projecting out with nuts on the back. */\nexport function openFace(k: Kit, d: ApiFlange) {\n  const rb = d.bore * 0.0127\n  k.lathe('machined', [[d.ring / 2 + d.boltDia * 0.6, -G + 0.0008], [rb, -G + 0.0008]], [0, 0, 0], 'y', 32)\n  k.torus('pipeBore', d.ring / 2, Math.max(0.003, d.boltDia * 0.16), [0, -G + 0.0008, 0], 'y', 6, 32)\n  const len = d.t + nutH(d) * 2 + d.boltDia * 0.6\n  k.boltCircle(STUD, NUT, d.bolts, d.boltCircle / 2, [0, -G - d.t - nutH(d) - d.boltDia * 0.3, 0], 'y', d.boltDia, len, [nutH(d) / 2])\n}\n\nexport type BallOp = 'gear' | 'lever' | 'actuator'\n\n/**\n * API 6D full-bore ball valve, flow along +Y between RF faces y = 0 (facing -Y) and y = Lv (facing +Y), operator on +Z.\n * Studs on the outlet face only (the inlet bolts to a studded flange). Returns the operator's far z.\n */\nexport function ballValve(k: Kit, nps: number, rating: Rating, Lv: number, op: BallOp = 'gear', open = true, studsIn = false, tag?: string) {\n  const f = flangeDims(nps, rating)\n  const D = pipeOD(nps)\n  asmeFlange(k, nps, rating, [0, 0, 0], '-y', studsIn)\n  asmeFlange(k, nps, rating, [0, Lv, 0], 'y', true)\n  const fl = f.hubLength + f.raisedFaceHeight\n  const Rb = Math.max(f.od * 0.5, D * 0.95)\n  const rB = D / 2 - pipeWall(nps, '80')\n  ring(k, 'valveBody', D * 0.6, rB, fl - 0.01, Lv - fl + 0.01, 28, false)\n  hollowBall(k, 'valveBody', Rb, Math.min(1, (Lv - 2 * fl) / (2 * Rb) + 0.3), rB, Lv / 2)\n  bore(k, rB, 0, Lv)\n  flangeBore(k, nps, rating, [0, 0, 0], '-y')\n  flangeBore(k, nps, rating, [0, Lv, 0], 'y')\n  k.at([0, Lv / 2 - Rb * 0.3, 0], undefined, () => ring(k, 'valveBody', Rb * 1.02, D * 0.6, -f.boltDia * 0.7, f.boltDia * 0.7, 28, false))\n  k.around(8, Rb * 0.94, [0, Lv / 2 - Rb * 0.3 + f.boltDia * 0.7, 0], 'y', (q) => k.hex(NUT, f.boltDia * 1.6, f.boltDia * 0.8, q, 'y'), Math.PI / 8)\n  k.cyl('valveBody', Rb * 0.3, Rb * 0.3, [0, Lv / 2, -Rb * 0.92], 'z', 16)\n  k.hex('brass', 0.02, 0.016, [Rb * 0.88, Lv / 2 + Rb * 0.2, 0], 'x')\n  if (tag) tagPlate(k, tag, [-Rb * 0.5, Lv / 2, Rb * 0.6], 'z', Math.min(0.12, Rb * 0.8))\n  const z0 = Rb * 0.85\n  k.cyl('valveBody', Rb * 0.3, 0.06, [0, Lv / 2, z0 + 0.03], 'z', 18)\n  k.cyl('valveBody', Rb * 0.4, 0.02, [0, Lv / 2, z0 + 0.07], 'z', 18)\n  const zt = z0 + 0.08\n  const q = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), open ? 0 : Math.PI / 2)\n  if (op === 'lever') {\n    k.cyl(SS, 0.012, 0.04, [0, Lv / 2, zt + 0.02], 'z', 10)\n    const len = Math.max(0.2, 0.15 + 0.06 * nps)\n    const a = open ? Math.PI / 2 : 0\n    k.box(HW, [0.03, len, 0.012], [Math.cos(a) * len * 0.45, Lv / 2 + Math.sin(a) * len * 0.45, zt + 0.045], new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), a - Math.PI / 2))\n    return zt + 0.06\n  }\n  if (op === 'gear') {\n    const gb = Math.max(0.12, D * 0.9)\n    k.rbox('actuatorBlue', [gb * 1.1, gb * 0.9, gb * 0.6], gb * 0.06, [0, Lv / 2, zt + gb * 0.3])\n    k.cyl('actuatorBlue', gb * 0.16, gb * 0.35, [gb * 0.75, Lv / 2, zt + gb * 0.3], 'x', 14)\n    k.handwheel('handwheel', Math.max(0.25, gb * 2.2), [gb * 0.95, Lv / 2, zt + gb * 0.3], 'x', 5)\n    k.box(open ? 'safetyGreen' : HW, [0.025, gb * 0.7, 0.012], [0, Lv / 2, zt + gb * 0.6 + 0.006], q)\n    return zt + gb * 0.6\n  }\n  // scotch-yoke actuator with spring can (fail-close), hydraulic cylinder, limit-switch box\n  const a = Math.max(0.18, D * 1.25)\n  const ya = Lv / 2\n  const za = zt + a * 0.4\n  k.rbox('actuatorYellow', [a * 1.0, a * 0.9, a * 0.8], a * 0.06, [0, ya, za])\n  k.cyl('actuatorYellow', a * 0.42, a * 1.9, [-a * 0.5 - a * 0.95, ya, za], 'x', 32)\n  k.cyl('actuatorYellow', a * 0.46, a * 0.07, [-a * 0.5 - a * 1.9, ya, za], 'x', 32)\n  k.cyl('actuatorYellow', a * 0.46, a * 0.07, [-a * 0.5 - 0.02, ya, za], 'x', 32)\n  k.around(6, a * 0.44, [0, ya, za], 'x', (pp) => k.rod(SS, [-a * 0.5, pp[1], pp[2]], [-a * 0.5 - a * 1.95, pp[1], pp[2]], 0.008, 6))\n  k.cyl('actuatorYellow', a * 0.26, a * 0.95, [a * 0.5 + a * 0.475, ya, za], 'x', 24)\n  k.cyl('actuatorYellow', a * 0.29, a * 0.05, [a * 0.5 + a * 0.95, ya, za], 'x', 24)\n  k.around(4, a * 0.27, [0, ya, za], 'x', (pp) => k.rod(SS, [a * 0.5, pp[1], pp[2]], [a * 0.5 + a * 0.98, pp[1], pp[2]], 0.006, 6), Math.PI / 4)\n  k.rbox('stainless', [a * 0.45, a * 0.3, a * 0.28], 0.01, [0, ya, za + a * 0.4 + a * 0.14])\n  k.cyl(open ? 'safetyGreen' : HW, a * 0.08, 0.03, [0, ya, za + a * 0.68 + 0.015], 'z', 14)\n  k.rbox('stainless', [a * 0.35, a * 0.28, a * 0.22], 0.01, [a * 0.3, ya - a * 0.6, za])\n  tubingRun(k, [[a * 0.3, ya - a * 0.6 - a * 0.14, za], [a * 0.3, ya - a * 0.75, za], [a * 0.9, ya - a * 0.75, za], [a * 0.9, ya, za - a * 0.2]])\n  return za + a * 0.7\n}\n\nfunction tubingRun(k: Kit, pts: V3[]) {\n  k.tube('tubing', pts, 0.005, 6, 0.03)\n}\n"
    },
    {
      "path": "wellhead/_api6a.ts",
      "type": "registry:file",
      "target": "components/deck-twin/wellhead/_api6a.ts",
      "content": "// API 6A type 6B / 6BX flange and full-bore gate valve dimensions (typical, from published 6A tables; verify against the current edition).\nimport { inch, nearest, type Rating } from '../lib/standards'\n\nexport type Psi = 2000 | 3000 | 5000 | 10000 | 15000\n\nexport interface ApiFlange {\n  /** nominal bore, inches (e.g. 2.0625) */\n  bore: number\n  od: number\n  /** total thickness, ring-joint face to back */\n  t: number\n  bolts: number\n  boltDia: number\n  boltCircle: number\n  /** ring groove pitch diameter (approx) */\n  ring: number\n  /** 6BX (true for 10k+, and for 13-5/8 and up at 5k) */\n  bx: boolean\n}\n\n// inches: [OD, thickness, bolt circle, bolt count, bolt dia]\ntype Row = [od: number, t: number, bc: number, n: number, bd: number]\n\nconst T: Record<Psi, Record<number, Row>> = {\n  2000: {\n    2.0625: [6.5, 1.31, 5.0, 8, 0.625], 2.5625: [7.5, 1.44, 5.88, 8, 0.75], 3.125: [8.25, 1.56, 6.62, 8, 0.75],\n    4.0625: [10.75, 1.81, 8.5, 8, 0.875], 7.0625: [14.0, 2.19, 11.5, 12, 1.0], 9: [16.5, 2.5, 13.75, 12, 1.125],\n    11: [20.0, 2.81, 17.0, 16, 1.25], 13.625: [22.0, 2.94, 19.25, 20, 1.25], 16.75: [27.0, 3.31, 23.75, 20, 1.5],\n    21.25: [32.0, 3.88, 28.5, 24, 1.625],\n  },\n  3000: {\n    2.0625: [8.5, 1.81, 6.5, 8, 0.875], 2.5625: [9.62, 1.94, 7.5, 8, 1.0], 3.125: [9.5, 1.81, 7.5, 8, 0.875],\n    4.0625: [11.5, 2.06, 9.25, 8, 1.125], 7.0625: [15.0, 2.5, 12.5, 12, 1.125], 9: [18.5, 2.81, 15.5, 12, 1.375],\n    11: [21.5, 3.06, 18.5, 16, 1.375], 13.625: [24.0, 3.44, 21.0, 20, 1.375], 16.75: [27.75, 3.94, 24.25, 20, 1.625],\n    20.75: [33.75, 4.75, 29.5, 20, 2.0],\n  },\n  5000: {\n    2.0625: [8.5, 1.81, 6.5, 8, 0.875], 2.5625: [9.62, 1.94, 7.5, 8, 1.0], 3.125: [10.5, 2.19, 8.0, 8, 1.125],\n    4.0625: [12.25, 2.44, 9.5, 8, 1.25], 5.125: [14.75, 3.19, 11.5, 8, 1.5], 7.0625: [15.5, 3.62, 12.5, 12, 1.375],\n    9: [19.0, 4.06, 15.5, 12, 1.625], 11: [23.0, 4.69, 19.0, 12, 1.875], 13.625: [26.5, 4.44, 23.25, 16, 1.625],\n    16.75: [30.38, 5.12, 26.62, 16, 1.875], 21.25: [39.38, 7.12, 35.38, 24, 2.0],\n  },\n  10000: {\n    1.8125: [7.38, 1.66, 5.75, 8, 0.75], 2.0625: [7.88, 1.73, 6.25, 8, 0.75], 2.5625: [9.12, 2.02, 7.25, 8, 0.875],\n    3.0625: [10.62, 2.3, 8.5, 8, 1.0], 4.0625: [12.44, 2.77, 10.19, 8, 1.125], 5.125: [14.06, 3.12, 11.81, 12, 1.125],\n    7.0625: [18.88, 4.06, 15.88, 12, 1.5], 9: [21.75, 4.88, 18.75, 16, 1.5], 11: [25.75, 5.56, 22.25, 16, 1.75],\n    13.625: [30.25, 6.62, 26.5, 20, 1.875], 16.75: [34.31, 6.62, 30.56, 24, 1.875], 18.75: [40.94, 8.78, 36.44, 24, 2.25],\n    21.25: [45.0, 9.5, 40.25, 24, 2.5],\n  },\n  15000: {\n    1.8125: [8.19, 1.78, 6.31, 8, 0.875], 2.0625: [8.75, 2.0, 6.88, 8, 0.875], 2.5625: [10.0, 2.25, 7.88, 8, 1.0],\n    3.0625: [11.31, 2.53, 9.06, 8, 1.125], 4.0625: [14.19, 3.09, 11.44, 8, 1.375], 5.125: [16.5, 3.88, 13.63, 12, 1.5],\n    7.0625: [19.88, 4.69, 16.88, 16, 1.5], 9: [25.5, 5.75, 21.75, 16, 1.875], 11: [32.0, 7.38, 28.0, 20, 2.0],\n    13.625: [34.88, 8.06, 30.38, 20, 2.25], 18.75: [45.75, 10.06, 40.0, 20, 3.0],\n  },\n}\n\n/** API 6B / 6BX flange for a nominal bore (snapped to the nearest size the pressure class defines). Typical dims. */\nexport function apiFlange(bore: number, psi: Psi = 5000): ApiFlange {\n  const tbl = T[psi] ?? T[5000]\n  const b = nearest(Object.keys(tbl).map(Number), bore)\n  const [od, t, bc, n, bd] = tbl[b]\n  return { bore: b, od: inch(od), t: inch(t), bolts: n, boltDia: inch(bd), boltCircle: inch(bc), ring: inch(bc - bd * 1.9), bx: psi >= 10000 || b >= 13.625 }\n}\n\n// Full-bore flanged gate valve face-to-face, inches (typical API 6A, slab gate).\nconst GATE: Record<Psi, Record<number, number>> = {\n  2000: { 2.0625: 11.62, 2.5625: 13.12, 3.125: 14.12, 4.0625: 17.12, 7.0625: 22.12 },\n  3000: { 2.0625: 14.62, 2.5625: 16.62, 3.125: 17.12, 4.0625: 20.12, 7.0625: 24.12 },\n  5000: { 2.0625: 14.62, 2.5625: 16.62, 3.125: 18.62, 4.0625: 21.62, 5.125: 28.62, 7.0625: 29.0 },\n  10000: { 1.8125: 18.25, 2.0625: 20.5, 2.5625: 22.25, 3.0625: 24.38, 4.0625: 26.38, 5.125: 29.0, 7.0625: 35.0 },\n  15000: { 1.8125: 18.0, 2.0625: 19.0, 2.5625: 21.0, 3.0625: 23.56, 4.0625: 29.0, 5.125: 35.0, 7.0625: 40.0 },\n}\n\nexport function apiGateLength(bore: number, psi: Psi = 5000): number {\n  const tbl = GATE[psi] ?? GATE[5000]\n  return inch(tbl[nearest(Object.keys(tbl).map(Number), bore)])\n}\n\n/** Nearest bore the pressure class actually defines (2-9/16 vs 3-1/8 at 5k, 3-1/16 at 10k, …). */\nexport function apiBore(bore: number, psi: Psi = 5000): number {\n  return apiFlange(bore, psi).bore\n}\n\n/** ASME B16.5 class whose bolt pattern matches the API 6B class (2000→600, 3000→900, 5000→1500); 10k+ → 2500 is the closest, not interchangeable. */\nexport function asmeEquivalent(psi: Psi): Rating {\n  return psi <= 2000 ? 600 : psi <= 3000 ? 900 : psi <= 5000 ? 1500 : 2500\n}\n\n/** Line-pipe NPS for an API nominal bore (2-1/16 → 2, 3-1/16 / 3-1/8 → 3, 4-1/16 → 4, 13-5/8 → 14…). */\nexport function boreNps(bore: number): number {\n  return nearest([1.5, 2, 2.5, 3, 4, 5, 6, 8, 10, 12, 14, 16, 18, 20], bore - 0.07)\n}\n\nexport const BORE_LABEL: Record<number, string> = {\n  1.8125: '1-13/16\"', 2.0625: '2-1/16\"', 2.5625: '2-9/16\"', 3.0625: '3-1/16\"', 3.125: '3-1/8\"', 4.0625: '4-1/16\"',\n  5.125: '5-1/8\"', 7.0625: '7-1/16\"', 9: '9\"', 11: '11\"', 13.625: '13-5/8\"', 16.75: '16-3/4\"', 18.75: '18-3/4\"', 20.75: '20-3/4\"', 21.25: '21-1/4\"',\n}\n"
    }
  ],
  "categories": [
    "wellhead"
  ],
  "meta": {
    "export": "Riser",
    "anchor": "base",
    "standards": [
      "API RP 2RD",
      "API 6D",
      "ASME B31.4 / B31.8",
      "ASME B16.5",
      "ISO 13623"
    ]
  }
}