{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "mud-pump",
  "type": "registry:component",
  "title": "Drilling mud pump",
  "description": "Horizontal single-acting triplex (or 4–6 cylinder) mud pump on an oilfield skid, sized on a 14\" stroke, 2200 hp class unit: 5.5 m over skids, discharge centreline 1.25 m, 10\" class 150 suction manifold, 5\" 10K discharge through a strainer cross. Forged fluid-end modules with hammer-lug valve and cylinder-head covers, liner retainers, pony rods that stroke when `running`, liner-wash header and drip pan. Bladder pulsation dampener and reset relief valve on the cross, two top-mounted AC motors driving each end of the pinion shaft by banded V-belts behind mesh guards or by enclosed gear cases (`drive`).",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "drilling/mud-pump.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/drilling/mud-pump.tsx",
      "content": "// Drilling mud pump package (API 7K): horizontal single-acting triplex / multiplex, forged fluid end, twin top-mounted AC motors, belt or gear drive.\nimport * as THREE from 'three'\nimport { useEffect, useMemo, useRef } from 'react'\nimport { useFrame } from '@react-three/fiber'\nimport { Kit, type Built, type MatKey, type V3 } from '../lib/kit'\nimport { paint } from '../lib/materials'\nimport { KitMesh, type GroupProps } from '../lib/mesh'\nimport { blindFlange, flange, gauge, junctionBox, motor, motorSize, skid, tagPlate } from '../lib/parts'\nimport { port, type Port } from '../lib/ports'\nimport { Rotors, fanRotor, type RotorSpec } from '../lib/rotors'\nimport { meshGuardMaterial } from '../lib/textures'\nimport { pipeOD, pipeWall, type Rating } from '../lib/standards'\nimport { atZ, hammerCap, hullBand, hullFace, needleValve } from './_drill'\n\nexport interface MudPumpProps {\n  /** pumping cylinders (3 = triplex) */\n  cylinders?: number\n  /** input power per AC motor, kW */\n  kw?: number\n  drive?: 'belt' | 'gear'\n  suctionNps?: number\n  dischargeNps?: number\n  dischargeRating?: Rating\n  /** pump paint */\n  color?: string\n  tag?: string\n  /** turn the drive and stroke the pony rods */\n  running?: boolean\n}\n\nexport const defaults = {\n  cylinders: 3,\n  kw: 860,\n  drive: 'belt' as 'belt' | 'gear',\n  suctionNps: 10,\n  dischargeNps: 5,\n  dischargeRating: 2500 as Rating,\n  color: '#35608c',\n  tag: 'P-3101A',\n  running: true,\n}\n\ntype P = typeof defaults\n\n// typical 14\" stroke / 2200 hp class: 5.5 m over skids, discharge CL 1.25 m, suction CL 0.5 m\nfunction layout(p: Omit<P, 'tag' | 'running' | 'color'>) {\n  const n = Math.max(3, Math.min(6, Math.round(p.cylinders)))\n  const pitch = 0.56\n  const span = (n - 1) * pitch\n  const pw = span + 0.7\n  const L = 5.54 + Math.max(0, n - 3) * 0.1\n  const W = Math.max(2.31, pw + 0.5)\n  const yb = 0.248\n  const yc = 0.94\n  const yd = 1.251\n  const ys = 0.505\n  const xFe = 1.65\n  const xFf = 2.4\n  const xV = 2.1\n  const xD = 1.83\n  const zEnd = span / 2 + pitch / 2\n  const zX = zEnd + 0.3\n  const crossH = 0.3\n  const dOut: V3 = [xD, yd, zX + crossH / 2 + 0.36]\n  const sIn: V3 = [xV, ys, -(zEnd + 0.55)]\n  const rv: V3 = [1.42, yd + 0.42, zX + 0.42]\n  return { n, pitch, span, pw, L, W, yb, yc, yd, ys, xFe, xFf, xV, xD, zEnd, zX, crossH, dOut, sIn, rv }\n}\n\nexport function ports(props: MudPumpProps = {}): Port[] {\n  const p = { ...defaults, ...props }\n  const l = layout(p)\n  return [\n    port('suction', 'Suction', l.sIn, [0, 0, -1], p.suctionNps, 150),\n    port('discharge', 'Discharge', l.dOut, [0, 0, 1], p.dischargeNps, p.dischargeRating),\n    port('relief', 'Relief valve outlet', l.rv, [0, 0, 1], 3, 150, 'relief'),\n  ]\n}\n\nexport function MudPump({ tag = defaults.tag, ...rest }: MudPumpProps & GroupProps) {\n  const { cylinders, kw, drive, suctionNps, dischargeNps, dischargeRating, color, running, ...group } = { ...defaults, ...rest }\n  const out = useMemo(\n    () => build({ cylinders, kw, drive, suctionNps, dischargeNps, dischargeRating, color, tag, running }),\n    [cylinders, kw, drive, suctionNps, dischargeNps, dischargeRating, color, tag],\n  )\n  useEffect(() => () => [out.built, out.rod.built, ...out.rotors.map((r) => r.built)].forEach((b) => b.geometry.dispose()), [out])\n  return (\n    <group {...group}>\n      <KitMesh data={out.built} />\n      <Rotors rotors={out.rotors} running={running} />\n      <Strokes data={out.rod.built} at={out.rod.at} amp={out.rod.amp} running={running} />\n    </group>\n  )\n}\n\nexport default MudPump\n\n/** Pony-rod assemblies reciprocating with equal phase spacing. */\nfunction Strokes({ data, at, amp, running }: { data: Built; at: V3[]; amp: number; running: boolean }) {\n  const refs = useRef<(THREE.Group | null)[]>([])\n  const t = useRef(0)\n  useFrame((_, dt) => {\n    if (!running) return\n    t.current += Math.min(dt, 0.1) * Math.PI * 2 * (60 / 60)\n    refs.current.forEach((g, i) => g && (g.position.x = amp * Math.sin(t.current + (i * Math.PI * 2) / at.length)))\n  })\n  return (\n    <>\n      {at.map((p, i) => (\n        <group key={i} position={p}>\n          <group ref={(g) => void (refs.current[i] = g)}>\n            <KitMesh data={data} shadows={false} />\n          </group>\n        </group>\n      ))}\n    </>\n  )\n}\n\nfunction build(p: P) {\n  const k = new Kit()\n  const l = layout(p)\n  const { n, pitch, span, pw, L, W, yb, yc, yd, ys, xFe, xFf, xV, xD, zEnd, zX, crossH } = l\n  const B: MatKey = paint(p.color)\n  const FE: MatKey = paint('#5b6168', { metalness: 0.45, roughness: 0.45 })\n  const rotors: RotorSpec[] = []\n  const zs = Array.from({ length: n }, (_, i) => -span / 2 + i * pitch)\n\n  // oilfield skid\n  k.at([0, 0, 0], undefined, () => skid(k, L, W, 'HEB 240', 'structSteelDark'))\n\n  // power end: crankcase with bull-gear hump (crank CL at xc, yk), flat front deck over the crossheads\n  const xc = -1.45\n  const yk = 1.25\n  const Rg = 0.9\n  const a0 = Math.asin((1.62 - yk) / Rg)\n  const topY = (x: number) => (x > xc + Rg * Math.cos(a0) ? 1.62 : yk + Math.sqrt(Math.max(0, Rg * Rg - (x - xc) ** 2)))\n  const xr = xc - Rg\n  const sh = new THREE.Shape()\n  sh.moveTo(xr, yb)\n  sh.lineTo(0.6, yb)\n  sh.lineTo(0.6, 1.62)\n  sh.lineTo(xc + Rg * Math.cos(a0), 1.62)\n  sh.absarc(xc, yk, Rg, a0, Math.PI, false)\n  sh.lineTo(xr, yb)\n  k.extrude(B, sh, pw, atZ(-pw / 2), 48)\n  // base flange, side ribs and split line\n  k.box(B, [0.6 - xr + 0.12, 0.05, pw + 0.12], [(0.6 + xr) / 2, yb + 0.025, 0])\n  for (const s of [-1, 1]) {\n    for (const x of [-2.1, -1.45, -0.8, 0.1]) k.box(B, [0.04, topY(x) - yb - 0.12, 0.016], [x, (topY(x) + yb) / 2 - 0.05, s * (pw / 2 + 0.008)])\n    k.box(B, [0.6 - xr, 0.03, 0.016], [(0.6 + xr) / 2, yk, s * (pw / 2 + 0.008)])\n    // crankshaft main bearing cover (flush, under the drive guard)\n    k.cyl(B, 0.32, 0.012, [xc, yk, s * (pw / 2 + 0.006)], 'z', 40)\n    k.around(10, 0.27, [xc, yk, s * (pw / 2 + 0.014)], 'z', (q) => k.hex('nut', 0.026, 0.006, q, 'z'))\n  }\n  k.cyl('brass', 0.03, 0.12, [xc - 0.3, topY(xc - 0.3) + 0.05, pw / 2 - 0.25], 'y', 12)\n  k.sphere('brass', 0.045, [xc - 0.3, topY(xc - 0.3) + 0.12, pw / 2 - 0.25], undefined, 12)\n  // oil level sight glass and drain\n  k.cyl('glass', 0.035, 0.012, [0.35, yb + 0.35, pw / 2 + 0.006], 'z', 20)\n  k.cyl('brass', 0.045, 0.01, [0.35, yb + 0.35, pw / 2 + 0.003], 'z', 20)\n  k.hex('nut', 0.05, 0.03, [xr - 0.015, yb + 0.12, 0], 'x')\n\n  // crosshead extension (nose) with inspection covers, diaphragm stuffing boxes on the front wall\n  const xN0 = 0.6\n  const xN1 = 1.1\n  const hN = 1.45\n  k.box(B, [xN1 - xN0, hN - yb, pw - 0.1], [(xN0 + xN1) / 2, (hN + yb) / 2, 0])\n  k.box(B, [0.04, hN - yb + 0.04, pw - 0.02], [xN1 - 0.02, (hN + yb) / 2, 0])\n  for (const z of zs) {\n    k.rbox(B, [0.34, 0.025, pitch * 0.7], 0.01, [(xN0 + xN1) / 2, hN + 0.0125, z])\n    k.around(6, 0.16, [(xN0 + xN1) / 2, hN + 0.03, z], 'y', (q) => k.hex('nut', 0.022, 0.014, q, 'y'))\n    k.cyl(B, 0.1, 0.06, [xN1 + 0.03, yc, z], 'x', 28)\n    k.cyl('stainless', 0.075, 0.03, [xN1 + 0.075, yc, z], 'x', 24)\n    k.boltCircle('boltXylan', 'nut', 6, 0.085, [xN1 + 0.02, yc, z], 'x', 0.012, 0.06, [0.05])\n  }\n  // stay rods (power end to fluid end) and the drip pan under the pony rods\n  for (const s of [-1, 1])\n    for (const y of [yc - 0.22, yc + 0.22]) {\n      k.rod('stainless', [xN1, y, s * (zEnd - 0.06)], [xFe + 0.03, y, s * (zEnd - 0.06)], 0.028, 12)\n      k.hex('nut', 0.075, 0.05, [xFe + 0.04, y, s * (zEnd - 0.06)], 'x')\n    }\n  k.box('stainless', [xFe - xN1, 0.008, pw - 0.15], [(xN1 + xFe) / 2, 0.62, 0])\n  for (const s of [-1, 1]) k.box('stainless', [xFe - xN1, 0.08, 0.006], [(xN1 + xFe) / 2, 0.66, s * (pw / 2 - 0.08)])\n  k.cyl('stainless', 0.03, 0.3, [(xN1 + xFe) / 2, 0.47, -pw / 2 + 0.2], 'y', 12)\n  k.box('structSteelDark', [0.06, 0.62 - yb, 0.06], [(xN1 + xFe) / 2, (0.62 + yb) / 2, pw / 2 - 0.2])\n\n  // liner wash header over the pony rods, nozzle drop per cylinder, supply hose to a skid-edge hose connection\n  const yw = yc + 0.34\n  const xw = (xN1 + xFe) / 2\n  k.pipe('stainless', [xw, yw, -zEnd], [xw, yw, zEnd], 0.034, 0.003)\n  for (const z of zs) k.tube('tubing', [[xw, yw, z], [xw, yc + 0.14, z], [xw + 0.06, yc + 0.1, z]], 0.006)\n  for (const s of [-1, 1]) k.box('stainless', [0.04, yw - 1.45 + 0.02, 0.03], [xw, yw - 0.02, s * zEnd * 0.6])\n  k.tube('rubberBlack', [[xw, yw, -zEnd], [xw, yw, -zEnd - 0.12], [xw, 0.4, -zEnd - 0.12], [xw, 0.4, -W / 2 + 0.08]], 0.02, 10, 0.12)\n  k.cyl('brass', 0.03, 0.08, [xw, 0.4, -W / 2 + 0.06], 'z', 12)\n\n  // fluid end modules: liner retainer, forged block, cylinder head cover (front), valve cover (top), suction boss (below)\n  const fy0 = 0.68\n  const fy1 = 1.2\n  for (const z of zs) {\n    k.rbox(FE, [xFf - xFe, fy1 - fy0, pitch - 0.035], 0.03, [(xFe + xFf) / 2, (fy0 + fy1) / 2, z])\n    k.cyl(FE, 0.19, 0.07, [xFe - 0.035, yc, z], 'x', 32)\n    k.boltCircle('boltXylan', 'nut', 8, 0.155, [xFe - 0.1, yc, z], 'x', 0.022, 0.13, [0.03])\n    k.lathe('pipeBore', [[0.085, -0.001], [0, -0.001]], [xFe - 0.07, yc, z], '-x', 24)\n    k.cyl(FE, 0.18, 0.1, [xFf + 0.05, yc, z], 'x', 32)\n    k.at([xFf + 0.1, yc, z], 'x', () => hammerCap(k, FE, 0.17, 0.11))\n    k.at([xV, fy1, z], 'y', () => {\n      k.cyl(FE, 0.16, 0.08, [0, 0.04, 0], 'y', 32)\n      k.at([0, 0.08, 0], 'y', () => hammerCap(k, FE, 0.15, 0.1))\n    })\n    k.cyl(FE, 0.13, fy0 - ys, [xV, (fy0 + ys) / 2, z], 'y', 28)\n    // suction valve cover access plug on the outboard face\n    k.cyl(FE, 0.03, 0.02, [xFf - 0.15, fy0 + 0.08, z + pitch / 2 - 0.018], 'z', 12)\n  }\n  // fluid end support legs\n  for (const s of [-1, 1]) {\n    k.box('structSteelDark', [0.5, fy0 - yb, 0.04], [xFe + 0.3, (fy0 + yb) / 2, s * (zEnd - 0.04)])\n    k.box('structSteelDark', [0.6, 0.03, 0.2], [xFe + 0.3, yb + 0.015, s * (zEnd - 0.04)])\n  }\n\n  // discharge manifold bar across the module tops, strainer cross at the +Z end\n  k.rbox(FE, [0.22, 0.22, zX - (-zEnd) + 0.02], 0.02, [xD, yd, (zX - zEnd) / 2])\n  k.rbox(FE, [crossH, crossH, crossH], 0.025, [xD, yd, zX])\n  const studs = (c: V3, axis: 'x' | '-x' | 'y' | 'z', R: number) => k.around(8, R, c, axis, (q) => k.hex('nut', 0.04, 0.026, q, axis), Math.PI / 8)\n  // strainer access cap (+X)\n  k.cyl(FE, 0.13, 0.05, [xD + crossH / 2 + 0.025, yd, zX], 'x', 28)\n  k.at([xD + crossH / 2 + 0.05, yd, zX], 'x', () => hammerCap(k, FE, 0.12, 0.09))\n  studs([xD + crossH / 2 + 0.01, yd, zX], 'x', 0.12)\n  // discharge outlet: spool to the 5\" flange (+Z)\n  const d = l.dOut\n  const fl = flange(k, p.dischargeNps, p.dischargeRating, d, 'z', true)\n  k.pipe(FE, [xD, yd, zX + crossH / 2 - 0.01], [xD, yd, d[2] - fl], pipeOD(p.dischargeNps) * 1.25, pipeWall(p.dischargeNps, 'XXS') * 1.4)\n  needleValve(k, [xD, yd + pipeOD(p.dischargeNps) * 0.62, (zX + crossH / 2 + d[2] - fl) / 2], 'y')\n  gauge(k, [xD, yd + pipeOD(p.dischargeNps) * 0.62 + 0.17, (zX + crossH / 2 + d[2] - fl) / 2], 'x', 0.15, 'bar', 700)\n  // pressure transducer on the cross\n  k.cyl('stainless', 0.022, 0.12, [xD, yd - crossH / 2 - 0.06, zX], 'y', 12)\n  k.rbox('stainless', [0.06, 0.09, 0.06], 0.01, [xD, yd - crossH / 2 - 0.16, zX])\n  k.tube('cable', [[xD, yd - crossH / 2 - 0.2, zX], [xD, 0.5, zX], [xD - 0.4, 0.45, zX - 0.1], [xD - 0.4, yb + 0.05, zX - 0.1]], 0.008)\n\n  // bladder pulsation dampener on the cross: neck flange, forged sphere, charging valve + gauge\n  const yN = yd + crossH / 2\n  const Rs = 0.32\n  k.cyl(FE, 0.1, 0.12, [xD, yN + 0.06, zX], 'y', 28)\n  k.cyl(FE, 0.19, 0.06, [xD, yN + 0.15, zX], 'y', 36)\n  studs([xD, yN + 0.14, zX], 'y', 0.16)\n  k.sphere('actuatorRed', Rs, [xD, yN + 0.18 + Rs * 0.92, zX], undefined, 40)\n  k.cyl('actuatorRed', Rs * 0.55, 0.08, [xD, yN + 0.18 + 0.04, zX], 'y', 28)\n  const yTop = yN + 0.18 + Rs * 1.92\n  k.cyl(FE, 0.06, 0.06, [xD, yTop, zX], 'y', 20)\n  k.cyl('brass', 0.015, 0.06, [xD, yTop + 0.06, zX], 'y', 10)\n  gauge(k, [xD + 0.08, yTop + 0.06, zX], 'x', 0.1, 'bar', 200)\n  k.rod('stainless', [xD, yTop + 0.06, zX], [xD + 0.08, yTop + 0.06, zX], 0.008, 8)\n  tagPlate(k, 'N₂ PRECHARGE', [xD, yN + 0.18 + Rs * 0.92, zX + Rs + 0.002], 'z', 0.16)\n\n  // reset relief valve on the cross (-X branch), discharge up and over to a flanged outlet\n  const xb = xD - crossH / 2\n  const xr0 = 1.42\n  k.pipe(FE, [xb + 0.01, yd, zX], [xr0 + 0.06, yd, zX], 0.11, 0.025)\n  k.cyl(FE, 0.09, 0.05, [xr0 + 0.08, yd, zX], 'x', 24)\n  k.rbox('actuatorRed', [0.16, 0.24, 0.16], 0.02, [xr0, yd + 0.04, zX])\n  k.cyl('actuatorRed', 0.06, 0.24, [xr0, yd + 0.28, zX], 'y', 20)\n  k.rod('stainless', [xr0, yd + 0.4, zX], [xr0 - 0.22, yd + 0.48, zX], 0.012, 8)\n  k.cyl('safetyYellow', 0.02, 0.06, [xr0 - 0.22, yd + 0.48, zX], 'z', 10)\n  const rv = l.rv\n  const rfl = flange(k, 3, 150, rv, 'z', true)\n  k.tube(FE, [[xr0, yd + 0.04, zX + 0.08], [xr0, yd + 0.04, zX + 0.18], [xr0, rv[1], zX + 0.18], [xr0, rv[1], rv[2] - rfl]], pipeOD(3) / 2, 16, 0.12)\n\n  // suction manifold under the modules: hollow 10\" header, blind end, suction flange (-Z), saddles\n  const sOD = pipeOD(p.suctionNps)\n  const s = l.sIn\n  const sfl = flange(k, p.suctionNps, 150, s, '-z', true)\n  const bf: V3 = [xV, ys, zEnd + 0.28]\n  const bfl = flange(k, p.suctionNps, 150, bf, 'z')\n  blindFlange(k, p.suctionNps, 150, bf, 'z')\n  k.pipe(FE, [xV, ys, bf[2] - bfl], [xV, ys, s[2] + sfl], sOD, pipeWall(p.suctionNps, 'STD'))\n  for (const z of [-zEnd + 0.1, zEnd - 0.1]) {\n    k.box('structSteelDark', [0.2, ys - sOD / 2 - yb, 0.03], [xV, (ys - sOD / 2 + yb) / 2, z])\n    k.box('structSteelDark', [0.3, 0.02, 0.12], [xV, yb + 0.01, z])\n  }\n  // priming vent on the manifold top at the inlet end\n  needleValve(k, [xV, ys + sOD / 2 - 0.005, -zEnd - 0.22], 'y')\n\n  // AC drilling motors on a fabricated deck over the crankcase, each driving one end of the pinion shaft\n  const m = motorSize(p.kw, 6)\n  const Rm = Math.max(0.36, m.R)\n  const Lm = Math.max(1.5, m.L)\n  const Hm = Math.max(0.45, Rm + 0.08)\n  const yDeck = 2.25\n  const ym = yDeck + Hm\n  const xPin = -0.65\n  const yPin = 1.0\n  const motors = [\n    { x: -1.6, s: -1 },\n    { x: -0.35, s: 1 },\n  ]\n  k.box('structSteelDark', [2.6, 0.04, pw - 0.1], [-0.95, yDeck - 0.02, 0])\n  for (const x of [-2.2, -1.5, -0.8, -0.05, 0.3])\n    for (const sz of [-1, 1]) {\n      const yt = topY(x)\n      k.box('structSteelDark', [0.16, yDeck - 0.04 - yt + 0.02, 0.16], [x, (yDeck - 0.04 + yt) / 2, sz * (pw / 2 - 0.2)])\n    }\n  const gW = 0.22\n  const zG0 = pw / 2 + 0.02\n  for (const mo of motors) {\n    const q = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), (-mo.s * Math.PI) / 2)\n    const zFace = mo.s * (pw / 2 - 0.02)\n    k.with(new THREE.Matrix4().compose(new THREE.Vector3(mo.x, ym, zFace), q, new THREE.Vector3(1, 1, 1)), () => motor(k, { R: Rm, L: Lm, H: Hm, tbox: 'top', mat: 'motorPaint', cable: false }))\n    // force-ventilation blower on the non-drive end\n    const zB = zFace - mo.s * Lm * 0.82\n    k.rbox('motorPaint', [0.5, 0.36, 0.42], 0.03, [mo.x, ym + Rm + 0.26, zB])\n    k.cyl('motorPaint', 0.11, 0.3, [mo.x + 0.32, ym + Rm + 0.26, zB], 'x', 24)\n    k.box('motorPaint', [0.3, Rm * 0.3, 0.3], [mo.x, ym + Rm + 0.03, zB])\n    k.disc('rubberBlack', 0.13, [mo.x - 0.252, ym + Rm + 0.26, zB], '-x', 24)\n    // power cables down to a junction box on the deck\n    const xj = mo.x + mo.s * (Rm + 0.22)\n    junctionBox(k, [xj, yDeck, zFace - mo.s * 0.5], 'y', [0.3, 0.3, 0.16], 'stainless')\n    k.spline('cable', [[mo.x, ym + Rm + 0.3, zFace - mo.s * Lm * 0.4], [mo.x + mo.s * Rm * 0.9, ym + Rm * 0.5, zFace - mo.s * 0.45], [xj, yDeck + 0.2, zFace - mo.s * 0.5]], 0.025, 8, 24)\n    const zg = mo.s * zG0\n    const zS = mo.s * (zG0 + gW / 2)\n    const rM = 0.19\n    const rP = 0.6\n    if (p.drive === 'belt') {\n      // multi-groove sheaves turning inside an expanded-mesh guard, banded V-belt set\n      const sheave = (r: number, w: number) => {\n        const kk = new Kit()\n        const prof: [number, number][] = [[r * 0.25, -w / 2], [r, -w / 2]]\n        const g = 5\n        for (let i = 0; i < g; i++) {\n          const y0 = -w / 2 + (w * (i + 0.15)) / g\n          const y1 = -w / 2 + (w * (i + 0.85)) / g\n          prof.push([r, y0], [r - 0.025, (y0 + y1) / 2], [r, y1])\n        }\n        prof.push([r, w / 2], [r * 0.25, w / 2])\n        kk.lathe('castIron', prof, [0, 0, 0], 'y', 48)\n        kk.cyl('castIron', r * 0.85, w * 0.35, [0, 0, 0], 'y', 40)\n        for (let i = 0; i < 6; i++) {\n          const a = (i / 6) * Math.PI * 2\n          kk.box('castIron', [r * 0.6, w * 0.3, 0.05], [Math.cos(a) * r * 0.55, 0, Math.sin(a) * r * 0.55], new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), -a))\n        }\n        kk.cyl('stainless', r * 0.22, w * 1.1, [0, 0, 0], 'y', 24)\n        kk.box('safetyYellow', [r * 0.3, w * 0.36, 0.03], [r * 0.6, 0, 0])\n        return kk.build()\n      }\n      const wrap = (built: Built, x: number, y: number, rpm: number): RotorSpec => ({ built, axis: 'y', pos: [x, y, zS], rot: [Math.PI / 2, 0, 0], rpm })\n      rotors.push(wrap(sheave(rM, 0.13), mo.x, ym, 180), wrap(sheave(rP, 0.13), xPin, yPin, 57))\n      hullBand(k, 'rubberBlack', [[xPin, yPin, rP - 0.012], [mo.x, ym, rM - 0.012]], zS - 0.05, 0.1, 0.018)\n      const gc: [number, number, number][] = [[xPin, yPin, rP + 0.08], [mo.x, ym, rM + 0.08]]\n      hullBand(k, 'safetyYellow', gc, mo.s > 0 ? zg : zg - gW, gW, 0.02)\n      k.add(meshGuardMaterial(), hullFace(gc, mo.s * (zG0 + gW), mo.s < 0, 8))\n      k.add('safetyYellow', hullFace(gc, zg, mo.s > 0))\n    } else {\n      // enclosed gear drive: cast case over input pinion and output gear, ribs, breather, oil level gauge\n      const gc: [number, number, number][] = [[xPin, yPin, rP + 0.05], [mo.x, ym, rM + 0.12]]\n      hullBand(k, 'motorPaint', gc, mo.s > 0 ? zg : zg - gW - 0.08, gW + 0.08, 0.03)\n      k.add('motorPaint', hullFace(gc, mo.s * (zG0 + gW + 0.08), mo.s < 0))\n      k.add('motorPaint', hullFace(gc, zg, mo.s > 0))\n      const ribs = 5\n      for (let i = 1; i < ribs; i++) {\n        const t = i / ribs\n        k.box('motorPaint', [0.04, 0.04, gW + 0.12], [xPin + (mo.x - xPin) * t, yPin + (ym - yPin) * t, mo.s * (zG0 + (gW + 0.08) / 2)])\n      }\n      k.cyl('brass', 0.025, 0.1, [mo.x, ym + rM + 0.17, mo.s * (zG0 + gW / 2)], 'y', 10)\n      k.cyl('glass', 0.03, 0.01, [xPin, yPin - 0.2, mo.s * (zG0 + gW + 0.085)], 'z', 16)\n      k.cyl('motorPaint', rP * 0.4, 0.04, [xPin, yPin, mo.s * (zG0 + gW + 0.1)], 'z', 32)\n    }\n    // motor cooling fan behind the cowl\n    rotors.push(fanRotor('flangeSteel', Rm * 0.85, [mo.x, ym, zFace - mo.s * (Lm + Math.max(0.12, Rm * 0.6) - 0.01)], 'z', 300, { blades: 7, thick: 0.004, pitch: 0.12, hub: Rm * 0.2, hubLen: 0.016 }))\n  }\n  // pinion shaft bearing caps (visible inside the guards)\n  for (const s of [-1, 1]) k.cyl(B, 0.17, 0.04, [xPin, yPin, s * (pw / 2 + 0.01)], 'z', 28)\n\n  // lube oil system at the rear: motor-pump set, duplex filter, cooler tubing into the crankcase\n  const lm = motorSize(5.5, 4)\n  const xl = xr - 0.22\n  k.with(new THREE.Matrix4().compose(new THREE.Vector3(xl, yb + lm.H, -0.2), new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), -Math.PI / 2), new THREE.Vector3(1, 1, 1)), () =>\n    motor(k, { R: lm.R, L: lm.L, H: lm.H, tbox: 'top' }),\n  )\n  k.cyl('castIron', 0.07, 0.14, [xl, yb + lm.H, -0.2 - 0.09], 'z', 20)\n  for (const dz of [0.35, 0.55]) {\n    k.cyl('stainless', 0.07, 0.36, [xl, yb + 0.2, dz], 'y', 24)\n    k.dome('stainless', 0.07, [xl, yb + 0.38, dz], 'y', Math.PI / 2, 0.4)\n  }\n  k.tube('tubing', [[xl, yb + lm.H, -0.36], [xl, yb + 0.55, -0.36], [xl, yb + 0.55, 0.35], [xl, yb + 0.4, 0.35]], 0.014)\n  k.tube('tubing', [[xl, yb + 0.38, 0.55], [xl, yb + 0.6, 0.55], [xr + 0.01, yb + 0.6, 0.55]], 0.014)\n\n  // nameplates, tag, earth boss\n  k.box('nameplate', [0.3, 0.18, 0.004], [0.3, 1.3, pw / 2 + 0.002])\n  tagPlate(k, p.tag, [-L / 2 + 0.7, 0.13, W / 2 + 0.002], 'z', 0.32)\n  tagPlate(k, p.tag, [-0.4, 0.13, -W / 2 - 0.002], '-z', 0.32)\n  k.cyl('brass', 0.015, 0.03, [-L / 2 + 0.3, 0.13, W / 2 + 0.015], 'z', 10)\n\n  // reciprocating pony rod + clamp, local frame centred on the stroke mid-point\n  const rk = new Kit()\n  const rodL = xFe - 0.07 - xN1 - 0.09 + 0.24\n  rk.cyl('stainless', 0.045, rodL, [0, 0, 0], 'x', 20)\n  rk.cyl('castIron', 0.075, 0.1, [0, 0, 0], 'x', 24)\n  for (const s of [-1, 1]) {\n    rk.box('castIron', [0.08, 0.04, 0.05], [0, s * 0.085, 0])\n    rk.rod('boltXylan', [0, s * 0.085, -0.05], [0, s * 0.085, 0.05], 0.01, 6)\n  }\n  const xm = (xN1 + 0.09 + xFe - 0.07) / 2\n  return { built: k.build(), rotors, rod: { built: rk.build(), at: zs.map((z) => [xm, yc, z] as V3), amp: 0.1 } }\n}\n"
    },
    {
      "path": "drilling/_drill.ts",
      "type": "registry:file",
      "target": "components/deck-twin/drilling/_drill.ts",
      "content": "// Shared drilling & subsea parts: API 6A flanges and gate valves, hammer-lug covers, convex hulls, ROV buckets, hubs and clamps, mudmats.\nimport * as THREE from 'three'\nimport { mat4, type Kit, type MatKey, type V3 } from '../lib/kit'\nimport { flange, liftingLug } from '../lib/parts'\nimport { flange as asmeFlange, inch, nearest, pipeOD, type Rating } from '../lib/standards'\n\n// ------------------------------------------------------------------ API 6A (typical 6B / 6BX dims, inches: OD, thickness, bolt circle, bolts, bolt dia)\n\nexport type Psi = 5000 | 10000 | 15000\n\ntype Row = [od: number, t: number, bc: number, n: number, bd: number]\n\nconst API: Record<Psi, Record<number, Row>> = {\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  },\n  10000: {\n    2.0625: [7.88, 1.73, 6.25, 8, 0.75], 2.5625: [9.12, 2.02, 7.25, 8, 0.875], 3.0625: [10.62, 2.3, 8.5, 8, 1.0],\n    4.0625: [12.44, 2.77, 10.19, 8, 1.125], 5.125: [14.06, 3.12, 11.81, 12, 1.125], 7.0625: [18.88, 4.06, 15.88, 12, 1.5],\n  },\n  15000: {\n    2.0625: [8.75, 2.0, 6.88, 8, 0.875], 2.5625: [10.0, 2.25, 7.88, 8, 1.0], 3.0625: [11.31, 2.53, 9.06, 8, 1.125],\n    4.0625: [14.19, 3.09, 11.44, 8, 1.375], 5.125: [16.5, 3.88, 13.63, 12, 1.5], 7.0625: [19.88, 4.69, 16.88, 16, 1.5],\n  },\n}\n\n// typical API 6A slab-gate face-to-face, inches\nconst GATE: Record<Psi, Record<number, number>> = {\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: { 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: { 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 interface ApiFlange {\n  bore: number\n  od: number\n  t: number\n  bolts: number\n  boltDia: number\n  boltCircle: number\n  ring: number\n}\n\nexport function apiFlange(bore: number, psi: Psi = 10000): ApiFlange {\n  const tbl = API[psi] ?? API[10000]\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) }\n}\n\nexport function apiGateLength(bore: number, psi: Psi = 10000): number {\n  const tbl = GATE[psi] ?? GATE[10000]\n  return inch(tbl[nearest(Object.keys(tbl).map(Number), bore)])\n}\n\n/** ring-gasket half gap at an RTJ joint */\nexport const G = 0.003\nconst rBore = (d: ApiFlange) => d.bore * 0.0127\n\n/** Hollow tube along local Y. */\nexport function tubeY(k: Kit, mat: MatKey, ro: number, ri: number, y0: number, y1: number, seg = 28) {\n  k.lathe(mat, [[ri, y0], [ro, y0], [ro, y1], [ri, y1]], [0, 0, 0], 'y', seg)\n  k.lathe('pipeBore', [[ri, Math.max(y0, y1)], [ri, Math.min(y0, y1)]], [0, 0, 0], 'y', seg)\n}\n\n/** Integral API flange, ring face at local y = face, body on side s (+1 above, -1 below). */\nexport function apiFlangeAt(k: Kit, d: ApiFlange, face: number, s: 1 | -1, mat: MatKey) {\n  const f0 = face + s * G\n  const f1 = face + s * (G + d.t)\n  const lo = Math.min(f0, f1)\n  const hi = Math.max(f0, f1)\n  const ch = Math.min(0.006, d.t * 0.12)\n  const rb = rBore(d)\n  const R = d.od / 2\n  const prof: [number, number][] = s > 0 ? [[rb, lo], [R, lo], [R, hi - ch], [R - ch, hi], [rb, hi]] : [[rb, lo], [R - ch, lo], [R, lo + ch], [R, hi], [rb, hi]]\n  k.lathe(mat, prof, [0, 0, 0], 'y', d.bolts >= 12 ? 40 : 28)\n}\n\n/** Studs, nuts and ring gasket for two API flanges meeting at local y = yj. */\nexport function apiJoint(k: Kit, d: ApiFlange, yj: number) {\n  const nh = d.boltDia * 0.95\n  const so = d.boltDia * 0.4\n  const len = 2 * (G + d.t) + 2 * nh + 2 * so\n  k.boltCircle('boltXylan', 'nut', d.bolts, d.boltCircle / 2, [0, yj - len / 2, 0], 'y', d.boltDia, len, [so + nh / 2, len - so - nh / 2])\n  k.torus('stainless', d.ring / 2, Math.max(0.004, d.boltDia * 0.18), [0, yj, 0], 'y', 4, 28)\n}\n\n/** Flanged spool between API faces y = 0 and y = L (local Y). */\nexport function apiSpool(k: Kit, d: ApiFlange, L: number, mat: MatKey) {\n  apiFlangeAt(k, d, 0, 1, mat)\n  apiFlangeAt(k, d, L, -1, mat)\n  tubeY(k, mat, d.od * 0.3, rBore(d), G + d.t - 0.002, L - G - d.t + 0.002, 24)\n}\n\n/** Open API face at local y = 0 (flange body below): machined face and ring groove; optional studs for a studded outlet. */\nexport function apiStuddedFace(k: Kit, d: ApiFlange, studs = true) {\n  const nh = d.boltDia * 0.95\n  k.lathe('machined', [[d.ring / 2 + d.boltDia * 0.6, -G + 0.0008], [rBore(d), -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.001, 0], 'y', 6, 32)\n  if (studs) k.boltCircle('boltXylan', 'nut', d.bolts, d.boltCircle / 2, [0, -d.boltDia * 1.2, 0], 'y', d.boltDia, d.boltDia * 1.2 + G * 2 + d.t + nh * 1.4, [d.boltDia * 1.2 + G * 2 + d.t + nh * 0.5])\n}\n\nexport type GateOp = { kind: 'handwheel'; D: number; mat?: MatKey } | { kind: 'hydraulic'; r: number; len: number }\n\n/** API 6A slab gate valve between flange faces y = 0 and y = L, bonnet along local +Z. */\nexport function apiGate(k: Kit, d: ApiFlange, L: number, op: GateOp, mat: MatKey = 'valveBody') {\n  apiFlangeAt(k, d, 0, 1, mat)\n  apiFlangeAt(k, d, L, -1, mat)\n  const rb = rBore(d)\n  tubeY(k, mat, d.od * 0.3, rb, G + d.t - 0.002, L - G - d.t + 0.002, 24)\n  const w = d.od * 0.8\n  const dz = d.od * 0.92\n  const hb = Math.max(0.05, Math.min(d.od * 1.0, L - 2 * (G + d.t) - 0.02))\n  const yc = L / 2\n  k.rbox(mat, [w, hb, dz], Math.min(0.025, w * 0.1), [0, yc, 0])\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  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    const sr = Math.max(0.011, d.od * 0.045)\n    k.cyl(mat, rbo * 0.62, 0.04, [0, yc, z + 0.02], 'z', 18)\n    k.cyl('stainless', sr, 0.12, [0, yc, z + 0.06], 'z', 12)\n    k.handwheel(op.mat ?? 'actuatorRed', 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 z + 0.13\n  }\n  k.cyl(mat, rbo * 0.85, 0.05, [0, yc, z + 0.025], 'z', 24)\n  z += 0.05\n  const { r, len } = op\n  k.cyl('actuatorYellow', r * 1.08, 0.03, [0, yc, z + 0.015], 'z', 32)\n  k.cyl('actuatorYellow', r, len, [0, yc, z + 0.03 + len / 2], 'z', 32)\n  k.cyl('actuatorYellow', 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('stainless', [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('stainless', 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  k.cyl('nameplate', r * 1.002, 0.04, [0, yc, z + 0.03 + len * 0.62], 'z', 32)\n  return zt + 0.1\n}\n\n/** 1/2\" needle valve on a boss along +axis from base, T-handle to the side. Returns the outlet end. */\nexport function needleValve(k: Kit, base: V3, axis: 'x' | 'y' | 'z' | '-x' | '-z', handle: MatKey = 'actuatorRed'): V3 {\n  const v: V3 = axis === 'y' ? [0, 1, 0] : axis === 'z' ? [0, 0, 1] : 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('stainless', 0.032, 0.02, p(0.01), axis)\n  k.box('stainless', [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('stainless', 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('stainless', p(0.06), p(0.105), 0.008, 8)\n  return p(0.105)\n}\n\n/** Threaded hammer-lug cover (mud pump valve cover, union nut) on local +Y from y = 0: body, three lugs, centre boss. */\nexport function hammerCap(k: Kit, mat: MatKey, R: number, h: number, lugs = 3) {\n  k.cyl(mat, R, h, [0, h / 2, 0], 'y', 32)\n  k.cyl(mat, R * 0.96, 0.006, [0, h + 0.003, 0], 'y', 32)\n  for (let i = 0; i < lugs; i++) {\n    const a = (i / lugs) * Math.PI * 2\n    k.box(mat, [R * 0.42, h * 0.8, R * 0.32], [Math.cos(a) * (R + R * 0.14), h * 0.45, Math.sin(a) * (R + R * 0.14)], new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), -a))\n  }\n  k.cyl(mat, R * 0.35, h * 0.3, [0, h + h * 0.15, 0], 'y', 20)\n}\n\n// ------------------------------------------------------------------ 2D helpers\n\n/** Convex hull (CCW) of circles sampled at n points each, as a THREE.Shape in XY. */\nexport function hullOfCircles(circles: [number, number, number][], n = 32): THREE.Shape {\n  const hull = hullPoints(circles, n)\n  const sh = new THREE.Shape()\n  hull.forEach(([x, y], i) => (i ? sh.lineTo(x, y) : sh.moveTo(x, y)))\n  sh.closePath()\n  return sh\n}\n\n/** Plate band of `thick` × `depth` (along +Z from z0) following the hull outline of the circles: guard rims, belts. */\nexport function hullBand(k: Kit, mat: MatKey, circles: [number, number, number][], z0: number, depth: number, thick: number, n = 48) {\n  const h = hullPoints(circles, n)\n  for (let i = 0; i < h.length; i++) {\n    const [x0, y0] = h[i]\n    const [x1, y1] = h[(i + 1) % h.length]\n    const len = Math.hypot(x1 - x0, y1 - y0)\n    if (len < 1e-5) continue\n    const nx = (y1 - y0) / len\n    const ny = -(x1 - x0) / len\n    const q = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), Math.atan2(y1 - y0, x1 - x0))\n    k.box(mat, [len + thick * 0.6, thick, depth], [(x0 + x1) / 2 - (nx * thick) / 2, (y0 + y1) / 2 - (ny * thick) / 2, z0 + depth / 2], q)\n  }\n}\n\n/** Flat convex face (triangle fan) over the hull of the circles at z = z0, facing +Z (or -Z when flip). UVs scaled by uvs per metre. */\nexport function hullFace(circles: [number, number, number][], z0: number, flip = false, uvs = 1, n = 48): THREE.BufferGeometry {\n  const h = hullPoints(circles, n)\n  const cx = h.reduce((a, p) => a + p[0], 0) / h.length\n  const cy = h.reduce((a, p) => a + p[1], 0) / h.length\n  const pos: number[] = [cx, cy, z0]\n  const uv: number[] = [cx * uvs, cy * uvs]\n  for (const [x, y] of h) pos.push(x, y, z0), uv.push(x * uvs, y * uvs)\n  const idx: number[] = []\n  for (let i = 0; i < h.length; i++) {\n    const a = 1 + i\n    const b = 1 + ((i + 1) % h.length)\n    if (flip) idx.push(0, b, a)\n    else idx.push(0, a, b)\n  }\n  const g = new THREE.BufferGeometry()\n  g.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3))\n  g.setAttribute('uv', new THREE.Float32BufferAttribute(uv, 2))\n  g.setIndex(idx)\n  g.computeVertexNormals()\n  return g\n}\n\nexport function hullPoints(circles: [number, number, number][], n = 32): [number, number][] {\n  const pts: [number, number][] = []\n  for (const [x, y, r] of circles) for (let i = 0; i < n; i++) pts.push([x + Math.cos((i / n) * Math.PI * 2) * r, y + Math.sin((i / n) * Math.PI * 2) * r])\n  pts.sort((a, b) => a[0] - b[0] || a[1] - b[1])\n  const cross = (o: number[], a: number[], b: number[]) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])\n  const lo: [number, number][] = []\n  for (const p of pts) {\n    while (lo.length >= 2 && cross(lo[lo.length - 2], lo[lo.length - 1], p) <= 0) lo.pop()\n    lo.push(p)\n  }\n  const up: [number, number][] = []\n  for (let i = pts.length - 1; i >= 0; i--) {\n    const p = pts[i]\n    while (up.length >= 2 && cross(up[up.length - 2], up[up.length - 1], p) <= 0) up.pop()\n    up.push(p)\n  }\n  return lo.slice(0, -1).concat(up.slice(0, -1))\n}\n\n/** Matrix placing a shape drawn in XY onto the plane z = z0 (extrusion runs +Z). */\nexport const atZ = (z0: number) => mat4([0, 0, z0])\n\n// ------------------------------------------------------------------ subsea\n\n/** Mudmat at seabed y = 0: plate with edge skirts and stiffeners, top at y = t. */\nexport function mudmat(k: Kit, sx: number, sz: number, opts: { skirt?: number; mat?: MatKey; t?: number; padeyes?: boolean } = {}) {\n  const mat = opts.mat ?? 'equipmentPaint'\n  const t = opts.t ?? 0.05\n  const sk = opts.skirt ?? 0.25\n  // skirts penetrate the seabed; plate rests on y = 0\n  k.box(mat, [sx, t, sz], [0, t / 2, 0])\n  for (const s of [-1, 1]) {\n    k.box(mat, [sx, sk, 0.02], [0, -sk / 2 + 0.001, s * (sz / 2 - 0.01)])\n    k.box(mat, [0.02, sk, sz], [s * (sx / 2 - 0.01), -sk / 2 + 0.001, 0])\n  }\n  // top stiffeners (flat bar grid)\n  const nx = Math.max(2, Math.round(sx / 1.2))\n  const nz = Math.max(2, Math.round(sz / 1.2))\n  for (let i = 1; i < nx; i++) k.box(mat, [0.02, 0.12, sz - 0.04], [-sx / 2 + (sx * i) / nx, t + 0.06, 0])\n  for (let i = 1; i < nz; i++) k.box(mat, [sx - 0.04, 0.12, 0.02], [0, t + 0.06, -sz / 2 + (sz * i) / nz])\n  // water evacuation holes with short vent pipes\n  for (const sx2 of [-1, 1]) for (const sz2 of [-1, 1]) k.cyl(mat, 0.05, 0.1, [sx2 * sx * 0.25, t + 0.05, sz2 * sz * 0.25], 'y', 14, undefined, true)\n  if (opts.padeyes !== false) for (const sx2 of [-1, 1]) for (const sz2 of [-1, 1]) liftingLug(k, [sx2 * (sx / 2 - 0.2), t, sz2 * (sz / 2 - 0.2)], 'x', 1.6, mat)\n  return t\n}\n\n/** Sacrificial Al-Zn-In anode on two stand-off cores, long axis along `along`, centre at c. */\nexport function anode(k: Kit, c: V3, along: 'x' | 'z' | 'y' = 'x', len = 0.6) {\n  const size: V3 = along === 'x' ? [len, 0.1, 0.12] : along === 'z' ? [0.12, 0.1, len] : [0.12, len, 0.1]\n  k.rbox('aluminium', size, 0.02, c)\n  const off = len * 0.55\n  for (const s of [-1, 1]) {\n    const e: V3 = along === 'x' ? [c[0] + s * off, c[1], c[2]] : along === 'z' ? [c[0], c[1], c[2] + s * off] : [c[0], c[1] + s * off, c[2]]\n    k.box('stainless', [0.03, 0.03, 0.03], e)\n  }\n}\n\n/** API 17D class-4 style ROV torque bucket (open funnel, docking slots) on a back plate, opening facing +axis from base. */\nexport function rovBucket(k: Kit, base: V3, axis: 'x' | 'z' | '-x' | '-z' | 'y', mat: MatKey = 'actuatorYellow') {\n  k.at(base, axis, () => {\n    k.box(mat, [0.32, 0.02, 0.32], [0, 0.01, 0])\n    k.lathe(mat, [[0.07, 0.02], [0.085, 0.02], [0.085, 0.14], [0.11, 0.2], [0.1, 0.2], [0.075, 0.145], [0.07, 0.145]], [0, 0, 0], 'y', 28)\n    k.lathe('pipeBore', [[0.07, 0.145], [0.07, 0.025]], [0, 0, 0], 'y', 24)\n    k.cyl('stainless', 0.025, 0.04, [0, 0.04, 0], 'y', 6)\n    // docking slots and position indicator\n    for (const s of [-1, 1]) k.box('nut', [0.016, 0.05, 0.02], [s * 0.086, 0.17, 0])\n    k.box('nut', [0.05, 0.004, 0.012], [0, 0.022, 0.13])\n  })\n}\n\n/** Subsea hub (API 17R style) on local +Y from base along `axis`: neck, tapered hub profile, seal face, optional protection cap. Face at base + len. */\nexport function hub(k: Kit, base: V3, axis: 'x' | 'z' | '-x' | '-z' | 'y', boreR: number, len = 0.35, mat: MatKey = 'equipmentPaint', cap = false) {\n  const R = Math.max(0.12, boreR * 2.4)\n  k.at(base, axis, () => {\n    k.lathe(mat, [[boreR, 0], [R * 0.62, 0], [R * 0.62, len * 0.5], [R * 0.75, len * 0.55], [R, len * 0.78], [R, len - 0.01], [R * 0.95, len], [boreR, len]], [0, 0, 0], 'y', 40)\n    k.lathe('pipeBore', [[boreR, len], [boreR, -0.05]], [0, 0, 0], 'y', 24)\n    k.lathe('machined', [[R * 0.9, len + 0.001], [boreR, len + 0.001]], [0, 0, 0], 'y', 32)\n    if (cap) {\n      k.lathe('actuatorYellow', [[R * 1.04, len * 0.7], [R * 1.12, len * 0.7], [R * 1.12, len + 0.12], [0, len + 0.12]], [0, 0, 0], 'y', 32)\n      k.cyl('nut', 0.03, 0.08, [0, len + 0.16, 0], 'y', 6)\n    }\n  })\n  return R\n}\n\n// ------------------------------------------------------------------ chokes and frames\n\n/** Matrix whose local +Y runs along `flow` and local +Z along `stem`, origin at o. */\nexport function flowFrame(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}\n\n/** Angle choke body centre to inlet face (along -X) and outlet face (along -Y). */\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\n/** API 6A / 16C angle choke at the origin: inlet face -X, outlet face -Y, bonnet and operator along +X. Returns the operator's far x and its hose port. */\nexport function angleChoke(k: Kit, dIn: ApiFlange, dOut: ApiFlange, op: 'handwheel' | 'hydraulic', mat: MatKey = 'valveBody'): { end: number; hose: V3 | null } {\n  const { B, inlet, outlet } = chokeDims(dIn, dOut)\n  k.rbox(mat, [B, B, B * 0.92], Math.min(0.03, B * 0.12), [0, 0, 0])\n  const ri = rBore(dIn)\n  const ro = rBore(dOut)\n  k.at([0, 0, 0], '-x', () => tubeY(k, mat, dIn.od * 0.3, ri, B / 2 - 0.01, inlet - dIn.t - G + 0.002, 24))\n  k.at([0, 0, 0], '-y', () => tubeY(k, mat, dOut.od * 0.32, ro, B / 2 - 0.01, outlet - dOut.t - G + 0.002, 24))\n  k.at([-inlet, 0, 0], '-x', () => apiFlangeAt(k, dIn, 0, -1, mat))\n  k.at([0, -outlet, 0], '-y', () => apiFlangeAt(k, dOut, 0, -1, mat))\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  // bonnet nut with hammer lugs, wear-sleeve bleed plug\n  k.hex('brass', 0.022, 0.016, [0, B * 0.25, B * 0.46 + 0.008], 'z')\n  let x = B / 2\n  const rn = B * 0.42\n  k.cyl(mat, rn * 0.9, 0.02, [x + 0.01, 0, 0], 'x', 24)\n  x += 0.02\n  k.cyl(mat, 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(mat, [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)], new THREE.Quaternion().setFromEuler(new THREE.Euler(-a, 0, 0)))\n  }\n  x += 0.07\n  k.cyl(mat, rn * 0.55, 0.1, [x + 0.05, 0, 0], 'x', 20)\n  x += 0.1\n  // position indicator sleeve with scale ring\n  k.cyl('stainless', 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 === 'handwheel') {\n    k.cyl('stainless', 0.014, 0.1, [x + 0.05, 0, 0], 'x', 10)\n    k.handwheel('actuatorRed', Math.max(0.3, B * 1.3), [x + 0.07, 0, 0], 'x', 4)\n    return { end: x + 0.1, hose: null }\n  }\n  // hydraulic rotary-gear actuator: gear housing, double-acting cylinder, stroke-position transmitter, manual override\n  k.cyl(mat, rn * 0.35, 0.05, [x + 0.025, 0, 0], 'x', 14)\n  x += 0.05\n  k.rbox('actuatorYellow', [0.2, 0.2, 0.22], 0.02, [x + 0.1, 0, 0])\n  k.cyl('actuatorYellow', 0.07, 0.42, [x + 0.1, 0.0, 0.0], 'z', 24)\n  for (const s of [-1, 1]) {\n    k.cyl('actuatorYellow', 0.078, 0.03, [x + 0.1, 0, s * 0.21], 'z', 24)\n    k.cyl('brass', 0.012, 0.035, [x + 0.1, 0.07, s * 0.19], 'y', 10)\n  }\n  k.rbox('stainless', [0.08, 0.1, 0.08], 0.01, [x + 0.25, 0.04, 0])\n  k.cyl('brass', 0.012, 0.03, [x + 0.25, -0.025, 0], 'y', 8)\n  k.hex('nut', 0.04, 0.03, [x + 0.1, 0.115, 0], 'y')\n  k.box('nameplate', [0.08, 0.05, 0.002], [x + 0.1, 0, 0.112])\n  return { end: x + 0.3, hose: [x + 0.1, 0.105, 0.19] }\n}\n\n/** ROV grab bar: round bar on two stand-offs between a and b, stand-off along `out`. */\nexport function grabBar(k: Kit, a: V3, b: V3, out: V3, mat: MatKey = 'actuatorYellow') {\n  const o = (p: V3, s: number): V3 => [p[0] + out[0] * s, p[1] + out[1] * s, p[2] + out[2] * s]\n  k.rod(mat, o(a, 0.12), o(b, 0.12), 0.019, 10)\n  for (const p of [a, b]) k.rod(mat, p, o(p, 0.12), 0.015, 8)\n}\n\n/**\n * Subsea trunnion ball valve between flange faces at local y = 0 and y = L (flow along local +Y), stem and ROV\n * gearbox along local +Z, class-4 bucket opening along local +X. Flanges are ASME B16.5 with studs.\n */\nexport function subseaBall(k: Kit, nps: number, rating: Rating, L: number, opts: { mat?: MatKey; bucket?: boolean } = {}) {\n  const mat = opts.mat ?? 'equipmentPaint'\n  const f = asmeFlange(nps, rating)\n  const ro = pipeOD(nps) / 2\n  const R = Math.max(ro * 1.55, f.od * 0.5)\n  const fl0 = flange(k, nps, rating, [0, 0, 0], '-y', true)\n  const fl1 = flange(k, nps, rating, [0, L, 0], 'y', true)\n  k.lathe(mat, [[ro * 0.9, fl0], [ro * 1.15, fl0], [R * 0.9, L / 2 - R * 0.75], [R, L / 2 - R * 0.35], [R, L / 2 + R * 0.35], [R * 0.9, L / 2 + R * 0.75], [ro * 1.15, L - fl1], [ro * 0.9, L - fl1]], [0, 0, 0], 'y', 40)\n  // body joint flanges with studs\n  for (const y of [L / 2 - R * 0.55, L / 2 + R * 0.55]) {\n    k.cyl(mat, R * 1.08, 0.05, [0, y, 0], 'y', 40)\n    k.around(12, R * 1.0, [0, y + 0.035, 0], 'y', (p) => k.hex('nut', 0.05, 0.03, p, 'y'), Math.PI / 12)\n  }\n  // trunnion boss, stem housing, gearbox, bucket\n  k.cyl(mat, R * 0.45, R * 0.4, [0, L / 2, -R - R * 0.15], 'z', 28)\n  k.cyl(mat, R * 0.45, R * 0.5, [0, L / 2, R + R * 0.2], 'z', 28)\n  const zg = R + R * 0.45\n  k.rbox('actuatorYellow', [R * 1.5, R * 1.2, R * 0.9], 0.04, [0, L / 2, zg + R * 0.45])\n  k.cyl('actuatorYellow', R * 0.35, R * 0.5, [R * 0.75, L / 2, zg + R * 0.45], 'x', 24)\n  k.box('nut', [0.12, 0.02, 0.012], [0, L / 2 + R * 0.6, zg + R * 0.9 + 0.006])\n  if (opts.bucket !== false) rovBucket(k, [R * 1.05 + 0.25, L / 2, zg + R * 0.45], 'x')\n  k.rod('actuatorYellow', [R * 1.0, L / 2, zg + R * 0.45], [R * 1.05 + 0.25, L / 2, zg + R * 0.45], 0.05, 16)\n  return { R, top: zg + R * 0.9 }\n}\n"
    }
  ],
  "categories": [
    "drilling"
  ],
  "meta": {
    "export": "MudPump",
    "anchor": "base",
    "standards": [
      "API 7K",
      "API 6A",
      "ASME B16.5",
      "ISO 14120"
    ]
  }
}