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
@@ -112,6 +112,7 @@ export const DoorSystem = () => {
// Editing a scene material a door slot references must rebuild that door
// (door meshes are built by this system, not <GeometrySystem>).
useEffect(() => {
void sceneMaterials
const nodes = useScene.getState().nodes
for (const node of Object.values(nodes)) {
if (node?.type !== 'door') continue
@@ -1109,6 +1110,7 @@ function addDoorLeaf(
hingeX,
hingeSide,
swingRotation,
openRotationY,
segments,
contentPadding,
handle,
@@ -1134,6 +1136,10 @@ function addDoorLeaf(
hingeX: number
hingeSide: 'left' | 'right'
swingRotation: number
// Leaf rotation (radians, about the hinge Y axis) at fully-open. The GLB
// exporter reads this off the leaf group to bake an open/close clip; it is
// the kinematic endpoint, independent of the current `swingRotation`.
openRotationY: number
segments: DoorNode['segments']
contentPadding: DoorNode['contentPadding']
handle: boolean
@@ -1164,6 +1170,10 @@ function addDoorLeaf(
const leafGroup = new THREE.Group()
leafGroup.position.set(hingeX, 0, 0)
leafGroup.rotation.y = swingRotation
// Marks this group as the swing leaf and records its fully-open angle so the
// GLB exporter can bake an open/close animation clip from a single pose. The
// exporter strips this marker before writing the file.
leafGroup.userData.pascalSwingLeaf = { axis: 'y', openRotationY }
mesh.add(leafGroup)
const addLeafBox = (
@@ -2486,6 +2496,7 @@ function updateDoorMesh(rawNode: DoorNode, mesh: THREE.Mesh) {
hingeX: -insideWidth / 2,
hingeSide: 'left',
swingRotation: -clampedSwingAngle * swingDirectionSign,
openRotationY: (-Math.PI / 2) * swingDirectionSign,
segments,
contentPadding,
handle,
@@ -2514,6 +2525,7 @@ function updateDoorMesh(rawNode: DoorNode, mesh: THREE.Mesh) {
hingeX: insideWidth / 2,
hingeSide: 'right',
swingRotation: clampedSwingAngle * swingDirectionSign,
openRotationY: (Math.PI / 2) * swingDirectionSign,
segments,
contentPadding,
handle,
@@ -2545,6 +2557,7 @@ function updateDoorMesh(rawNode: DoorNode, mesh: THREE.Mesh) {
hingeX,
hingeSide: hingesSide,
swingRotation: clampedSwingAngle * swingDirectionSign * hingeDirectionSign,
openRotationY: (Math.PI / 2) * swingDirectionSign * hingeDirectionSign,
segments,
contentPadding,
handle,