124 lines
4.5 KiB
TypeScript

import { describe, expect, test } from 'bun:test'
import { PipeSegmentNode } from '@pascal-app/core'
import { getPipeFittingPorts } from '../pipe-fitting/ports'
import { planPipeBranchTap, planPipeElbowAtPort } from './auto-fitting'
import type { RunBodyHit, ScenePort } from './ports'
type Point = [number, number, number]
function port(position: Point, direction: Point): ScenePort {
return {
id: 'end',
nodeId: 'pipe-segment_test' as ScenePort['nodeId'],
position,
direction,
diameter: 2,
system: 'waste',
}
}
function drain(path: Point[], system: 'waste' | 'vent' = 'waste'): PipeSegmentNode {
return PipeSegmentNode.parse({
object: 'node',
parentId: null,
visible: true,
metadata: {},
name: 'Drain',
path,
diameter: 3,
pipeMaterial: 'pvc',
system,
})
}
function dist(a: readonly number[], b: readonly number[]): number {
return Math.hypot(a[0]! - b[0]!, a[1]! - b[1]!, a[2]! - b[2]!)
}
function dot(a: readonly number[], b: readonly number[]): number {
return a[0]! * b[0]! + a[1]! * b[1]! + a[2]! * b[2]!
}
describe('planPipeElbowAtPort', () => {
test('90° bend mates both collars through the fitting port math', () => {
const plan = planPipeElbowAtPort(port([3, -0.05, 0], [1, 0, 0]), [0, 0, 1], 2)
expect(plan).not.toBeNull()
const ports = getPipeFittingPorts(plan!.fitting)
const inlet = ports.find((p) => p.id === 'inlet')!
const outlet = ports.find((p) => p.id === 'outlet')!
expect(dist(plan!.fitting.position, [3, -0.05, 0])).toBeLessThan(1e-6)
expect(dist(inlet.position, plan!.trimmedPortPoint)).toBeLessThan(1e-6)
expect(dot(inlet.direction, [1, 0, 0])).toBeCloseTo(-1, 6)
expect(dist(outlet.position, plan!.collarPoint)).toBeLessThan(1e-6)
expect(dot(outlet.direction, [0, 0, 1])).toBeCloseTo(1, 6)
expect(plan!.fitting.system).toBe('waste')
})
test('straight continuation → no fitting', () => {
expect(planPipeElbowAtPort(port([3, 0, 0], [1, 0, 0]), [1, 0, 0], 2)).toBeNull()
})
})
describe('planPipeBranchTap', () => {
function hit(node: PipeSegmentNode, segmentIndex: number, point: Point): RunBodyHit {
return { nodeId: node.id, segmentIndex, point }
}
test('horizontal drain tap → square sanitary tee', () => {
const run = drain([
[0, 0, 0],
[6, -0.125, 0],
])
const plan = planPipeBranchTap(run, hit(run, 0, [3, -0.0625, 0]), [0, 0, 1], 2)
expect(plan).not.toBeNull()
// DWV side taps mint a SQUARE sanitary tee (see planPipeBranchTap /
// PipeFittingNode schema): the branch enters perpendicular to the run
// regardless of the drawn lead-in angle, matching the duct tee tap.
expect(plan!.fitting.fittingType).toBe('sanitary-tee')
const ports = getPipeFittingPorts(plan!.fitting)
const branch = ports.find((p) => p.id === 'branch')!
const inlet = ports.find((p) => p.id === 'inlet')!
const outlet = ports.find((p) => p.id === 'outlet')!
// Branch leaves square to the run axis (the projected-perpendicular entry).
const axis = [6 / Math.hypot(6, 0.125), -0.125 / Math.hypot(6, 0.125), 0]
expect(Math.abs(dot(branch.direction, axis))).toBeLessThan(1e-6)
expect(branch.direction[2]).toBeGreaterThan(0.6)
// Split halves mate the run collars.
const upstream = plan!.runUpdate.data.path
expect(dist(upstream[upstream.length - 1]!, inlet.position)).toBeLessThan(1e-6)
expect(dist(plan!.runTail.path[0]!, outlet.position)).toBeLessThan(1e-6)
// Branch starts at the collar.
expect(dist(plan!.branchCollar, branch.position)).toBeLessThan(1e-6)
})
test('vertical stack tap → sanitary tee, branch square', () => {
const stack = drain([
[0, 0, 0],
[0, 3, 0],
])
const plan = planPipeBranchTap(stack, hit(stack, 0, [0, 1.5, 0]), [1, 0, 0], 2)
expect(plan).not.toBeNull()
expect(plan!.fitting.fittingType).toBe('sanitary-tee')
const branch = getPipeFittingPorts(plan!.fitting).find((p) => p.id === 'branch')!
expect(dot(branch.direction, [1, 0, 0])).toBeCloseTo(1, 6)
expect(Math.abs(branch.direction[1])).toBeLessThan(1e-6)
})
test('tap too close to a run end → null', () => {
const run = drain([
[0, 0, 0],
[6, 0, 0],
])
expect(planPipeBranchTap(run, hit(run, 0, [0.05, 0, 0]), [0, 0, 1], 2)).toBeNull()
})
test('branch parallel to the run → null', () => {
const run = drain([
[0, 0, 0],
[6, 0, 0],
])
expect(planPipeBranchTap(run, hit(run, 0, [3, 0, 0]), [1, 0, 0], 2)).toBeNull()
})
})