WallSystem: throttle adjacent-wall rebuild during drag
Endpoint drags fire markDirty(wallId) on every pointermove tick. The old behavior rebuilt the dragged wall AND every wall sharing a junction on every tick — in a 4-corner room with doors, that's 4× the CSG +miter pass per tick. Visible as drag lag. New behavior: the dragged wall rebuilds every tick (so the drag tracks the cursor with full fidelity, cutouts and all). Adjacent walls are queued in pendingAdjacentByLevel and rebuilt on the trailing edge — 80ms after the dirty stream stops. The corners snap into their correct miter joins ~80ms after release, which is the standard CAD-app "rubber-band the dragged element, fix neighbors on commit" pattern. Module-level singleton state for the queue + timestamp — WallSystem is mounted exactly once globally, so module state is the right scope. Expected speedup: - t-junction drag: ~3× (was 3 walls/tick, now 1) - 4-corner room with door per wall: ~4× The trailing flush condition (!hasDirtyWalls && now - lastWallDirtyAtMs >= DRAG_FLUSH_MS) means single edits (non-drag) pay an 80ms latency before neighbors miter correctly. Acceptable for now; the real fix is the affordance/tool port (Milestone C) which will explicitly signal "drag in progress" so we can drop the heuristic. Until then this is a substantial drag-perf win for zero risk. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
375914a07c
commit
60117e848b
@@ -311,38 +311,67 @@ function assignWallMaterialGroups(
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
let useFrameNb = 0
|
let useFrameNb = 0
|
||||||
|
|
||||||
|
// ─── Drag-throttle state (singleton — one WallSystem mounted globally) ──
|
||||||
|
//
|
||||||
|
// Endpoint drags fire `markDirty(wallId)` on every pointermove tick. Without
|
||||||
|
// throttling, each tick rebuilds the dragged wall (~1 CSG + miter pass) AND
|
||||||
|
// every adjacent wall sharing a corner (3–4× in a t-junction or room).
|
||||||
|
// Visible as drag lag, especially on walls with door/window cutouts.
|
||||||
|
//
|
||||||
|
// Strategy: rebuild the dragged wall every tick (so the drag follows the
|
||||||
|
// cursor with full fidelity), but defer adjacent rebuilds to a trailing-
|
||||||
|
// edge flush DRAG_FLUSH_MS after the dirty stream stops. Visually, neighbor
|
||||||
|
// corners stay at their pre-drag miter until release, then snap into place
|
||||||
|
// within ~80ms. Standard CAD-app behavior. Speeds up t-junction drags ~3×,
|
||||||
|
// 4-corner-room drags ~4×.
|
||||||
|
const DRAG_FLUSH_MS = 80
|
||||||
|
let lastWallDirtyAtMs = 0
|
||||||
|
const pendingAdjacentByLevel = new Map<string, Set<string>>()
|
||||||
|
|
||||||
export const WallSystem = () => {
|
export const WallSystem = () => {
|
||||||
const dirtyNodes = useScene((state) => state.dirtyNodes)
|
const dirtyNodes = useScene((state) => state.dirtyNodes)
|
||||||
const clearDirty = useScene((state) => state.clearDirty)
|
const clearDirty = useScene((state) => state.clearDirty)
|
||||||
|
|
||||||
useFrame(() => {
|
useFrame(() => {
|
||||||
if (dirtyNodes.size === 0) return
|
const hasDirty = dirtyNodes.size > 0
|
||||||
|
const hasPending = pendingAdjacentByLevel.size > 0
|
||||||
|
if (!hasDirty && !hasPending) return
|
||||||
|
|
||||||
const nodes = useScene.getState().nodes
|
const nodes = useScene.getState().nodes
|
||||||
|
const now = performance.now()
|
||||||
|
|
||||||
// Collect dirty walls and their levels
|
// Collect dirty walls and their levels
|
||||||
const dirtyWallsByLevel = new Map<string, Set<string>>()
|
const dirtyWallsByLevel = new Map<string, Set<string>>()
|
||||||
|
|
||||||
useFrameNb += 1
|
useFrameNb += 1
|
||||||
dirtyNodes.forEach((id) => {
|
if (hasDirty) {
|
||||||
const node = nodes[id]
|
dirtyNodes.forEach((id) => {
|
||||||
if (!node || node.type !== 'wall') return
|
const node = nodes[id]
|
||||||
|
if (!node || node.type !== 'wall') return
|
||||||
|
|
||||||
const levelId = node.parentId
|
const levelId = node.parentId
|
||||||
if (!levelId) return
|
if (!levelId) return
|
||||||
|
|
||||||
if (!dirtyWallsByLevel.has(levelId)) {
|
if (!dirtyWallsByLevel.has(levelId)) {
|
||||||
dirtyWallsByLevel.set(levelId, new Set())
|
dirtyWallsByLevel.set(levelId, new Set())
|
||||||
}
|
}
|
||||||
dirtyWallsByLevel.get(levelId)?.add(id)
|
dirtyWallsByLevel.get(levelId)?.add(id)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasDirtyWalls = dirtyWallsByLevel.size > 0
|
||||||
|
if (hasDirtyWalls) {
|
||||||
|
lastWallDirtyAtMs = now
|
||||||
|
}
|
||||||
|
|
||||||
// Process each level that has dirty walls
|
// Process each level that has dirty walls
|
||||||
for (const [levelId, dirtyWallIds] of dirtyWallsByLevel) {
|
for (const [levelId, dirtyWallIds] of dirtyWallsByLevel) {
|
||||||
const levelWalls = getLevelWalls(levelId)
|
const levelWalls = getLevelWalls(levelId)
|
||||||
const miterData = calculateLevelMiters(levelWalls)
|
const miterData = calculateLevelMiters(levelWalls)
|
||||||
|
|
||||||
// Update dirty walls
|
// Update dirty walls — always, no throttling. The dragged wall must
|
||||||
|
// follow the cursor with full fidelity (cutouts and all).
|
||||||
for (const wallId of dirtyWallIds) {
|
for (const wallId of dirtyWallIds) {
|
||||||
const mesh = sceneRegistry.nodes.get(wallId) as THREE.Mesh
|
const mesh = sceneRegistry.nodes.get(wallId) as THREE.Mesh
|
||||||
if (mesh) {
|
if (mesh) {
|
||||||
@@ -352,17 +381,37 @@ export const WallSystem = () => {
|
|||||||
// If mesh not found, keep it dirty for next frame
|
// If mesh not found, keep it dirty for next frame
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update adjacent walls that share junctions
|
// Adjacent walls sharing junctions — *defer* during active drag
|
||||||
|
// (dirty arrived this frame), flush on the trailing edge.
|
||||||
const adjacentWallIds = getAdjacentWallIds(levelWalls, dirtyWallIds)
|
const adjacentWallIds = getAdjacentWallIds(levelWalls, dirtyWallIds)
|
||||||
|
let pending = pendingAdjacentByLevel.get(levelId)
|
||||||
|
if (!pending) {
|
||||||
|
pending = new Set()
|
||||||
|
pendingAdjacentByLevel.set(levelId, pending)
|
||||||
|
}
|
||||||
for (const wallId of adjacentWallIds) {
|
for (const wallId of adjacentWallIds) {
|
||||||
if (!dirtyWallIds.has(wallId)) {
|
if (!dirtyWallIds.has(wallId)) {
|
||||||
const mesh = sceneRegistry.nodes.get(wallId) as THREE.Mesh
|
pending.add(wallId)
|
||||||
if (mesh) {
|
|
||||||
updateWallGeometry(wallId, miterData)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trailing-edge flush: if no new dirty marks for DRAG_FLUSH_MS, the
|
||||||
|
// drag has ended — rebuild the queued neighbors so corners snap into
|
||||||
|
// their correct miter joins.
|
||||||
|
const quiet = !hasDirtyWalls && now - lastWallDirtyAtMs >= DRAG_FLUSH_MS
|
||||||
|
if (quiet && pendingAdjacentByLevel.size > 0) {
|
||||||
|
for (const [levelId, pendingIds] of pendingAdjacentByLevel) {
|
||||||
|
if (pendingIds.size === 0) continue
|
||||||
|
const levelWalls = getLevelWalls(levelId)
|
||||||
|
const miterData = calculateLevelMiters(levelWalls)
|
||||||
|
for (const wallId of pendingIds) {
|
||||||
|
const mesh = sceneRegistry.nodes.get(wallId) as THREE.Mesh
|
||||||
|
if (mesh) updateWallGeometry(wallId, miterData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pendingAdjacentByLevel.clear()
|
||||||
|
}
|
||||||
}, 4)
|
}, 4)
|
||||||
|
|
||||||
return null
|
return null
|
||||||
|
|||||||
Reference in New Issue
Block a user