ai-chatbot/services/pascal-editor/packages/nodes/src/shared/pipe-run-translation-offset.test.ts

139 lines
3.8 KiB
TypeScript

import { describe, expect, test } from 'bun:test'
import {
type AnyNode,
PipeFittingNode,
PipeSegmentNode,
type PortConnection,
} from '@pascal-app/core'
import { getPipeFittingPorts } from '../pipe-fitting/ports'
import { planPipeElbowAtPort } from './auto-fitting'
import { planPipeRunTranslationOffsets } from './pipe-run-translation-offset'
import type { ScenePort } from './ports'
type Point = [number, number, number]
function drain(path: Point[]): PipeSegmentNode {
return PipeSegmentNode.parse({
object: 'node',
parentId: null,
visible: true,
metadata: {},
name: 'Drain',
path,
diameter: 3,
pipeMaterial: 'pvc',
system: 'waste',
})
}
function runConnection(run: PipeSegmentNode): PortConnection {
return {
kind: 'run',
nodeId: run.id,
startPath: run.path,
}
}
function fittingConnection(fitting: PipeFittingNode): PortConnection {
return {
kind: 'rigid-node',
nodeId: fitting.id,
startPosition: fitting.position,
}
}
function runPort(run: PipeSegmentNode, point: Point, direction: Point): ScenePort {
return {
id: 'end',
nodeId: run.id,
position: point,
direction,
diameter: run.diameter,
system: run.system,
}
}
function portLike(position: Point, direction: Point): ScenePort {
return {
id: 'x',
nodeId: 'x' as AnyNode['id'],
position,
direction,
diameter: 3,
system: 'waste',
}
}
function distSq(a: readonly number[], b: readonly number[]): number {
const dx = a[0]! - b[0]!
const dy = a[1]! - b[1]!
const dz = a[2]! - b[2]!
return dx * dx + dy * dy + dz * dz
}
describe('planPipeRunTranslationOffsets', () => {
test('slides a connected pipe sideways by adding bends and a connector', () => {
const moved = drain([
[0, 0, 0],
[4, 0, 0],
])
const partner = drain([
[-4, 0, 0],
[0, 0, 0],
])
const translatedPath = moved.path.map((p) => [p[0], p[1], p[2] - 1.2] as Point)
const result = planPipeRunTranslationOffsets({
pipe: moved,
translatedPath,
profile: { diameter: moved.diameter, pipeMaterial: moved.pipeMaterial },
connections: [runConnection(partner)],
scenePorts: [runPort(partner, [0, 0, 0], [1, 0, 0])],
nodesById: {
[moved.id]: moved as AnyNode,
[partner.id]: partner as AnyNode,
},
})
expect(result).not.toBeNull()
if (!result) return
expect(result.fittings).toHaveLength(2)
expect(result.connectors).toHaveLength(1)
expect(result.updates.some((u) => u.id === partner.id)).toBe(true)
expect(result.pipePath[0]![2]).toBeLessThan(0)
})
test('re-aims an existing pipe elbow and inserts the missing connector', () => {
const elbowPlan = planPipeElbowAtPort(portLike([0, 0, 0], [1, 0, 0]), [0, 0, -1], 3, 'pvc')
expect(elbowPlan).toBeTruthy()
if (!elbowPlan) return
const elbow = PipeFittingNode.parse(elbowPlan.fitting)
const branchPort = getPipeFittingPorts(elbow).find(
(p) => distSq(p.position, elbowPlan.collarPoint) < 1e-9,
)!
const moved = drain([
[...branchPort.position],
[branchPort.position[0] + 4, branchPort.position[1], branchPort.position[2]],
])
const translatedPath = moved.path.map((p) => [p[0], p[1], p[2] - 1.2] as Point)
const result = planPipeRunTranslationOffsets({
pipe: moved,
translatedPath,
profile: { diameter: moved.diameter, pipeMaterial: moved.pipeMaterial },
connections: [fittingConnection(elbow)],
scenePorts: [{ ...branchPort, nodeId: elbow.id }],
nodesById: {
[moved.id]: moved as AnyNode,
[elbow.id]: elbow as AnyNode,
},
})
expect(result).not.toBeNull()
if (!result) return
expect(result.fittings).toHaveLength(1)
expect(result.connectors).toHaveLength(1)
expect(result.updates.some((u) => u.id === elbow.id)).toBe(true)
})
})