{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "magnetic-flowmeter",
  "type": "registry:component",
  "title": "Electromagnetic flowmeter",
  "description": "Electromagnetic (mag) flowmeter: flanged sensor tube with PTFE or rubber liner flared over the raised faces, stainless grounding rings with tabs and braided earth straps, coil housing and an integral Ex d transmitter on a junction neck. Face-to-face from the ISO 13359 short series; ports sit on the grounding ring faces.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "instruments/magnetic-flowmeter.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/instruments/magnetic-flowmeter.tsx",
      "content": "// Electromagnetic flowmeter: lined flanged sensor with coil housing, grounding rings with earth straps and an integral Ex d transmitter. ISO 13359 lay length, ISO 6817, ASME B16.5.\nimport { Kit, type MatKey, type V3 } from '../lib/kit'\nimport { paint } from '../lib/materials'\nimport { KitMesh, useKit, type GroupProps } from '../lib/mesh'\nimport { port, type Port } from '../lib/ports'\nimport { dn, flange as flangeDims, pipeOD, pipeWall, type Rating } from '../lib/standards'\nimport { SS, exHead, tag as tagPlate, flange, segFor } from './_parts'\n\nexport interface MagneticFlowmeterProps {\n  /** nominal pipe size, inches */\n  nps?: number\n  rating?: Rating\n  liner?: 'ptfe' | 'rubber'\n  housing?: MatKey\n  tag?: string\n}\n\nexport const defaults = { nps: 4, rating: 300 as Rating, liner: 'ptfe' as 'ptfe' | 'rubber', housing: 'actuatorBlue' as MatKey, tag: 'FT-4101' }\n\ntype P = typeof defaults\n\nconst RING_T = 0.003 // grounding ring thickness\n\n/** ISO 13359 short face-to-face, m. */\nfunction layLength(nps: number) {\n  const d = dn(nps)\n  const table: [number, number][] = [[50, 0.2], [80, 0.2], [100, 0.25], [150, 0.3], [200, 0.35], [250, 0.45], [300, 0.5], [350, 0.55], [400, 0.6], [500, 0.65], [600, 0.78]]\n  for (const [max, L] of table) if (d <= max) return L\n  return Math.max(0.78, d * 1.3e-3)\n}\n\nexport function ports(props: MagneticFlowmeterProps = {}): Port[] {\n  const p = { ...defaults, ...props }\n  const h = layLength(p.nps) / 2 + RING_T\n  return [port('A', 'End A', [-h, 0, 0], [-1, 0, 0], p.nps, p.rating), port('B', 'End B', [h, 0, 0], [1, 0, 0], p.nps, p.rating)]\n}\n\nexport function MagneticFlowmeter(props: MagneticFlowmeterProps & GroupProps) {\n  const { nps, rating, liner, housing, tag, ...group } = { ...defaults, ...props }\n  const p = { nps, rating, liner, housing, tag }\n  const data = useKit((k) => build(k, p), Object.values(p))\n  return (\n    <group {...group}>\n      <KitMesh data={data} />\n    </group>\n  )\n}\n\nexport default MagneticFlowmeter\n\nfunction build(k: Kit, p: P) {\n  const L = layLength(p.nps)\n  const D = pipeOD(p.nps)\n  const f = flangeDims(p.nps, p.rating)\n  const lt = p.liner === 'ptfe' ? 0.003 : 0.005\n  const ri = D / 2 - pipeWall(p.nps, 'STD') - lt\n  const rfR = f.raisedFace / 2\n  const lining: MatKey = p.liner === 'ptfe' ? paint('#ecebe4', { metalness: 0, roughness: 0.6, clearcoat: 0, weather: 0 }) : 'rubberBlack'\n  const seg = segFor(f.od)\n  const xBack = L / 2 - 0.002 - f.raisedFaceHeight - f.thickness\n  const Rh = Math.max(D / 2 * 1.25, f.od / 2 * 0.9)\n\n  k.lathe(lining, [[ri, L / 2], [ri, -L / 2]], [0, 0, 0], 'x', seg)\n  for (const s of [-1, 1] as const) {\n    const ax = s > 0 ? 'x' : '-x'\n    flange(k, p.nps, p.rating, [s * (L / 2 - 0.002), 0, 0], ax)\n    k.at([s * (L / 2 - 0.002), 0, 0], ax, () => {\n      // liner flared over the raised face, then the grounding ring\n      k.lathe(lining, [[rfR * 0.97, 0], [rfR * 0.97, 0.002], [ri, 0.002]], [0, 0, 0], 'y', seg)\n      k.lathe(SS, [[rfR * 0.96, 0.002], [rfR * 0.96, 0.002 + RING_T], [ri, 0.002 + RING_T], [ri, 0.002]], [0, 0, 0], 'y', seg)\n    })\n    // ring tab (between bolts at +Z) and braided earth strap to a boss on the housing\n    const xr = s * (L / 2 + RING_T / 2)\n    k.box(SS, [RING_T, 0.022, f.od / 2 + 0.03 - rfR * 0.9], [xr, 0, (f.od / 2 + 0.03 + rfR * 0.9) / 2])\n    const boss: V3 = [s * (xBack - 0.03), 0, Rh]\n    k.hex(SS, 0.016, 0.008, [boss[0], 0, Rh + 0.003], 'z')\n    k.spline('copper', [[xr, 0, f.od / 2 + 0.02], [s * (L / 2 - 0.01), 0.004, f.od / 2 + 0.035], [s * (xBack - 0.01), 0.004, Rh + 0.02], [boss[0], 0, Rh + 0.007]], 0.003, 6, 24)\n  }\n  // coil housing between the flange backs, with weld bands\n  const len = 2 * xBack - 0.004\n  k.cyl('equipmentPaint', Rh, len, [0, 0, 0], 'x', segFor(2 * Rh))\n  for (const s of [-1, 1]) k.cyl('equipmentPaint', Rh + 0.004, 0.012, [s * (len / 2 - 0.006), 0, 0], 'x', segFor(2 * Rh))\n  for (const s of [-1, 1]) k.cyl('equipmentPaint', 0.02, 0.008, [s * len * 0.25, -Rh - 0.003, 0], 'y', 12)\n\n  // integral transmitter on a junction neck\n  k.cyl(p.housing, 0.036, 0.07, [0, Rh + 0.03, 0], 'y', 24)\n  k.cyl(p.housing, 0.042, 0.016, [0, Rh + 0.064, 0], 'y', 24)\n  exHead(k, [0, Rh + 0.072 + 0.011 + 0.048 - 0.004, 0], p.housing)\n  tagPlate(k, p.tag, [0, Rh + 0.03, 0.0365], 'z', 0.06)\n}\n"
    },
    {
      "path": "instruments/_parts.ts",
      "type": "registry:file",
      "target": "components/deck-twin/instruments/_parts.ts",
      "content": "// Shared instrument parts: Ex d transmitter heads, coplanar sensors, valve manifolds, tube fittings, pipe stands, flanged thermowells and level chambers.\nimport * as THREE from 'three'\nimport { Kit, axisVec, type Axis, type MatKey, type V3 } from '../lib/kit'\nimport { tagPlate } from '../lib/parts'\nimport { flange as flangeDims, pipeOD, pipeWall, type Rating, type Schedule } from '../lib/standards'\nimport { lcdMaterial } from '../lib/textures'\n\nexport const SS: MatKey = 'stainless'\n/** 1/2\" OD instrument tubing, radius m */\nexport const TUBE_R = 0.00635\n/** height from the coplanar flange underside to the head centreline, m */\nexport const SENSOR_H = 0.182\n\nconst V = (v: V3) => new THREE.Vector3(...v)\nconst A = (v: THREE.Vector3) => v.toArray() as V3\nexport const addV = (a: V3, d: V3, s = 1): V3 => [a[0] + d[0] * s, a[1] + d[1] * s, a[2] + d[2] * s]\n\n/** Radial segments for a round part of diameter d, m (smooth silhouettes on big flanges and bodies). */\nexport const segFor = (d: number) => (d > 0.5 ? 72 : d > 0.25 ? 56 : d > 0.1 ? 44 : 32)\n\n/** B16.5 weld-neck flange, raised face at `face` pointing along +axis, with an open bore. Returns face-to-weld length. */\nexport function flange(k: Kit, nps: number, rating: Rating, face: V3, axis: Axis, studs = false) {\n  const f = flangeDims(nps, rating)\n  const ro = pipeOD(nps) / 2\n  const ri = ro - pipeWall(nps, '80')\n  const hubR = Math.max(f.hubDia / 2, ro * 1.05)\n  const rf = f.raisedFaceHeight\n  const L = f.hubLength + rf\n  const seg = segFor(f.od)\n  const v = axisVec(axis)\n  const base = A(V(face).addScaledVector(v, -L))\n  const back = L - rf - f.thickness\n  const o = A(v)\n  k.lathe('flangeSteel', [[ro * 0.999, 0], [hubR, Math.max(back - 0.004, 0.001)], [hubR + 0.004, back], [f.od / 2, back], [f.od / 2, L - rf], [f.raisedFace / 2, L - rf]], base, o, seg)\n  k.lathe('machined', [[f.raisedFace / 2, L - rf], [f.raisedFace / 2, L], [ri, L]], base, o, seg)\n  k.lathe('pipeBore', [[ri, L], [ri, 0]], base, o, segFor(ri * 2))\n  if (studs) {\n    const st = A(V(face).addScaledVector(v, -rf - f.thickness - f.boltDia))\n    k.boltCircle('boltXylan', 'nut', f.bolts, f.boltCircle / 2, st, axis, f.boltDia, f.thickness + f.boltDia * 2.4, [f.boltDia * 0.45])\n  }\n  return L\n}\n\n/** Blind flange with gasket and double-nutted studs closing a face that points along +axis. */\nexport function blindFlange(k: Kit, nps: number, rating: Rating, face: V3, axis: Axis) {\n  const f = flangeDims(nps, rating)\n  const v = axisVec(axis)\n  const t = f.thickness * 1.15\n  k.cyl('flangeSteel', f.od / 2, t, A(V(face).addScaledVector(v, 0.0045 + t / 2)), A(v), segFor(f.od))\n  k.cyl('gasket', f.raisedFace / 2 + 0.006, 0.0045, A(V(face).addScaledVector(v, 0.00225)), A(v), segFor(f.raisedFace))\n  const studL = f.thickness * 2.2 + t + f.boltDia * 2\n  const st = A(V(face).addScaledVector(v, -f.thickness - f.raisedFaceHeight - f.boltDia))\n  k.boltCircle('boltXylan', 'nut', f.bolts, f.boltCircle / 2, st, axis, f.boltDia, studL, [f.boltDia * 0.45, studL - f.boltDia * 0.45])\n}\n\n/** Hollow pipe between two points with diameter-scaled segments. */\nexport function pipe(k: Kit, a: V3, b: V3, nps: number, mat: MatKey = 'pipePaint', schedule: Schedule = 'STD') {\n  const od = pipeOD(nps)\n  k.pipe(mat, a, b, od, pipeWall(nps, schedule), segFor(od))\n}\n\n/** Axis perpendicular to `dir`, horizontal where possible (for T-bar handles). */\nfunction crossAxis(dir: V3): V3 {\n  const d = V(dir).normalize()\n  if (Math.abs(d.y) > 0.9) return [1, 0, 0]\n  return A(new THREE.Vector3(0, 1, 0).cross(d).normalize())\n}\n\n/**\n * Dual-compartment Ex d housing, body axis along Z: LCD cover on +Z, terminal cover on -Z, conduit entries on ±X\n * (cable gland on +X, stopping plug on -X), neck boss underneath. Centre at `c`. Returns the gland exit point.\n */\nexport function exHead(k: Kit, c: V3, mat: MatKey, opts: { lcd?: boolean; r?: number } = {}): V3 {\n  const R = opts.r ?? 0.048\n  const s = R / 0.048\n  const L = 0.1 * s\n  const [x, y, z] = c\n  k.cyl(mat, R, L, c, 'z', 32)\n  k.cyl(mat, 0.026 * s, 0.03 * s, [x, y - R + 0.004 * s, z], 'y', 20)\n  for (const d of [-1, 1]) {\n    k.cyl(mat, R * 1.1, 0.024 * s, [x, y, z + d * (L / 2 + 0.012 * s)], 'z', 32)\n    k.torus(mat, R * 1.1, 0.0028 * s, [x, y, z + d * (L / 2 + 0.006 * s)], 'z', 6, 32)\n    k.hex('nut', 0.008 * s, 0.006 * s, [x + R * 0.75, y - R * 0.75, z + d * (L / 2 + 0.003 * s)], 'z')\n  }\n  const zf = z + L / 2 + 0.024 * s\n  if (opts.lcd ?? true) {\n    k.torus(mat, R * 0.86, 0.004 * s, [x, y, zf], 'z', 6, 32)\n    k.disc(lcdMaterial(), R * 0.76, [x, y, zf + 0.0006], 'z', 28)\n    k.disc('glass', R * 0.84, [x, y, zf + 0.0022], 'z', 28)\n  }\n  for (const d of [-1, 1]) k.cyl(mat, 0.014 * s, 0.024 * s, [x + d * (R + 0.006 * s), y, z + 0.02 * s], 'x', 14)\n  k.hex('brass', 0.027 * s, 0.018 * s, [x + R + 0.026 * s, y, z + 0.02 * s], 'x')\n  k.cyl('brass', 0.011 * s, 0.014 * s, [x + R + 0.042 * s, y, z + 0.02 * s], 'x', 12)\n  k.cyl('rubberBlack', 0.0078 * s, 0.01 * s, [x + R + 0.053 * s, y, z + 0.02 * s], 'x', 10)\n  k.hex(SS, 0.025 * s, 0.01 * s, [x - R - 0.023 * s, y, z + 0.02 * s], 'x')\n  k.box('nameplate', [0.052 * s, 0.002, 0.03 * s], [x, y + R + 0.0008, z])\n  k.cyl(SS, 0.005 * s, 0.008 * s, [x, y - R * 0.6, z - L / 2 - 0.004 * s], 'z', 8)\n  return [x + R + 0.058 * s, y, z + 0.02 * s]\n}\n\n/** Coplanar sensor module on a flange whose underside is at `b`. dp adds the H / L marks. Returns the head centre. */\nexport function coplanarSensor(k: Kit, b: V3, mat: MatKey, dp = false): V3 {\n  const [x, y, z] = b\n  k.box(SS, [0.086, 0.024, 0.056], [x, y + 0.012, z])\n  for (const dx of [-0.032, 0.032]) for (const dz of [-0.02, 0.02]) k.hex(SS, 0.012, 0.007, [x + dx, y + 0.0275, z + dz])\n  for (const s of [-1, 1]) {\n    k.cyl(SS, 0.0055, 0.012, [x + s * 0.049, y + 0.012, z], 'x', 8)\n    k.hex(SS, 0.011, 0.007, [x + s * 0.058, y + 0.012, z], 'x')\n  }\n  if (dp) for (const s of [-1, 1]) k.box('nameplate', [0.012, 0.01, 0.0015], [x + s * 0.03, y + 0.012, z + 0.0285])\n  k.cyl(SS, 0.043, 0.008, [x, y + 0.028, z], 'y', 28)\n  k.cyl(SS, 0.04, 0.07, [x, y + 0.067, z], 'y', 28)\n  k.torus(SS, 0.04, 0.002, [x, y + 0.08, z], 'y', 4, 28)\n  k.cyl(SS, 0.02, 0.034, [x, y + 0.117, z], 'y', 16)\n  return exHeadOn(k, [x, y + 0.134, z], mat)\n}\n\nfunction exHeadOn(k: Kit, neckTop: V3, mat: MatKey): V3 {\n  const c: V3 = [neckTop[0], neckTop[1] + 0.048, neckTop[2]]\n  exHead(k, c, mat)\n  return c\n}\n\n/** Small-bore valve operator out of a block face: bonnet, packing nut, stem and T-bar. */\nexport function valveHandle(k: Kit, pos: V3, dir: V3, bar: MatKey = 'handwheel', s = 1) {\n  const d = V(dir).normalize()\n  const dd = A(d)\n  k.hex(SS, 0.021 * s, 0.014 * s, addV(pos, dd, 0.007 * s), dd)\n  k.hex(SS, 0.016 * s, 0.01 * s, addV(pos, dd, 0.019 * s), dd)\n  k.cyl(SS, 0.0035 * s, 0.016 * s, addV(pos, dd, 0.031 * s), dd, 8)\n  k.cyl(bar, 0.0045 * s, 0.056 * s, addV(pos, dd, 0.04 * s), crossAxis(dd), 8)\n}\n\n/** Twin-ferrule compression fitting at a port facing `dir`; the tube leaves 0.026 m out. */\nexport function tubeFitting(k: Kit, pos: V3, dir: V3) {\n  const dd = A(V(dir).normalize())\n  k.hex(SS, 0.024, 0.012, addV(pos, dd, 0.006), dd)\n  k.cyl(SS, 0.008, 0.004, addV(pos, dd, 0.014), dd, 12)\n  k.hex(SS, 0.021, 0.01, addV(pos, dd, 0.021), dd)\n  return addV(pos, dd, 0.026)\n}\n\n/** Instrument tubing along a polyline with tight bends. */\nexport function tubing(k: Kit, pts: V3[]) {\n  k.tube('tubing', pts, TUBE_R, 10, 0.038)\n}\n\nexport interface ManifoldGeom {\n  top: number\n  bottom: number\n  /** process inlets on the underside */\n  inlets: V3[]\n  w: number\n  d: number\n}\n\n/**\n * Bar-stock valve manifold, flat back on -Z. 2-valve: block + bleed. 3-valve: 2 block + equalise.\n * 5-valve: 2 block + 2 equalise (front) + 2 vents (ends). `b` is the bottom centre.\n */\nexport function manifold(k: Kit, b: V3, n: 2 | 3 | 5): ManifoldGeom {\n  const [x, y, z] = b\n  const w = n === 2 ? 0.06 : n === 3 ? 0.1 : 0.12\n  const h = n === 2 ? 0.075 : 0.06\n  const d = 0.05\n  k.box(SS, [w, h, d], [x, y + h / 2, z])\n  const front = z + d / 2\n  const yc = y + h / 2\n  if (n === 2) {\n    valveHandle(k, [x, y + h * 0.62, front], [0, 0, 1], 'handwheel')\n    valveHandle(k, [x - w / 2, y + h * 0.3, z], [-1, 0, 0], 'actuatorRed', 0.75)\n    k.hex('brass', 0.014, 0.01, [x + w / 2 + 0.005, y + h * 0.3, z], 'x')\n  } else {\n    for (const s of [-1, 1]) valveHandle(k, [x + s * w * 0.32, y + h * 0.33, front], [0, 0, 1], 'handwheel')\n    valveHandle(k, [x, y + h * 0.72, front], [0, 0, 1], 'actuatorRed', 0.8)\n    if (n === 5) for (const s of [-1, 1]) valveHandle(k, [x + s * w / 2, yc, z], [s, 0, 0], 'actuatorRed', 0.7)\n    else for (const s of [-1, 1]) k.hex('brass', 0.014, 0.01, [x + s * (w / 2 + 0.005), yc, z], 'x')\n  }\n  const inlets: V3[] = n === 2 ? [[x, y, z]] : [[x - 0.027, y, z], [x + 0.027, y, z]]\n  for (const p of inlets) k.cyl(SS, 0.011, 0.004, addV(p, [0, -1, 0], 0.002), 'y', 12)\n  return { top: y + h, bottom: y, inlets, w, d }\n}\n\n/** 2\" pipe stand (galvanised, Sch 80) on a gusseted base plate with four anchor bolts. Top capped at `top`. */\nexport function pipeStand(k: Kit, top: number) {\n  const od = pipeOD(2)\n  const tp = 0.016\n  const P = 0.26\n  k.box('galvanised', [P, tp, P], [0, tp / 2, 0])\n  k.pipe('galvanised', [0, tp, 0], [0, top, 0], od, pipeWall(2, '80'), 24)\n  k.cyl('galvanised', od / 2 + 0.002, 0.006, [0, top + 0.003, 0], 'y', 24)\n  const g = new THREE.Shape()\n  g.moveTo(0, 0)\n  g.lineTo(0.08, 0)\n  g.lineTo(0.08, 0.012)\n  g.lineTo(0, 0.1)\n  g.closePath()\n  for (let i = 0; i < 4; i++) {\n    const a = (i * Math.PI) / 2\n    const m = new THREE.Matrix4().makeRotationY(a).multiply(new THREE.Matrix4().makeTranslation(od / 2 - 0.001, tp, -0.004))\n    k.extrude('galvanised', g, 0.008, m)\n  }\n  for (const sx of [-1, 1])\n    for (const sz of [-1, 1]) {\n      const p: V3 = [sx * (P / 2 - 0.035), tp, sz * (P / 2 - 0.035)]\n      k.cyl('galvanised', 0.016, 0.003, addV(p, [0, 1, 0], 0.0015), 'y', 16)\n      k.hex('nut', 0.028, 0.014, addV(p, [0, 1, 0], 0.01), 'y')\n      k.cyl('galvanised', 0.008, 0.035, addV(p, [0, 1, 0], 0.012), 'y', 8)\n    }\n  k.cyl('galvanised', 0.008, 0.012, [od / 2 + 0.005, 0.22, 0], 'x', 8)\n  return od / 2\n}\n\n/**\n * Bracket plate on the stand front (+Z) from y0 to y1, x0 to x1, held by two U-bolts at `uy`. Returns the plate front z.\n */\nexport function standBracket(k: Kit, rs: number, y0: number, y1: number, x0: number, x1: number, uy: number[]) {\n  const t = 0.006\n  k.box('galvanised', [x1 - x0, y1 - y0, t], [(x0 + x1) / 2, (y0 + y1) / 2, rs + t / 2])\n  const R = rs + 0.006\n  for (const y of uy) {\n    k.torus('galvanised', R, 0.005, [0, y, 0], 'y', 6, 20, Math.PI)\n    for (const s of [-1, 1]) {\n      k.rod('galvanised', [s * R, y, 0], [s * R, y, rs + t + 0.016], 0.005, 8)\n      k.hex('nut', 0.016, 0.008, [s * R, y, rs + t + 0.004], 'z')\n      k.cyl('galvanised', 0.012, 0.002, [s * R, y, rs + t + 0.001], 'z', 12)\n    }\n  }\n  return rs + t\n}\n\n/** Tube clamp holding a vertical tube at (x, y, z) off the stand front. */\nexport function tubeClamp(k: Kit, rs: number, x: number, y: number, z: number) {\n  k.box('galvanised', [0.028, 0.022, z - rs], [x, y, (z + rs) / 2])\n  k.torus('galvanised', TUBE_R + 0.003, 0.0025, [x, y, z], 'y', 6, 16)\n}\n\n/** Tubing-to-flange adapter: weld-neck flange with its face at `face` pointing along `axis`, tube entering the hub. Returns the tube entry point. */\nexport function tubeFlange(k: Kit, nps: number, rating: Rating, face: V3, axis: Axis) {\n  const L = flange(k, nps, rating, face, axis)\n  const d = A(axisVec(axis))\n  const hub = addV(face, d, -L)\n  k.cyl(SS, pipeOD(nps) / 2, 0.014, addV(hub, d, -0.007), A(axisVec(axis).negate()), 16, TUBE_R * 1.4)\n  return addV(hub, d, -0.014)\n}\n\n/** Cable from a gland that drops and runs back to `end` (a tray or deck) as a smooth spline. */\nexport function cable(k: Kit, pts: V3[]) {\n  k.spline('cable', pts, 0.0065, 6, 48)\n}\n\n/** Wire-tied stamped tag plate facing +normal. */\nexport function tag(k: Kit, text: string, pos: V3, normal: Axis, w = 0.075) {\n  if (text) tagPlate(k, text, pos, normal, w)\n}\n\n/**\n * Flanged thermowell with its raised face at y = 0 facing -Y: solid forged flange, tapered stem below to `insertion`,\n * lagging extension, union and nipple above. Returns the top y.\n */\nexport function flangedThermowell(k: Kit, nps: number, rating: Rating, insertion: number, extension: number) {\n  const f = flangeDims(nps, rating)\n  const rf = f.raisedFaceHeight\n  const t = f.thickness\n  k.lathe('machined', [[0.004, 0], [f.raisedFace / 2, 0], [f.raisedFace / 2, rf]], [0, 0, 0], 'y', segFor(f.od))\n  k.lathe('flangeSteel', [[f.raisedFace / 2, rf], [f.od / 2, rf], [f.od / 2, rf + t], [0.02, rf + t]], [0, 0, 0], 'y', segFor(f.od))\n  k.around(f.bolts, f.boltCircle / 2, [0, rf + t + 0.0004, 0], 'y', (p) => k.cyl('pipeBore', f.boltDia * 0.56, 0.001, p, 'y', 10), Math.PI / f.bolts)\n  const r0 = Math.min(0.0135, pipeOD(nps) / 2 - 0.006)\n  k.cyl(SS, r0 + 0.003, 0.012, [0, -0.006, 0], 'y', 20)\n  k.cyl(SS, 0.0095, insertion - 0.012, [0, -0.012 - (insertion - 0.012) / 2, 0], 'y', 20, r0)\n  k.dome(SS, 0.0095, [0, -insertion, 0], '-y', Math.PI / 2, 1, 16)\n  let y = rf + t\n  k.cyl(SS, 0.02, 0.012, [0, y + 0.006, 0], 'y', 20)\n  k.hex(SS, 0.036, 0.018, [0, y + 0.021, 0])\n  y += 0.03\n  k.cyl(SS, 0.0107, extension, [0, y + extension / 2, 0], 'y', 14)\n  y += extension\n  k.hex(SS, 0.036, 0.016, [0, y + 0.008, 0])\n  k.cyl(SS, 0.017, 0.006, [0, y + 0.019, 0], 'y', 16)\n  k.hex(SS, 0.032, 0.016, [0, y + 0.03, 0])\n  y += 0.038\n  return y\n}\n\n/** Blind-flanged end of a vertical chamber, flange face at y facing `dir` (±1), with a 1/2\" valve and plug. */\nfunction chamberEnd(k: Kit, nps: number, rating: Rating, y: number, dir: 1 | -1, valve: boolean) {\n  const face: V3 = [0, y, 0]\n  flange(k, nps, rating, face, dir > 0 ? 'y' : '-y', true)\n  blindFlange(k, nps, rating, face, dir > 0 ? 'y' : '-y')\n  if (!valve) return\n  const f = flangeDims(nps, rating)\n  const yo = y + dir * (0.0045 + f.thickness * 1.15)\n  const off = 0\n  k.cyl(SS, 0.0107, 0.05, [off, yo + dir * 0.025, 0], 'y', 12)\n  k.box(SS, [0.034, 0.04, 0.034], [off, yo + dir * 0.07, 0])\n  valveHandle(k, [off, yo + dir * 0.07, 0.017], [0, 0, 1], 'actuatorRed', 0.9)\n  k.cyl(SS, 0.0107, 0.03, [off, yo + dir * 0.105, 0], 'y', 12)\n  k.hex(SS, 0.026, 0.014, [off, yo + dir * 0.127, 0])\n}\n\nexport interface ChamberOpts {\n  /** centre-to-centre of the side connections, m */\n  cc: number\n  chamberNps: number\n  connNps: number\n  rating: Rating\n  /** side connection face distance from the chamber axis, m */\n  standout: number\n  /** top end: a blind flange with vent valve, or a flange face left for a GWR head */\n  top: 'vent' | 'gwr'\n  mat: MatKey\n}\n\nexport function chamberDims(o: Pick<ChamberOpts, 'cc'>) {\n  return { yTop: o.cc / 2 + 0.22, yBot: -o.cc / 2 - 0.3 }\n}\n\n/**\n * External level cage: vertical pipe on the origin, side connections facing -X at y = ±cc/2 (weld-neck flanges,\n * faces at x = -standout), blind bottom with drain valve, top either blind + vent valve or bare for a GWR.\n * Returns the top flange face y.\n */\nexport function chamber(k: Kit, o: ChamberOpts) {\n  const { yTop, yBot } = chamberDims(o)\n  const D = pipeOD(o.chamberNps)\n  const f = flangeDims(o.chamberNps, o.rating)\n  const L = f.hubLength + f.raisedFaceHeight\n  pipe(k, [0, yBot + L, 0], [0, yTop - L, 0], o.chamberNps, o.mat, '80')\n  chamberEnd(k, o.chamberNps, o.rating, yBot, -1, true)\n  if (o.top === 'vent') chamberEnd(k, o.chamberNps, o.rating, yTop, 1, true)\n  else flange(k, o.chamberNps, o.rating, [0, yTop, 0], 'y')\n  const fc = flangeDims(o.connNps, o.rating)\n  const Lc = fc.hubLength + fc.raisedFaceHeight\n  for (const y of [-o.cc / 2, o.cc / 2]) {\n    flange(k, o.connNps, o.rating, [-o.standout, y, 0], '-x')\n    pipe(k, [-D / 2 + 0.004, y, 0], [-o.standout + Lc, y, 0], o.connNps, o.mat, '80')\n    k.torus('flangeSteel', pipeOD(o.connNps) / 2 + 0.0008, 0.0025, [-D / 2 - 0.002, y, 0], 'x', 6, 24)\n  }\n  return { yTop, yBot, D }\n}\n\n\n/**\n * Solid instrument flange (blind type) with its raised face at the origin facing -up and the body along +up,\n * blind bolt-hole marks on the back. Returns the back-face offset along up.\n */\nexport function instFlange(k: Kit, nps: number, rating: Rating, up: Axis) {\n  const f = flangeDims(nps, rating)\n  const rf = f.raisedFaceHeight\n  const t = f.thickness\n  const seg = segFor(f.od)\n  k.at([0, 0, 0], A(axisVec(up)), () => {\n    k.lathe('machined', [[0.004, 0], [f.raisedFace / 2, 0], [f.raisedFace / 2, rf]], [0, 0, 0], 'y', seg)\n    k.lathe('flangeSteel', [[f.raisedFace / 2, rf], [f.od / 2, rf], [f.od / 2, rf + t], [0.02, rf + t]], [0, 0, 0], 'y', seg)\n    k.around(f.bolts, f.boltCircle / 2, [0, rf + t + 0.0004, 0], 'y', (p) => k.cyl('pipeBore', f.boltDia * 0.56, 0.001, p, 'y', 10), Math.PI / f.bolts)\n  })\n  return rf + t\n}\n"
    }
  ],
  "categories": [
    "instruments"
  ],
  "meta": {
    "export": "MagneticFlowmeter",
    "anchor": "inline",
    "standards": [
      "ISO 13359",
      "ISO 6817",
      "ASME B16.5",
      "IEC 60079-1"
    ]
  }
}