{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "slip-on-flange",
  "type": "registry:component",
  "title": "Slip-on flange",
  "description": "ASME B16.5 slip-on flange on a pipe stub: short tapered hub, bore B a little over the pipe OD, drilled bolt holes, RF / FF / RTJ facing. The pipe end is set back from the face by the inside fillet weld leg (the smaller of the wall and 6 mm) and an outside fillet weld closes the hub end, as ASME B31.3 Fig. 328.5.2B shows. Hub length Y comes from the B16.5 slip-on table for each class. Where B16.5 has no slip-on flange (Class 1500 above NPS 2-1/2, Class 2500), the next lower class that has one is used.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "piping/slip-on-flange.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/piping/slip-on-flange.tsx",
      "content": "// Slip-on flange: ASME B16.5 SO flange slid over the pipe with the pipe end set back, inside and outside fillet welds per ASME B31.3.\nimport type { Kit } from '../lib/kit'\nimport { KitMesh, useKit, type GroupProps } from '../lib/mesh'\nimport { port, type Port } from '../lib/ports'\nimport { pipeOD, pipeWall, type Rating, type Schedule } from '../lib/standards'\nimport { arc, definedRating, facingHeight, filletWeld, looseFlange, marking, pipeStub, seg, type Facing } from './_flanges'\n\nexport interface SlipOnFlangeProps {\n  /** nominal pipe size, inches (B16.5 slip-on: 1/2–24) */\n  nps?: number\n  rating?: Rating\n  facing?: Facing\n  schedule?: Schedule\n  /** pipe stub behind the hub, m */\n  stub?: number\n  /** stamped marking, empty for the standard marking */\n  tag?: string\n}\n\nexport const defaults = { nps: 4, rating: 150 as Rating, facing: 'RF' as Facing, schedule: '40' as Schedule, stub: 0.2, tag: '' }\n\n// Port A is the gasket face at the origin, pointing -X; hub and pipe run along +X.\nexport function ports(p: SlipOnFlangeProps = {}): Port[] {\n  const { nps, rating } = { ...defaults, ...p }\n  const n = Math.min(nps, 24)\n  return [port('A', 'Flange face', [0, 0, 0], [-1, 0, 0], n, definedRating('SO', n, rating))]\n}\n\nexport function SlipOnFlange({ nps = defaults.nps, rating = defaults.rating, facing = defaults.facing, schedule = defaults.schedule, stub = defaults.stub, tag = defaults.tag, ...group }: SlipOnFlangeProps & GroupProps) {\n  const data = useKit((k) => build(k, Math.min(nps, 24), definedRating('SO', Math.min(nps, 24), rating), facing, schedule, Math.max(0, stub), tag), [nps, rating, facing, schedule, stub, tag])\n  return (\n    <group {...group}>\n      <KitMesh data={data} />\n    </group>\n  )\n}\n\nexport default SlipOnFlange\n\nfunction build(k: Kit, nps: number, rating: Rating, facing: Facing, schedule: Schedule, stub: number, tag: string) {\n  const h = facingHeight(facing, nps, rating)\n  const base: [number, number, number] = [h, 0, 0]\n  const dir: [number, number, number] = [-1, 0, 0]\n  const i = looseFlange(k, base, dir, { kind: 'SO', nps, rating, facing, schedule, marking: tag || marking('SO', nps, rating, facing) })\n  const ro = pipeOD(nps) / 2\n  const wt = pipeWall(nps, schedule)\n  const ri = ro - wt\n  // B31.3: inside fillet no larger than the wall or 6 mm; pipe end set back so the weld stays clear of the face\n  const leg = Math.min(wt, 0.006)\n  const yEnd = -leg - 0.0015\n  const L = i.Y + yEnd + stub\n  pipeStub(k, [h - yEnd, 0, 0], [1, 0, 0], L, nps, schedule, 'open', stub > 0 ? 'bevel' : 'square')\n  const n = seg(ro * 2)\n  k.lathe('flangeSteel', [[i.B, yEnd + leg], ...arc(i.B, yEnd, leg, Math.PI / 2, Math.PI, 4).slice(1, -1).map(([r, y]) => [r + leg * 0.12, y + leg * 0.12] as [number, number]), [i.B - leg, yEnd]], base, dir, n)\n  k.lathe('machined', [[i.B - leg, yEnd], [ri, yEnd]], base, dir, n)\n  // outside fillet at the hub end: 1.4 x wall, limited by the hub\n  filletWeld(k, base, dir, ro, -i.Y, Math.min(wt * 1.4, 0.014), n)\n}\n"
    },
    {
      "path": "piping/_flanges.ts",
      "type": "registry:file",
      "target": "components/deck-twin/piping/_flanges.ts",
      "content": "// Flange-type geometry: ASME B16.5 weld-neck, slip-on, socket-weld, threaded and lap-joint bodies, drilled plates, RF / FF / RTJ facings, NPT threads, fillet welds and pipe stubs.\nimport * as THREE from 'three'\nimport { latheCrisp, mat4, perp, uvScale, type Kit, type MatKey, type Profile, type V3 } from '../lib/kit'\nimport { textMaterial } from '../lib/textures'\nimport { GASKET_SPIRAL_WOUND, flange as flangeDims, inch, mm, nearest, pipeOD, pipeWall, stubEnd, studLength, tableKeys, type Rating, type Schedule } from '../lib/standards'\n\nexport type Facing = 'RF' | 'FF' | 'RTJ'\nexport type FlangeKind = 'WN' | 'SO' | 'SW' | 'THD' | 'LJ'\nexport const GAP = GASKET_SPIRAL_WOUND\nexport const LAND = 0.0016\nexport const BEVEL_TAN = Math.tan((37.5 * Math.PI) / 180)\nconst BEVEL = BEVEL_TAN\n\nexport const add = (a: V3, d: V3, s = 1): V3 => [a[0] + d[0] * s, a[1] + d[1] * s, a[2] + d[2] * s]\nexport const seg = (d: number) => (d > 0.5 ? 64 : d > 0.25 ? 48 : d > 0.1 ? 40 : 32)\nconst look = <T>(t: Record<number, T>, nps: number): T => t[nearest(tableKeys(t), nps)]\nconst has = (t: Record<number, unknown> | undefined, nps: number) => !!t && t[nearest(tableKeys(t), nps)] !== undefined && Math.abs(nearest(tableKeys(t), nps) - nps) < 0.01\n\n// ------------------------------------------------------------------ B16.5 tables (mm)\n\n// Slip-on / threaded / socket-weld length through hub Y, raised face excluded.\nconst SO_Y: Partial<Record<Rating, Record<number, number>>> = {\n  150: { 0.5: 14, 0.75: 14, 1: 16, 1.25: 19, 1.5: 21, 2: 24, 2.5: 27, 3: 29, 3.5: 30, 4: 32, 5: 35, 6: 38, 8: 43, 10: 48, 12: 54, 14: 56, 16: 62, 18: 67, 20: 71, 24: 81 },\n  300: { 0.5: 21, 0.75: 24, 1: 25, 1.25: 25, 1.5: 29, 2: 32, 2.5: 37, 3: 41, 3.5: 43, 4: 46, 5: 49, 6: 51, 8: 60, 10: 65, 12: 71, 14: 75, 16: 81, 18: 87, 20: 94, 24: 105 },\n  600: { 0.5: 22, 0.75: 25, 1: 27, 1.25: 29, 1.5: 32, 2: 37, 2.5: 41, 3: 46, 4: 54, 5: 60, 6: 67, 8: 76, 10: 86, 12: 92, 14: 94, 16: 106, 18: 117, 20: 127, 24: 140 },\n  900: { 0.5: 32, 0.75: 35, 1: 41, 1.25: 41, 1.5: 44, 2: 57, 2.5: 64, 3: 54, 4: 70, 5: 79, 6: 86, 8: 102, 10: 108, 12: 117, 14: 130, 16: 133, 18: 152, 20: 159, 24: 203 },\n  1500: { 0.5: 32, 0.75: 35, 1: 41, 1.25: 41, 1.5: 44, 2: 57, 2.5: 64 },\n}\n// Slip-on bore B (also the socket bore of socket-weld flanges).\nconst SO_B: Record<number, number> = {\n  0.5: 22.2, 0.75: 27.7, 1: 34.5, 1.25: 43.2, 1.5: 49.5, 2: 61.9, 2.5: 74.6, 3: 90.7, 3.5: 103.4, 4: 116.1, 5: 143.8,\n  6: 170.7, 8: 221.5, 10: 276.2, 12: 327, 14: 359.2, 16: 410.5, 18: 461.8, 20: 513.1, 24: 616,\n}\n// Socket-weld flange socket depth D.\nconst SW_D: Record<number, number> = { 0.5: 9.5, 0.75: 11.2, 1: 12.7, 1.25: 14.2, 1.5: 15.7, 2: 17.5, 2.5: 19.1, 3: 20.6 }\n\n/** True when B16.5 defines this flange type at this size and class. */\nexport function defined(kind: FlangeKind, nps: number, rating: Rating) {\n  if (kind === 'WN') return true\n  if (rating === 2500 && kind !== 'THD' && kind !== 'LJ') return false\n  if (kind === 'SW') return nps <= (rating >= 900 ? 2.5 : 3)\n  if (rating === 1500) return nps <= 2.5\n  if (rating === 2500) return nps <= 12\n  return nps <= 24\n}\n\n/** The requested class if B16.5 defines this flange type there, else the next lower class that does. */\nexport function definedRating(kind: FlangeKind, nps: number, rating: Rating): Rating {\n  const all: Rating[] = [2500, 1500, 900, 600, 300, 150]\n  return all.find((r) => r <= rating && defined(kind, nps, r)) ?? 150\n}\n\n/** Slip-on / threaded / socket-weld / lap-joint length through hub, raised face excluded. */\nexport function hubLength(kind: FlangeKind, nps: number, rating: Rating) {\n  const f = flangeDims(nps, rating)\n  if (kind === 'WN') return f.hubLength\n  const t = SO_Y[rating]\n  // approx: outside the B16.5 slip-on table, 0.58 x the weld-neck hub\n  let y = has(t, nps) ? mm(look(t!, nps)) : f.hubLength * 0.58\n  // approx: B16.5 lap-joint hubs are longer than slip-on from NPS 14 in Class 150 / 300\n  if (kind === 'LJ' && nps >= 14 && rating <= 300) y *= 1.35\n  return Math.max(y, f.thickness + 0.004)\n}\nexport const soBore = (nps: number) => (nps <= 24 ? mm(look(SO_B, nps)) : pipeOD(nps) + 0.006)\nexport const swDepth = (nps: number) => mm(look(SW_D, nps))\n/** B16.5 bolt hole: bolt + 1/8\" up to 1-3/4\" bolts, + 1/4\" above. */\nexport const boltHole = (bd: number) => bd + inch(bd <= inch(1.75) + 1e-6 ? 0.125 : 0.25)\n\n// Octagonal ring grooves by ring number [P pitch dia, E depth, F width], mm (B16.5 / B16.20).\nconst RING: Record<number, [number, number, number]> = {\n  11: [34.14, 5.54, 7.14], 12: [39.67, 6.35, 8.74], 13: [42.88, 6.35, 8.74], 14: [44.45, 6.35, 8.74], 15: [47.63, 6.35, 8.74],\n  16: [50.8, 6.35, 8.74], 17: [57.15, 6.35, 8.74], 18: [60.33, 6.35, 8.74], 19: [65.07, 6.35, 8.74], 20: [68.27, 6.35, 8.74],\n  21: [72.23, 7.92, 11.91], 22: [82.55, 6.35, 8.74], 23: [82.55, 7.92, 11.91], 24: [95.25, 7.92, 11.91], 25: [101.6, 6.35, 8.74],\n  26: [101.6, 7.92, 11.91], 27: [107.95, 7.92, 11.91], 28: [111.13, 9.53, 13.49], 29: [114.3, 6.35, 8.74], 31: [123.83, 7.92, 11.91],\n  32: [127, 9.53, 13.49], 33: [131.78, 6.35, 8.74], 34: [131.78, 7.92, 11.91], 35: [136.53, 7.92, 11.91], 36: [149.23, 6.35, 8.74],\n  37: [149.23, 7.92, 11.91], 38: [157.18, 11.13, 16.66], 39: [161.93, 7.92, 11.91], 40: [171.45, 6.35, 8.74], 41: [180.98, 7.92, 11.91],\n  42: [190.5, 12.7, 19.84], 43: [193.68, 6.35, 8.74], 44: [193.68, 7.92, 11.91], 45: [211.12, 7.92, 11.91], 46: [211.14, 9.53, 13.49],\n  47: [228.6, 12.7, 19.84], 48: [247.65, 6.35, 8.74], 49: [269.88, 7.92, 11.91], 50: [269.88, 11.13, 16.66], 51: [279.4, 14.27, 23.01],\n  52: [304.8, 6.35, 8.74], 53: [323.85, 7.92, 11.91], 54: [323.85, 11.13, 16.66], 55: [342.9, 17.48, 30.18], 56: [381, 6.35, 8.74],\n  57: [381, 7.92, 11.91], 58: [381, 14.27, 23.01], 59: [396.88, 6.35, 8.74], 60: [406.4, 17.48, 33.32], 61: [419.1, 7.92, 11.91],\n  62: [419.1, 11.13, 16.66], 63: [419.1, 15.88, 26.97], 64: [454.03, 6.35, 8.74], 65: [469.9, 7.92, 11.91], 66: [469.9, 11.13, 16.66],\n  67: [469.9, 17.48, 30.18], 68: [517.53, 6.35, 8.74], 69: [533.4, 7.92, 11.91], 70: [533.4, 12.7, 19.84], 71: [533.4, 17.48, 30.18],\n  72: [558.8, 6.35, 8.74], 73: [584.2, 9.53, 13.49], 74: [584.2, 12.7, 19.84], 75: [584.2, 17.48, 33.32], 76: [673.1, 6.35, 8.74],\n  77: [692.15, 11.13, 16.66], 78: [692.15, 15.88, 26.97], 79: [692.15, 20.62, 36.53],\n}\nconst RN_300 = { 0.5: 11, 0.75: 13, 1: 16, 1.25: 18, 1.5: 20, 2: 23, 2.5: 26, 3: 31, 3.5: 34, 4: 37, 5: 41, 6: 45, 8: 49, 10: 53, 12: 57, 14: 61, 16: 65, 18: 69, 20: 73, 24: 77 }\nconst RN_900 = { 0.5: 12, 0.75: 14, 1: 16, 1.25: 18, 1.5: 20, 2: 24, 2.5: 27, 3: 31, 4: 37, 5: 41, 6: 45, 8: 49, 10: 53, 12: 57, 14: 62, 16: 66, 18: 70, 20: 74, 24: 78 }\n// B16.5 ring number per class and NPS.\nconst RING_NO: Record<Rating, Record<number, number>> = {\n  150: { 1: 15, 1.25: 17, 1.5: 19, 2: 22, 2.5: 25, 3: 29, 3.5: 33, 4: 36, 5: 40, 6: 43, 8: 48, 10: 52, 12: 56, 14: 59, 16: 64, 18: 68, 20: 72, 24: 76 },\n  300: RN_300,\n  600: RN_300,\n  900: RN_900,\n  1500: { ...RN_900, 3: 35, 4: 39, 5: 44, 6: 46, 8: 50, 10: 54, 12: 58, 14: 63, 16: 67, 18: 71, 20: 75, 24: 79 },\n  2500: { 0.5: 13, 0.75: 16, 1: 18, 1.25: 21, 1.5: 23, 2: 26, 2.5: 28, 3: 32, 4: 38, 5: 42, 6: 47, 8: 51, 10: 55, 12: 60 },\n}\n\n/** RTJ groove for a flange: ring number, pitch dia, depth, top width and raised-portion dia K (m). */\nexport function ringGroove(nps: number, rating: Rating) {\n  const f0 = flangeDims(nps, rating)\n  // approx: B16.47 Series A ring grooves scaled from the raised-face diameter\n  if (nps > 24) return { ring: 'R', pitch: f0.raisedFace * 0.9, depth: mm(rating <= 300 ? 11.13 : 15.88), width: mm(rating <= 300 ? 16.66 : 26.97), K: f0.raisedFace * 0.9 + mm(rating <= 300 ? 29 : 39) }\n  const t = has(RING_NO[rating], nps) ? RING_NO[rating] : RING_NO[300]\n  const no = look(t, nps)\n  const [p, e, w] = RING[no]\n  const f = flangeDims(nps, rating)\n  // approx: K = P + F + 12 mm (fits the B16.5 K column within a few mm), kept inside the bolt holes\n  const K = Math.min(mm(p + w + 12), f.boltCircle - boltHole(f.boltDia) - 0.004)\n  return { ring: `R${no}`, pitch: mm(p), depth: mm(e), width: mm(w), K }\n}\n\n/** Facing height above the flange plate (port offset). */\nexport function facingHeight(facing: Facing, nps: number, rating: Rating) {\n  if (facing === 'FF') return 0\n  if (facing === 'RTJ') return ringGroove(nps, rating).depth\n  return flangeDims(nps, rating).raisedFaceHeight\n}\n\n/** NPT threads per inch. */\nexport const nptTpi = (nps: number) => (nps <= 0.375 ? 18 : nps <= 0.75 ? 14 : nps <= 2 ? 11.5 : 8)\n\n// ------------------------------------------------------------------ primitives\n\n/** Orthonormal frame (u, w, v) about dir, matching Kit.boltCircle / perp. */\nexport function frame(dir: V3) {\n  const v = new THREE.Vector3(...dir).normalize()\n  const [u, w] = perp(dir)\n  return { u, w, v }\n}\n\nconst circle = (cx: number, cy: number, r: number, n: number, cw: boolean) => {\n  const pts: THREE.Vector2[] = []\n  for (let i = 0; i < n; i++) {\n    const a = ((cw ? -i : i) / n) * Math.PI * 2\n    pts.push(new THREE.Vector2(cx + Math.cos(a) * r, cy + Math.sin(a) * r))\n  }\n  return pts\n}\n\nexport interface Drill {\n  n: number\n  bc: number\n  hole: number\n}\n\n/**\n * Flat plate with real bolt holes (and a central opening rIn) from `back` along `dir`, thickness t. Hole phase matches\n * Kit.boltCircle. `bore` draws the central opening wall; the front face can take its own material.\n */\nexport function drilledPlate(k: Kit, mat: MatKey, frontMat: MatKey, back: V3, dir: V3, t: number, rIn: number, rOut: number, d: Drill | null, n = seg(rOut * 2), bore = false) {\n  const { u, w, v } = frame(dir)\n  const M = new THREE.Matrix4().makeBasis(u, w, v).setPosition(...back)\n  const contour = circle(0, 0, rOut, n, false)\n  const holes: { c: [number, number]; r: number; pts: THREE.Vector2[] }[] = []\n  if (rIn > 1e-5) holes.push({ c: [0, 0], r: rIn, pts: circle(0, 0, rIn, n, true) })\n  if (d)\n    for (let i = 0; i < d.n; i++) {\n      const a = Math.PI / d.n + (i / d.n) * Math.PI * 2\n      const c: [number, number] = [Math.cos(a) * d.bc / 2, Math.sin(a) * d.bc / 2]\n      holes.push({ c, r: d.hole / 2, pts: circle(c[0], c[1], d.hole / 2, d.hole > 0.04 ? 20 : 14, true) })\n    }\n  const tris = THREE.ShapeUtils.triangulateShape(contour, holes.map((h) => h.pts))\n  const all = [...contour, ...holes.flatMap((h) => h.pts)]\n  const face = (z: number, nz: number) => {\n    const pos: number[] = []\n    const nor: number[] = []\n    for (const p of all) pos.push(p.x, p.y, z), nor.push(0, 0, nz)\n    const idx: number[] = []\n    for (const [a, b, c] of tris) nz > 0 ? idx.push(a, b, c) : idx.push(a, c, b)\n    const g = new THREE.BufferGeometry()\n    g.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3))\n    g.setAttribute('normal', new THREE.Float32BufferAttribute(nor, 3))\n    g.setIndex(idx)\n    return g\n  }\n  k.add(frontMat, face(t, 1), M)\n  k.add(mat, face(0, -1), M.clone())\n  // walls: outer (normals out), holes (normals into the hole)\n  const pos: number[] = []\n  const nor: number[] = []\n  const idx: number[] = []\n  const wall = (cx: number, cy: number, r: number, m: number, inward: boolean) => {\n    const b = pos.length / 3\n    for (let j = 0; j <= m; j++) {\n      const a = ((inward ? -j : j) / m) * Math.PI * 2\n      const c = Math.cos(a)\n      const s = Math.sin(a)\n      const sg = inward ? -1 : 1\n      pos.push(cx + c * r, cy + s * r, 0, cx + c * r, cy + s * r, t)\n      nor.push(c * sg, s * sg, 0, c * sg, s * sg, 0)\n    }\n    for (let j = 0; j < m; j++) {\n      const a0 = b + j * 2\n      if (inward) idx.push(a0, a0 + 1, a0 + 2, a0 + 1, a0 + 3, a0 + 2)\n      else idx.push(a0, a0 + 2, a0 + 1, a0 + 1, a0 + 2, a0 + 3)\n    }\n  }\n  wall(0, 0, rOut, n, false)\n  holes.forEach((h, i) => (i > 0 || bore || rIn <= 1e-5 ? wall(h.c[0], h.c[1], h.r, h.pts.length, true) : null))\n  const g = new THREE.BufferGeometry()\n  g.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3))\n  g.setAttribute('normal', new THREE.Float32BufferAttribute(nor, 3))\n  g.setIndex(idx)\n  k.add(mat, g, M.clone())\n}\n\n/** Quarter-ish arc points from angle a0 to a1 about (cr, cy) in the (r, y) plane. */\nexport function arc(cr: number, cy: number, r: number, a0: number, a1: number, n = 5): Profile {\n  return Array.from({ length: n + 1 }, (_, i) => {\n    const a = a0 + ((a1 - a0) * i) / n\n    return [cr + Math.cos(a) * r, cy + Math.sin(a) * r] as [number, number]\n  })\n}\n\n/** Sawtooth thread profile between y0 and y1 (walks y0 → y1). External: crest at r, root at r - h. Internal: crest at r, root at r + h. */\nexport function thread(r0: number, r1: number, y0: number, y1: number, pitch: number, internal: boolean): Profile {\n  const n = Math.max(1, Math.round(Math.abs(y1 - y0) / pitch))\n  const h = pitch * 0.8 * 0.7\n  const p: Profile = []\n  for (let i = 0; i <= n; i++) {\n    const s = i / n\n    const r = r0 + (r1 - r0) * s\n    const y = y0 + (y1 - y0) * s\n    p.push([r, y])\n    if (i < n) p.push([r + (internal ? h : -h), y + (y1 - y0) / n / 2])\n  }\n  return p\n}\n\n/** Hollow pipe stub from `start` along `dir`. Ends: 'bevel' (B16.25 37.5° with land), 'square', 'open', 'thread' (NPT). */\nexport type End = 'bevel' | 'square' | 'open' | 'thread'\nexport function pipeStub(k: Kit, start: V3, dir: V3, L: number, nps: number, schedule: Schedule, endA: End, endB: End, mat: MatKey = 'pipePaint', ro = pipeOD(nps) / 2, ri = ro - pipeWall(nps, schedule)) {\n  if (L <= 1e-5) return\n  const n = seg(ro * 2)\n  const wall = ro - ri\n  const bev = Math.min(Math.max(0, wall - LAND) * BEVEL, L / 3)\n  const pitch = 0.0254 / nptTpi(nps)\n  // NPT effective thread length L2 = (0.8 D + 6.8) / tpi\n  const thr = Math.min(L * 0.6, inch((0.8 * (ro * 2) / 0.0254 + 6.8) / nptTpi(nps)))\n  let y0 = 0\n  let y1 = L\n  if (endA === 'bevel') k.lathe('machined', [[ri, 0], [ri + LAND, 0], [ro, bev]], start, dir, n), (y0 = bev)\n  else if (endA === 'square') k.lathe(mat, [[ri, 0], [ro, 0]], start, dir, n)\n  else if (endA === 'thread') {\n    k.lathe('machined', [[ri, 0], [ro - thr / 32 - pitch * 0.4, 0], ...thread(ro - thr / 32, ro, 0.0003, thr, pitch, false)], start, dir, n)\n    y0 = thr\n  }\n  if (endB === 'bevel') k.lathe('machined', [[ro, L - bev], [ri + LAND, L], [ri, L]], start, dir, n), (y1 = L - bev)\n  else if (endB === 'square') k.lathe(mat, [[ro, L], [ri, L]], start, dir, n)\n  else if (endB === 'thread') {\n    k.lathe('machined', [...thread(ro, ro - thr / 32, L - thr, L - 0.0003, pitch, false), [ro - thr / 32 - pitch * 0.4, L], [ri, L]], start, dir, n)\n    y1 = L - thr\n  }\n  if (y1 > y0) k.lathe(mat, [[ro, y0], [ro, y1]], start, dir, n)\n  k.lathe('pipeBore', [[ri, L], [ri, 0]], start, dir, n)\n}\n\n/** Fillet weld ring: legs along the pipe (-axis from y) and on a face at y; profile slightly convex. */\nexport function filletWeld(k: Kit, base: V3, dir: V3, r: number, y: number, leg: number, n = seg(r * 2)) {\n  const p: Profile = []\n  for (let i = 0; i <= 6; i++) {\n    const s = i / 6\n    const bulge = Math.sin(s * Math.PI) * leg * 0.12\n    p.push([r + leg * s + bulge * 0.7, y - leg * (1 - s) + bulge * 0.7])\n  }\n  k.lathe('flangeSteel', p, base, dir, n)\n}\n\n/** Circumferential butt weld cap. */\nexport function buttWeld(k: Kit, pos: V3, dir: V3, r: number, wall: number) {\n  const w = Math.max(0.005, Math.min(0.02, wall * 1.6))\n  k.torus('flangeSteel', r + 0.0006, w / 2, pos, dir, 6, seg(r * 2))\n}\n\n/** Stamped marking on a cylindrical rim: a thin band of text centred on angle `at` (radians from the frame's u axis). */\nexport function stamp(k: Kit, text: string, base: V3, dir: V3, r: number, y0: number, y1: number, at = 0) {\n  const h = Math.abs(y1 - y0)\n  if (h < 0.004 || !text) return\n  const phi = Math.min(h * 4, r * 2.2) / r\n  const g = uvScale(latheCrisp([[r + 0.0004, Math.min(y0, y1)], [r + 0.0004, Math.max(y0, y1)]], 12, at - phi / 2, phi), 1, 1 / h)\n  k.add(textMaterial(text, '#878d91', '#2b3035', 512, 128), g, mat4(base, dir))\n}\n\n// ------------------------------------------------------------------ loose flange body\n\n/**\n * Gasket facing on a plate whose front is at `base` (axis dir): RF with serrations, RTJ raised portion with the octagonal\n * groove, FF flat ring, or (lapBore > 0) the radiused lap-joint bore edge. Returns facing height, outer radius and the\n * axial position where the bore wall starts.\n */\nexport function facingLathe(k: Kit, fc: Facing, nps: number, rating: Rating, rb: number, base: V3, dir: V3, n: number, M: MatKey = 'flangeSteel', lapBore = 0) {\n  const f = flangeDims(nps, rating)\n  if (lapBore > 0) {\n    const r = Math.min(inch(0.12 + nps * 0.025), inch(0.5))\n    k.lathe('machined', arc(lapBore + r, -r, r, Math.PI / 2, Math.PI, 5), base, dir, n)\n    return { h: 0, rFace: lapBore + r, yb: -r }\n  }\n  if (fc === 'RF') {\n    const h = f.raisedFaceHeight\n    const rFace = f.raisedFace / 2\n    // serrated finish suggested by a few fine concentric grooves\n    const p: Profile = [[rFace, 0], [rFace, h]]\n    const step = (rFace - rb) / 7\n    for (let i = 1; i < 7; i++) {\n      const r = rFace - step * i\n      p.push([r + 0.0006, h], [r, h - 0.0003], [r - 0.0006, h])\n    }\n    p.push([rb + 0.001, h], [rb, h - 0.001])\n    k.lathe('machined', p, base, dir, n)\n    return { h, rFace, yb: h - 0.001 }\n  }\n  if (fc === 'RTJ') {\n    const g = ringGroove(nps, rating)\n    const h = g.depth\n    const rFace = g.K / 2\n    const bw = Math.max(0.0008, g.width / 2 - h * Math.tan((23 * Math.PI) / 180))\n    k.lathe(M, [[rFace, 0], [rFace, h]], base, dir, n)\n    k.lathe('machined', [[rFace, h], [g.pitch / 2 + g.width / 2, h], [g.pitch / 2 + bw, 0.0003], [g.pitch / 2 - bw, 0.0003], [g.pitch / 2 - g.width / 2, h], [rb + 0.001, h], [rb, h - 0.001]], base, dir, n)\n    return { h, rFace, yb: h - 0.001 }\n  }\n  const rFace = (rb + f.boltCircle / 2 - boltHole(f.boltDia)) / 2\n  k.lathe('machined', [[rFace, 0], [rb + 0.001, 0], [rb, -0.001]], base, dir, n)\n  return { h: 0, rFace, yb: -0.001 }\n}\n\nexport interface FlangeOpts {\n  kind: FlangeKind\n  nps: number\n  rating: Rating\n  facing: Facing\n  /** pipe schedule for the WN / SW bore */\n  schedule: Schedule\n  /** bolt holes drilled (true) or a plain disc */\n  drilled?: boolean\n  marking?: string\n  mat?: MatKey\n}\n\nexport interface FlangeInfo {\n  /** facing height above the plate front */\n  h: number\n  /** plate thickness */\n  t: number\n  /** plate front to hub end */\n  Y: number\n  /** bore radius the line sees */\n  bore: number\n  /** slip-on / socket / lap bore radius */\n  B: number\n  /** socket depth from the hub end (SW) */\n  D: number\n  od: number\n}\n\n/**\n * One loose B16.5 flange: plate front face at `base`, facing pointing along `dir`, hub running back along -dir.\n * Bolt holes are real. Returns the key dimensions.\n */\nexport function looseFlange(k: Kit, base: V3, dir: V3, o: FlangeOpts): FlangeInfo {\n  const { kind, nps, rating, facing, schedule } = o\n  const f = flangeDims(nps, rating)\n  const M: MatKey = o.mat ?? 'flangeSteel'\n  const ro = pipeOD(nps) / 2\n  const n = seg(f.od)\n  const lj = kind === 'LJ'\n  const fc: Facing = lj ? 'FF' : facing\n  // lap-joint flanges are flat-faced; 150 / 300 tf includes the 1.6 mm that other types spend on the raised face\n  const t = f.thickness + (lj && rating <= 300 ? f.raisedFaceHeight : 0)\n  const Y = Math.max(hubLength(kind, nps, rating), t + 0.004)\n  const hubR = Math.max(f.hubDia / 2, ro * 1.05)\n  const riPipe = ro - pipeWall(nps, schedule)\n  const B = kind === 'WN' ? riPipe : soBore(nps) / 2\n  const pitch = 0.0254 / nptTpi(nps)\n  const hth = pitch * 0.56\n  const D = kind === 'SW' ? swDepth(nps) : 0\n  // radius of the bore where it meets the face\n  const rb = kind === 'WN' || kind === 'SW' ? riPipe : kind === 'THD' ? ro - hth - (Y - 0.003) / 32 : B\n  const bore = rb\n  const { h, rFace, yb } = facingLathe(k, fc, nps, rating, rb, base, dir, n, M, lj ? B : 0)\n  // drilled plate between rFace and the OD\n  drilledPlate(k, M, fc === 'FF' && !lj ? 'machined' : M, add(base, dir, -t), dir, t, rFace, f.od / 2, o.drilled === false ? null : { n: f.bolts, bc: f.boltCircle, hole: boltHole(f.boltDia) }, n)\n  // hub: outer surface from the hub end up to the back face\n  const fil = Math.min(inch(0.25 + nps * 0.012), (Y - t) * 0.35)\n  const y0 = -Y\n  const hubTop: Profile = [...arc(hubR + fil, -t - fil, fil, Math.PI, Math.PI / 2, 5), ...(rFace > hubR + fil ? [[rFace, -t] as [number, number]] : [])]\n  if (kind === 'WN') {\n    const bev = Math.max(0, ro - riPipe - LAND) * BEVEL\n    const neck = Math.min((Y - t) * 0.12, (ro - riPipe) * 1.5)\n    k.lathe('machined', [[riPipe, y0], [riPipe + LAND, y0], [ro, y0 + bev]], base, dir, n)\n    k.lathe(M, [[ro, y0 + bev], [ro, y0 + bev + neck], [hubR, -t - fil], ...hubTop], base, dir, n)\n    k.lathe('pipeBore', [[riPipe, yb], [riPipe, y0]], base, dir, n)\n  } else {\n    // slip-on style hubs taper slightly to the hub end\n    const rHe = Math.max(B + (ro - riPipe) * 1.6 + 0.002, hubR - (Y - t) * 0.1)\n    const hub: Profile = [[rHe, y0], [hubR, -t - fil], ...hubTop]\n    let rIn: number\n    if (kind === 'SO' || kind === 'LJ') {\n      k.lathe('machined', [[B, yb], [B, y0 + 0.002], [B + 0.002, y0]], base, dir, n)\n      rIn = B + 0.002\n    } else if (kind === 'SW') {\n      const yS = y0 + D\n      k.lathe('pipeBore', [[riPipe, yb], [riPipe, yS]], base, dir, n)\n      k.lathe('machined', [[riPipe, yS], [B, yS], [B, y0 + 0.0015], [B + 0.0015, y0]], base, dir, n)\n      rIn = B + 0.0015\n    } else {\n      // NPT internal thread, largest at the hub end, tapering 1:16 on diameter towards the face; 45° entry chamfer\n      const r1 = rb + (Y - 0.003) / 32\n      const ch = pitch * 1.5\n      k.lathe('machined', [[rb, yb], ...thread(rb, r1, yb - 0.001, y0 + ch, pitch, true), [r1 + ch, y0]], base, dir, n)\n      rIn = r1 + ch\n    }\n    k.lathe(M, [[rIn, y0], ...hub], base, dir, n)\n  }\n  if (o.marking) stamp(k, o.marking, base, dir, f.od / 2, -t * 0.82, -t * 0.18)\n  return { h, t, Y, bore, B, D, od: f.od }\n}\n\n/** Default stamped marking. */\nexport const marking = (kind: string, nps: number, rating: Rating, facing: Facing, extra = '') =>\n  `${nps}\" ${rating}# ${kind} ${facing} ${extra} A105N`.replace(/\\s+/g, ' ')\n\n// ------------------------------------------------------------------ inline end flanges with half joint studs\n\n/** Weld-neck RF end flange whose face is a port at `face` pointing along `dir`, plus the back half of the joint studs. Returns face to weld end. */\nexport function endFlange(k: Kit, nps: number, rating: Rating, face: V3, dir: V3, schedule: Schedule = 'STD', studs = true, mat?: MatKey) {\n  const f = flangeDims(nps, rating)\n  const base = add(face, dir, -f.raisedFaceHeight)\n  const i = looseFlange(k, base, dir, { kind: 'WN', nps, rating, facing: 'RF', schedule, mat })\n  if (studs) {\n    const half = studLength(nps, rating) / 2\n    k.boltCircle('boltXylan', 'nut', f.bolts, f.boltCircle / 2, add(face, dir, GAP / 2 - half), dir, f.boltDia, half, [f.boltDia * 0.6])\n  }\n  return i.Y + f.raisedFaceHeight\n}\n\n/** Lap-joint stub end (B16.9 long or MSS SP-43 short pattern): lap face at `face` pointing along dir. Returns length, lap radius, wall. */\nexport function stubEndGeo(k: Kit, face: V3, dir: V3, nps: number, schedule: Schedule, pattern: 'long' | 'short', mat: MatKey = 'flangeSteel') {\n  const s = stubEnd(nps)\n  // typical: MSS SP-43 short-pattern lengths\n  const short = inch(nps <= 1.5 ? 2 : nps <= 3.5 ? 2.5 : nps <= 5 ? 3 : nps <= 12 ? 4 : 6)\n  const L = pattern === 'long' ? s.length : short\n  const ro = pipeOD(nps) / 2\n  const wt = pipeWall(nps, schedule)\n  const ri = ro - wt\n  const G = s.lapDia / 2\n  const n = seg(G * 2)\n  const fr = Math.max(wt * 1.2, 0.003)\n  const bev = Math.max(0, wt - LAND) * BEVEL\n  k.lathe('machined', [[G, -0.002], [G - 0.002, 0], [ri + 0.0008, 0], [ri, -0.0008]], face, dir, n)\n  k.lathe(mat, [[ro, -L + bev], [ro, -wt - fr], ...arc(ro + fr, -wt - fr, fr, Math.PI, Math.PI / 2, 4), [G - 0.001, -wt], [G, -wt + 0.001], [G, -0.002]], face, dir, n)\n  k.lathe('pipeBore', [[ri, -0.0008], [ri, -L]], face, dir, n)\n  k.lathe('machined', [[ri, -L], [ri + LAND, -L], [ro, -L + bev]], face, dir, n)\n  return { L, G, wt }\n}\n\n// ------------------------------------------------------------------ inline assemblies\n\n/** Weld end to raised face of a WN RF flange. */\nexport const flangeLen = (nps: number, rating: Rating) => {\n  const f = flangeDims(nps, rating)\n  return f.hubLength + f.raisedFaceHeight\n}\n\n/** Inline flanged ends: WN RF flanges at x = ±L/2 facing out with half joint studs, plus pipe nipples of length `neck` inward. Returns the nipple inner ends. */\nexport function inlineEnds(k: Kit, nps: number, rating: Rating, L: number, neck: number, schedule: Schedule = 'STD', mat: MatKey = 'pipePaint') {\n  const fl = endFlange(k, nps, rating, [-L / 2, 0, 0], [-1, 0, 0], schedule)\n  endFlange(k, nps, rating, [L / 2, 0, 0], [1, 0, 0], schedule)\n  const ro = pipeOD(nps) / 2\n  const wt = pipeWall(nps, schedule)\n  const xa = -L / 2 + fl\n  const xb = L / 2 - fl\n  if (neck > 0) {\n    pipeStub(k, [xa, 0, 0], [1, 0, 0], neck, nps, schedule, 'open', 'square', mat)\n    pipeStub(k, [xb - neck, 0, 0], [1, 0, 0], neck, nps, schedule, 'square', 'open', mat)\n    buttWeld(k, [xa, 0, 0], [1, 0, 0], ro, wt)\n    buttWeld(k, [xb, 0, 0], [1, 0, 0], ro, wt)\n  }\n  return { xa: xa + neck, xb: xb - neck, fl }\n}\n\n/** Sinusoidal-convolution metal bellows along +X from x0: n convolutions of pitch q, root radius rr, height h, wall t, with cuffs. Returns its length. */\nexport function bellows(k: Kit, x0: number, n: number, q: number, rr: number, h: number, t: number, cuff: number, mat: MatKey = 'stainless') {\n  const out: Profile = [[rr, 0]]\n  const L = n * q + 2 * cuff\n  const m = 14\n  for (let i = 0; i <= n * m; i++) {\n    const y = cuff + (i / m) * q\n    out.push([rr + h * (0.5 - 0.5 * Math.cos((2 * Math.PI * i) / m)), y])\n  }\n  out.push([rr, L])\n  const segs = seg((rr + h) * 2)\n  k.lathe(mat, out, [x0, 0, 0], 'x', segs)\n  k.lathe(mat, out.map(([r, y]) => [r - t, y] as [number, number]).reverse(), [x0, 0, 0], 'x', segs)\n  return L\n}\n\n/** Wafer clamped between two WN RF flanges: flanges with faces at ±(w/2 + gasket), spiral-wound gaskets and full studs spanning. */\nexport function waferFlanges(k: Kit, nps: number, rating: Rating, w: number, schedule: Schedule = 'STD', stub = 0.12) {\n  const f = flangeDims(nps, rating)\n  const xf = w / 2 + GAP\n  for (const s of [-1, 1]) {\n    const face: V3 = [s * xf, 0, 0]\n    const dir: V3 = [s, 0, 0]\n    const base = add(face, dir, f.raisedFaceHeight)\n    const i = looseFlange(k, base, [-s, 0, 0], { kind: 'WN', nps, rating, facing: 'RF', schedule })\n    const we = s * (xf + f.raisedFaceHeight + i.Y)\n    if (stub > 0) {\n      pipeStub(k, [we, 0, 0], dir, stub, nps, schedule, 'open', 'bevel')\n      buttWeld(k, [we, 0, 0], dir, pipeOD(nps) / 2, pipeWall(nps, schedule))\n    }\n    k.lathe('gasket', [[pipeOD(nps) / 2 + 0.003, s * (w / 2) + (s < 0 ? -GAP : 0)], [f.raisedFace / 2 - 0.002, s * (w / 2) + (s < 0 ? -GAP : 0)], [f.raisedFace / 2 - 0.002, s * (w / 2) + (s < 0 ? 0 : GAP)], [pipeOD(nps) / 2 + 0.003, s * (w / 2) + (s < 0 ? 0 : GAP)]], [0, 0, 0], 'x', seg(f.raisedFace))\n  }\n  const span = 2 * (xf + f.raisedFaceHeight + f.thickness) + f.boltDia * 2.2\n  k.boltCircle('boltXylan', 'nut', f.bolts, f.boltCircle / 2, [-span / 2, 0, 0], 'x', f.boltDia, span, [f.boltDia * 0.5, span - f.boltDia * 0.5])\n  return { xf, f }\n}\n\n/** Fig 1502 union ends relative to the seal plane (typical proportions). */\nexport function hammerSpan(nps: number) {\n  const od = pipeOD(nps)\n  const nutL = od * 1.08\n  return { xm: -nutL * 0.72 - od * 1.25, xf: nutL * 0.28 + od * 1.35 * 0.55 }\n}\n\n/**\n * typical Fig 1502 hammer union about the X axis, seal plane at x = jx: male sub with nose and red three-lug wing nut\n * (-X side), female sub with ACME thread and lip seal (+X side). Returns the outer ends of both subs.\n */\nexport function hammerUnion(k: Kit, jx: number, nps: number, mat: MatKey = 'valveBody', label = '1502') {\n  const od = pipeOD(nps)\n  const ri = od / 2 - pipeWall(nps, '160')\n  const ringR = od * 0.98\n  const nutL = od * 1.08\n  const rBody = od * 0.62\n  const rFem = ringR * 0.84\n  const n = seg(ringR * 2)\n  const { xm: dm, xf: df } = hammerSpan(nps)\n  const xm = jx + dm\n  const xf = jx + df\n  // male sub: body, nose shoulder under the nut, seal face at jx\n  k.lathe(mat, [[ri, xm], [rBody, xm], [rBody, jx - nutL * 0.8], [rFem * 0.92, jx - nutL * 0.62], [rFem * 0.92, jx - 0.002], [ri + 0.004, jx]], [0, 0, 0], 'x', n)\n  // female sub: ACME thread band showing beyond the nut, shoulder, body\n  const pitch = 0.0254 / 3\n  k.lathe('machined', thread(rFem, rFem, jx + nutL * 0.2, jx + nutL * 0.28 + pitch * 2, pitch, false), [0, 0, 0], 'x', n)\n  k.lathe(mat, [[ri + 0.004, jx], [rFem, jx + 0.003], [rFem, jx + nutL * 0.2], [rFem, jx + nutL * 0.28 + pitch * 2], [rFem * 1.04, jx + nutL * 0.34], [rFem * 1.04, jx + nutL * 0.42], [rBody, jx + nutL * 0.5], [rBody, xf], [ri, xf]], [0, 0, 0], 'x', n)\n  k.lathe('pipeBore', [[ri, xf], [ri, xm]], [0, 0, 0], 'x', n)\n  // lip seal ring proud of the bore at the joint\n  k.lathe('rubberBlack', [[ri, jx + 0.006], [ri - 0.002, jx + 0.003], [ri - 0.002, jx - 0.001], [ri, jx - 0.003]], [0, 0, 0], 'x', n)\n  // wing nut: ring with three hammer lugs\n  const x0 = jx - nutL * 0.72\n  const x1 = jx + nutL * 0.28\n  k.lathe('actuatorRed', [[rFem * 1.01, x0], [ringR * 0.96, x0], [ringR, x0 + 0.004], [ringR, x1 - 0.004], [ringR * 0.96, x1], [rFem * 1.01, x1]], [0, 0, 0], 'x', n)\n  k.lathe('actuatorRed', [[rFem * 1.01, x1], [rFem * 1.01, x0]], [0, 0, 0], 'x', n)\n  const lugH = od * 0.5\n  for (let i = 0; i < 3; i++) {\n    const a = Math.PI / 2 + (i / 3) * Math.PI * 2\n    const r = ringR + lugH / 2 - 0.004\n    const c = new THREE.Vector3(0, Math.sin(a), Math.cos(a))\n    // hammer lug with a striking face\n    k.rbox('actuatorRed', [nutL * 0.78, lugH, od * 0.3], od * 0.03, [(x0 + x1) / 2, c.y * r, c.z * r], [0, c.y, c.z])\n  }\n  stamp(k, label, [0, 0, 0], [1, 0, 0], ringR, x0 + nutL * 0.2, x1 - nutL * 0.2, Math.PI / 3)\n  return { xm, xf, ri, rBody, od }\n}\n\n/** Hex prism (across flats af) with a round through-hole, from `from` along unit `dir` for len. */\nexport function hexRing(k: Kit, af: number, rIn: number, from: V3, dir: V3, len: number, mat: MatKey = 'flangeSteel') {\n  const R = af / Math.sqrt(3)\n  const sh = new THREE.Shape()\n  for (let i = 0; i < 6; i++) {\n    const a = (i / 6) * Math.PI * 2 + Math.PI / 6\n    if (i) sh.lineTo(R * Math.cos(a), R * Math.sin(a))\n    else sh.moveTo(R * Math.cos(a), R * Math.sin(a))\n  }\n  sh.closePath()\n  if (rIn > 0) {\n    const h = new THREE.Path()\n    h.absarc(0, 0, rIn, 0, Math.PI * 2, true)\n    sh.holes.push(h)\n  }\n  const q = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 0, 1), new THREE.Vector3(...dir))\n  k.extrude(mat, sh, len, new THREE.Matrix4().compose(new THREE.Vector3(...from), q, new THREE.Vector3(1, 1, 1)), 24)\n}\n"
    }
  ],
  "categories": [
    "piping"
  ],
  "meta": {
    "export": "SlipOnFlange",
    "anchor": "fitting",
    "standards": [
      "ASME B16.5",
      "ASME B31.3",
      "ASME B16.20"
    ]
  }
}