{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "disconnector",
  "type": "registry:component",
  "title": "HV disconnector with earthing switch",
  "description": "Outdoor three-pole disconnector on four tubular columns and cross beams. Centre-break: two rotating post insulators per phase on bearings, carrying contact arms that meet in the middle (male and finger contacts) and swing 90° to the same side as `open` goes to 1. Pantograph: a support insulator and a rotating operating insulator, with scissor arms folding as it opens. Each phase has a hinged earthing blade that rises to the terminal when `earthed` and the main contacts are open. Motor operating mechanism with drive shaft and interphase coupling rods; manual earthing-switch mechanism.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "onshore/disconnector.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/onshore/disconnector.tsx",
      "content": "// Outdoor three-pole disconnector, centre-break or pantograph, with earthing switch and motor operating mechanism. IEC 62271-102, IEC 62271-1.\nimport * as THREE from 'three'\nimport type { Kit, MatKey, V3 } from '../lib/kit'\nimport { KitMesh, useKit, type GroupProps } from '../lib/mesh'\nimport { tagPlate } from '../lib/parts'\nimport { paint } from '../lib/materials'\nimport { alu, cabinet, gradingRing, hv, pad, tubeColumn } from './_hv'\nimport { clamp, insulator, porcelain, silicone } from './_onshore'\n\nexport interface DisconnectorProps {\n  /** rated voltage, kV */\n  kv?: number\n  style?: 'centre-break' | 'pantograph'\n  /** main contacts, 0 closed … 1 fully open */\n  open?: number\n  /** earthing switch closed (only with the main contacts open) */\n  earthed?: boolean\n  insulation?: 'porcelain' | 'composite'\n  tag?: string\n}\n\nexport const defaults = { kv: 145, style: 'centre-break' as 'centre-break' | 'pantograph', open: 0, earthed: false, insulation: 'porcelain' as 'porcelain' | 'composite', tag: '' }\n\nexport function Disconnector(props: DisconnectorProps & GroupProps) {\n  const { kv, style, open, earthed, insulation, tag, ...group } = { ...defaults, ...props }\n  const data = useKit((k) => build(k, { kv, style, open, earthed, insulation, tag }), [kv, style, open, earthed, insulation, tag])\n  return (\n    <group {...group}>\n      <KitMesh data={data} />\n    </group>\n  )\n}\n\nexport default Disconnector\n\nfunction build(k: Kit, p: typeof defaults) {\n  const d = hv(p.kv)\n  const ins: MatKey = p.insulation === 'composite' ? silicone() : porcelain()\n  const P = d.phase\n  const S = p.style === 'pantograph' ? 1.2 : 1.0 + 0.0085 * d.kv\n  const op = clamp(p.open, 0, 1)\n  const earthed = p.earthed && op > 0.95\n  const yb = d.sup\n  const XC = P + 0.5\n  // four columns and two cross beams carrying the phase frames\n  for (const sx of [-1, 1]) for (const sz of [-1, 1]) tubeColumn(k, sx * XC, sz * (S / 2), yb, 0.219, sz < 0)\n  for (const sz of [-1, 1]) k.box('galvanised', [2 * XC + 0.3, 0.22, 0.2], [0, yb + 0.11, sz * (S / 2)])\n  const yf = yb + 0.22\n  // motor operating mechanism on the front-left column, vertical drive shaft, interphase coupling rods\n  const mx = -XC\n  const mz = S / 2 + 0.22\n  k.rbox(cabinet(), [0.55, 0.75, 0.32], 0.02, [mx, 1.35, mz])\n  k.box(cabinet(), [0.5, 0.7, 0.01], [mx, 1.35, mz + 0.165])\n  k.box('stainless', [0.02, 0.12, 0.03], [mx + 0.2, 1.35, mz + 0.18])\n  k.cyl(paint('#2a2d30'), 0.03, 0.06, [mx - 0.15, 1.6, mz + 0.18], 'z', 12)\n  k.rod('galvanised', [mx, 1.73, mz], [mx, yf + 0.05, mz], 0.022, 10)\n  k.box('galvanised', [0.08, 0.06, 0.25], [mx, yf + 0.05, (mz + S / 2) / 2])\n  k.rod('galvanised', [mx, yf + 0.1, S / 2], [P, yf + 0.1, S / 2 + 0.15], 0.016, 8)\n  k.tube('cable', [[mx, 0.98, mz], [mx, 0.4, mz + 0.05], [mx + 0.3, 0.02, mz + 0.4]], 0.016)\n  if (p.tag) tagPlate(k, p.tag, [mx, 1.55, mz + 0.171], 'z', 0.18)\n  // earthing switch operating shaft along the rear frame with its manual mechanism\n  k.rod('galvanised', [-XC, yf + 0.06, -S / 2 - 0.2], [XC - 0.2, yf + 0.06, -S / 2 - 0.2], 0.025, 10)\n  k.rbox(cabinet(), [0.35, 0.45, 0.25], 0.02, [-XC, 1.2, -S / 2 - 0.25])\n  k.rod('galvanised', [-XC, 1.42, -S / 2 - 0.25], [-XC, yf + 0.06, -S / 2 - 0.25], 0.018, 8)\n  for (let i = -1; i <= 1; i++) {\n    const x = i * P\n    k.box('galvanised', [0.16, 0.12, S + 0.5], [x, yf + 0.06, 0])\n    if (p.style === 'centre-break') centreBreak(k, x, yf + 0.12, S, d, ins, op)\n    else pantograph(k, x, yf + 0.12, S, d, ins, op)\n    // earthing blade hinged on the -Z end, swings up to the terminal\n    const hz = -S / 2 - 0.2\n    const L = d.ins + 0.35\n    const a = earthed ? Math.PI / 2 - 0.08 : 0.04\n    const tip: V3 = [x + 0.12, yf + 0.06 + Math.sin(a) * L, hz - Math.cos(a) * L * (earthed ? -0.05 : 1)]\n    k.box('galvanised', [0.1, 0.14, 0.1], [x + 0.12, yf + 0.06, hz])\n    k.rod(alu(), [x + 0.12, yf + 0.06, hz], tip, 0.022, 10)\n    k.sphere(alu(), 0.035, tip, undefined, 10)\n    k.box(alu(), [0.06, 0.1, 0.12], [x + 0.12, yf + 0.12 + d.ins + 0.2, -S / 2 - 0.1])\n  }\n}\n\nfunction head(k: Kit, x: number, y: number, z: number) {\n  k.cyl(alu(), 0.13, 0.16, [x, y + 0.08, z], 'y', 24)\n}\n\nfunction centreBreak(k: Kit, x: number, y: number, S: number, d: ReturnType<typeof hv>, ins: MatKey, op: number) {\n  const th = (op * Math.PI) / 2\n  for (const s of [-1, 1]) {\n    const z = (s * S) / 2\n    // rotating base bearing, insulator, head with the terminal pad facing out\n    k.cyl('galvanised', 0.16, 0.08, [x, y + 0.04, z], 'y', 24)\n    k.rod('galvanised', [x, y + 0.03, z], [x + 0.25, y + 0.03, z + s * 0.05], 0.015, 6)\n    insulator(k, [x, y + 0.08, z], d.ins, d.r, ins)\n    const yt = y + 0.08 + d.ins\n    head(k, x, yt, z)\n    pad(k, [x, yt + 0.08, z + s * 0.13], [0, 0, s], 1.1)\n    // contact arm rotates about the insulator axis; both arms open to the same side\n    const len = S / 2 - 0.04\n    const dir = new THREE.Vector3(-s * Math.sin(-s * th), 0, -s * Math.cos(-s * th))\n    const a = new THREE.Vector3(x, yt + 0.12, z)\n    const b = a.clone().addScaledVector(dir, len)\n    k.rod(alu(), a.toArray() as V3, b.toArray() as V3, 0.04, 14)\n    if (s < 0) {\n      k.cyl(alu(), 0.05, 0.16, b.clone().addScaledVector(dir, -0.04).toArray() as V3, dir.toArray() as V3, 14)\n      k.sphere(alu(), 0.05, b.toArray() as V3, undefined, 12)\n    } else {\n      for (const o of [-0.04, 0.04]) k.box(alu(), [0.015, 0.1, 0.12], b.clone().add(new THREE.Vector3(o, 0, 0).applyAxisAngle(new THREE.Vector3(0, 1, 0), -s * th)).toArray() as V3, new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), -s * th))\n    }\n    if (d.ring) gradingRing(k, [x, yt - 0.1, z], d.r * 3, 'y', 0.03)\n  }\n}\n\nfunction pantograph(k: Kit, x: number, y: number, S: number, d: ReturnType<typeof hv>, ins: MatKey, op: number) {\n  // fixed support insulator carrying the pantograph, rotating operating insulator beside it\n  insulator(k, [x, y, -S * 0.15], d.ins, d.r * 1.2, ins)\n  insulator(k, [x, y, S * 0.35], d.ins, d.r * 0.8, ins)\n  const yt = y + d.ins\n  k.rbox(alu(), [0.3, 0.25, S * 0.7], 0.04, [x, yt + 0.12, S * 0.1])\n  pad(k, [x, yt + 0.12, -S * 0.25], [0, 0, -1], 1.1)\n  // scissor arms: closed reach ≈ 0.9 × insulator height above the head, folded when open\n  const reach = 0.25 + (1 - op) * d.ins * 0.9\n  const w = Math.sqrt(Math.max(0.01, (d.ins * 0.5) ** 2 - (reach / 4) ** 2))\n  const y0 = yt + 0.25\n  const pts = (side: number): V3[] => [[x, y0, 0], [x, y0 + reach / 4, side * w], [x, y0 + reach / 2, 0], [x, y0 + (3 * reach) / 4, side * w], [x, y0 + reach, 0]]\n  for (const side of [-1, 1]) {\n    const q = pts(side)\n    for (let i = 0; i < q.length - 1; i++) k.rod(alu(), q[i], q[i + 1], 0.025, 10)\n    for (const c of q) k.sphere(alu(), 0.04, c, undefined, 10)\n  }\n  k.box(alu(), [0.08, 0.06, 0.5], [x, y0 + reach + 0.03, 0])\n  for (const s of [-1, 1]) k.box(alu(), [0.06, 0.18, 0.04], [x, y0 + reach + 0.12, s * 0.22])\n  if (d.ring) gradingRing(k, [x, yt - 0.1, -S * 0.15], d.r * 3.2, 'y', 0.03)\n}\n"
    },
    {
      "path": "onshore/_hv.ts",
      "type": "registry:file",
      "target": "components/deck-twin/onshore/_hv.ts",
      "content": "// Outdoor HV switchyard parts: insulation levels per rated voltage, piers, tubular supports, terminal pads, grading rings, disc insulator strings.\nimport * as THREE from 'three'\nimport type { Axis, Kit, MatKey, V3 } from '../lib/kit'\nimport { axisVec } from '../lib/kit'\nimport { paint } from '../lib/materials'\nimport { anchorBolt, earthBond, plinth } from './_onshore'\n\nexport const KV = [72.5, 145, 245, 420] as const\nexport type Kv = (typeof KV)[number]\n\n/**\n * IEC 62271-1 / IEC 60071 insulation levels and typical outdoor dimensions per rated voltage:\n * BIL (kV), post insulator height (IEC 60273 class), phase spacing, live-part support height (IEC 61936-1 ≥ 2.25 m), insulator core radius.\n */\nexport function hv(kv: number) {\n  const table: Record<number, { bil: number; ins: number; phase: number; r: number; ring: boolean }> = {\n    72.5: { bil: 325, ins: 0.77, phase: 1.6, r: 0.07, ring: false },\n    145: { bil: 650, ins: 1.5, phase: 2.5, r: 0.085, ring: false },\n    245: { bil: 1050, ins: 2.3, phase: 3.5, r: 0.1, ring: true },\n    420: { bil: 1425, ins: 3.65, phase: 5.5, r: 0.12, ring: true },\n  }\n  const key = KV.reduce((a, b) => (Math.abs(b - kv) < Math.abs(a - kv) ? b : a))\n  return { kv: key, sup: 2.5, ...table[key] }\n}\n\nexport const alu = () => paint('#c9ccce', { metalness: 0.85, roughness: 0.35, clearcoat: 0 })\nexport const cabinet = () => paint('#b9bdb8', { metalness: 0.4, roughness: 0.45, clearcoat: 0.2 })\n\n/** Galvanised tubular support column on a concrete pier: base plate, anchors, gussets, top plate. Returns the top y. */\nexport function tubeColumn(k: Kit, x: number, z: number, h: number, od = 0.273, earth = true) {\n  const pier = 0.3\n  plinth(k, x, z, od + 0.5, od + 0.5, pier)\n  k.box('galvanised', [od + 0.2, 0.025, od + 0.2], [x, pier + 0.0125, z])\n  for (const a of [-1, 1]) for (const b of [-1, 1]) anchorBolt(k, [x + a * (od / 2 + 0.06), pier + 0.025, z + b * (od / 2 + 0.06)], 0.024)\n  for (let i = 0; i < 4; i++) {\n    const q = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), (i * Math.PI) / 2)\n    const off = new THREE.Vector3(od / 2 + 0.04, 0, 0).applyQuaternion(q)\n    k.box('galvanised', [0.08, 0.18, 0.012], [x + off.x, pier + 0.115, z + off.z], q)\n  }\n  k.cyl('galvanised', od / 2, h - pier - 0.05, [x, (h + pier) / 2, z], 'y', 24)\n  k.box('galvanised', [od + 0.1, 0.025, od + 0.1], [x, h - 0.0125, z])\n  if (earth) earthBond(k, [x, pier + 0.45, z + od / 2], [0, 0, 1], 0, 0.45)\n  return h\n}\n\n/** Flat aluminium terminal pad (NEMA 4-hole) pointing along dir from pos. */\nexport function pad(k: Kit, pos: V3, dir: V3, s = 1) {\n  const d = new THREE.Vector3(...dir).normalize()\n  const q = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(1, 0, 0), d)\n  const c = new THREE.Vector3(...pos).addScaledVector(d, 0.07 * s)\n  k.box(alu(), [0.14 * s, 0.1 * s, 0.016 * s], c.toArray() as V3, q)\n  k.rod(alu(), pos, c.clone().addScaledVector(d, -0.04 * s).toArray() as V3, 0.025 * s, 10)\n  for (const a of [-1, 1]) for (const b of [-1, 1]) {\n    const h = c.clone().add(new THREE.Vector3(a * 0.03 * s, b * 0.025 * s, 0).applyQuaternion(q))\n    k.cyl('nut', 0.009 * s, 0.03 * s, h.toArray() as V3, new THREE.Vector3(0, 0, 1).applyQuaternion(q).toArray() as V3, 6)\n  }\n}\n\n/** Corona / grading ring (aluminium tube torus) around axis at pos. */\nexport function gradingRing(k: Kit, pos: V3, R: number, axis: Axis = 'y', r = 0.025) {\n  k.torus(alu(), R, r, pos, axis, 10, 40)\n  const v = axisVec(axis)\n  const [u] = [new THREE.Vector3(1, 0, 0).cross(v).lengthSq() > 0.01 ? new THREE.Vector3(1, 0, 0) : new THREE.Vector3(0, 0, 1)]\n  for (const s of [-1, 1]) {\n    const p = new THREE.Vector3(...pos).addScaledVector(u, s * R)\n    k.rod(alu(), new THREE.Vector3(...pos).addScaledVector(u, s * R * 0.25).toArray() as V3, p.toArray() as V3, 0.012, 6)\n  }\n}\n\n/** Cap-and-pin glass disc string from a (yoke at the structure) to b (dead-end clamp), discs at 146 mm pitch. */\nexport function discString(k: Kit, a: V3, b: V3, discD = 0.255, mat: MatKey = paint('#5d7f7a', { metalness: 0.1, roughness: 0.08, clearcoat: 0.8, transparent: true, opacity: 0.85 })) {\n  const A = new THREE.Vector3(...a)\n  const B = new THREE.Vector3(...b)\n  const d = B.clone().sub(A)\n  const L = d.length()\n  d.normalize()\n  const pitch = 0.146\n  const fit = 0.35\n  const n = Math.max(2, Math.floor((L - 2 * fit) / pitch))\n  const at = (t: number) => A.clone().addScaledVector(d, t).toArray() as V3\n  k.rod('galvanised', at(0), at(fit), 0.012, 8)\n  k.rod('galvanised', at(L - fit), at(L), 0.014, 8)\n  k.box('galvanised', [0.06, 0.12, 0.012], at(fit * 0.5), new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), d))\n  const s0 = fit + (L - 2 * fit - (n - 1) * pitch) / 2\n  for (let i = 0; i < n; i++) {\n    const t = s0 + i * pitch\n    k.lathe(mat, [[0.02, 0], [discD / 2, 0.03], [discD / 2 * 0.98, 0.045], [discD * 0.18, 0.06], [0.03, 0.09]], at(t - 0.045), d.toArray() as V3, 20)\n    k.cyl('galvanised', 0.03, 0.05, at(t - 0.06), d.toArray() as V3, 10)\n  }\n  k.rod('galvanised', at(fit - 0.02), at(L - fit + 0.02), 0.008, 6)\n  return at(L)\n}\n\n/** Catenary conductor between a and b with sag at mid-span (twin optional). */\nexport function conductor(k: Kit, a: V3, b: V3, sag: number, r = 0.016, twin = 0) {\n  const A = new THREE.Vector3(...a)\n  const B = new THREE.Vector3(...b)\n  const across = new THREE.Vector3().subVectors(B, A).cross(new THREE.Vector3(0, 1, 0)).normalize()\n  for (const off of twin ? [-twin / 2, twin / 2] : [0]) {\n    const pts: THREE.Vector3[] = []\n    for (let i = 0; i <= 24; i++) {\n      const t = i / 24\n      pts.push(A.clone().lerp(B, t).addScaledVector(across, off).add(new THREE.Vector3(0, -4 * sag * t * (1 - t), 0)))\n    }\n    k.add(alu(), new THREE.TubeGeometry(new THREE.CatmullRomCurve3(pts), 48, r, 6, false))\n  }\n}\n"
    },
    {
      "path": "onshore/_onshore.ts",
      "type": "registry:file",
      "target": "components/deck-twin/onshore/_onshore.ts",
      "content": "// Shared onshore parts: plinths, anchor bolts, earth bonds, ISO 14122 stairs, shedded insulators, lattice steel and swivel joints.\nimport * as THREE from 'three'\nimport { axisVec, type Axis, type Kit, type MatKey, type V3 } from '../lib/kit'\nimport { paint } from '../lib/materials'\nimport { grating, handrail, member } from '../lib/parts'\nimport { stairFlight } from '../lib/standards'\n\nexport const add = (a: V3, b: V3, s = 1): V3 => [a[0] + b[0] * s, a[1] + b[1] * s, a[2] + b[2] * s]\nexport const deg = (d: number) => (d * Math.PI) / 180\nexport const clamp = (v: number, a: number, b: number) => Math.min(b, Math.max(a, v))\n\nexport const porcelain = () => paint('#6e3420', { metalness: 0.05, roughness: 0.12, clearcoat: 0.9, clearcoatRoughness: 0.1, weather: 0 })\nexport const silicone = () => paint('#8b9399', { metalness: 0, roughness: 0.55, clearcoat: 0.2, clearcoatRoughness: 0.5, weather: 0.3 })\nexport const earthLead = () => paint('#5c9e33', { metalness: 0, roughness: 0.6, clearcoat: 0, weather: 0 })\nexport const lightGrey = () => paint('#cfd2cf', { metalness: 0.2, roughness: 0.5, clearcoat: 0.25, weather: 1 })\n\n/** Concrete plinth from y0 up to y0 + h with a small arris, centred on (cx, cz). */\nexport function plinth(k: Kit, cx: number, cz: number, sx: number, sz: number, h: number, y0 = 0) {\n  const c = Math.min(0.025, h * 0.2)\n  k.box('concrete', [sx, h - c, sz], [cx, y0 + (h - c) / 2, cz])\n  k.box('concrete', [sx - 2 * c, c, sz - 2 * c], [cx, y0 + h - c / 2, cz])\n}\n\n/** Cast-in anchor bolt standing on a surface at p: washer, double nut, projecting thread. */\nexport function anchorBolt(k: Kit, p: V3, dia = 0.024) {\n  k.cyl('galvanised', dia * 1.1, 0.005, [p[0], p[1] + 0.0025, p[2]], 'y', 12)\n  k.hex('nut', dia * 1.85, dia * 0.9, [p[0], p[1] + 0.005 + dia * 0.45, p[2]])\n  k.hex('nut', dia * 1.85, dia * 0.9, [p[0], p[1] + 0.005 + dia * 1.4, p[2]])\n  k.cyl('boltXylan', dia / 2, dia * 2.6, [p[0], p[1] + dia * 1.3, p[2]], 'y', 8)\n}\n\n/** Steel base plate with its top at y, on a grout bed, anchored near each corner. */\nexport function basePlate(k: Kit, cx: number, y: number, cz: number, sx: number, sz: number, t = 0.025, dia = 0.024, mat: MatKey = 'galvanised') {\n  k.box(mat, [sx, t, sz], [cx, y - t / 2, cz])\n  const ix = sx / 2 - dia * 2.2\n  const iz = sz / 2 - dia * 2.2\n  for (const a of [-1, 1]) for (const b of [-1, 1]) anchorBolt(k, [cx + a * ix, y, cz + b * iz], dia)\n}\n\n/** Earth boss with a bolted lug and a green/yellow bonding lead down to an inspection pit at grade. */\nexport function earthBond(k: Kit, pos: V3, out: V3, ground = 0, pitOffset = 0.3) {\n  const o = new THREE.Vector3(...out).normalize().toArray() as V3\n  k.cyl('brass', 0.017, 0.03, add(pos, o, 0.015), o, 10)\n  k.box('copper', [0.03, 0.05, 0.004], add(add(pos, o, 0.032), [0, -0.012, 0]), o)\n  k.hex('nut', 0.022, 0.012, add(pos, o, 0.04), o)\n  const lug = add(add(pos, o, 0.035), [0, -0.035, 0])\n  const pit: V3 = [pos[0] + o[0] * pitOffset, ground, pos[2] + o[2] * pitOffset]\n  if (pos[1] - ground > 0.12) {\n    k.spline(earthLead(), [lug, add(lug, [0, -0.08, 0]), add([pit[0], ground + 0.25, pit[2]], o, -pitOffset * 0.3), [pit[0], ground + 0.1, pit[2]], [pit[0], ground + 0.03, pit[2]]], 0.0065, 6, 24)\n    k.cyl('concrete', 0.14, 0.05, [pit[0], ground + 0.025, pit[2]], 'y', 16)\n    k.cyl('castIron', 0.1, 0.008, [pit[0], ground + 0.054, pit[2]], 'y', 16)\n  }\n}\n\n/** ISO 14122-3 straight stair: first riser at `foot`, climbing along `dir` by `rise`. Channel stringers, grating treads, handrails. Returns the horizontal run. */\nexport function stair(k: Kit, foot: V3, dir: Axis, rise: number, width = 0.9, rails: 'both' | 'left' | 'right' | 'none' = 'both') {\n  const f = stairFlight(rise)\n  const n = f.steps\n  const g = f.going\n  const q = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(1, 0, 0), axisVec(dir).setY(0).normalize())\n  const w = width / 2\n  const slope = f.riser / g\n  k.with(new THREE.Matrix4().compose(new THREE.Vector3(...foot), q, new THREE.Vector3(1, 1, 1)), () => {\n    const sd = 0.18\n    const yAt = (x: number) => f.riser + x * slope - 0.07\n    const x0 = (sd * 0.45 + 0.07 - f.riser) / slope\n    const run = (n - 1) * g\n    for (const s of [-1, 1]) {\n      const z = s * (w + 0.04)\n      member(k, [x0, yAt(x0), z], [run, yAt(run), z], 'UPN 180', 'galvanised', [-slope, 1, 0])\n      k.box('galvanised', [0.22, 0.015, 0.16], [x0 + 0.06, 0.0075, z])\n    }\n    for (let i = 1; i < n; i++) {\n      const y = i * f.riser\n      grating(k, [(i - 1) * g + g / 2 + 0.0125, y, 0], g + 0.025, width)\n      k.box('safetyYellow', [0.03, 0.032, width], [(i - 1) * g + 0.015, y - 0.016, 0])\n    }\n    const pts = (z: number): V3[] => [[0, f.riser * 0.5, z], [run, rise, z]]\n    if (rails === 'both' || rails === 'left') handrail(k, pts(w + 0.04), 'safetyYellow')\n    if (rails === 'both' || rails === 'right') handrail(k, pts(-w - 0.04), 'safetyYellow')\n  })\n  return (n - 1) * g\n}\n\n/** Shedded post insulator from `base` along `axis` for L: galvanised end caps, alternating sheds of radius ~2.2 r. */\nexport function insulator(k: Kit, base: V3, L: number, r: number, mat: MatKey, axis: Axis = 'y') {\n  const v = axisVec(axis).toArray() as V3\n  const cap = Math.min(0.08, L * 0.08)\n  const prof: [number, number][] = [[0, cap], [r, cap]]\n  const body = L - 2 * cap\n  const n = Math.max(3, Math.round(body / Math.max(0.045, r * 0.75)))\n  const pitch = body / n\n  let y = cap\n  for (let i = 0; i < n; i++) {\n    const big = i % 2 === 0 ? 2.3 : 1.85\n    prof.push([r * 1.05, y + pitch * 0.15], [r * big, y + pitch * 0.42], [r * (big - 0.1), y + pitch * 0.55], [r, y + pitch * 0.62])\n    y += pitch\n    prof.push([r, y])\n  }\n  prof.push([0, L - cap])\n  k.lathe(mat, prof, base, v, 20)\n  for (const t of [cap / 2, L - cap / 2]) k.cyl('galvanised', r * 1.25, cap, add(base, v, t), v, 18)\n  for (const t of [0.006, L - 0.006]) k.cyl('galvanised', r * 1.9, 0.012, add(base, v, t), v, 18)\n}\n\n/** Square lattice column of four angle legs with zig-zag bracing on every face, (cx, cz) centre, from y0 to y1. */\nexport function latticeColumn(k: Kit, cx: number, cz: number, y0: number, y1: number, w: number, leg = 'L100x100x10', brace = 'L50x50x5', mat: MatKey = 'galvanised') {\n  const h = w / 2\n  const c: [number, number][] = [[-h, -h], [h, -h], [h, h], [-h, h]]\n  for (const [x, z] of c) member(k, [cx + x, y0, cz + z], [cx + x, y1, cz + z], leg, mat, [-x, 0, -z])\n  const H = y1 - y0\n  const nb = Math.max(1, Math.round(H / w))\n  const dy = H / nb\n  for (let f = 0; f < 4; f++) {\n    const [ax, az] = c[f]\n    const [bx, bz] = c[(f + 1) % 4]\n    for (let i = 0; i < nb; i++) {\n      const ya = y0 + i * dy\n      const flip = (i + f) % 2 === 0\n      const p0: V3 = [cx + (flip ? ax : bx), ya, cz + (flip ? az : bz)]\n      const p1: V3 = [cx + (flip ? bx : ax), ya + dy, cz + (flip ? bz : az)]\n      member(k, p0, p1, brace, mat, [ax + bx, 0, az + bz])\n    }\n    member(k, [cx + ax, y1 - 0.03, cz + az], [cx + bx, y1 - 0.03, cz + bz], brace, mat)\n  }\n}\n\n/** Swivel joint between a and b: two ball-race housings, seal ring, grease nipples and plug bolts. od = pipe OD. */\nexport function swivel(k: Kit, a: V3, b: V3, od: number, mat: MatKey = 'flangeSteel') {\n  const A = new THREE.Vector3(...a)\n  const d = new THREE.Vector3(...b).sub(A)\n  const L = d.length()\n  d.normalize()\n  const v = d.toArray() as V3\n  const at = (t: number) => A.clone().addScaledVector(d, t).toArray() as V3\n  const R = od * 0.82\n  const ri = od * 0.44\n  ring(k, mat, ri, od * 0.56, L, at(L / 2), v)\n  k.cyl('pipeBore', ri, L, at(L / 2), v, 32, undefined, true)\n  ring(k, mat, od * 0.55, R, L * 0.36, at(L * 0.3), v)\n  ring(k, 'machined', od * 0.55, R * 0.96, L * 0.04, at(L * 0.5), v)\n  ring(k, mat, od * 0.55, R * 0.92, L * 0.3, at(L * 0.69), v)\n  for (const t of [0.12, 0.88]) ring(k, mat, od * 0.55, od * 0.7, L * 0.06, at(L * t), v)\n  const [u, w] = perpOf(d)\n  for (let i = 0; i < 6; i++) {\n    const ang = (i / 6) * Math.PI * 2\n    const r = new THREE.Vector3().addScaledVector(u, Math.cos(ang) * R).addScaledVector(w, Math.sin(ang) * R)\n    const p = new THREE.Vector3(...at(L * 0.3)).add(r)\n    k.hex('nut', Math.max(0.018, od * 0.06), 0.012, p.toArray() as V3, r.clone().normalize().toArray() as V3)\n  }\n  const gp = new THREE.Vector3(...at(L * 0.69)).addScaledVector(u, R * 0.92)\n  k.cyl('brass', Math.max(0.006, od * 0.02), 0.03, gp.clone().addScaledVector(u, 0.012).toArray() as V3, u.toArray() as V3, 8)\n}\n\nfunction perpOf(d: THREE.Vector3): [THREE.Vector3, THREE.Vector3] {\n  const ref = Math.abs(d.y) > 0.9 ? new THREE.Vector3(1, 0, 0) : new THREE.Vector3(0, 1, 0)\n  const u = new THREE.Vector3().crossVectors(d, ref).normalize()\n  return [u, new THREE.Vector3().crossVectors(d, u).normalize()]\n}\n\n/** Annular ring (hollow short cylinder) centred on pos along axis, open through the middle. */\nexport function ring(k: Kit, mat: MatKey, ri: number, ro: number, h: number, pos: V3, axis: Axis = 'y', seg = 36) {\n  const v = axisVec(axis)\n  const base = new THREE.Vector3(...pos).addScaledVector(v, -h / 2).toArray() as V3\n  k.lathe(mat, [[ro, 0], [ro, h], [ri, h], [ri, 0], [ro, 0]], base, v.toArray() as V3, seg)\n}\n\n/** Hydraulic cylinder from a (barrel end clevis) to b (rod eye). */\nexport function hydraulicCylinder(k: Kit, a: V3, b: V3, bore: number, mat: MatKey = 'actuatorBlue') {\n  const A = new THREE.Vector3(...a)\n  const B = new THREE.Vector3(...b)\n  const L = A.distanceTo(B)\n  if (L < 1e-3) return\n  const d = B.clone().sub(A).normalize()\n  const bl = Math.min(L * 0.62, L - 0.1)\n  const p = (t: number) => A.clone().addScaledVector(d, t).toArray() as V3\n  k.rod(mat, p(0.04), p(bl), bore / 2, 16)\n  k.rod(mat, p(0.04), p(0.1), bore * 0.62, 16)\n  k.rod(mat, p(bl - 0.06), p(bl), bore * 0.62, 16)\n  k.rod('stainless', p(bl), p(L - 0.04), bore * 0.28, 12)\n  const side = Math.abs(d.x) > 0.9 ? 'z' : 'x'\n  for (const t of [0.02, L - 0.02]) k.cyl('flangeSteel', bore * 0.45, bore * 0.6, p(t), side, 12)\n  k.rod('tubing', add(p(0.12), [0, bore * 0.6, 0]), add(p(bl - 0.08), [0, bore * 0.6, 0]), 0.006, 6)\n}\n\n/**\n * Trapezoidal profiled sheet (wall or roof cladding): profile runs along `u` for `length`, ribs stand out along `n`,\n * extruded `height` along `w`; `origin` is the (0, 0, 0) corner. Typical 35 mm rib at 200 mm pitch.\n */\nexport function profiledSheet(k: Kit, mat: MatKey, length: number, height: number, origin: V3, u: V3, n: V3, w: V3, pitch = 0.2, depth = 0.035) {\n  const top = pitch * 0.2\n  const slope = pitch * 0.15\n  const pts: [number, number][] = [[0, 0]]\n  const ribs = Math.max(1, Math.round(length / pitch))\n  const sc = length / (ribs * pitch)\n  for (let i = 0; i < ribs; i++) {\n    const a = i * pitch * sc\n    const m = a + (pitch * sc) / 2\n    pts.push([m - (top / 2 + slope) * sc, 0], [m - (top / 2) * sc, depth], [m + (top / 2) * sc, depth], [m + (top / 2 + slope) * sc, 0])\n  }\n  pts.push([length, 0])\n  const sh = new THREE.Shape()\n  pts.forEach(([x, y], i) => (i ? sh.lineTo(x, y) : sh.moveTo(x, y)))\n  for (let i = pts.length - 1; i >= 0; i--) sh.lineTo(pts[i][0], pts[i][1] - 0.0015)\n  sh.closePath()\n  const m = new THREE.Matrix4().makeBasis(new THREE.Vector3(...u), new THREE.Vector3(...n), new THREE.Vector3(...w)).setPosition(...origin)\n  k.extrude(mat, sh, height, m, 1)\n}\n"
    }
  ],
  "categories": [
    "onshore"
  ],
  "meta": {
    "export": "Disconnector",
    "anchor": "base",
    "standards": [
      "IEC 62271-102",
      "IEC 62271-1",
      "IEC 60273",
      "IEC 61936-1"
    ]
  }
}