{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "wellhead-housing",
  "type": "registry:component",
  "title": "Wellhead (casing head and spools)",
  "description": "Surface wellhead without a tree, on a conductor and landing plate. It has a casing head plus casing and tubing head spools (one to three sections by height), studded annulus outlets with gate valves, B16.5 crossovers and gauges on one side and VR-plugged blind outlets on the other, hanger lockdown screws, secondary-seal test ports and the SCSSV control-line exit. The top is either a blind flange with a bleed valve and gauge, or an open ring-joint face ready for a tree or BOP. The `annulus-*` ports are numbered from the tubing head down.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "wellhead/wellhead-housing.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/wellhead/wellhead-housing.tsx",
      "content": "// Surface wellhead without a tree: casing head, casing / tubing head spools, annulus outlet valves, lockdown screws, test ports, top blind or open. API 6A.\nimport type { Kit } from '../lib/kit'\nimport { KitMesh, useKit, type GroupProps } from '../lib/mesh'\nimport { gauge, tagPlate } from '../lib/parts'\nimport { port, type Port } from '../lib/ports'\nimport type { Psi } from './_api6a'\nimport { buildWellhead, wellheadLayout } from './_wellhead'\nimport { G, bore, flangeAt, joint, needleValve, openFace } from './_wellparts'\n\nexport interface WellheadHousingProps {\n  pressureRating?: Psi\n  /** casing head + spools height, m (< 0.6 compact, ≤ 1.3 two sections, else three) */\n  wellheadHeight?: number\n  /** tubing head top: blind flange with gauge, or open face ready for a tree / BOP */\n  top?: 'blind' | 'open'\n  tag?: string\n}\n\nexport const defaults = { pressureRating: 5000 as Psi, wellheadHeight: 1.1, top: 'blind' as 'blind' | 'open', tag: 'WH-101' }\n\ntype P = typeof defaults\n\nexport function ports(props: WellheadHousingProps = {}): Port[] {\n  const p = { ...defaults, ...props } as P\n  const W = wellheadLayout({ psi: p.pressureRating, height: p.wellheadHeight, crossovers: true })\n  return W.outlets.map((o) => port(o.id, `${o.id.replace('annulus-', '').toUpperCase()} annulus`, o.face, o.u, o.nps, o.rating, 'utility'))\n}\n\nexport function WellheadHousing(props: WellheadHousingProps & GroupProps) {\n  const { pressureRating, wellheadHeight, top, tag, ...group } = { ...defaults, ...props }\n  const p = { pressureRating, wellheadHeight, top, 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 WellheadHousing\n\nfunction build(k: Kit, p: P) {\n  const W = buildWellhead(k, { psi: p.pressureRating, height: p.wellheadHeight, crossovers: true })\n  const F = W.F11\n  const sp = W.spools[W.spools.length - 1]\n  const rb = Math.max(sp.ft.od, sp.fb?.od ?? 0) * 0.36\n  bore(k, Math.min(rb * 0.55, 0.16), sp.y0 + 0.05, W.T - G)\n  k.at([0, W.T, 0], undefined, () => {\n    if (p.top === 'blind') {\n      joint(k, F, 0)\n      flangeAt(k, F, 0, 1, 'flangeSteel')\n      // test / bleed port with needle valve and gauge, lifting eye\n      needleValve(k, [0, G + F.t, 0], 'y')\n      k.rod('stainless', [0, G + F.t + 0.105, 0], [0, G + F.t + 0.16, 0], 0.008, 8)\n      gauge(k, [0, G + F.t + 0.22, 0], 'z', 0.1, 'bar', p.pressureRating >= 10000 ? 1000 : 400)\n      k.torus('flangeSteel', 0.05, 0.014, [F.od * 0.3, G + F.t + 0.05, 0], 'z', 8, 20)\n      k.box('flangeSteel', [0.06, 0.04, 0.04], [F.od * 0.3, G + F.t + 0.01, 0])\n    } else openFace(k, F)\n  })\n  tagPlate(k, p.tag, [0, (sp.y0 + W.T) / 2, -rb - 0.003], '-z', 0.16)\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": "WellheadHousing",
    "anchor": "base",
    "standards": [
      "API 6A",
      "API 6B / 6BX",
      "ASME B16.5"
    ]
  }
}