{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "metering-pump",
  "type": "registry:component",
  "title": "Diaphragm metering pump (API 675)",
  "description": "Hydraulically actuated diaphragm metering pump for chemical injection: IEC motor on a C-face adapter, worm-gear housing with oil window and breather, micrometer stroke adjuster (the `stroke` setting moves the barrel), plunger housing and bolted diaphragm head with refill valve and rupture gauge, double-ball check valves with the suction flange facing down and the discharge flange facing up. Mounted on an anchored pedestal with the liquid end overhanging for the suction pipe.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "rotating/metering-pump.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/rotating/metering-pump.tsx",
      "content": "// API 675 hydraulically actuated diaphragm metering pump: IEC motor, worm-gear drive with micrometer stroke adjuster, diaphragm liquid end with double-ball check valves.\nimport { useEffect, useMemo } from 'react'\nimport { Kit, type MatKey } from '../lib/kit'\nimport { KitMesh, type GroupProps } from '../lib/mesh'\nimport { gauge, motor, tagPlate } from '../lib/parts'\nimport { port, type Port } from '../lib/ports'\nimport { Rotors, cowlFan, type RotorSpec } from '../lib/rotors'\nimport { type Rating } from '../lib/standards'\nimport { anchorBolt, clamp, flangeLen, flangedBranch, motorEnvelope } from './_pumps'\n\nexport interface MeteringPumpProps {\n  suctionNps?: number\n  dischargeNps?: number\n  suctionRating?: Rating\n  /** discharge flange class */\n  rating?: Rating\n  /** motor rated power, kW */\n  kw?: number\n  /** stroke length setting, 0 … 100 % (micrometer position) */\n  stroke?: number\n  housing?: MatKey\n  tag?: string\n  running?: boolean\n}\n\nexport const defaults = {\n  suctionNps: 1,\n  dischargeNps: 0.5,\n  suctionRating: 300 as Rating,\n  rating: 1500 as Rating,\n  kw: 1.5,\n  stroke: 60,\n  housing: 'actuatorBlue' as MatKey,\n  running: true,\n}\n\ntype P = Omit<typeof defaults, 'running'> & { tag?: string }\n\nfunction layout(p: P) {\n  const kw = clamp(p.kw, 0.25, 30)\n  const m = motorEnvelope(kw, 4)\n  const hb = 0.3 // pedestal top\n  const g = clamp(0.24 + 0.03 * Math.sqrt(kw), 0.26, 0.45) // gearbox cube, typical\n  const ys = hb + Math.max(m.H, g * 0.45) // worm (motor) shaft height\n  const yh = ys + g * 0.15 // plunger / diaphragm axis\n  const Rh = clamp(0.08 + 0.025 * Math.sqrt(kw), 0.09, 0.2) // diaphragm head radius\n  const xG = 0\n  const xH = xG + g / 2 + 0.18 // head centre (diaphragm plane)\n  const vS = 0.13 // check-valve cartridge length, typical\n  const ySuc = yh - Rh - vS - flangeLen(p.suctionNps, p.suctionRating) - 0.03\n  const yDis = yh + Rh + vS + flangeLen(p.dischargeNps, p.rating) + 0.03\n  const xm = xG - g / 2 - 0.12\n  const x0 = xm - m.L - Math.max(0.12, m.R * 0.6) - 0.05\n  const x1 = xG + g / 2 + 0.05\n  const cx = (x0 + xH + 0.1) / 2\n  return { kw, m, hb, g, ys, yh, Rh, xG, xH, vS, ySuc, yDis, xm, x0, x1, cx }\n}\n\nexport function ports(props: MeteringPumpProps = {}): Port[] {\n  const p = { ...defaults, ...props }\n  const L = layout(p)\n  return [\n    port('suction', 'Suction', [L.xH - L.cx, L.ySuc, 0], [0, -1, 0], p.suctionNps, p.suctionRating),\n    port('discharge', 'Discharge', [L.xH - L.cx, L.yDis, 0], [0, 1, 0], p.dischargeNps, p.rating),\n  ]\n}\n\nexport function MeteringPump({ tag, ...rest }: MeteringPumpProps & GroupProps) {\n  const { suctionNps, dischargeNps, suctionRating, rating, kw, stroke, housing, running, ...group } = { ...defaults, ...rest }\n  const { built, rotors } = useMemo(\n    () => build({ suctionNps, dischargeNps, suctionRating, rating, kw, stroke, housing, tag }),\n    [suctionNps, dischargeNps, suctionRating, rating, kw, stroke, housing, 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 MeteringPump\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 H = p.housing\n  const { hb, g, ys, yh, Rh, xG, xH, vS, xm, x0, x1, m } = L\n  const D: MatKey = 'equipmentPaintDark'\n\n  // fabricated pedestal: top plate, legs, base plate with anchors; liquid end overhangs for the suction pipe\n  const W = Math.max(0.42, m.R * 2 + 0.16, g + 0.1)\n  k.box(D, [x1 - x0, 0.02, W], [(x0 + x1) / 2, hb - 0.01, 0])\n  for (const s of [-1, 1]) k.box(D, [x1 - x0, hb - 0.04, 0.012], [(x0 + x1) / 2, (hb - 0.02) / 2 + 0.01, s * (W / 2 - 0.006)])\n  for (const s of [-1, 1]) k.box(D, [0.012, hb - 0.04, W], [s > 0 ? x1 - 0.006 : x0 + 0.006, (hb - 0.02) / 2 + 0.01, 0])\n  k.box(D, [x1 - x0 + 0.1, 0.016, W + 0.1], [(x0 + x1) / 2, 0.008, 0])\n  for (const sx of [x0 + 0.03, x1 - 0.03]) for (const sz of [-1, 1]) anchorBolt(k, [sx, 0.016, sz * (W / 2 + 0.025)], 0.014)\n  k.box(D, [0.05, 0.05, 0.012], [x0 + 0.08, hb * 0.5, W / 2 + 0.006])\n  k.cyl('brass', 0.01, 0.025, [x0 + 0.2, hb * 0.5, W / 2 + 0.012], 'z', 8)\n\n  // worm gear housing with oil level window, breather, drain, cooling ribs\n  const yg = hb + g * 0.5\n  k.rbox(H, [g, g, g * 0.85], g * 0.12, [xG, yg, 0])\n  k.box(H, [g + 0.05, 0.02, g * 0.85 + 0.05], [xG, hb + 0.01, 0])\n  for (const s of [-1, 1]) for (let i = 0; i < 3; i++) k.box(H, [0.01, g * 0.7, 0.02], [xG - g * 0.25 + i * g * 0.25, yg, s * (g * 0.425 + 0.01)])\n  k.cyl('brass', 0.022, 0.01, [xG + g * 0.2, hb + g * 0.3, g * 0.425 + 0.005], 'z', 16)\n  k.disc('glass', 0.017, [xG + g * 0.2, hb + g * 0.3, g * 0.425 + 0.0105], 'z', 16)\n  k.cyl('brass', 0.008, 0.03, [xG - g * 0.25, hb + g + 0.015, -g * 0.2], 'y', 8)\n  k.sphere('brass', 0.014, [xG - g * 0.25, hb + g + 0.035, -g * 0.2], undefined, 10)\n  k.hex('nut', 0.026, 0.015, [xG + g * 0.35, hb + 0.04, g * 0.425 + 0.008], 'z')\n  k.box('nameplate', [g * 0.4, g * 0.22, 0.002], [xG - g * 0.1, yg + g * 0.18, g * 0.425 + 0.002])\n  // motor adapter (C-face lantern)\n  k.cyl(H, m.R * 0.75, 0.1, [xG - g / 2 - 0.05, ys, 0], 'x', 24, m.R * 0.6)\n\n  // micrometer stroke adjuster on top: sleeve, graduated barrel, knob, lock lever\n  const st = clamp(p.stroke, 0, 100) / 100\n  const xa = xG + g * 0.15\n  const ya = hb + g\n  k.cyl(H, 0.045, 0.06, [xa, ya + 0.03, 0], 'y', 20)\n  k.cyl('stainless', 0.03, 0.08 + 0.03 * st, [xa, ya + 0.1 + 0.015 * st, 0], 'y', 20)\n  for (let i = 0; i < 5; i++) k.torus('nut', 0.0305, 0.0015, [xa, ya + 0.075 + i * 0.012, 0], 'y', 4, 24)\n  k.cyl('handwheel', 0.05, 0.035, [xa, ya + 0.16 + 0.03 * st, 0], 'y', 18)\n  k.rod('actuatorRed', [xa + 0.03, ya + 0.09, 0], [xa + 0.1, ya + 0.1, 0.03], 0.006, 6)\n  k.sphere('actuatorRed', 0.012, [xa + 0.1, ya + 0.1, 0.03], undefined, 8)\n\n  // hydraulic diaphragm liquid end: plunger housing, intermediate ring, head with bolt circle\n  const xi0 = xG + g / 2\n  k.cyl(H, Rh * 0.75, 0.1, [xi0 + 0.05, yh, 0], 'x', 28, Rh * 0.9)\n  k.cyl(H, Rh * 1.08, 0.04, [xi0 + 0.12, yh, 0], 'x', 32)\n  k.cyl('stainless', Rh, 0.08, [xH, yh, 0], 'x', 32)\n  k.cyl('stainless', Rh * 0.65, 0.03, [xH + 0.055, yh, 0], 'x', 28)\n  k.boltCircle('boltXylan', 'nut', 8, Rh * 0.86, [xi0 + 0.09, yh, 0], 'x', 0.01, 0.15, [0.005, 0.145], Math.PI / 8)\n  // refill / relief valve and diaphragm-rupture pressure gauge\n  k.cyl('stainless', 0.018, 0.05, [xi0 + 0.06, yh + Rh * 0.8, -Rh * 0.3], 'y', 12)\n  k.cyl('brass', 0.02, 0.04, [xi0 + 0.06, yh + Rh * 0.8 + 0.045, -Rh * 0.3], 'y', 12)\n  k.rod('stainless', [xH, yh, Rh * 0.9], [xH, yh, Rh + 0.06], 0.007, 8)\n  gauge(k, [xH, yh, Rh + 0.08], 'z', 0.06, 'bar', 250)\n\n  // double-ball check valve cartridges, suction below, discharge above\n  const xv = xH\n  for (const s of [-1, 1]) {\n    const y0 = yh + s * Rh * 0.85\n    const y1 = yh + s * (Rh + vS)\n    k.box('stainless', [0.07, Math.abs(y1 - y0) * 0.35, 0.07], [xv, y0 + s * Math.abs(y1 - y0) * 0.175, 0])\n    k.cyl('stainless', 0.03, Math.abs(y1 - y0) * 0.65, [xv, y1 - s * Math.abs(y1 - y0) * 0.325, 0], 'y', 18)\n    k.hex('nut', 0.075, 0.02, [xv, y1 - s * Math.abs(y1 - y0) * 0.6, 0])\n  }\n  flangedBranch(k, 'stainless', [xv, yh - Rh - vS - 0.005, 0], [xv, L.ySuc, 0], p.suctionNps, p.suctionRating, '80')\n  flangedBranch(k, 'stainless', [xv, yh + Rh + vS + 0.005, 0], [xv, L.yDis, 0], p.dischargeNps, p.rating, '80')\n\n  // motor (B3/B5) and its fan\n  k.at([xm, ys, 0], undefined, () => motor(k, { R: m.R, L: m.L, H: ys - hb, tbox: 'top' }))\n  tagPlate(k, `${m.frame} ${p.kw} kW`, [xm - m.L * 0.5, ys, m.R * 1.13], 'z', Math.max(0.06, m.R * 0.6))\n  rotors.push(cowlFan(m.R, m.L, 160, [xm, ys, 0]))\n  if (p.tag) tagPlate(k, p.tag, [(x0 + x1) / 2, hb * 0.55, W / 2 + 0.002], 'z', 0.14)\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": "MeteringPump",
    "anchor": "base",
    "standards": [
      "API 675",
      "IEC 60034 / 60072",
      "ASME B16.5"
    ]
  }
}