Phase 5 Stage D moves: live-drag mesh.position for slab / ceiling / fence

User-reported regressions on the 1:1 legacy ports: slab/ceiling moves
were slow (polygon CSG rebuilds per scene.update tick), fence moves
teleported briefly on commit (residual mesh.position offset survived
the geometry rebuild). All three now use the same live-drag pattern
the legacy fence move was designed for:

- During drag, write only to `sceneRegistry.nodes.get(id).position`
  + `useLiveTransforms`. No `scene.update`, no polygon rebuild, no
  React re-render of geometry.
- History stays UNPAUSED — scene state isn't changing.
- On commit, a single `scene.update` writes the translated
  polygon (or fence start/end + linked-fence cascade). Recorded as one
  natural undo step.
- Tools leave `mesh.position` at the drag delta on commit;
  GeometrySystem / CeilingSystem reset it to (0,0,0) when they
  rebuild the geometry on the next frame. By the time position
  clears, the new geometry is in place — no teleport.

Two framework changes enable this:
- `GeometrySystem` (viewer/systems/geometry) now resets
  `group.position` + `group.rotation` after every rebuild, matching
  the legacy `FenceSystem.updateFenceGeometry` behavior. Tools that
  translate the group during live-drag can rely on the reset.
- Legacy `CeilingSystem.updateCeilingGeometry` extends its existing
  `position.y` reset to cover X/Z too — previously it left X/Z at the
  drag delta after rebuild, double-translating the visual.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-05-18 09:12:15 -04:00
co-authored by Claude Opus 4.7
parent 1e15e10185
commit f4ea07e05b
5 changed files with 240 additions and 323 deletions
@@ -54,7 +54,13 @@ function updateCeilingGeometry(node: CeilingNode, mesh: THREE.Mesh) {
gridMesh.geometry = newGeo.clone()
}
// Position at the ceiling height
// Position at the ceiling height and reset X/Z so live-drag mesh
// offsets (set by move tools during the drag) don't leak into the
// canonical position after the rebuild. Matches the pattern used by
// FenceSystem.updateFenceGeometry / GeometrySystem (both fully reset
// position+rotation after rebuild).
mesh.position.x = 0
mesh.position.z = 0
mesh.position.y = (node.height ?? 2.5) - 0.01 // Slight offset to avoid z-fighting with upper-level slabs
}
@@ -75,6 +75,15 @@ export const GeometrySystem = () => {
for (const child of [...built.children]) {
group.add(child)
}
// Reset transform — matches the legacy per-kind systems
// (e.g. FenceSystem.updateFenceGeometry) which clear
// mesh.position/rotation when rebuilding. Tools that translate
// the group via `mesh.position` for live-drag visuals rely on
// this reset to restore the canonical position after scene
// state catches up (otherwise the geometry rebuild renders on
// top of the residual offset → double-translation teleport).
group.position.set(0, 0, 0)
group.rotation.set(0, 0, 0)
clearDirty(id as AnyNodeId)
})