{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "between-bearings-pump",
  "type": "registry:component",
  "title": "Multistage pump (API 610 BB3)",
  "description": "Axially split multistage between-bearings pump: casing with stage bulges, back-to-back crossover and a heavy single row of studs with nuts on top of the horizontal split, side-side nozzles in the lower half (rotor lifts out without disturbing piping), casing vent and drain valves, seal chambers with API 682 Plan 23 seal coolers at both ends, bearing housings with X-Y probes, RTDs and ring-oil constant-level oilers (or pressure lube from the optional console), guarded spacer coupling and a 2-pole motor (HV with top air-to-air cooler from 1 MW) on a drain-rim baseplate. Stages, kW and nozzle sizes drive the length and frame.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "rotating/between-bearings-pump.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/rotating/between-bearings-pump.tsx",
      "content": "// API 610 BB3 axially split multistage pump: bolted horizontal split casing, bearing housings at both ends, guarded coupling, IEC/HV motor, forced-feed lube oil console, drain-rim baseplate.\nimport { useEffect, useMemo } from 'react'\nimport { Kit, type MatKey, type V3 } from '../lib/kit'\nimport { KitMesh, type GroupProps } from '../lib/mesh'\nimport { gauge, liftingLug, motor, tagPlate } from '../lib/parts'\nimport { port, type Port } from '../lib/ports'\nimport { Rotors, cowlFan, guardedCoupling, type RotorSpec } from '../lib/rotors'\nimport { flange as flangeDims, pipeOD, type Rating } from '../lib/standards'\nimport { anchorBolt, clamp, controlStation, drainRimBaseplate, flangeLen, flangedBranch, motorEnvelope, mountPad, oiler, pipeGauge, sealCooler, smallValve, topCooler } from './_pumps'\n\nexport interface BetweenBearingsPumpProps {\n  suctionNps?: number\n  dischargeNps?: number\n  rating?: Rating\n  /** motor rated power, kW */\n  kw?: number\n  stages?: number\n  casing?: MatKey\n  /** forced-feed lube oil console on the baseplate */\n  lubeConsole?: boolean\n  tag?: string\n  running?: boolean\n}\n\nexport const defaults = {\n  suctionNps: 8,\n  dischargeNps: 6,\n  rating: 900 as Rating,\n  kw: 1500,\n  stages: 6,\n  casing: 'machineryGreen' as MatKey,\n  lubeConsole: true,\n  running: true,\n}\n\ntype P = Omit<typeof defaults, 'running'> & { tag?: string }\n\nfunction layout(p: P) {\n  const st = clamp(Math.round(p.stages), 2, 14)\n  const m = motorEnvelope(p.kw, 2)\n  const Rimp = 0.12 + 0.014 * Math.max(p.dischargeNps, p.suctionNps * 0.8)\n  const Rc = Rimp + 0.09\n  const sp = 0.13 + Rimp * 0.3 // stage pitch, typical\n  const Lc = st * sp + 0.55\n  const fOD = Math.max(flangeDims(p.suctionNps, p.rating).od, flangeDims(p.dischargeNps, p.rating).od)\n  const hb = 0.32\n  const Rm = m.R\n  const ys = hb + Math.max(m.H, Rc + 0.32, Rc * 0.35 + fOD / 2 + 0.12)\n  const yN = ys - Rc * 0.35\n  const bhR = Math.max(0.12, Rc * 0.42)\n  const bhL = Math.max(0.3, Rc * 1.0)\n  const xDE = -Lc / 2 - 0.28 - bhL / 2\n  const xNDE = Lc / 2 + 0.28 + bhL / 2\n  const xSE = xDE - bhL / 2 - 0.12\n  const xMS = xSE - 0.36\n  const stub = 0.1 + Rm * 0.2\n  const xm = xMS - stub + 0.02\n  const cowl = Math.max(0.12, Rm * 0.6)\n  const x0 = xm - m.L - cowl - 0.2\n  const xCon = xNDE + bhL / 2 + 0.25\n  const x1 = p.lubeConsole ? xCon + 1.3 : xNDE + bhL / 2 + 0.25\n  const cx = (x0 + x1) / 2\n  const zFace = (nps: number) => Rc + 0.32 + pipeOD(nps) * 0.3 + flangeLen(nps, p.rating)\n  const xS = Lc / 2 - 0.12 - pipeOD(p.suctionNps) * 0.6\n  const xD = -Lc / 2 + 0.12 + pipeOD(p.dischargeNps) * 0.6\n  const W = Math.max(2 * (Rc + 0.62), 2 * Rm * 1.15 + 0.2, 1.1)\n  return { st, m, Rimp, Rc, sp, Lc, hb, ys, yN, bhR, bhL, xDE, xNDE, xSE, xMS, xm, x0, x1, cx, xCon, xS, xD, zFace, W }\n}\n\nexport function ports(props: BetweenBearingsPumpProps = {}): Port[] {\n  const p = { ...defaults, ...props }\n  const L = layout(p)\n  return [\n    port('suction', 'Suction', [L.xS - L.cx, L.yN, L.zFace(p.suctionNps)], [0, 0, 1], p.suctionNps, p.rating),\n    port('discharge', 'Discharge', [L.xD - L.cx, L.yN, L.zFace(p.dischargeNps)], [0, 0, 1], p.dischargeNps, p.rating),\n  ]\n}\n\nexport function BetweenBearingsPump({ tag, ...rest }: BetweenBearingsPumpProps & GroupProps) {\n  const { suctionNps, dischargeNps, rating, kw, stages, casing, lubeConsole, running, ...group } = { ...defaults, ...rest }\n  const { built, rotors } = useMemo(\n    () => build({ suctionNps, dischargeNps, rating, kw, stages, casing, lubeConsole, tag }),\n    [suctionNps, dischargeNps, rating, kw, stages, casing, lubeConsole, tag],\n  )\n  useEffect(() => () => [built, ...rotors.map((r) => r.built)].forEach((b) => b.geometry.dispose()), [built, rotors])\n  return (\n    <group {...group}>\n      <KitMesh data={built} />\n      <Rotors rotors={rotors} running={running} />\n    </group>\n  )\n}\n\nexport default BetweenBearingsPump\n\nfunction build(p: P) {\n  const k = new Kit()\n  const L = layout(p)\n  const rotors: RotorSpec[] = []\n  k.at([-L.cx, 0, 0], undefined, () => draw(k, p, L, rotors))\n  for (const r of rotors) r.pos = [r.pos[0] - L.cx, r.pos[1], r.pos[2]]\n  return { built: k.build(), rotors }\n}\n\nfunction draw(k: Kit, p: P, L: ReturnType<typeof layout>, rotors: RotorSpec[]) {\n  const C = p.casing\n  const { Rc, Lc, sp, st, hb, ys, yN, bhR, bhL, xDE, xNDE, xSE, xMS, xm, W } = L\n  const x0c = -Lc / 2\n  const x1c = Lc / 2\n\n  // casing barrel: end hubs, stage bulges, horizontal split flange\n  k.lathe(C, [\n    [Rc * 0.55, x0c - 0.12],\n    [Rc * 0.8, x0c - 0.02],\n    [Rc, x0c + 0.12],\n    [Rc, x1c - 0.12],\n    [Rc * 0.8, x1c + 0.02],\n    [Rc * 0.55, x1c + 0.12],\n  ], [0, ys, 0], 'x', 48)\n  const xs0 = -((st - 1) * sp) / 2\n  for (let i = 0; i < st; i++) {\n    const x = xs0 + i * sp\n    k.lathe(C, [[Rc, x - sp * 0.32], [Rc * 1.07, x - sp * 0.2], [Rc * 1.07, x + sp * 0.2], [Rc, x + sp * 0.32]], [0, ys, 0], 'x', 48)\n  }\n  // back-to-back crossover over the top\n  const xc0 = xs0 + ((st - 1) * sp) * 0.15\n  const xc1 = xs0 + ((st - 1) * sp) * 0.85\n  const rX = Rc * 0.32\n  k.tube(C, [[xc0, ys + Rc * 0.6, 0], [xc0, ys + Rc + rX * 0.6, -Rc * 0.15], [xc1, ys + Rc + rX * 0.6, -Rc * 0.15], [xc1, ys + Rc * 0.6, 0]], rX, 20, rX * 1.4)\n  const fw = Rc + 0.1\n  const ft = 0.07\n  k.box(C, [Lc + 0.1, ft, fw * 2], [0, ys, 0])\n  const nStud = Math.max(6, Math.round(Lc / 0.11))\n  for (const s of [-1, 1])\n    for (let i = 0; i < nStud; i++) {\n      const x = x0c + 0.05 + ((Lc - 0.1) * i) / (nStud - 1)\n      const z = s * (fw - 0.04)\n      k.cyl('boltXylan', 0.016, 0.1, [x, ys + 0.04, z], 'y', 8)\n      k.hex('nut', 0.05, 0.03, [x, ys + ft / 2 + 0.015, z])\n    }\n  for (const sx of [-1, 1]) liftingLug(k, [sx * Lc * 0.3, ys + Rc * 1.0, 0], 'x', 1.2, C)\n  // casing vent valve on top, drain line with valve and plug underneath\n  k.cyl(C, 0.018, 0.06, [x1c - 0.2, ys + Rc * 0.98 + 0.02, Rc * 0.15], 'y', 10)\n  smallValve(k, [x1c - 0.2, ys + Rc * 0.98 + 0.08, Rc * 0.15], 0.8)\n  k.cyl(C, 0.03, 0.06, [0, ys - Rc * 1.05, 0], 'y', 12)\n  k.rod('tubing', [0, ys - Rc * 1.05, 0], [0, hb + 0.14, 0], 0.016, 10)\n  smallValve(k, [0, hb + 0.1, 0.0], 0.8, 'z')\n  k.hex('nut', 0.04, 0.03, [0, hb + 0.05, 0])\n\n  // side-side nozzles on the lower half\n  for (const [x, nps] of [[L.xS, p.suctionNps], [L.xD, p.dischargeNps]] as [number, number][]) {\n    const od = pipeOD(nps)\n    const zf = L.zFace(nps)\n    k.lathe(C, [[0, Rc * 0.4], [od * 0.62, Rc * 0.4], [od * 0.56, Rc + 0.12], [od * 0.44, Rc + 0.12]], [x, yN, 0], 'z', 32)\n    flangedBranch(k, C, [x, yN, Rc * 0.6], [x, yN, zf], nps, p.rating, '80')\n    pipeGauge(k, [x + od * 0.6 + 0.06, yN + od / 2, Rc + 0.2], [0, 0, 1], nps === p.suctionNps ? 40 : 400)\n  }\n\n  // casing support feet at both ends (centreline mounted)\n  for (const sx of [-1, 1]) {\n    const x = sx * (Lc / 2 + 0.02)\n    for (const sz of [-1, 1]) {\n      k.box(C, [0.16, 0.06, 0.18], [x, ys - ft / 2 - 0.03, sz * (fw + 0.05)])\n      k.box('equipmentPaintDark', [0.16, ys - ft / 2 - 0.06 - hb, 0.16], [x, (ys - ft / 2 - 0.06 + hb) / 2, sz * (fw + 0.05)])\n      k.box('equipmentPaintDark', [0.24, 0.02, 0.24], [x, hb + 0.01, sz * (fw + 0.05)])\n      for (const dx of [-0.05, 0.05]) k.hex('nut', 0.036, 0.022, [x + dx, ys - ft / 2 + 0.012, sz * (fw + 0.05)])\n    }\n  }\n\n  // seal chambers, cartridge seal glands, bearing brackets and housings at both ends\n  const rsh = Math.max(0.035, L.m.shaftDia * 0.55)\n  for (const [xh, s] of [[xDE, -1], [xNDE, 1]] as [number, number][]) {\n    const xe = s * (Lc / 2 + 0.12)\n    k.cyl(C, Rc * 0.42, 0.14, [xe + s * 0.07, ys, 0], 'x', 32)\n    k.cyl('stainless', Rc * 0.36, 0.05, [xe + s * 0.165, ys, 0], 'x', 28)\n    k.boltCircle('boltXylan', 'nut', 4, Rc * 0.3, [xe + s * 0.13, ys, 0], s > 0 ? 'x' : '-x', 0.012, 0.08, [0.07], Math.PI / 4)\n    // bracket arms from the casing end to the housing\n    const xa = xe + s * 0.2\n    const xb = xh - (s * bhL) / 2\n    for (const sz of [-1, 1]) k.box(C, [Math.abs(xb - xa) + 0.04, bhR * 0.5, 0.04], [(xa + xb) / 2, ys - bhR * 0.4, sz * bhR * 0.75])\n    k.box(C, [Math.abs(xb - xa) + 0.04, 0.04, bhR * 1.5], [(xa + xb) / 2, ys - bhR * 0.65, 0])\n    k.lathe(C, [\n      [bhR * 0.75, -bhL / 2],\n      [bhR, -bhL / 2 + 0.03],\n      [bhR, bhL / 2 - 0.03],\n      [bhR * 0.75, bhL / 2],\n    ], [xh, ys, 0], 'x', 36)\n    for (let i = 0; i < 5; i++) k.cyl(C, bhR * 1.1, 0.008, [xh - bhL * 0.3 + i * bhL * 0.15, ys, 0], 'x', 32)\n    k.box(C, [bhL * 0.9, 0.035, bhR * 2.3], [xh, ys, 0])\n    for (const sz of [-1, 1]) for (const dx of [-bhL * 0.3, bhL * 0.3]) k.hex('nut', 0.03, 0.02, [xh + dx, ys + 0.03, sz * bhR * 1.05])\n    // vibration probes (X-Y) and an RTD\n    for (const a of [Math.PI / 4, (3 * Math.PI) / 4]) {\n      const q: V3 = [xh - s * bhL * 0.2, ys + Math.sin(a) * bhR, Math.cos(a) * bhR]\n      k.rod('stainless', q, [q[0], ys + Math.sin(a) * (bhR + 0.07), Math.cos(a) * (bhR + 0.07)], 0.012, 10)\n    }\n    k.cyl('brass', 0.022, 0.012, [xh, ys - bhR * 0.4, bhR * 0.92 + 0.006], 'z', 16)\n    k.disc('glass', 0.017, [xh, ys - bhR * 0.4, bhR * 0.92 + 0.0125], 'z', 16)\n    // ring-oil lubrication: constant-level oiler (pressure lube from the console replaces it)\n    if (!p.lubeConsole) oiler(k, [xh + s * bhL * 0.25, ys - bhR * 0.45, bhR * 0.88], [0, 0, 1])\n    // bearing RTD head on top\n    k.cyl('stainless', 0.008, 0.06, [xh + s * bhL * 0.25, ys + bhR + 0.03, 0], 'y', 8)\n    k.cyl('stainless', 0.022, 0.035, [xh + s * bhL * 0.25, ys + bhR + 0.075, 0], 'y', 14)\n    // Plan 23 seal cooler on the -Z side, loop from the gland and back\n    const cx = xe + s * 0.17\n    const zc = -(fw + 0.3)\n    const sc = sealCooler(k, [cx, hb, zc], ys + Rc * 0.6)\n    const gy = ys + Rc * 0.36\n    k.tube('tubing', [[cx, gy, -0.03], [cx, gy + 0.05, -0.03], [cx, gy + 0.05, zc + 0.02], [cx, sc.top[1] + 0.06, zc + 0.02], [cx, sc.top[1] + 0.06, zc], [cx, sc.top[1], zc]], 0.008)\n    k.tube('tubing', [[cx, ys - Rc * 0.36, -0.03], [cx, ys - Rc * 0.36 - 0.05, -0.03], [cx, ys - Rc * 0.36 - 0.05, zc + 0.08], [cx, sc.bottom[1] - 0.05, zc + 0.08], [cx, sc.bottom[1] - 0.05, zc], [cx, sc.bottom[1], zc]], 0.008)\n    // pedestal under the housing\n    k.box(C, [bhL * 0.5, ys - bhR * 0.8 - hb, bhR * 1.2], [xh, (ys - bhR * 0.8 + hb) / 2, 0])\n    k.box('flangeSteel', [bhL * 0.7, 0.02, bhR * 1.6], [xh, hb + 0.01, 0])\n  }\n  // NDE thrust bearing end cover with axial probes\n  k.cyl(C, bhR * 0.8, 0.06, [xNDE + bhL / 2 + 0.03, ys, 0], 'x', 32)\n  k.boltCircle('boltXylan', 'nut', 6, bhR * 0.68, [xNDE + bhL / 2 + 0.03, ys, 0], 'x', 0.01, 0.05, [0.04])\n  for (const dz of [-0.04, 0.04]) k.rod('stainless', [xNDE + bhL / 2 + 0.06, ys + 0.03, dz], [xNDE + bhL / 2 + 0.14, ys + 0.03, dz], 0.01, 8)\n  k.cyl('flangeSteel', rsh, xNDE - xSE + 0.0, [(xNDE + xSE) / 2, ys, 0], 'x', 16)\n\n  // coupling + motor\n  const Rm = L.m.R\n  const Lm = L.m.L\n  k.at([xm, ys, 0], undefined, () => motor(k, { R: Rm, L: Lm, H: ys - hb, tbox: 'side', cable: true }))\n  tagPlate(k, `${L.m.frame} ${p.kw} kW 2P`, [xm - Lm * 0.62, ys + Rm * 0.62, Rm * 0.82], [0, 0.5, 0.87], Math.max(0.12, Rm * 0.45))\n  if (p.kw >= 1000) topCooler(k, xm, ys, Rm, Lm)\n  for (const fx of [xm - Lm * 0.82, xm - Lm * 0.18]) for (const s of [-1, 1]) mountPad(k, fx, hb, s * Rm * 0.78, 0.24, 0.22, 1)\n  rotors.push(guardedCoupling(k, xMS - 0.02, xSE + 0.04, ys, Math.max(0.18, rsh * 4.2), hb, 'safetyYellow', 165, 0, rsh))\n  rotors.push(cowlFan(Rm, Lm, 165, [xm, ys, 0]))\n\n  drainRimBaseplate(k, L.x1 - L.x0, W, hb, 'equipmentPaintDark', (L.x0 + L.x1) / 2)\n  if (p.tag) tagPlate(k, p.tag, [(L.x0 + L.x1) / 2, hb * 0.5, W / 2 + 0.002], 'z', 0.24)\n  controlStation(k, [L.x0 + 0.2, hb, W / 2 - 0.15])\n\n  if (p.lubeConsole) lubeConsole(k, L)\n}\n\n// forced-feed lube oil console: reservoir, main + aux pumps, twin cooler, duplex filter, supply / return to both housings\nfunction lubeConsole(k: Kit, L: ReturnType<typeof layout>) {\n  const { xCon, hb, W, ys, bhR, xDE, xNDE } = L\n  const cx = xCon + 0.55\n  const LW = Math.min(W - 0.2, 1.0)\n  const rH = 0.55\n  const G: MatKey = 'equipmentPaint'\n  k.rbox(G, [1.0, rH, LW], 0.02, [cx, hb + rH / 2, 0])\n  k.box(G, [1.04, 0.02, LW + 0.04], [cx, hb + rH + 0.01, 0])\n  k.rod('glass', [cx + 0.505, hb + 0.1, LW * 0.3], [cx + 0.505, hb + rH - 0.08, LW * 0.3], 0.012, 8)\n  k.cyl('castIron', 0.04, 0.05, [cx + 0.3, hb + rH + 0.045, -LW * 0.3], 'y', 14)\n  k.cyl('brass', 0.012, 0.06, [cx - 0.35, hb + rH + 0.05, -LW * 0.35], 'y', 8)\n  k.sphere('brass', 0.022, [cx - 0.35, hb + rH + 0.09, -LW * 0.35], undefined, 10)\n  // main + auxiliary pumps with small vertical motors\n  for (const dz of [-0.22, 0.22]) {\n    const x = cx - 0.2\n    const y0 = hb + rH + 0.02\n    k.cyl('machineryGreen', 0.06, 0.12, [x, y0 + 0.06, dz], 'y', 18)\n    k.cyl('machineryGreen', 0.05, 0.1, [x, y0 + 0.17, dz], 'y', 18)\n    k.cyl('motorPaint', 0.08, 0.26, [x, y0 + 0.35, dz], 'y', 24)\n    k.cyl('motorPaint', 0.085, 0.06, [x, y0 + 0.51, dz], 'y', 24)\n    k.rbox('motorPaint', [0.08, 0.08, 0.06], 0.008, [x, y0 + 0.35, dz + Math.sign(dz) * 0.1])\n  }\n  // duplex filter with changeover valve, cooler, PCV\n  for (const dz of [-0.1, 0.1]) {\n    k.cyl('stainless', 0.06, 0.32, [cx + 0.2, hb + rH + 0.2, dz], 'y', 18)\n    k.dome('stainless', 0.06, [cx + 0.2, hb + rH + 0.36, dz], 'y', Math.PI / 2, 0.5, 18)\n  }\n  k.box('valveBody', [0.08, 0.08, 0.14], [cx + 0.2, hb + rH + 0.07, 0])\n  k.handwheel('handwheel', 0.12, [cx + 0.2, hb + rH + 0.15, 0], 'y', 2)\n  k.cyl(G, 0.1, 0.7, [cx + 0.05, hb + rH + 0.75, 0], 'x', 24)\n  for (const s of [-1, 1]) k.cyl('flangeSteel', 0.12, 0.03, [cx + 0.05 + s * 0.35, hb + rH + 0.75, 0], 'x', 24)\n  for (const s of [-1, 1]) k.box(G, [0.04, 0.25, 0.04], [cx + 0.05 + s * 0.25, hb + rH + 0.55, 0])\n  gauge(k, [cx + 0.42, hb + rH + 0.5, LW / 2 - 0.05], 'z', 0.08, 'bar', 6)\n  k.rod('stainless', [cx + 0.42, hb + rH, LW / 2 - 0.08], [cx + 0.42, hb + rH + 0.46, LW / 2 - 0.08], 0.008, 8)\n  // supply header to both bearings and drains back\n  const yS = ys + bhR + 0.18\n  const zP = -bhR - 0.12\n  k.tube('tubing', [[cx + 0.2, hb + rH + 0.4, -0.1], [cx + 0.2, yS, -0.1], [cx + 0.2, yS, zP], [xDE, yS, zP], [xDE, yS, 0], [xDE, ys + bhR, 0]], 0.012, 8, 0.05)\n  k.tube('tubing', [[xNDE, yS, zP], [xNDE, yS, 0], [xNDE, ys + bhR, 0]], 0.012, 8, 0.05)\n  for (const xh of [xDE, xNDE]) k.tube('equipmentPaintDark', [[xh, ys - bhR * 0.9, -bhR * 0.6], [xh, hb + 0.08, -bhR * 0.6], [xh, hb + 0.08, -W / 2 + 0.12], [cx - 0.45, hb + 0.08, -W / 2 + 0.12], [cx - 0.45, hb + 0.2, -LW / 2 + 0.05]], 0.022, 10, 0.06)\n  for (const dz of [-0.22, 0.22]) anchorBolt(k, [cx + 0.45, hb, dz], 0.014)\n}\n"
    },
    {
      "path": "rotating/_pumps.ts",
      "type": "registry:file",
      "target": "components/deck-twin/rotating/_pumps.ts",
      "content": "// Shared pump, motor and fan geometry: plates, guards, vertical motor, drain-rim baseplate, flanged branches, control stations.\nimport * as THREE from 'three'\nimport { Kit, mat4, uvScale, type MatKey, type Orient, type V3 } from '../lib/kit'\nimport { flange, gauge, motorSize, tagPlate } from '../lib/parts'\nimport { fanGuardMaterial, meshGuardMaterial } from '../lib/textures'\nimport { flange as flangeDims, pipeOD, pipeWall, type Rating } from '../lib/standards'\n\nexport const TAU = Math.PI * 2\nexport const clamp = (v: number, a: number, b: number) => Math.min(b, Math.max(a, v))\n\n/** Quaternion turning local +Z onto n (plates facing n). */\nexport const faceQ = (n: V3) => new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 0, 1), new THREE.Vector3(...n).normalize())\n\n/** Flat ring (rIn = 0: disc) between y0 and y1 along `axis` from `pos`. */\nexport function annulus(k: Kit, mat: MatKey, rIn: number, rOut: number, y0: number, y1: number, pos: V3 = [0, 0, 0], axis: Orient = 'y', seg = 40) {\n  k.lathe(mat, [[rIn, y0], [rOut, y0], [rOut, y1], [rIn, y1], [rIn, y0]], pos, axis, seg)\n}\n\n/** Open (uncapped) cylinder between two points. */\nexport function openTube(k: Kit, mat: MatKey, a: V3, b: V3, r: number, seg = 24, r2?: number) {\n  const va = new THREE.Vector3(...a)\n  const d = new THREE.Vector3(...b).sub(va)\n  const L = d.length()\n  if (L < 1e-6) return\n  const mid = va.addScaledVector(d, 0.5)\n  k.add(mat, new THREE.CylinderGeometry(r2 ?? r, r, L, seg, 1, true), mat4([mid.x, mid.y, mid.z], d.toArray() as V3))\n}\n\n/** Expanded-metal guard tube between two points. */\nexport function meshTube(k: Kit, a: V3, b: V3, r: number, r2?: number, color?: string) {\n  const va = new THREE.Vector3(...a)\n  const d = new THREE.Vector3(...b).sub(va)\n  const L = d.length()\n  const g = new THREE.CylinderGeometry(r2 ?? r, r, L, 28, 1, true)\n  uvScale(g, TAU * Math.max(r, r2 ?? r) * 8, L * 8)\n  const mid = va.addScaledVector(d, 0.5)\n  k.add(meshGuardMaterial(color), g, mat4([mid.x, mid.y, mid.z], d.toArray() as V3))\n}\n\n/** Expanded-metal flat panel w × h facing +normal. */\nexport function meshPanel(k: Kit, w: number, h: number, pos: V3, normal: V3, color?: string) {\n  const g = new THREE.PlaneGeometry(w, h)\n  uvScale(g, w * 8, h * 8)\n  const m = new THREE.Matrix4().compose(new THREE.Vector3(...pos), faceQ(normal), new THREE.Vector3(1, 1, 1))\n  k.add(meshGuardMaterial(color), g, m)\n}\n\n/** Plate from a to b: width w along `side` (made perpendicular), thickness t. */\nexport function strut(k: Kit, mat: MatKey, a: V3, b: V3, w: number, t: number, side: V3) {\n  const A = new THREE.Vector3(...a)\n  const Y = new THREE.Vector3(...b).sub(A)\n  const L = Y.length()\n  if (L < 1e-6) return\n  Y.normalize()\n  const X = new THREE.Vector3(...side)\n  X.addScaledVector(Y, -X.dot(Y)).normalize()\n  const Z = new THREE.Vector3().crossVectors(X, Y)\n  const m = new THREE.Matrix4().makeBasis(X, Y, Z).setPosition(A.addScaledVector(Y, L / 2))\n  k.add(mat, new THREE.BoxGeometry(w, L, t), m)\n}\n\n/** Triangular gusset in the radial plane at angle `ang` about +Y, r0 → r1 at yPlate, height h (h < 0 hangs below). */\nexport function gusset(k: Kit, mat: MatKey, ang: number, r0: number, r1: number, yPlate: number, h: number, t: number) {\n  const s = new THREE.Shape()\n  s.moveTo(r0, yPlate)\n  s.lineTo(r1, yPlate)\n  s.lineTo(r0, yPlate + h)\n  s.closePath()\n  const m = new THREE.Matrix4().makeRotationY(ang).multiply(new THREE.Matrix4().makeTranslation(0, 0, -t / 2))\n  k.extrude(mat, s, t, m, 1)\n}\n\n/** Toothed (cooling-fin) TEFC frame section in the XY plane. */\nexport function finnedShape(R: number, finH: number, nFin: number) {\n  const sh = new THREE.Shape()\n  const step = TAU / nFin\n  const finW = Math.min(0.012, step * R * 0.28)\n  const da = finW / 2 / R\n  const pts: [number, number][] = []\n  for (let i = 0; i < nFin; i++) {\n    const a = i * step\n    for (let s = 0; s < 3; s++) {\n      const aa = a - step / 2 + (s / 3) * (step / 2 - da)\n      pts.push([Math.cos(aa) * R, Math.sin(aa) * R])\n    }\n    pts.push([Math.cos(a - da) * R, Math.sin(a - da) * R])\n    pts.push([Math.cos(a - da * 0.6) * (R + finH), Math.sin(a - da * 0.6) * (R + finH)])\n    pts.push([Math.cos(a + da * 0.6) * (R + finH), Math.sin(a + da * 0.6) * (R + finH)])\n    pts.push([Math.cos(a + da) * R, Math.sin(a + da) * R])\n  }\n  pts.forEach(([x, y], i) => (i ? sh.lineTo(x, y) : sh.moveTo(x, y)))\n  sh.closePath()\n  return sh\n}\n\n/** Weld-neck flange length (weld end to raised face). */\nexport const flangeLen = (nps: number, rating: Rating) => {\n  const f = flangeDims(nps, rating)\n  return f.hubLength + f.raisedFaceHeight\n}\n\n/** Branch pipe from `from` along `dir` to a WN flange (with studs) whose raised face is at `face`. */\nexport function flangedBranch(k: Kit, mat: MatKey, from: V3, face: V3, nps: number, rating: Rating, schedule: '40' | '80' | 'STD' = '80') {\n  const d = new THREE.Vector3(...face).sub(new THREE.Vector3(...from)).normalize()\n  const L = flange(k, nps, rating, face, d.toArray() as V3, true)\n  const end = new THREE.Vector3(...face).addScaledVector(d, -L).toArray() as V3\n  const ro = pipeOD(nps) / 2\n  k.pipe(mat, from, end, pipeOD(nps), pipeWall(nps, schedule), nps >= 6 ? 32 : 20)\n  // bore through the flange hub up to the face, and a dark end where the branch opens into the casing\n  openTube(k, 'pipeBore', end, new THREE.Vector3(...face).addScaledVector(d, -0.0005).toArray() as V3, ro * 0.88, nps >= 6 ? 32 : 20)\n  k.disc('pipeBore', ro - pipeWall(nps, schedule) + 0.001, from, d.toArray() as V3, 24)\n}\n\n/** IEC motor envelope from kW; above the IEC 60072 range (1 MW) the frame grows with the cube root of power. */\nexport function motorEnvelope(kw: number, poles: 2 | 4 | 6 = 4) {\n  const m = motorSize(kw, poles)\n  const s = kw > 1000 ? Math.cbrt(kw / 1000) : 1\n  return { ...m, R: m.R * s, L: m.L * s, H: m.H * s, frame: kw > 1000 ? 'HV' : m.frame }\n}\n\nexport interface VerticalMotorOpts {\n  /** frame radius (without fins), m */\n  R: number\n  /** frame length flange to fan end, m */\n  H: number\n  /** D-flange underside height, m */\n  yF: number\n  mat?: MatKey\n  /** text on the nameplate, e.g. \"IEC 315M 160 kW\" */\n  plate?: string\n  /** height of the deck the cables run down to */\n  deck?: number\n  canopy?: boolean\n}\n\n/** Vertical TEFC motor (IM V1): D-flange, finned frame, end shields, fan cowl, drip canopy, terminal + heater boxes. Returns the top height. */\nexport function verticalMotor(k: Kit, o: VerticalMotorOpts) {\n  const { R: Rm, H: Hm, yF } = o\n  const drv = o.mat ?? 'motorPaint'\n  const deck = o.deck ?? 0\n  const finH = Math.max(0.012, Rm * 0.1)\n  const shL = Math.max(0.05, Rm * 0.22)\n  annulus(k, drv, 0, Rm * 1.12, yF, yF + 0.05, [0, 0, 0], 'y', 48)\n  k.boltCircle('boltXylan', 'nut', 8, Rm * 1.04, [0, yF - 0.04, 0], 'y', Math.max(0.012, Rm * 0.04), 0.12, [0.105], Math.PI / 8)\n  const y0f = yF + 0.05 + shL\n  const yM1 = yF + 0.05 + Hm\n  const y1f = yM1 - shL\n  k.lathe(drv, [[Rm * 0.6, yF + 0.05], [Rm + finH * 0.6, yF + 0.05 + shL * 0.55], [Rm + finH * 0.6, y0f]], [0, 0, 0], 'y', 48)\n  const nFin = Math.max(28, Math.round(Rm * 100))\n  k.extrude(drv, finnedShape(Rm, finH, nFin), y1f - y0f, new THREE.Matrix4().makeTranslation(0, y0f, 0).multiply(new THREE.Matrix4().makeRotationX(-Math.PI / 2)), 2)\n  k.lathe(drv, [[Rm + finH * 0.6, y1f], [Rm + finH * 0.6, y1f + shL * 0.4], [Rm * 0.7, yM1]], [0, 0, 0], 'y', 48)\n  k.boltCircle('nut', 'nut', 8, Rm + finH * 0.3, [0, yF + 0.05 + shL * 0.7, 0], 'y', Math.max(0.008, Rm * 0.03), 0.03, [0.02])\n  const cR = Rm + finH + 0.015\n  const cowlH = Math.max(0.12, Rm * 0.45)\n  k.lathe(drv, [\n    [cR - 0.005, y1f - shL * 0.3],\n    [cR, y1f - shL * 0.3],\n    [cR, yM1 + cowlH * 0.8],\n    [cR * 0.94, yM1 + cowlH],\n    [cR * 0.94 - 0.004, yM1 + cowlH],\n  ], [0, 0, 0], 'y', 48)\n  k.disc('rubberBlack', cR * 0.9, [0, yM1 + cowlH - 0.02, 0], 'y', 32)\n  k.disc(fanGuardMaterial(), cR * 0.92, [0, yM1 + cowlH + 0.001, 0], 'y', 32)\n  let top = yM1 + cowlH\n  if (o.canopy !== false) {\n    const yCan = yM1 + cowlH + Math.max(0.06, Rm * 0.14)\n    for (let i = 0; i < 4; i++) {\n      const a = (i / 4) * TAU\n      k.rod(drv, [Math.cos(a) * cR * 0.88, yM1 + cowlH, Math.sin(a) * cR * 0.88], [Math.cos(a) * cR * 0.88, yCan, Math.sin(a) * cR * 0.88], 0.01, 6)\n    }\n    const hc = Math.max(0.04, Rm * 0.1)\n    k.lathe(drv, [[cR * 1.12, yCan - 0.04], [cR * 1.12, yCan], [cR * 0.4, yCan + hc], [0, yCan + hc]], [0, 0, 0], 'y', 48)\n    k.lathe(drv, [[0, yCan - 0.005], [cR * 1.1, yCan - 0.04]], [0, 0, 0], 'y', 48)\n    top = yCan + hc\n  }\n  // terminal box (+Z), anti-condensation heater / RTD box (-Z), cables to the deck\n  const tb = clamp(Rm * 0.75, 0.14, 0.5)\n  const yT = yF + Hm * 0.45\n  const zT = Rm + finH + tb * 0.28\n  k.box(drv, [tb * 0.5, tb * 0.5, finH + 0.03], [0, yT, Rm + finH * 0.5])\n  k.rbox(drv, [tb * 1.1, tb * 0.9, tb * 0.55], tb * 0.06, [0, yT, zT])\n  k.rbox(drv, [tb * 1.0, tb * 0.8, tb * 0.06], tb * 0.03, [0, yT, zT + tb * 0.3])\n  k.around(6, tb * 0.42, [0, yT, zT + tb * 0.335], 'z', (p) => k.hex('nut', 0.016, 0.01, p, 'z'), Math.PI / 6)\n  for (const dx of [-tb * 0.25, tb * 0.25]) {\n    k.cyl('brass', tb * 0.08, tb * 0.14, [dx, yT - tb * 0.5, zT], 'y', 12)\n    k.spline('cable', [[dx, yT - tb * 0.56, zT], [dx, yT - tb * 1.4, zT + 0.04], [dx * 1.4, (yT + deck) / 2, zT + 0.1], [dx * 1.6, deck + 0.03, zT + 0.12]], tb * 0.065, 8, 30)\n  }\n  const ta = tb * 0.5\n  const zA = -(Rm + finH + ta * 0.22)\n  k.rbox(drv, [ta, ta * 0.8, ta * 0.45], ta * 0.08, [0, yT + tb * 0.2, zA])\n  k.cyl('brass', ta * 0.1, ta * 0.18, [0, yT + tb * 0.2 - ta * 0.45, zA], 'y', 10)\n  k.spline('cable', [[0, yT + tb * 0.2 - ta * 0.5, zA], [0, yT - 0.4, zA - 0.08], [0.1, deck + 0.03, zA - 0.1]], 0.012, 6, 24)\n  // lifting lugs, nameplate, earth boss, bearing vibration probes\n  for (const s of [-1, 1]) {\n    const x = s * (Rm + finH + 0.03)\n    k.box(drv, [0.08, 0.16, 0.035], [x, y1f - 0.06, 0])\n    k.torus('nut', 0.035, 0.009, [x + s * 0.02, y1f + 0.03, 0], 'z', 6, 14)\n  }\n  const pn = new THREE.Vector3(0.707, 0, 0.707)\n  const pr = Rm + finH + 0.006\n  const pw = Math.max(0.1, Rm * 0.5)\n  k.box(drv, [pw * 0.6, pw * 0.4, finH + 0.01], [pn.x * (Rm + finH * 0.5), yF + Hm * 0.68, pn.z * (Rm + finH * 0.5)], faceQ([pn.x, 0, pn.z]))\n  if (o.plate) tagPlate(k, o.plate, [pn.x * pr, yF + Hm * 0.68, pn.z * pr], [pn.x, 0, pn.z], pw)\n  else k.box('nameplate', [pw, pw * 0.6, 0.003], [pn.x * pr, yF + Hm * 0.68, pn.z * pr], faceQ([pn.x, 0, pn.z]))\n  k.cyl('brass', 0.012, 0.03, [-(Rm + finH * 0.5) * 0.7, yF + 0.2, (Rm + finH * 0.5) * 0.7], [-0.7, 0, 0.7], 8)\n  for (const a of [Math.PI / 2 + 0.6, Math.PI / 2 - 0.6]) {\n    const u: V3 = [Math.cos(a), 0, Math.sin(a)]\n    k.rod('stainless', [u[0] * (Rm * 0.75), yF + 0.05 + shL * 0.5, u[2] * (Rm * 0.75)], [u[0] * (Rm * 0.75 + 0.08), yF + 0.05 + shL * 0.5 + 0.04, u[2] * (Rm * 0.75 + 0.08)], 0.012, 8)\n  }\n  return top\n}\n\n/** Drain-rim baseplate (API 610): channels, sloped deck, rim, grout holes, anchor bolts, lifting lugs, drain. Top at y = hb. */\nexport function drainRimBaseplate(k: Kit, L: number, W: number, hb: number, mat: MatKey = 'equipmentPaintDark', x0 = 0) {\n  const t = 0.012\n  k.at([x0, 0, 0], undefined, () => {\n    k.box(mat, [L, 0.016, W + 0.08], [0, 0.008, 0])\n    for (const s of [-1, 1]) {\n      k.box(mat, [L, hb - 0.016, t], [0, hb / 2 + 0.008, s * (W / 2 - t / 2)])\n      k.box(mat, [t, hb - 0.016, W], [s * (L / 2 - t / 2), hb / 2 + 0.008, 0])\n      const n = Math.round(L / 0.45)\n      for (let i = 1; i < n; i++) k.box(mat, [0.01, hb - 0.03, 0.04], [-L / 2 + (i * L) / n, hb / 2 + 0.008, s * (W / 2 + 0.02)])\n    }\n    k.box(mat, [L - 2 * t, 0.01, W - 2 * t], [0, hb - 0.03, 0])\n    for (const s of [-1, 1]) {\n      k.box(mat, [L, 0.045, 0.02], [0, hb - 0.005, s * (W / 2 - 0.01)])\n      k.box(mat, [0.02, 0.045, W], [s * (L / 2 - 0.01), hb - 0.005, 0])\n    }\n    for (const s of [-1, 1]) k.box(mat, [L - 0.1, hb - 0.05, 0.12], [0, (hb - 0.05) / 2 + 0.016, s * W * 0.28])\n    const nG = Math.max(2, Math.round(L / 0.9))\n    for (let i = 0; i < nG; i++) {\n      const gx = -L / 2 + (L * (i + 0.5)) / nG\n      k.torus(mat, 0.05, 0.008, [gx, hb - 0.022, 0], 'y', 6, 24)\n      k.disc('rubberBlack', 0.048, [gx, hb - 0.0245, 0], 'y', 24)\n    }\n    const nA = Math.max(3, Math.round(L / 0.7) + 1)\n    for (let i = 0; i < nA; i++) {\n      const ax = -L / 2 + 0.08 + ((L - 0.16) * i) / (nA - 1)\n      for (const s of [-1, 1]) anchorBolt(k, [ax, 0.016, s * (W / 2 + 0.035)])\n    }\n    for (const sx of [-1, 1])\n      for (const sz of [-1, 1]) {\n        k.box(mat, [0.1, 0.1, 0.02], [sx * (L / 2 - 0.12), hb * 0.55, sz * (W / 2 + 0.01)])\n        k.torus('nut', 0.025, 0.006, [sx * (L / 2 - 0.12), hb * 0.55 + 0.02, sz * (W / 2 + 0.022)], 'z', 6, 14)\n      }\n    k.cyl(mat, 0.022, 0.12, [L / 2 + 0.05, hb - 0.04, -W / 2 + 0.15], 'x', 12)\n    k.hex('nut', 0.05, 0.03, [L / 2 + 0.11, hb - 0.04, -W / 2 + 0.15], 'x')\n    k.cyl('brass', 0.015, 0.03, [-L / 2 + 0.3, hb * 0.5, W / 2 + 0.015], 'z', 10)\n  })\n}\n\n/** Anchor bolt with washer and nut on a plate whose top is at p[1]. */\nexport function anchorBolt(k: Kit, p: V3, d = 0.022) {\n  k.cyl('stainless', d * 0.9, 0.004, [p[0], p[1] + 0.002, p[2]], 'y', 16)\n  k.hex('nut', d * 1.9, d * 1.1, [p[0], p[1] + 0.004 + d * 0.55, p[2]])\n  k.cyl('boltXylan', d / 2, d * 3, [p[0], p[1] + d * 1.5, p[2]], 'y', 8)\n}\n\n/** Machined mounting pad with jack-screw block, top at y. */\nexport function mountPad(k: Kit, x: number, y: number, z: number, sx = 0.22, sz = 0.2, jack = 1, mat: MatKey = 'equipmentPaintDark') {\n  k.box('flangeSteel', [sx, 0.025, sz], [x, y - 0.0125, z])\n  if (!jack) return\n  k.box(mat, [0.04, 0.06, 0.06], [x + jack * (sx / 2 + 0.05), y + 0.02, z])\n  k.cyl('boltXylan', 0.008, 0.08, [x + jack * (sx / 2 + 0.03), y + 0.03, z], 'x', 6)\n}\n\n/** Local control station on a post (start / stop, E-stop, ammeter), facing +Z; base at `foot`. */\nexport function controlStation(k: Kit, foot: V3, face: V3 = [0, 0, 1], h = 1.2) {\n  const [lx, y0, lz] = foot\n  const q = faceQ(face)\n  const n = new THREE.Vector3(...face).normalize()\n  const P = (u: number, v: number, w: number): V3 => new THREE.Vector3(u, v, w).applyQuaternion(q).add(new THREE.Vector3(lx, 0, lz)).toArray() as V3\n  k.rod('structSteel', [lx, y0, lz], [lx, h + 0.25, lz], 0.03, 12)\n  k.box('structSteel', [0.15, 0.012, 0.15], [lx, y0 + 0.006, lz])\n  k.box('structSteel', [0.04, 0.3, 0.02], P(0, h, 0.04), q)\n  k.rbox('stainless', [0.24, 0.32, 0.13], 0.012, P(0, h, 0.115), q)\n  const tilt = q.clone().multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), 0.15))\n  k.box('stainless', [0.3, 0.012, 0.2], P(0, h + 0.18, 0.12), tilt)\n  const f = 0.18\n  k.cyl('safetyGreen', 0.016, 0.02, P(-0.055, h + 0.05, f), n.toArray() as V3, 16)\n  k.cyl('actuatorRed', 0.016, 0.02, P(0.055, h + 0.05, f), n.toArray() as V3, 16)\n  k.cyl('actuatorYellow', 0.04, 0.004, P(0, h - 0.07, f - 0.002), n.toArray() as V3, 24)\n  k.cyl('actuatorRed', 0.012, 0.03, P(0, h - 0.07, f + 0.012), n.toArray() as V3, 12)\n  k.dome('actuatorRed', 0.028, P(0, h - 0.07, f + 0.025), n.toArray() as V3, Math.PI / 2, 0.6)\n  gauge(k, P(0, h + 0.11, f - 0.01), n.toArray() as V3, 0.06, 'bar', 600)\n  k.hex('brass', 0.03, 0.02, P(0, h - 0.17, 0.11))\n  k.spline('cable', [P(0, h - 0.18, 0.11), P(0, h - 0.35, 0.06), P(0, y0 + 0.2, 0.04), P(0.1, y0 + 0.02, 0)], 0.008, 6, 24)\n}\n\n/** Pressure gauge on a root valve + siphon above a pipe top at `p` (dial facing +normal). */\nexport function pipeGauge(k: Kit, p: V3, normal: V3 = [0, 0, 1], max = 25, d = 0.1) {\n  const [x, y, z] = p\n  k.cyl('stainless', 0.012, 0.1, [x, y + 0.05, z], 'y', 8)\n  k.hex('stainless', 0.04, 0.035, [x, y + 0.09, z])\n  k.torus('stainless', 0.025, 0.006, [x, y + 0.14, z], [normal[0], 0, normal[2]], 6, 18)\n  k.cyl('stainless', 0.01, 0.05, [x, y + 0.19, z], 'y', 8)\n  gauge(k, [x + normal[0] * 0.02, y + 0.25, z + normal[2] * 0.02], normal, d, 'bar', max)\n}\n\n/** Spiral volute outline (Archimedean, r0 at the tongue to r1) with a straight tangential outlet of length `out` along +Y at u ∈ [r0, r1]. */\nexport function voluteShape(r0: number, r1: number, out: number, n = 48) {\n  const s = new THREE.Shape()\n  s.moveTo(r0, 0)\n  for (let i = 1; i <= n; i++) {\n    const th = (i / n) * TAU\n    const r = r0 + ((r1 - r0) * i) / n\n    s.lineTo(Math.cos(th) * r, Math.sin(th) * r)\n  }\n  s.lineTo(r1, out)\n  s.lineTo(r0, out)\n  s.closePath()\n  return s\n}\n\n/** Extruded shape with rounded (bevelled) edges, depth along local +Z centred on z = 0, placed by m. */\nexport function castExtrude(k: Kit, mat: MatKey, shape: THREE.Shape, depth: number, bevel: number, m: THREE.Matrix4, seg = 12) {\n  const g = new THREE.ExtrudeGeometry(shape, { depth: Math.max(depth - 2 * bevel, 0.001), bevelEnabled: true, bevelThickness: bevel, bevelSize: bevel, bevelSegments: 3, curveSegments: seg })\n  g.translate(0, 0, -(depth - 2 * bevel) / 2)\n  k.add(mat, g, m)\n}\n\n/** Top-mounted air-to-air cooler (IC611) on a horizontal HV motor whose DE face is at xm, shaft height ys. */\nexport function topCooler(k: Kit, xm: number, ys: number, Rm: number, Lm: number, mat: MatKey = 'motorPaint') {\n  const ch = Rm * 0.7\n  const y = ys + Rm * 1.12 + ch / 2\n  k.box(mat, [Lm * 0.3, Rm * 0.25, Rm * 0.9], [xm - Lm * 0.5, ys + Rm * 1.05, 0])\n  k.rbox(mat, [Lm * 0.75, ch, Rm * 1.7], 0.03, [xm - Lm * 0.5, y, 0])\n  for (let i = 0; i < 6; i++) k.box('rubberBlack', [Lm * 0.7, 0.01, Rm * 1.71], [xm - Lm * 0.5, y - ch * 0.35 + i * ch * 0.14, 0])\n  k.cyl(mat, ch * 0.45, 0.1, [xm - Lm * 0.875 - 0.05, y, 0], 'x', 28)\n  k.disc(fanGuardMaterial(), ch * 0.42, [xm - Lm * 0.875 - 0.101, y, 0], '-x', 24)\n}\n\n/** Constant-level oiler: arm from the bearing-housing surface point p along n, glass reservoir above. */\nexport function oiler(k: Kit, p: V3, n: V3 = [0, 0, 1], s = 1) {\n  const o = new THREE.Vector3(...p).addScaledVector(new THREE.Vector3(...n).normalize(), 0.07 * s)\n  k.rod('stainless', p, o.toArray() as V3, 0.008 * s, 8)\n  k.cyl('stainless', 0.03 * s, 0.03 * s, [o.x, o.y + 0.015 * s, o.z], 'y', 16)\n  k.cyl('glass', 0.032 * s, 0.09 * s, [o.x, o.y + 0.075 * s, o.z], 'y', 16)\n  k.dome('glass', 0.032 * s, [o.x, o.y + 0.12 * s, o.z], 'y', Math.PI / 2, 1)\n}\n\n/** Small valve (forged body, bonnet, handwheel) on a vertical line at p, handwheel up. */\nexport function smallValve(k: Kit, p: V3, s = 1, dir: Orient = 'y') {\n  k.at(p, dir, () => {\n    k.box('valveBody', [0.06 * s, 0.07 * s, 0.05 * s], [0, 0, 0])\n    k.cyl('valveBody', 0.016 * s, 0.07 * s, [0, 0.07 * s, 0], 'y', 10)\n    k.handwheel('handwheel', 0.07 * s, [0, 0.11 * s, 0], 'y')\n  })\n}\n\n/** API 682 Plan 23 seal cooler on a post: shell with bolted heads and cooling-water nozzles. Returns the top and bottom process connection points. */\nexport function sealCooler(k: Kit, foot: V3, yTop: number, r = 0.055) {\n  const [x, y0, z] = foot\n  const h = Math.max(0.3, r * 6)\n  const yc = yTop - h / 2\n  k.rod('structSteel', [x, y0, z], [x, yc - h / 2 - 0.02, z], 0.022, 10)\n  k.box('structSteel', [0.12, 0.012, 0.12], [x, y0 + 0.006, z])\n  k.box('structSteel', [0.03, 0.12, 0.008], [x, yc - h / 2 + 0.02, z - r - 0.004])\n  k.cyl('stainless', r, h, [x, yc, z], 'y', 20)\n  for (const s of [-1, 1]) {\n    k.cyl('stainless', r * 1.3, 0.02, [x, yc + s * (h / 2 + 0.01), z], 'y', 20)\n    k.boltCircle('boltXylan', 'nut', 6, r * 1.15, [x, yc + s * (h / 2 + 0.01) - 0.015, z], 'y', 0.006, 0.03, [0.005, 0.025])\n  }\n  for (const s of [-1, 1]) {\n    k.rod('stainless', [x, yc + s * h * 0.35, z], [x + r + 0.05, yc + s * h * 0.35, z], 0.01, 8)\n    k.cyl('flangeSteel', 0.018, 0.012, [x + r + 0.05, yc + s * h * 0.35, z], 'x', 12)\n  }\n  return { top: [x, yc + h / 2 + 0.02, z] as V3, bottom: [x, yc - h / 2 - 0.02, z] as V3 }\n}\n"
    }
  ],
  "categories": [
    "rotating"
  ],
  "meta": {
    "export": "BetweenBearingsPump",
    "anchor": "base",
    "standards": [
      "API 610 (BB3)",
      "API 614",
      "API 682",
      "IEC 60034 / 60072",
      "ASME B16.5"
    ]
  }
}