{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "christmas-tree",
  "type": "registry:component",
  "title": "Christmas tree (surface, API 6A)",
  "description": "Surface wellhead and christmas tree on a conductor: casing head, tubing head spool with annulus valves and SCSSV control-line exit, tubing-head adapter, manual lower master, hydraulic SSV upper master with a heat-sensitive lock-open (fusible) device, studded cross, swab valve and tree cap with EUE lift thread, needle valve and gauge. Spools carry secondary-seal test ports and lockdown screws. The production wing carries a hydraulic wing valve, flow tee with THP gauge and an angle choke whose outlet turns into a B16.5 flowline flange; the kill wing has a manual valve and crossover. API 6B/6BX flanges and gate lengths scale with pressure rating and bore.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "wellhead/christmas-tree.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/wellhead/christmas-tree.tsx",
      "content": "// Surface wellhead and christmas tree: casing head, tubing head spool, adapter, LMV, hydraulic SSV, studded cross, swab, cap, wings with choke. API 6A / API 6B-6BX.\nimport { Kit, eul, type V3 } from '../lib/kit'\nimport { KitMesh, useKit, type GroupProps } from '../lib/mesh'\nimport { flange as asmeFlange, gauge, pipeRun, tagPlate } from '../lib/parts'\nimport { port, type Port } from '../lib/ports'\nimport { elbow90, flange as flangeDims, pipeOD, pipeWall, type Rating } from '../lib/standards'\nimport { apiBore, apiFlange, apiGateLength, asmeEquivalent, type Psi } from './_api6a'\nimport { BASE_Y, buildWellhead } from './_wellhead'\nimport { BODY, G, NUT, SS, STUD, angleChoke, boredBlock, flangeBore, at, chokeDims, crossover, crossoverLength, flangeAt, flowTee, frame, gateValve, joint, needleValve, spacer, studded, tubing } from './_wellparts'\n\nexport interface ChristmasTreeProps {\n  /** API 6A working pressure, psi */\n  pressureRating?: Psi\n  /** tree nominal bore, inches (2-1/16 … 4-1/16) */\n  bore?: number\n  wellType?: 'producer' | 'injector' | 'gas'\n  /** casing head + tubing head spool height, m */\n  wellheadHeight?: number\n  /** flowline size after the choke, NPS */\n  flowlineNps?: number\n  /** ASME class of the flowline flange after the choke */\n  flowlineRating?: Rating\n  /** choke stem position, 0 closed … 1 open */\n  choke?: number\n  tag?: string\n}\n\nexport const defaults = {\n  pressureRating: 5000 as Psi,\n  bore: 3.125,\n  wellType: 'producer' as 'producer' | 'injector' | 'gas',\n  wellheadHeight: 1.1,\n  flowlineNps: 4,\n  flowlineRating: 1500 as Rating,\n  choke: 0.5,\n  tag: 'XT-101',\n}\n\ntype P = typeof defaults\n\nconst KILL_BORE = 2.0625\n\nfunction layout(p: P) {\n  const psi = p.pressureRating\n  const dB = apiFlange(p.bore, psi)\n  const dK = apiFlange(KILL_BORE, psi)\n  const dO = apiFlange(apiBore(p.flowlineNps + 0.0625, psi), psi)\n  const F11 = apiFlange(11, psi)\n  const T = BASE_Y + p.wellheadHeight\n  const hv = apiGateLength(dB.bore, psi)\n  const aH = G * 2 + F11.t + dB.t + 0.09\n  const yLMV = T + aH\n  const ySSV = yLMV + hv\n  const cH = Math.max(dB.od, dK.od) * 0.48\n  const yC = ySSV + hv + cH\n  const yS = yC + cH\n  // production wing (+X): wing valve, flow tee, angle choke, flowline spool turning down then +X\n  const Lw = hv\n  const Lt = Math.max(0.16, 2 * (G + dB.t) + 0.08)\n  const ch = chokeDims(dB, dO)\n  const xCh = cH + Lw + Lt + ch.inlet\n  const R = elbow90(p.flowlineNps)\n  const fl = flangeDims(p.flowlineNps, p.flowlineRating)\n  const flL = fl.hubLength + fl.raisedFaceHeight\n  const yOut = yC - ch.outlet\n  const yE = yOut - G - dO.t - 0.06 - R\n  const xP = xCh + R + 0.12 + flL\n  // kill wing (-X): manual gate valve + crossover\n  const Lk = apiGateLength(KILL_BORE, psi)\n  const kr = asmeEquivalent(psi)\n  const Lx = crossoverLength(dK, 2, kr, 0.08)\n  const xK = -(cH + Lk + Lx)\n  return { psi, dB, dK, dO, F11, T, hv, yLMV, ySSV, cH, yC, yS, Lw, Lt, ch, xCh, R, flL, yOut, yE, xP, Lk, kr, Lx, xK }\n}\n\nexport function ports(props: ChristmasTreeProps = {}): Port[] {\n  const p = { ...defaults, ...props } as P\n  const L = layout(p)\n  return [\n    port('production', p.wellType === 'injector' ? 'Injection inlet' : 'Production outlet', [L.xP, L.yE, 0], [1, 0, 0], p.flowlineNps, p.flowlineRating),\n    port('kill', 'Kill wing', [L.xK, L.yC, 0], [-1, 0, 0], 2, L.kr, 'utility'),\n  ]\n}\n\nexport function ChristmasTree(props: ChristmasTreeProps & GroupProps) {\n  const { pressureRating, bore, wellType, wellheadHeight, flowlineNps, flowlineRating, choke, tag, ...group } = { ...defaults, ...props }\n  const p = { pressureRating, bore, wellType, wellheadHeight, flowlineNps, flowlineRating, choke, 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 ChristmasTree\n\nfunction build(k: Kit, p: P) {\n  const L = layout(p)\n  const { dB, dK, dO, F11, T, hv, yLMV, ySSV, cH, yC, yS } = L\n  const dialMax = L.psi >= 10000 ? 1000 : 400\n  const hwD = 0.26 + dB.bore * 0.04\n\n  buildWellhead(k, { psi: L.psi, height: p.wellheadHeight })\n\n  // tubing-head adapter: 11\" bottom flange to the tree bore\n  k.at([0, T, 0], undefined, () => joint(k, F11, 0))\n  {\n    const yb0 = T + G + F11.t\n    const yb1 = yLMV - G - dB.t\n    const rw = Math.max(dB.od * 0.42, F11.od * 0.3)\n    k.lathe(BODY, [[0.05, T + G], [F11.od / 2, T + G], [F11.od / 2, yb0], [rw, yb0], [dB.od * 0.34, Math.max(yb0 + 0.01, yb1 - 0.02)], [dB.od * 0.34, yb1], [dB.od / 2, yb1], [dB.od / 2, yLMV - G], [0.05, yLMV - G], [0.05, T + G]], [0, 0, 0], 'y', 40)\n    k.hex('brass', 0.022, 0.016, [rw + 0.005, yb0 + 0.02, 0], 'x')\n  }\n\n  const vert = (y: number) => frame([0, y, 0], [0, 1, 0], [0, 0, 1])\n  // lower master (manual)\n  k.with(vert(yLMV), () => {\n    joint(k, dB, 0)\n    gateValve(k, dB, hv, { kind: 'handwheel', D: hwD })\n    joint(k, dB, hv)\n  })\n  // upper master = surface safety valve with hydraulic piston actuator\n  const ssvR = 0.09 + dB.bore * 0.012\n  let ssvPort: V3 | null = null\n  k.with(vert(ySSV), () => {\n    ssvPort = gateValve(k, dB, hv, { kind: 'hydraulic', r: ssvR, len: 0.2 + dB.bore * 0.015, fusible: true })\n  })\n  const ssvP = at(vert(ySSV), ssvPort!)\n\n  // studded cross\n  k.at([0, yC, 0], 'x', () => boredBlock(k, BODY, [2 * cH, 2 * cH, 2 * cH * 0.92], dB.bore * 0.0127, [0, 0, 0], 0.012))\n  k.with(frame([0, yC - cH, 0], [0, -1, 0], [0, 0, 1]), () => studded(k, dB, 0))\n  k.with(frame([0, yC + cH, 0], [0, 1, 0], [0, 0, 1]), () => studded(k, dB, 0))\n  k.with(frame([cH, yC, 0], [1, 0, 0], [0, 1, 0]), () => studded(k, dB, 0))\n  k.with(frame([-cH, yC, 0], [-1, 0, 0], [0, 1, 0]), () => studded(k, dK, 0))\n  tagPlate(k, p.tag, [0, yC, cH * 0.92 + 0.001], 'z', Math.min(0.17, cH * 1.6))\n\n  // swab valve and tree cap with needle valve and gauge\n  k.with(vert(yS), () => {\n    gateValve(k, dB, hv, { kind: 'handwheel', D: hwD * 0.9 })\n    joint(k, dB, hv)\n    flangeAt(k, dB, hv, 1)\n    let y = hv + G + dB.t\n    const rc = dB.od * 0.3\n    k.cyl(BODY, rc, 0.12, [0, y + 0.06, 0], 'y', 24)\n    y += 0.12\n    const ru = dB.od * 0.38\n    k.cyl(BODY, ru, 0.075, [0, y + 0.0375, 0], 'y', 24)\n    for (let i = 0; i < 3; i++) {\n      const a = (i / 3) * Math.PI * 2\n      k.box(BODY, [0.07, 0.05, 0.035], [Math.cos(a) * (ru + 0.03), y + 0.0375, Math.sin(a) * (ru + 0.03)], eul(0, -a, 0))\n    }\n    y += 0.075\n    // lift-thread nipple (EUE) on the cap\n    k.cyl(BODY, rc * 0.7, 0.07, [0, y + 0.035, 0], 'y', 20)\n    for (let i = 0; i < 6; i++) k.torus(BODY, rc * 0.7, 0.003, [0, y + 0.01 + i * 0.01, 0], 'y', 4, 24)\n    y += 0.07\n    needleValve(k, [0, y, 0], 'y')\n    k.cyl(SS, 0.008, 0.06, [0, y + 0.13, 0], 'y', 8)\n    gauge(k, [0, y + 0.2, 0], 'z', 0.1, 'bar', dialMax)\n  })\n\n  // production wing: hydraulic wing valve, flow tee (or check body on injectors), angle choke, flowline spool\n  const Mw = frame([cH, yC, 0], [1, 0, 0], [0, 1, 0])\n  let wingPort: V3 | null = null\n  k.with(Mw, () => {\n    wingPort = gateValve(k, dB, L.Lw, { kind: 'hydraulic', r: 0.08 + dB.bore * 0.01, len: 0.16 + dB.bore * 0.012 })\n    joint(k, dB, L.Lw)\n  })\n  const x1 = cH + L.Lw\n  const Mt = frame([x1, yC, 0], [1, 0, 0], [0, 1, 0])\n  k.with(Mt, () => {\n    if (p.wellType === 'injector') {\n      // swing-check body: spacer with a bolted cover on top\n      spacer(k, dB, L.Lt)\n      const r = dB.od * 0.3\n      k.cyl(BODY, r * 1.1, r + 0.03, [0, L.Lt / 2, (r + 0.03) / 2], 'z', 20)\n      k.cyl(BODY, r * 1.35, 0.03, [0, L.Lt / 2, r + 0.045], 'z', 24)\n      k.around(6, r * 1.2, [0, L.Lt / 2, r + 0.065], 'z', (q) => k.hex(NUT, 0.022, 0.014, q, 'z'))\n    } else {\n      const gz = flowTee(k, dB, L.Lt)\n      gauge(k, [0, L.Lt / 2, gz + 0.06], 'z', 0.1, 'bar', dialMax)\n    }\n    joint(k, dB, L.Lt)\n  })\n  if (p.wellType !== 'injector') k.cyl(SS, 0.008, 0.07, at(Mt, [0, L.Lt / 2, dB.od * 0.3 + 0.18]), 'y', 8)\n  // gas wells: MEG / methanol injection point on the wing\n  if (p.wellType === 'gas') {\n    const q = at(Mw, [0, L.Lw * 0.15, -(dB.od * 0.3)])\n    k.cyl(SS, 0.012, 0.06, [q[0], q[1], q[2] - 0.03], 'z', 10)\n    needleValve(k, [q[0], q[1], q[2] - 0.06], 'y', 'actuatorBlue')\n    tubing(k, [[q[0], q[1] + 0.165, q[2] - 0.06], [q[0], q[1] + 0.25, q[2] - 0.06], [q[0], q[1] + 0.25, -0.45], [q[0], BASE_Y + 0.03, -0.45]], 0.006)\n    // MEG supply bulkhead union on a stand at the deck\n    k.hex('brass', 0.03, 0.03, [q[0], BASE_Y + 0.03, -0.45], 'y')\n    k.box(SS, [0.12, 0.012, 0.12], [q[0], BASE_Y + 0.006, -0.45])\n  }\n  // angle choke, outlet down\n  const op = p.wellType === 'gas' ? 'actuator' : 'handwheel'\n  k.at([L.xCh, yC, 0], undefined, () => {\n    k.at([-L.ch.inlet, 0, 0], undefined, () => k.with(frame([0, 0, 0], [1, 0, 0], [0, 1, 0]), () => joint(k, dB, 0)))\n    angleChoke(k, dB, dO, op, p.choke)\n  })\n  // flowline spool: API companion at the choke outlet, LR elbow, B16.5 weld-neck\n  k.with(frame([L.xCh, L.yOut, 0], [0, -1, 0], [1, 0, 0]), () => {\n    joint(k, dO, 0)\n    flangeAt(k, dO, 0, 1, 'flangeSteel')\n  })\n  const nps = p.flowlineNps\n  const yTop = L.yOut - G - dO.t\n  const rp = pipeOD(nps) / 2\n  k.lathe('flangeSteel', [[rp - pipeWall(nps, '160'), yTop], [Math.max(rp * 1.3, dO.od * 0.3), yTop], [rp * 1.02, yTop - 0.04], [rp - pipeWall(nps, '160'), yTop - 0.04]], [0, 0, 0], 'y', 28)\n  pipeRun(k, [[L.xCh, yTop - 0.035, 0], [L.xCh, L.yE, 0], [L.xP - L.flL + 0.002, L.yE, 0]], nps, { mat: 'pipePaint', schedule: '160' })\n  asmeFlange(k, nps, p.flowlineRating, [L.xP, L.yE, 0], 'x', true)\n  flangeBore(k, nps, p.flowlineRating, [L.xP, L.yE, 0], 'x', '160')\n  // flowline support under the elbow\n  const ys = L.yE - rp\n  k.box('structSteel', [0.2, 0.012, rp * 1.6], [L.xP - L.flL - 0.15, ys - 0.006, 0])\n  const sx = L.xP - L.flL - 0.15\n  k.pipe('structSteel', [sx, 0.015, 0], [sx, ys - 0.012, 0], pipeOD(4), 0.006, 20)\n  k.box('structSteel', [0.25, 0.015, 0.25], [sx, 0.0075, 0])\n  for (const [dx, dz] of [[-1, -1], [-1, 1], [1, -1], [1, 1]]) k.hex(NUT, 0.03, 0.02, [sx + dx * 0.09, 0.025, dz * 0.09])\n  k.torus(STUD, rp + 0.008, 0.007, [sx, L.yE, 0], 'x', 6, 20, Math.PI)\n\n  // kill wing: manual gate valve, crossover to B16.5\n  k.with(frame([-cH, yC, 0], [-1, 0, 0], [0, 1, 0]), () => {\n    gateValve(k, dK, L.Lk, { kind: 'handwheel', D: 0.22 })\n    joint(k, dK, L.Lk)\n  })\n  k.with(frame([-(cH + L.Lk), yC, 0], [-1, 0, 0], [0, 0, 1]), () => crossover(k, dK, 2, L.kr, L.Lx))\n\n  // hydraulic control lines (SSV, wing valve) down to a bulkhead plate on the deck, operator side (+Z)\n  const HZ = 0.32\n  const bx = 0.32\n  k.box(SS, [0.22, 0.14, 0.012], [bx + 0.05, BASE_Y + 0.07, HZ + 0.32])\n  k.box('structSteel', [0.24, 0.012, 0.08], [bx + 0.05, BASE_Y + 0.006, HZ + 0.32])\n  if (wingPort) {\n    const q = at(Mw, wingPort)\n    tubing(k, [q, [q[0], q[1], HZ + 0.06], [bx + 0.1, q[1], HZ + 0.06], [bx + 0.1, BASE_Y + 0.07, HZ + 0.06], [bx + 0.1, BASE_Y + 0.07, HZ + 0.315]])\n  }\n  {\n    const q = ssvP\n    const x = Math.max(q[0] + 0.06, bx)\n    tubing(k, [q, [x, q[1], q[2]], [x, q[1], HZ], [x, BASE_Y + 0.07, HZ], [x, BASE_Y + 0.07, HZ + 0.315]])\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/_wellhead.ts",
      "type": "registry:file",
      "target": "components/deck-twin/wellhead/_wellhead.ts",
      "content": "// Shared surface wellhead stack: conductor and landing plate, casing head, casing / tubing head spools with annulus outlets. API 6A.\nimport { eul, type Kit, type V3 } from '../lib/kit'\nimport { gauge } from '../lib/parts'\nimport type { Rating } from '../lib/standards'\nimport { apiFlange, apiGateLength, asmeEquivalent, type ApiFlange, type Psi } from './_api6a'\nimport { G, NUT, SPOOL, SS, STUD, at, crossover, crossoverLength, flangeAt, frame, gateValve, joint, needleValve, studded, tubing } from './_wellparts'\n\nexport const BASE_Y = 0.04 // landing plate on the deck\n\nexport interface WellheadOpts {\n  psi: Psi\n  /** casing head + spools height above the landing plate, m */\n  height: number\n  /** annulus outlets end in a crossover to an ASME flange (ports) instead of a blind */\n  crossovers?: boolean\n}\n\ntype Spool = { y0: number; y1: number; fb: ApiFlange | null; ft: ApiFlange; dirs: V3[] }\n\nexport interface Outlet {\n  id: string\n  /** outward unit direction */\n  u: V3\n  y: number\n  R0: number\n  /** port face (crossover end) */\n  face: V3\n  nps: number\n  rating: Rating\n}\n\nexport function wellheadLayout(o: WellheadOpts) {\n  const psi = o.psi\n  const F11 = apiFlange(11, psi)\n  const F13 = apiFlange(13.625, psi >= 10000 ? 5000 : 3000)\n  const F20 = apiFlange(20.75, 3000)\n  const H = o.height\n  const y0 = BASE_Y\n  const T = y0 + H\n  const X: V3 = [1, 0, 0]\n  const D1: V3 = [Math.SQRT1_2, 0, -Math.SQRT1_2]\n  const spools: Spool[] =\n    H < 0.6\n      ? [{ y0, y1: T, fb: null, ft: F11, dirs: [X, D1] }]\n      : H <= 1.3\n        ? [\n            { y0, y1: y0 + 0.48 * H, fb: null, ft: F13, dirs: [D1] },\n            { y0: y0 + 0.48 * H, y1: T, fb: F13, ft: F11, dirs: [X] },\n          ]\n        : [\n            { y0, y1: y0 + 0.34 * H, fb: null, ft: F20, dirs: [D1] },\n            { y0: y0 + 0.34 * H, y1: y0 + 0.67 * H, fb: F20, ft: F13, dirs: [D1] },\n            { y0: y0 + 0.67 * H, y1: T, fb: F13, ft: F11, dirs: [X] },\n          ]\n  const ap: Psi = psi >= 10000 ? 10000 : 5000\n  const dA = apiFlange(2.0625, ap)\n  const LA = apiGateLength(2.0625, ap) * 0.8\n  const kr = asmeEquivalent(ap)\n  const Lx = crossoverLength(dA, 2, kr, 0.05)\n  const outlets: (Outlet & { spool: number; j: number })[] = []\n  const names = ['annulus-a', 'annulus-b', 'annulus-c', 'annulus-d']\n  // numbered from the tubing head (A annulus) downwards\n  for (let i = spools.length - 1; i >= 0; i--) {\n    const sp = spools[i]\n    const fbT = sp.fb ? sp.fb.t : 0.05\n    const rb = Math.max(sp.ft.od, sp.fb?.od ?? 0) * 0.36\n    const yb0 = sp.y0 + (sp.fb ? G : 0) + fbT\n    const yb1 = sp.y1 - G - sp.ft.t\n    const bodyH = yb1 - yb0\n    if (bodyH < 0.1) continue\n    sp.dirs.forEach((u, j) => {\n      const y = sp.dirs.length > 1 ? yb0 + bodyH * (j === 0 ? 0.68 : 0.3) : (yb0 + yb1) / 2\n      const R0 = rb + 0.05\n      const d = R0 + LA + Lx\n      outlets.push({ id: names[outlets.length] ?? `annulus-${outlets.length + 1}`, u, y, R0, face: [u[0] * d, y, u[2] * d], nps: 2, rating: kr, spool: i, j })\n    })\n  }\n  return { F11, F13, F20, T, spools, dA, LA, kr, Lx, outlets }\n}\n\n/** Conductor stub, landing plate and the wellhead spools. Returns the layout. */\nexport function buildWellhead(k: Kit, o: WellheadOpts) {\n  const W = wellheadLayout(o)\n  const { spools, dA, LA } = W\n\n  // conductor through the deck, landing plate with gussets and hold-down bolts\n  k.cyl('equipmentPaintDark', 0.381, 0.5, [0, -0.25, 0], 'y', 40)\n  k.cyl('equipmentPaintDark', 0.3, 0.02, [0, -0.49, 0], 'y', 32)\n  k.box('structSteel', [1.0, BASE_Y, 1.0], [0, BASE_Y / 2, 0])\n  for (let i = 0; i < 4; i++) {\n    const a = (i * Math.PI) / 2 + Math.PI / 4\n    k.box('structSteel', [0.012, 0.1, 0.09], [Math.cos(a) * 0.43, BASE_Y + 0.05, Math.sin(a) * 0.43], eul(0, -a, 0))\n  }\n  for (const [sx, sz] of [[-1, -1], [-1, 1], [1, -1], [1, 1]]) {\n    k.hex(NUT, 0.04, 0.025, [sx * 0.44, BASE_Y + 0.012, sz * 0.44])\n    k.cyl(STUD, 0.011, 0.05, [sx * 0.44, BASE_Y + 0.02, sz * 0.44], 'y', 8)\n  }\n\n  spools.forEach((sp, i) => {\n    const fbT = sp.fb ? sp.fb.t : 0.05\n    const fbOd = sp.fb ? sp.fb.od : Math.max(sp.ft.od, 0.7)\n    const rb = Math.max(sp.ft.od, sp.fb?.od ?? 0) * 0.36\n    const g0 = sp.y0 + (sp.fb ? G : 0)\n    const yb0 = g0 + fbT\n    const yb1 = sp.y1 - G - sp.ft.t\n    const ri = Math.min(rb * 0.55, 0.16)\n    const fil = Math.min(0.03, Math.max(0.005, (yb1 - yb0) * 0.15))\n    k.lathe(SPOOL, [[ri, g0], [fbOd / 2, g0], [fbOd / 2, yb0], [rb * 1.12, yb0], [rb, yb0 + fil], [rb, yb1 - fil], [rb * 1.12, yb1], [sp.ft.od / 2, yb1], [sp.ft.od / 2, sp.y1 - G], [ri, sp.y1 - G], [ri, g0]], [0, 0, 0], 'y', 48)\n    if (sp.fb) {\n      k.at([0, sp.y0, 0], undefined, () => joint(k, sp.fb!, 0))\n      // secondary-seal test port in the lower flange\n      k.hex('brass', 0.03, 0.02, [0, sp.y0 + G + fbT / 2, -(fbOd / 2 + 0.008)], 'z')\n    } else k.boltCircle(STUD, NUT, 12, fbOd / 2 - 0.035, [0, BASE_Y - 0.01, 0], 'y', 0.022, fbT + 0.05, [fbT + 0.022])\n    // hanger lockdown screws in the upper flange\n    const yl = sp.y1 - G - sp.ft.t / 2\n    k.around(8, sp.ft.od / 2, [0, yl, 0], 'y', (q) => {\n      const r = Math.hypot(q[0], q[2])\n      const n: V3 = [q[0] / r, 0, q[2] / r]\n      k.hex('brass', 0.045, 0.03, [q[0] + n[0] * 0.012, yl, q[2] + n[2] * 0.012], n)\n      k.rod(SS, [q[0] + n[0] * 0.02, yl, q[2] + n[2] * 0.02], [q[0] + n[0] * 0.055, yl, q[2] + n[2] * 0.055], 0.008, 8)\n    }, Math.PI / 8)\n    const bodyH = yb1 - yb0\n    if (bodyH < 0.1) return\n    sp.dirs.forEach((u, j) => {\n      const y = sp.dirs.length > 1 ? yb0 + bodyH * (j === 0 ? 0.68 : 0.3) : (yb0 + yb1) / 2\n      const padSize = Math.min(dA.od * 0.95, bodyH - 0.01, sp.dirs.length > 1 ? bodyH * 0.45 : 1)\n      for (const s of [1, -1]) {\n        const uu: V3 = [u[0] * s, 0, u[2] * s]\n        const R0 = rb + 0.05\n        const M = frame([uu[0] * R0, y, uu[2] * R0], uu, [0, 1, 0])\n        k.rod(SPOOL, [uu[0] * rb * 0.8, y, uu[2] * rb * 0.8], [uu[0] * (R0 - 0.03), y, uu[2] * (R0 - 0.03)], Math.min(0.06, padSize * 0.4), 16)\n        k.with(M, () => {\n          k.box(SPOOL, [padSize, 0.035, padSize], [0, -0.0175, 0])\n          studded(k, dA, 0)\n        })\n        if (s === 1) {\n          k.with(M, () => {\n            gateValve(k, dA, LA, { kind: 'handwheel', mat: 'actuatorYellow', D: 0.2 })\n            joint(k, dA, LA)\n            if (o.crossovers) k.at([0, LA, 0], undefined, () => crossover(k, dA, 2, W.kr, W.Lx))\n            else {\n              flangeAt(k, dA, LA, 1, 'flangeSteel')\n              needleValve(k, [0, LA + G + dA.t, 0], 'y', 'actuatorYellow')\n            }\n          })\n          if (!o.crossovers) gauge(k, at(M, [0, LA + G + dA.t + 0.16, 0]), uu, 0.1, 'bar', 100)\n          else {\n            // annulus pressure gauge on a needle valve on top of the gate valve body\n            const q = at(M, [0, LA * 0.5, 0])\n            k.rod(SS, [q[0], q[1] + 0.03, q[2]], [q[0], q[1] + 0.2, q[2]], 0.008, 8)\n            gauge(k, [q[0], q[1] + 0.26, q[2]], [-uu[2], 0, uu[0]], 0.1, 'bar', 100)\n          }\n        } else {\n          // VR-plugged outlet: blind flange with a needle valve\n          k.with(M, () => {\n            flangeAt(k, dA, 0, 1, 'flangeSteel')\n            needleValve(k, [0, G + dA.t, 0], 'y', 'actuatorYellow')\n          })\n        }\n      }\n    })\n    // tubing head: SCSSV control-line exit tubed down to the deck\n    if (i === spools.length - 1) {\n      const yc = yb0 + bodyH * 0.5\n      k.hex(SS, 0.034, 0.025, [0, yc, rb + 0.01], 'z')\n      k.box(SS, [0.04, 0.04, 0.05], [0, yc, rb + 0.06])\n      k.box('actuatorRed', [0.012, 0.06, 0.012], [0.025, yc + 0.03, rb + 0.06])\n      tubing(k, [[0, yc, rb + 0.08], [0, yc, 0.5], [0, BASE_Y + 0.03, 0.5], [-0.05, BASE_Y + 0.03, 0.56]])\n      k.box(SS, [0.12, 0.08, 0.01], [-0.05, BASE_Y + 0.04, 0.565])\n    }\n  })\n  return W\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": "ChristmasTree",
    "anchor": "base",
    "standards": [
      "API 6A",
      "API 6B / 6BX",
      "API 14D (SSV)",
      "ASME B16.5"
    ]
  }
}