{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "orifice-flowmeter",
  "type": "registry:component",
  "title": "Orifice flowmeter",
  "description": "Orifice meter run: ASME B16.36 weld-neck orifice flange pair (class 300 minimum, 1.5 in minimum thickness) with paddle plate with a bored orifice (beta prop) and a stamped tab, spiral-wound gaskets, studs and jack screws. Flange taps 1 in from each face (the opposite pair plugged) feed root valves and close-coupled tubing to a 5-valve manifold and DP transmitter. Upstream and downstream straight lengths are set in pipe diameters.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "instruments/orifice-flowmeter.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/instruments/orifice-flowmeter.tsx",
      "content": "// Orifice flowmeter: ASME B16.36 orifice flange pair with paddle plate, jack screws and flange taps, root valves, close-coupled 5-valve manifold and DP transmitter on a short meter run. ISO 5167-2, ASME B16.36 / B16.5.\nimport * as THREE from 'three'\nimport { Kit, type MatKey, type V3 } from '../lib/kit'\nimport { textMaterial } from '../lib/textures'\nimport { KitMesh, useKit, type GroupProps } from '../lib/mesh'\nimport { weldBead } from '../lib/parts'\nimport { port, type Port } from '../lib/ports'\nimport { GASKET_SPIRAL_WOUND, flange as flangeDims, inch, pipeOD, pipeWall, type Rating } from '../lib/standards'\nimport { SS, cable, coplanarSensor, manifold, tag as tagPlate, tubeFitting, tubing, valveHandle, flange, pipe, segFor } from './_parts'\n\nexport interface OrificeFlowmeterProps {\n  /** nominal pipe size, inches */\n  nps?: number\n  rating?: Rating\n  /** orifice bore / pipe bore (β) */\n  beta?: number\n  /** straight length from the plate to the inlet flange face, pipe diameters */\n  upstream?: number\n  /** straight length from the plate to the outlet flange face, pipe diameters */\n  downstream?: number\n  housing?: MatKey\n  tag?: string\n}\n\nexport const defaults = { nps: 6, rating: 600 as Rating, beta: 0.6, upstream: 5, downstream: 3, housing: 'actuatorBlue' as MatKey, tag: 'FT-1001' }\n\ntype P = typeof defaults\n\nfunction dims(p: P) {\n  const D = pipeOD(p.nps)\n  const ofRating = Math.max(p.rating, 300) as Rating\n  const f = flangeDims(p.nps, ofRating)\n  const t = Math.max(f.thickness, inch(1.5)) // B16.36 minimum orifice flange thickness\n  const hub = Math.max(f.hubLength, t + inch(1))\n  const tp = p.nps <= 8 ? 0.0032 : p.nps <= 14 ? 0.0064 : 0.0095 // typical plate thickness\n  const half = tp / 2 + GASKET_SPIRAL_WOUND\n  const fe = flangeDims(p.nps, p.rating)\n  const endL = fe.hubLength + fe.raisedFaceHeight\n  const weldOF = half + f.raisedFaceHeight + hub\n  const minRun = weldOF + endL + 0.08\n  const xA = -Math.max(p.upstream * D, minRun)\n  const xB = Math.max(p.downstream * D, minRun)\n  return { D, f, t, hub, tp, half, endL, weldOF, xA, xB }\n}\n\nexport function ports(props: OrificeFlowmeterProps = {}): Port[] {\n  const p = { ...defaults, ...props }\n  const { xA, xB } = dims(p)\n  return [port('A', 'Inlet (upstream)', [xA, 0, 0], [-1, 0, 0], p.nps, p.rating), port('B', 'Outlet (downstream)', [xB, 0, 0], [1, 0, 0], p.nps, p.rating)]\n}\n\nexport function OrificeFlowmeter(props: OrificeFlowmeterProps & GroupProps) {\n  const { nps, rating, beta, upstream, downstream, housing, tag, ...group } = { ...defaults, ...props }\n  const p = { nps, rating, beta, upstream, downstream, 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 OrificeFlowmeter\n\n/** Weld-neck orifice flange, raised face at x = face pointing along s. */\nfunction orificeFlange(k: Kit, nps: number, f: ReturnType<typeof flangeDims>, t: number, hub: number, face: number, s: 1 | -1) {\n  const ro = pipeOD(nps) / 2\n  const ri = ro - pipeWall(nps, '80')\n  const rf = f.raisedFaceHeight\n  const hubR = Math.max(f.hubDia / 2, ro * 1.05)\n  const seg = segFor(f.od)\n  k.at([face, 0, 0], s > 0 ? 'x' : '-x', () => {\n    k.lathe('flangeSteel', [[ro * 0.999, -hub - rf], [hubR, -rf - t - 0.006], [hubR + 0.004, -rf - t], [f.od / 2, -rf - t], [f.od / 2, -rf], [f.raisedFace / 2, -rf]], [0, 0, 0], 'y', seg)\n    k.lathe('machined', [[f.raisedFace / 2, -rf], [f.raisedFace / 2, 0], [ri, 0]], [0, 0, 0], 'y', seg)\n    k.lathe('pipeBore', [[ri, 0], [ri, -hub - rf]], [0, 0, 0], 'y', segFor(2 * ri))\n  })\n}\n\nfunction build(k: Kit, p: P) {\n  const { D, f, t, hub, tp, half, endL, weldOF, xA, xB } = dims(p)\n  const rf = f.raisedFaceHeight\n  const g = GASKET_SPIRAL_WOUND\n\n  // meter run: end flanges, spools, orifice flanges\n  flange(k, p.nps, p.rating, [xA, 0, 0], '-x')\n  flange(k, p.nps, p.rating, [xB, 0, 0], 'x')\n  pipe(k, [xA + endL, 0, 0], [-weldOF, 0, 0], p.nps, 'pipePaint', '80')\n  pipe(k, [weldOF, 0, 0], [xB - endL, 0, 0], p.nps, 'pipePaint', '80')\n  for (const x of [xA + endL, -weldOF, weldOF, xB - endL]) weldBead(k, p.nps, [x, 0, 0], 'x')\n  orificeFlange(k, p.nps, f, t, hub, -half, 1)\n  orificeFlange(k, p.nps, f, t, hub, half, -1)\n\n  // paddle plate, gaskets, stamped tab towards +Z\n  const plateR = f.boltCircle / 2 - f.boltDia * 0.75\n  const ri = D / 2 - pipeWall(p.nps, '80')\n  const rb = Math.min(0.75, Math.max(0.2, p.beta)) * ri\n  k.lathe(SS, [[rb, -tp / 2], [plateR, -tp / 2], [plateR, tp / 2], [rb, tp / 2], [rb, -tp / 2]], [0, 0, 0], 'x', segFor(2 * plateR))\n  const rg = f.raisedFace / 2 + 0.006\n  for (const s of [-1, 1]) k.lathe('gasket', [[ri, -g / 2], [rg, -g / 2], [rg, g / 2], [ri, g / 2], [ri, -g / 2]], [s * (tp / 2 + g / 2), 0, 0], 'x', segFor(2 * rg))\n  const tabEnd = f.od / 2 + 0.065\n  k.box(SS, [tp, 0.038, tabEnd - plateR + 0.01], [0, 0, (tabEnd + plateR - 0.01) / 2])\n  // tab stamping: size, class, bore ratio (read from upstream side)\n  const stamp = textMaterial(`${p.nps}\" ${p.rating}# B${Math.min(0.75, Math.max(0.2, p.beta)).toFixed(3)}`, '#d4d8dc', '#202326', 512, 96)\n  for (const s of [-1, 1]) k.plane(stamp, 0.056, 0.026, [s * (tp / 2 + 0.0004), 0, (f.od / 2 + tabEnd) / 2], new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 0, 1), new THREE.Vector3(s, 0, 0)))\n\n  // studs through both flanges; bolt holes sit between the mid-angles a = i·2π/n (a = 0 is +Z, a = 3π/2 is +Y)\n  const studL = 2 * (t + rf) + 2 * g + tp + f.boltDia * 2.6\n  k.boltCircle('boltXylan', 'nut', f.bolts, f.boltCircle / 2, [-studL / 2, 0, 0], 'x', f.boltDia, studL, [f.boltDia * 0.8, studL - f.boltDia * 0.8])\n  const aj = ((2 * Math.PI) / f.bolts) * Math.round(f.bolts / 8)\n  for (const a of [aj, aj + Math.PI]) {\n    const y = -Math.sin(a) * (f.boltCircle / 2)\n    const z = Math.cos(a) * (f.boltCircle / 2)\n    const x0 = -(half + rf + t) - 0.012\n    k.rod('boltXylan', [x0, y, z], [half + rf, y, z], f.boltDia * 0.42, 8)\n    k.hex('nut', f.boltDia * 1.7, f.boltDia * 0.8, [x0, y, z], 'x')\n  }\n\n  // flange taps 1\" from each face, root valves, tubing to a close-coupled 5-valve manifold\n  const yTap = f.od / 2\n  const yR = yTap + 0.041 + 0.022\n  const xt = half + 0.0254\n  for (const s of [-1, 1]) {\n    const x = s * xt\n    k.cyl(SS, 0.0107, 0.048, [x, yTap + 0.02, 0], 'y', 12)\n    k.box(SS, [0.034, 0.044, 0.034], [x, yR, 0])\n    valveHandle(k, [x, yR, -s * 0.017], [0, 0, -s])\n  }\n  // second tap on each flange, 180° opposite, plugged\n  for (const s of [-1, 1]) {\n    k.cyl(SS, 0.012, 0.012, [s * xt, -yTap - 0.004, 0], 'y', 12)\n    k.hex(SS, 0.024, 0.012, [s * xt, -yTap - 0.016, 0])\n  }\n  const yM = yR + 0.022 + 0.026 + 0.11\n  const m = manifold(k, [0, yM, 0], 5)\n  m.inlets.forEach((inl, i) => {\n    const x = (i === 0 ? -1 : 1) * xt\n    const a = tubeFitting(k, [x, yR + 0.022, 0], [0, 1, 0])\n    const b = tubeFitting(k, inl, [0, -1, 0])\n    tubing(k, [a, [x, a[1] + 0.025, 0], [inl[0], b[1] - 0.025, 0], b])\n  })\n  const head = coplanarSensor(k, [0, m.top, 0], p.housing, true)\n  tagPlate(k, p.tag, [xt, yR - 0.004, 0.0178], 'z', 0.06)\n\n  const gl: V3 = [head[0] + 0.106, head[1], head[2] + 0.02]\n  const xs = weldOF + 0.04\n  const xe = Math.max(xs + 0.06, xB - endL - 0.04)\n  cable(k, [gl, [gl[0] + 0.05, gl[1] - 0.06, 0.03], [xs + 0.03, D / 2 + 0.12, 0.02], [xs + 0.02, D / 2 + 0.008, 0], [xe, D / 2 + 0.008, 0]])\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": "OrificeFlowmeter",
    "anchor": "inline",
    "standards": [
      "ISO 5167-2",
      "ASME B16.36",
      "ASME B16.5",
      "AGA 3 / API MPMS 14.3"
    ]
  }
}