{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "bop-stack",
  "type": "registry:component",
  "title": "BOP stack (surface)",
  "description": "Surface blowout preventer stack on a casing head: single ram, drilling spool, double ram and annular preventer, with bolted bonnets, operating cylinders and ram locks along X. The drilling spool carries the choke (+Z) and kill (-Z) lines, each with a manual gate and a hydraulic HCR valve and a crossover to a B16.5 flange. Hydraulic hoses run from a junction plate to every function, inside a yellow handling frame with padeyes. Body proportions are typical for a 13-5/8\" 10k stack and scale with bore and pressure.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "wellhead/bop-stack.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/wellhead/bop-stack.tsx",
      "content": "// Surface BOP stack: casing head, single ram, drilling spool with choke / kill HCR lines, double ram, annular, handling frame and hoses. API 16A / API 53 / API 6A.\nimport type { Kit, V3 } from '../lib/kit'\nimport { paint } from '../lib/materials'\nimport { KitMesh, useKit, type GroupProps } from '../lib/mesh'\nimport { gauge, liftingLug, member, tagPlate } from '../lib/parts'\nimport { port, type Port } from '../lib/ports'\nimport { apiFlange, apiGateLength, asmeEquivalent, type Psi } from './_api6a'\nimport { G, NUT, SPOOL, SS, STUD, at, bore, boredBlock, ring, crossover, crossoverLength, flangeAt, frame, gateValve, joint, openFace, studded } from './_wellparts'\n\nexport interface BopStackProps {\n  /** stack bore, inches (11, 13-5/8, 18-3/4, 21-1/4) */\n  bore?: number\n  pressureRating?: Psi\n  /** handling / transport frame */\n  handlingFrame?: boolean\n  /** rams closed (operators retracted pistons extended) */\n  closed?: boolean\n  tag?: string\n}\n\nexport const defaults = {\n  bore: 13.625,\n  pressureRating: 10000 as Psi,\n  handlingFrame: true,\n  closed: false,\n  tag: 'BOP-100',\n}\n\ntype P = typeof defaults\n\nconst RED = paint('#b3342a', { metalness: 0.3, roughness: 0.45, clearcoat: 0.4 })\nconst CHOKE_BORE = 3.0625\nconst KILL_BORE = 2.0625\nconst BASE_Y = 0.04\n\nfunction layout(p: P) {\n  const psi = p.pressureRating\n  const dF = apiFlange(p.bore, psi)\n  const f = (dF.bore / 13.625) ** 0.8 * (psi >= 15000 ? 1.12 : psi >= 10000 ? 1 : 0.88) // typical scale vs a 13-5/8\" 10k stack\n  const dC = apiFlange(CHOKE_BORE, psi)\n  const dK = apiFlange(KILL_BORE, psi)\n  const yA = BASE_Y + 0.6 * f\n  const hs = 1.0 * f\n  const hsp = Math.max(0.6, 2 * (G + dF.t) + Math.max(dC.od, 0.25) + 0.12)\n  const hd = 1.7 * f\n  const ha = 1.45 * f\n  const yB = yA + hs\n  const yC = yB + hsp\n  const yD = yC + hd\n  const yE = yD + ha\n  const ySp = (yB + yC) / 2\n  const Rsp = dF.od * 0.36\n  const kr = asmeEquivalent(psi)\n  const Lc = apiGateLength(dC.bore, psi)\n  const Lk = apiGateLength(dK.bore, psi)\n  const chokeEnd = Rsp + 0.08 + 2 * Lc + crossoverLength(dC, 3, kr)\n  const killEnd = Rsp + 0.08 + 2 * Lk + crossoverLength(dK, 2, kr)\n  const bl = dF.od * 1.3 // ram body length along X\n  const bonnet = 0.5 * f\n  const op = 0.42 * f\n  const xR = bl / 2 + bonnet + op + 0.12 * f // ram operator end\n  return { psi, dF, f, dC, dK, yA, yB, yC, yD, yE, hs, hsp, hd, ha, ySp, Rsp, kr, Lc, Lk, chokeEnd, killEnd, bl, bonnet, op, xR }\n}\n\nexport function ports(props: BopStackProps = {}): Port[] {\n  const p = { ...defaults, ...props } as P\n  const L = layout(p)\n  return [port('choke', 'Choke line', [0, L.ySp, L.chokeEnd], [0, 0, 1], 3, L.kr), port('kill', 'Kill line', [0, L.ySp, -L.killEnd], [0, 0, -1], 2, L.kr)]\n}\n\nexport function BopStack(props: BopStackProps & GroupProps) {\n  const { bore, pressureRating, handlingFrame, closed, tag, ...group } = { ...defaults, ...props }\n  const p = { bore, pressureRating, handlingFrame, closed, 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 BopStack\n\n/** Ram preventer between faces y0 and y1 with `n` ram cavities; bonnets and operators along ±X. Returns hose port points. */\nfunction ramBop(k: Kit, L: ReturnType<typeof layout>, y0: number, y1: number, n: 1 | 2, closed: boolean): V3[] {\n  const { dF, f, bl, bonnet, op } = L\n  k.at([0, y0, 0], undefined, () => flangeAt(k, dF, 0, 1, RED))\n  k.at([0, y1, 0], undefined, () => flangeAt(k, dF, 0, -1, RED))\n  const b0 = y0 + G + dF.t\n  const b1 = y1 - G - dF.t\n  const bd = dF.od * 1.02\n  const rb = dF.bore * 0.0127\n  boredBlock(k, RED, [bl, b1 - b0 + 0.02, bd], rb, [0, (b0 + b1) / 2, 0], 0.03 * f)\n  const hosePts: V3[] = []\n  for (let i = 0; i < n; i++) {\n    const yr = b0 + ((b1 - b0) * (i + 0.5)) / n\n    const hb = Math.min((b1 - b0) / n - 0.06, 0.5 * f)\n    for (const s of [-1, 1]) {\n      const x0 = s * (bl / 2)\n      // bonnet flange plate with bolts, bonnet body\n      k.rbox(RED, [0.06 * f, hb * 1.05, bd * 0.9], 0.015, [x0 + s * 0.03 * f, yr, 0])\n      for (const dy of [-1, 1]) for (const dz of [-0.36, -0.12, 0.12, 0.36]) k.hex(NUT, 0.06 * f, 0.03 * f, [x0 + s * 0.075 * f, yr + dy * hb * 0.42, dz * bd], 'x')\n      k.rbox(RED, [bonnet - 0.06 * f, hb * 0.86, bd * 0.7], 0.04 * f, [x0 + s * (0.06 * f + (bonnet - 0.06 * f) / 2), yr, 0])\n      // operating cylinder with end cap and tie rods\n      const xc0 = x0 + s * bonnet\n      const ro = Math.min(hb * 0.42, 0.26 * f)\n      k.cyl(RED, ro, op, [xc0 + (s * op) / 2, yr, 0], 'x', 32)\n      k.cyl(RED, ro * 1.08, 0.05 * f, [xc0 + s * (op - 0.025 * f), yr, 0], 'x', 32)\n      k.around(6, ro * 1.02, [0, yr, 0], 'x', (q) => k.hex(NUT, 0.045 * f, 0.03 * f, [xc0 + s * (op + 0.01), q[1], q[2]], 'x'))\n      // manual ram lock (locking screw) or tail rod, longer when the rams are open\n      const lock = (closed ? 0.08 : 0.2) * f\n      k.cyl(SS, 0.035 * f, lock + 0.06, [xc0 + s * (op + (lock + 0.06) / 2), yr, 0], 'x', 12)\n      k.hex(NUT, 0.1 * f, 0.05 * f, [xc0 + s * (op + lock + 0.06), yr, 0], 'x')\n      // open / close hydraulic ports on top of the bonnet\n      for (const dx of [0.15, 0.35]) {\n        const pp: V3 = [x0 + s * dx * bonnet * 2, yr + hb * 0.43, 0]\n        k.cyl('brass', 0.02, 0.06, [pp[0], pp[1] + 0.03, pp[2]], 'y', 10)\n        hosePts.push([pp[0], pp[1] + 0.06, pp[2]])\n      }\n    }\n  }\n  // body side outlets (blanked) and nameplate\n  k.box('nameplate', [0.3 * f, 0.18 * f, 0.004], [0, (b0 + b1) / 2, bd / 2 + 0.002])\n  return hosePts\n}\n\nfunction build(k: Kit, p: P) {\n  const L = layout(p)\n  const { dF, f, dC, dK, yA, yB, yC, yD, yE, ySp, Rsp } = L\n\n  // conductor through the deck, landing plate\n  k.cyl('equipmentPaintDark', 0.381, 0.4, [0, -0.2, 0], 'y', 40)\n  k.box('structSteel', [1.3 * f, BASE_Y, 1.3 * f], [0, BASE_Y / 2, 0])\n  // casing head with two annulus outlets (studded, gate valve)\n  const rb = dF.od * 0.36\n  k.lathe(SPOOL, [[dF.bore * 0.0127, BASE_Y], [dF.od * 0.55, BASE_Y], [dF.od * 0.55, BASE_Y + 0.06], [rb, BASE_Y + 0.08], [rb, yA - G - dF.t], [dF.od / 2, yA - G - dF.t], [dF.od / 2, yA - G], [dF.bore * 0.0127, yA - G], [dF.bore * 0.0127, BASE_Y]], [0, 0, 0], 'y', 48)\n  k.boltCircle(STUD, NUT, 12, dF.od * 0.5, [0, BASE_Y - 0.01, 0], 'y', 0.025, 0.1, [0.075])\n  const dA = apiFlange(2.0625, L.psi)\n  const yo = (BASE_Y + yA - dF.t) / 2\n  for (const s of [-1, 1]) {\n    const M = frame([s * (rb + 0.04), yo, 0], [s, 0, 0], [0, 1, 0])\n    k.rod(SPOOL, [s * rb * 0.8, yo, 0], [s * (rb + 0.01), yo, 0], 0.06, 16)\n    k.with(M, () => {\n      k.box(SPOOL, [dA.od * 0.9, 0.04, dA.od * 0.9], [0, -0.02, 0])\n      studded(k, dA, 0)\n      gateValve(k, dA, apiGateLength(2.0625, L.psi) * 0.8, { kind: 'handwheel', D: 0.22, mat: 'actuatorYellow' })\n      flangeAt(k, dA, apiGateLength(2.0625, L.psi) * 0.8, 1, 'flangeSteel')\n    })\n  }\n\n  // stack joints and bodies\n  for (const y of [yA, yB, yC, yD]) k.at([0, y, 0], undefined, () => joint(k, dF, 0))\n  const hoses: V3[][] = []\n  hoses.push(ramBop(k, L, yA, yB, 1, p.closed))\n  // drilling spool with choke (+Z) and kill (-Z) outlets\n  k.at([0, yB, 0], undefined, () => flangeAt(k, dF, 0, 1, SPOOL))\n  k.at([0, yC, 0], undefined, () => flangeAt(k, dF, 0, -1, SPOOL))\n  k.lathe(SPOOL, [[dF.bore * 0.0127, yB + G + dF.t - 0.005], [Rsp, yB + G + dF.t - 0.005], [Rsp, yC - G - dF.t + 0.005], [dF.bore * 0.0127, yC - G - dF.t + 0.005]], [0, 0, 0], 'y', 40)\n  // bore liner through the whole stack\n  bore(k, dF.bore * 0.0127, BASE_Y, yE - G)\n  const hcr: V3[] = []\n  for (const [s, d, Lv, end, nps] of [[1, dC, L.Lc, L.chokeEnd, 3], [-1, dK, L.Lk, L.killEnd, 2]] as const) {\n    const z0 = s * (Rsp + 0.08)\n    k.at([0, ySp, 0], s > 0 ? 'z' : '-z', () => {\n      ring(k, SPOOL, d.od * 0.3, d.bore * 0.0127, Rsp * 0.9, Math.abs(z0) - 0.04, 20)\n      boredBlock(k, SPOOL, [d.od * 0.95, 0.05, d.od * 0.95], d.bore * 0.0127, [0, Math.abs(z0) - 0.025, 0], 0.01)\n    })\n    // the main bore seen down the outlet\n    k.disc('pipeBore', d.bore * 0.0127 + 0.002, [0, ySp, s * (Rsp + 0.002)], s > 0 ? 'z' : '-z', 24)\n    const M = frame([0, ySp, z0], [0, 0, s], [0, 1, 0])\n    let hp: V3 | null = null\n    k.with(M, () => {\n      studded(k, d, 0)\n      gateValve(k, d, Lv, { kind: 'handwheel', D: 0.3 })\n      joint(k, d, Lv)\n      k.at([0, Lv, 0], undefined, () => {\n        hp = gateValve(k, d, Lv, { kind: 'hydraulic', r: 0.1 * f + 0.02, len: 0.28 * f }, 'valveBody')\n      })\n      joint(k, d, 2 * Lv)\n    })\n    k.with(frame([0, ySp, z0 + s * 2 * Lv], [0, 0, s], [1, 0, 0]), () => crossover(k, d, nps, L.kr, end - Math.abs(z0) - 2 * Lv))\n    if (hp) hcr.push(at(M, [hp[0], hp[1] + Lv, hp[2]]))\n    // valves on the outlet sit on a pipe stand\n    const ys = ySp - d.od / 2\n    const zs = s * (Math.abs(z0) + Lv * 1.5)\n    k.pipe('structSteel', [0, BASE_Y + 0.015, zs], [0, ys - 0.012, zs], 0.114, 0.006, 16)\n    k.box('structSteel', [0.25, 0.015, 0.25], [0, BASE_Y / 2 + 0.0075, zs])\n    k.box('structSteel', [d.od * 0.7, 0.012, 0.12], [0, ys - 0.006, zs])\n  }\n  hoses.push(ramBop(k, L, yC, yD, 2, p.closed))\n\n  // annular preventer: bottom flange, body, latched head, spherical cap, studded top\n  {\n    const Ra = dF.od * 0.98\n    const y0 = yD + G\n    const yt = yE - G - dF.t * 0.6\n    k.lathe(RED, [\n      [dF.bore * 0.0127, y0],\n      [dF.od / 2, y0],\n      [dF.od / 2, y0 + dF.t],\n      [Ra * 0.85, y0 + dF.t + 0.03],\n      [Ra, y0 + dF.t + 0.12 * L.f],\n      [Ra, y0 + L.ha * 0.48],\n      [Ra * 1.04, y0 + L.ha * 0.5],\n      [Ra * 1.04, y0 + L.ha * 0.56],\n      [Ra * 0.98, y0 + L.ha * 0.58],\n      [Ra * 0.9, y0 + L.ha * 0.7],\n      [Ra * 0.72, y0 + L.ha * 0.82],\n      [Ra * 0.5, yt - 0.04],\n      [dF.od * 0.42, yt],\n      [dF.bore * 0.0127, yt],\n    ], [0, 0, 0], 'y', 64)\n    // head latch screws round the body joint\n    k.around(16, Ra * 1.04, [0, y0 + L.ha * 0.53, 0], 'y', (q) => k.hex(NUT, 0.06 * L.f, 0.04 * L.f, q, [q[0], 0, q[2]]))\n    // studded top with ring groove and studs\n    k.lathe(RED, [[dF.bore * 0.0127, yt - 0.01], [dF.od * 0.42, yt - 0.01], [dF.od * 0.42, yt + dF.t * 0.6], [dF.bore * 0.0127, yt + dF.t * 0.6]], [0, 0, 0], 'y', 40)\n    k.at([0, yE, 0], undefined, () => openFace(k, { ...dF, t: dF.t * 0.6 }))\n    // open / close ports\n    for (const dy of [0.25, 0.4]) {\n      const y = y0 + L.ha * dy\n      k.cyl('brass', 0.025, 0.08, [Ra + 0.03, y, 0], 'x', 10)\n      hoses.push([[Ra + 0.07, y, 0]])\n    }\n    for (const a of [0, Math.PI]) liftingLug(k, [Math.cos(a) * Ra * 0.6, y0 + L.ha * 0.78, Math.sin(a) * Ra * 0.6], 'z', 1.4 * L.f, RED)\n  }\n  tagPlate(k, p.tag, [0, yE - L.ha * 0.6, dF.od * 0.98 * 0.99], 'z', 0.25)\n  gauge(k, [0.25, ySp + 0.3, Rsp + 0.08], 'z', 0.12, 'bar', L.psi >= 10000 ? 1000 : 400)\n  k.rod(SS, [0.25, ySp + 0.1, Rsp], [0.25, ySp + 0.3, Rsp + 0.04], 0.008, 8)\n\n  // handling frame: four columns, top and mid frames, padeyes; hose junction plate on a column\n  const hx = L.xR + 0.25\n  const hz = Math.max(dF.od * 0.75 + 0.35, 0.9)\n  const yTop = yE + 0.25\n  const hoseHub: V3 = [-hx + 0.15, yC, -hz + 0.15]\n  if (p.handlingFrame) {\n    const sec = 'HEB 200'\n    for (const sx of [-1, 1])\n      for (const sz of [-1, 1]) {\n        member(k, [sx * hx, 0, sz * hz], [sx * hx, yTop, sz * hz], sec, 'safetyYellow')\n        k.box('safetyYellow', [0.35, 0.025, 0.35], [sx * hx, 0.0125, sz * hz])\n      }\n    for (const y of [0.25, yTop - 0.1]) {\n      for (const sz of [-1, 1]) member(k, [-hx, y, sz * hz], [hx, y, sz * hz], sec, 'safetyYellow')\n      for (const sx of [-1, 1]) member(k, [sx * hx, y, -hz], [sx * hx, y, hz], sec, 'safetyYellow')\n    }\n    // diagonal braces on the X faces\n    for (const sx of [-1, 1]) member(k, [sx * hx, 0.35, -hz + 0.1], [sx * hx, yTop * 0.55, hz - 0.1], 'SHS 100x100x6.3', 'safetyYellow')\n    for (const sx of [-1, 1]) for (const sz of [-1, 1]) liftingLug(k, [sx * (hx - 0.05), yTop, sz * (hz - 0.05)], 'x', 2.2, 'safetyYellow')\n  } else {\n    // free-standing hose stand when there is no frame\n    member(k, [hoseHub[0] - 0.16, 0, hoseHub[2] + 0.2], [hoseHub[0] - 0.16, hoseHub[1] + 0.45, hoseHub[2] + 0.2], 'SHS 100x100x6.3', 'safetyYellow')\n    k.box('safetyYellow', [0.35, 0.02, 0.35], [hoseHub[0] - 0.16, 0.01, hoseHub[2] + 0.2])\n  }\n  // hose junction plate with bulkhead fittings\n  k.box(SS, [0.04, 0.9, 0.5], [hoseHub[0] - 0.12, hoseHub[1], hoseHub[2] + 0.2])\n  for (let j = 0; j < 11; j++) k.hex('brass', 0.035, 0.03, [hoseHub[0] - 0.09, hoseHub[1] - 0.35 + j * 0.07, hoseHub[2] + 0.2], 'x')\n  // hydraulic hoses from the junction plate to every function\n  const src = (i: number): V3 => [hoseHub[0] - 0.1, hoseHub[1] - 0.35 + i * 0.07, hoseHub[2] + 0.2]\n  let i = 0\n  for (const group of hoses)\n    for (const q of group) {\n      const a = src(i % 11)\n      const mid: V3 = [(a[0] + q[0]) / 2, Math.max(a[1], q[1]) + 0.25, (a[2] + q[2]) / 2 - 0.3]\n      k.spline('rubberBlack', [a, [a[0] + 0.2, a[1], a[2]], mid, [q[0], q[1] + 0.2, q[2]], q], 0.017, 6, 28)\n      i++\n    }\n  for (const q of hcr) {\n    const a = src(i % 11)\n    k.spline('rubberBlack', [a, [a[0] + 0.2, a[1], a[2]], [q[0] * 0.5, q[1] + 0.4, q[2]], q], 0.017, 6, 28)\n    i++\n  }\n}\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"
    },
    {
      "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"
    }
  ],
  "categories": [
    "wellhead"
  ],
  "meta": {
    "export": "BopStack",
    "anchor": "base",
    "standards": [
      "API 16A",
      "API 53",
      "API 6A",
      "API 6B / 6BX",
      "API 16D"
    ]
  }
}