Merge origin/main into feat/placement-interaction-overhaul

Resolve 7 conflicts keeping our snapping migration + floorplan perf work as
source of truth, combined with main's MEP run-continuation / Alt-detach /
latch handles. Rebuilt two import blocks the auto-merge silently truncated
(node-arrow-handles.tsx, duct-fitting/move-tool.tsx).

Verified: tsc clean across core/viewer/editor/nodes/mcp, 451 tests pass,
biome clean. Floorplan view-transform re-render storm confirmed pre-existing
(not introduced by this merge).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-06-28 16:22:47 -04:00
co-authored by Claude Opus 4.8
171 changed files with 19294 additions and 2224 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ export const pipeTrapDefinition: NodeDefinition<typeof PipeTrapNode> = {
metadata: {},
position: [0, 0, 0],
rotation: 0,
diameter: 1.5,
diameter: 2,
pipeMaterial: 'pvc',
armLengthM: 0,
}),
+23 -1
View File
@@ -1,9 +1,14 @@
import { Group, Mesh, TorusGeometry, Vector3 } from 'three'
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. */
@@ -19,8 +24,12 @@ const ARM_REACH_RADII = 3.2
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).
@@ -49,6 +58,19 @@ export function buildPipeTrapGeometry(node: PipeTrapNode): Group {
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
}
+1 -1
View File
@@ -27,7 +27,7 @@ const PipeTrapTool = () => {
const activeLevelId = useViewer((s) => s.selection.levelId)
const [cursor, setCursor] = useState<[number, number, number] | null>(null)
const [yaw, setYaw] = useState(0)
const [diameter] = useState(1.5)
const [diameter] = useState(pipeTrapDefinition.defaults().diameter)
const yawRef = useRef(0)
const diameterRef = useRef(diameter)
diameterRef.current = diameter