92 lines
3.6 KiB
TypeScript

import { DoubleSide, Group, Mesh, SphereGeometry, TorusGeometry, Vector3 } from 'three'
import { buildSection, INCHES_TO_METERS } from '../duct-segment/geometry'
import { createPipeMaterial } from '../pipe-segment/geometry'
import type { PipeTrapNode } from './schema'
const BEND_SEGMENTS = 24
const RADIAL_SEGMENTS = 20
/** Sphere hubs filling the U-bend → stub joints read as a coupling and,
* more importantly, hide the wedge gap left where the horizontal arm's
* flat end cap meets the bend's upward-facing opening at 90°. */
const HUB_RADIUS_FACTOR = 1.12
/** Inlet drop and arm reach in pipe radii — keeps the trap proportional
* to its size without per-size tuning. */
const INLET_DROP_RADII = 2.6
const ARM_REACH_RADII = 3.2
/**
* P-trap geometry in the LOCAL frame (origin at the trap weir, the low
* point of the U). Inlet stub rises +Y to the fixture tailpiece; a
* half-torus U-bend turns the flow; the trap arm runs +X toward the
* vented waste line. `<ParametricNodeRenderer>` applies position + yaw.
*/
export function buildPipeTrapGeometry(node: PipeTrapNode): Group {
const group = new Group()
const material = createPipeMaterial({ pipeMaterial: node.pipeMaterial, system: 'waste' })
// Double-sided so the thin pipe walls don't drop out at grazing angles,
// which read as cuts/holes on the bend and stub ends.
material.side = DoubleSide
const radius = (node.diameter * INCHES_TO_METERS) / 2
const bendR = radius * 1.6
const hubRadius = radius * HUB_RADIUS_FACTOR
// U-bend: half torus in the XY plane, opening upward. Sits so its two
// tops are at y = bendR (the inlet riser and the arm rise).
const bend = new Mesh(new TorusGeometry(bendR, radius, 12, BEND_SEGMENTS, Math.PI), material)
bend.rotation.z = Math.PI // open side up
bend.position.set(bendR, bendR, 0)
bend.name = 'pipe-trap-bend'
group.add(bend)
// Inlet riser: from the left top of the U straight up to the fixture.
const inletDrop = radius * INLET_DROP_RADII
const inletTop = new Vector3(0, bendR + inletDrop, 0)
const inletStub = buildSection(
new Vector3(0, bendR, 0),
inletTop,
radius,
material,
'pipe-trap-inlet',
)
if (inletStub) group.add(inletStub)
// Trap arm: from the right top of the U horizontally along +X.
const armReach = Math.max(radius * ARM_REACH_RADII, node.armLengthM)
const armStart = new Vector3(bendR * 2, bendR, 0)
const armEnd = new Vector3(bendR * 2 + armReach, bendR, 0)
const arm = buildSection(armStart, armEnd, radius, material, 'pipe-trap-arm')
if (arm) group.add(arm)
// Coupling hubs at the two U-bend tops where the straight stubs meet the
// torus. They fill the 90° miter wedge (the visible "cut") and read as
// the trap's slip-joint nuts.
for (const [i, center] of [
new Vector3(0, bendR, 0),
new Vector3(bendR * 2, bendR, 0),
].entries()) {
const hub = new Mesh(new SphereGeometry(hubRadius, RADIAL_SEGMENTS, 12), material)
hub.name = `pipe-trap-hub-${i}`
hub.position.copy(center)
group.add(hub)
}
return group
}
/** Local-frame port positions (before position/yaw): inlet at the top
* of the riser facing +Y, outlet at the end of the arm facing +X. */
export function localTrapPorts(node: PipeTrapNode): {
inlet: Vector3
outlet: Vector3
} {
const radius = (node.diameter * INCHES_TO_METERS) / 2
const bendR = radius * 1.6
const inletDrop = radius * INLET_DROP_RADII
const armReach = Math.max(radius * ARM_REACH_RADII, node.armLengthM)
return {
inlet: new Vector3(0, bendR + inletDrop, 0),
outlet: new Vector3(bendR * 2 + armReach, bendR, 0),
}
}