{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "electric-motor",
  "type": "registry:component",
  "title": "Electric motor (IEC TEFC)",
  "description": "Foot-mounted totally enclosed fan-cooled cage induction motor (IM B3, IC411): finned frame, end shields with condensate drain plugs and regreasing nipples from frame 225, fan cowl with grille, feet on machined soleplates with anchor bolts, terminal box, lifting eyes, earth boss and a nameplate showing the IEC 60072 frame chosen from kW and poles. Optional anti-condensation heater box; above 1 MW it becomes an HV machine with a top air-to-air cooler (IC611). Shaft along +X; the shaft end and cooling fan turn when running.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "rotating/electric-motor.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/rotating/electric-motor.tsx",
      "content": "// IEC TEFC foot-mounted induction motor (IM B3, IC411) sized from kW and poles per IEC 60072, on machined soleplates with anchor bolts.\nimport { useEffect, useMemo } from 'react'\nimport { Kit, type MatKey } from '../lib/kit'\nimport { paint } from '../lib/materials'\nimport { KitMesh, type GroupProps } from '../lib/mesh'\nimport { motor, tagPlate } from '../lib/parts'\nimport { Rotors, cowlFan, type RotorSpec } from '../lib/rotors'\nimport { anchorBolt, clamp, motorEnvelope, topCooler } from './_pumps'\n\nexport interface ElectricMotorProps {\n  /** rated power, kW */\n  kw?: number\n  poles?: 2 | 4 | 6\n  /** anti-condensation heater terminal box */\n  heater?: boolean\n  /** terminal box on the side (+Z) or on top */\n  terminalBox?: 'side' | 'top'\n  color?: string\n  tag?: string\n  running?: boolean\n}\n\nexport const defaults = {\n  kw: 75,\n  poles: 4 as 2 | 4 | 6,\n  heater: true,\n  terminalBox: 'side' as 'side' | 'top',\n  color: '#355f80',\n  running: true,\n}\n\nconst SOLE = 0.04 // soleplate + grout height\n\nfunction layout(kw: number, poles: 2 | 4 | 6) {\n  const m = motorEnvelope(clamp(kw, 0.37, 20000), poles)\n  const cowl = Math.max(0.12, m.R * 0.6)\n  const stub = 0.12 + m.R * 0.2\n  // centre the footprint (frame + cowl + shaft end) on x = 0\n  const xm = (m.L + cowl - stub) / 2\n  return { ...m, cowl, stub, xm, ys: SOLE + m.H }\n}\n\nexport function ElectricMotor({ tag, ...rest }: ElectricMotorProps & GroupProps) {\n  const { kw, poles, heater, terminalBox, color, running, ...group } = { ...defaults, ...rest }\n  const { built, rotors } = useMemo(() => build({ kw, poles, heater, terminalBox, color, tag }), [kw, poles, heater, terminalBox, color, tag])\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 ElectricMotor\n\nfunction build(p: Omit<typeof defaults, 'running'> & { tag?: string }) {\n  const k = new Kit()\n  const L = layout(p.kw, p.poles)\n  const { R, xm, ys } = L\n  const mat: MatKey = p.color === defaults.color ? 'motorPaint' : paint(p.color, { metalness: 0.35, roughness: 0.45, clearcoat: 0.4, clearcoatRoughness: 0.35, weather: 0.5 })\n  k.at([xm, ys, 0], undefined, () => motor(k, { R, L: L.L, H: ys - SOLE, tbox: p.terminalBox, mat, cable: false }))\n  if (p.kw > 1000) topCooler(k, xm, ys, R, L.L, mat)\n\n  // nameplate with the IEC frame\n  const pole = `${p.poles}P`\n  tagPlate(k, `${L.frame === 'HV' ? 'HV' : 'IEC ' + L.frame} ${p.kw} kW ${pole}`, [xm - L.L * 0.62, ys + R * 0.62, R * 0.82], [0, 0.5, 0.87], Math.max(0.1, R * 0.5))\n\n  // anti-condensation heater box on the -Z side with its gland\n  if (p.heater) {\n    const hb = Math.max(0.1, R * 0.4)\n    const fin = Math.max(0.012, R * 0.12)\n    const c: [number, number, number] = [xm - L.L * 0.4, ys + R * 0.1, -(R + fin + hb * 0.22)]\n    k.box(mat, [hb * 0.4, hb * 0.4, fin + 0.02], [c[0], c[1], -(R + fin * 0.5)])\n    k.rbox(mat, [hb, hb * 0.8, hb * 0.45], hb * 0.06, c)\n    k.cyl('brass', hb * 0.08, hb * 0.14, [c[0], c[1] - hb * 0.45, c[2]], 'y', 10)\n    k.box('nameplate', [hb * 0.6, hb * 0.25, 0.002], [c[0], c[1] + hb * 0.15, c[2] - hb * 0.226])\n  }\n\n  // regreasing nipples on both end shields from frame 225 (IEC 60072 shaft height), condensate drain plugs underneath\n  const shL = Math.max(0.04, R * 0.22)\n  for (const x of [xm - shL * 0.55, xm - L.L + shL * 0.55]) {\n    if (L.H >= 0.225) {\n      k.cyl('brass', 0.007, 0.03, [x, ys + R * 0.86, R * 0.25], 'y', 8)\n      k.sphere('brass', 0.01, [x, ys + R * 0.86 + 0.018, R * 0.25], undefined, 8)\n    }\n    k.hex('rubberBlack', 0.022, 0.012, [x, ys - R * 0.88, R * 0.2])\n  }\n\n  // turning shaft extension with key\n  const sr = Math.max(0.02, R * 0.13)\n  const r = new Kit()\n  r.cyl('stainless', sr * 1.01, L.stub * 0.9, [L.stub * 0.45, 0, 0], 'x', 20)\n  r.box('nut', [L.stub * 0.55, sr * 0.35, sr * 0.5], [L.stub * 0.55, sr * 0.95, 0])\n  const rotors: RotorSpec[] = [{ built: r.build(), axis: 'x', pos: [xm, ys, 0], rpm: 160 }, cowlFan(R, L.L, 160, [xm, ys, 0])]\n\n  // machined soleplates under each foot pair, anchor bolts\n  const fw = Math.max(0.06, R * 0.35)\n  for (const fx of [xm - L.L * 0.82, xm - L.L * 0.18]) {\n    k.box('flangeSteel', [fw * 2.4, SOLE, R * 1.56 + fw * 1.8], [fx, SOLE / 2, 0])\n    for (const s of [-1, 1]) for (const dx of [-fw * 0.9, fw * 0.9]) anchorBolt(k, [fx + dx, SOLE, s * (R * 0.78 + fw * 0.75)], Math.max(0.012, fw * 0.25))\n  }\n  // earth boss on a foot\n  k.cyl('brass', 0.012, 0.025, [xm - L.L * 0.82, SOLE + 0.03, R * 0.78 + fw * 0.7], 'z', 8)\n  if (p.tag) tagPlate(k, p.tag, [xm - L.L * 0.3, ys - R * 0.55, R * 0.86], [0, -0.3, 0.95], Math.max(0.08, R * 0.4))\n  return { built: k.build(), rotors }\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": "ElectricMotor",
    "anchor": "base",
    "standards": [
      "IEC 60034",
      "IEC 60072-1",
      "EN 50347"
    ]
  }
}