{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "level-switch",
  "type": "registry:component",
  "title": "Vibrating fork level switch",
  "description": "Vibrating fork point level switch for side mounting: flanged process connection (port process, facing -X, origin on the face), extension tube and two flat tines set side by side so the gap drains vertically, a finned thermal neck and a compact housing with status LEDs behind a window and two cable entries underneath. Fork length from the flange face is a prop.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "instruments/level-switch.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/instruments/level-switch.tsx",
      "content": "// Vibrating fork level switch: flanged process connection, tuning fork with vertical drain gap, thermal neck and a compact housing with status LEDs and two cable entries. IEC 62828, ASME B16.5.\nimport { Kit, type MatKey } from '../lib/kit'\nimport { KitMesh, useKit, type GroupProps } from '../lib/mesh'\nimport { port, type Port } from '../lib/ports'\nimport type { Rating } from '../lib/standards'\nimport { SS, instFlange, tag as tagPlate } from './_parts'\n\nexport interface LevelSwitchProps {\n  /** process flange size, inches */\n  nps?: number\n  rating?: Rating\n  /** fork length from the flange face to the tip, m */\n  length?: number\n  housing?: MatKey\n  tag?: string\n}\n\nexport const defaults = { nps: 2, rating: 300 as Rating, length: 0.102, housing: 'aluminium' as MatKey, tag: 'LSHH-1101' }\n\nexport function ports(p: LevelSwitchProps = {}): Port[] {\n  const { nps, rating } = { ...defaults, ...p }\n  return [port('process', 'Process flange', [0, 0, 0], [-1, 0, 0], nps, rating)]\n}\n\nexport function LevelSwitch({ nps = defaults.nps, rating = defaults.rating, length = defaults.length, housing = defaults.housing, tag = defaults.tag, ...group }: LevelSwitchProps & GroupProps) {\n  const data = useKit((k) => build(k, nps, rating, Math.max(0.06, length), housing, tag), [nps, rating, length, housing, tag])\n  return (\n    <group {...group}>\n      <KitMesh data={data} />\n    </group>\n  )\n}\n\nexport default LevelSwitch\n\nfunction build(k: Kit, nps: number, rating: Rating, length: number, housing: MatKey, tag: string) {\n  // process side (-X): extension tube, fork root, two flat tines side by side so the gap is vertical\n  const xr = -length + 0.044\n  k.cyl(SS, 0.0125, -xr, [xr / 2, 0, 0], 'x', 20)\n  k.cyl(SS, 0.0135, 0.008, [xr + 0.004, 0, 0], 'x', 20)\n  for (const z of [-0.0065, 0.0065]) {\n    k.box(SS, [0.012, 0.012, 0.004], [xr - 0.006, 0, z])\n    k.rbox(SS, [0.032, 0.022, 0.004], 0.0018, [xr - 0.012 - 0.016, 0, z])\n  }\n  // flange, hex, thermal neck, housing with LED cover facing +X\n  let x = instFlange(k, nps, rating, 'x')\n  k.hex(SS, 0.05, 0.018, [x + 0.009, 0, 0], 'x')\n  x += 0.018\n  k.cyl(SS, 0.016, 0.05, [x + 0.025, 0, 0], 'x', 20)\n  for (let i = 0; i < 4; i++) k.cyl(SS, 0.02, 0.003, [x + 0.008 + i * 0.011, 0, 0], 'x', 20)\n  x += 0.05\n  k.cyl(housing, 0.046, 0.075, [x + 0.0375, 0, 0], 'x', 32)\n  k.cyl(housing, 0.049, 0.022, [x + 0.075 + 0.011, 0, 0], 'x', 32)\n  k.torus(housing, 0.049, 0.0025, [x + 0.075 + 0.003, 0, 0], 'x', 6, 32)\n  const xc = x + 0.097\n  k.disc('glass', 0.03, [xc + 0.0005, 0, 0], 'x', 28)\n  for (const [y, z] of [[0.012, -0.012], [0.012, 0], [0.012, 0.012]] as const) k.disc('lampLens', 0.0035, [xc + 0.001, y, z], 'x', 12)\n  // two cable entries underneath: gland and stopping plug\n  for (const [z, gland] of [[-0.022, true], [0.022, false]] as const) {\n    k.cyl(housing, 0.012, 0.012, [x + 0.04, -0.046, z], 'y', 12)\n    if (gland) {\n      k.hex('brass', 0.026, 0.018, [x + 0.04, -0.06, z], 'y')\n      k.cyl('rubberBlack', 0.008, 0.012, [x + 0.04, -0.074, z], 'y', 10)\n    } else k.hex(SS, 0.024, 0.01, [x + 0.04, -0.057, z], 'y')\n  }\n  k.box('nameplate', [0.05, 0.002, 0.03], [x + 0.04, 0.0465, 0])\n  tagPlate(k, tag, [x + 0.0375, 0, 0.0468], 'z', 0.05)\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": "LevelSwitch",
    "anchor": "fitting",
    "standards": [
      "IEC 62828",
      "IEC 61508 (SIL)",
      "ASME B16.5"
    ]
  }
}