Fix editor interaction, loading, and IFC cleanup (#385)

* fix: update site labels after camera swaps

* fix: gate scene display until viewer is ready

* fix: render scene loader on first paint

* fix: keep loader during pending scene graph

* feat: add wasd camera panning

* feat: add x delete mode shortcut

* feat: add floorplan north compass

* fix: move floorplan compass to lower corner

* fix: progressively rebuild heavy scene geometry

* fix: simplify noisy ifc wall output
This commit is contained in:
Wassim SAMAD
2026-06-08 15:04:28 -04:00
committed by GitHub
parent ce6f999310
commit 812b7306e8
15 changed files with 1197 additions and 36 deletions
@@ -11,7 +11,7 @@ import {
useScene,
} from '@pascal-app/core'
import { useFrame } from '@react-three/fiber'
import { useEffect } from 'react'
import { useEffect, useRef } from 'react'
import * as THREE from 'three'
import {
createSurfaceRoleMaterial,
@@ -28,6 +28,9 @@ let revealMaterial: THREE.Material = defaultRevealMaterial
let glassMaterial: THREE.Material = defaultGlassMaterial
const DOOR_RENDER_DEFAULTS = DoorNodeSchema.parse({ id: 'door_render_default' })
const MAX_DOOR_REBUILDS_PER_FRAME = 16
const DOOR_PROGRESSIVE_DIRTY_THRESHOLD = MAX_DOOR_REBUILDS_PER_FRAME
const DOOR_PROGRESSIVE_TIME_BUDGET_MS = 8
// Legacy/unparsed door nodes can miss schema-defaulted fields (segments,
// columnRatios, dividerThickness, …) and crash the geometry build. Re-apply the
@@ -47,6 +50,7 @@ export const DoorSystem = () => {
const shading = useViewer((state) => state.shading)
const textures = useViewer((state) => state.textures)
const colorPreset = useViewer((state) => state.colorPreset)
const materialRevisionRef = useRef<string | null>(null)
// Subscribe so an override-only update (no scene write) still re-runs
// the component, letting the gate below pick up the latest dirtyNodes
// set from the same render pass that received the override-publishing
@@ -59,13 +63,17 @@ export const DoorSystem = () => {
glassMaterial = textures ? defaultGlassMaterial : joineryMaterial
useEffect(() => {
const materialRevision = `${shading}:${textures ? 'textures' : 'solid'}:${colorPreset}`
if (materialRevisionRef.current === materialRevision) return
materialRevisionRef.current = materialRevision
const nodes = useScene.getState().nodes
for (const node of Object.values(nodes)) {
if (node?.type === 'door') {
useScene.getState().dirtyNodes.add(node.id as AnyNodeId)
}
}
}, [shading, textures, colorPreset])
})
useFrame(() => {
if (dirtyNodes.size === 0) return
@@ -75,13 +83,35 @@ export const DoorSystem = () => {
glassMaterial = textures ? defaultGlassMaterial : frameJoineryMaterial
const nodes = useScene.getState().nodes
const dirtyDoorIds: AnyNodeId[] = []
dirtyNodes.forEach((id) => {
const node = nodes[id]
if (!node || node.type !== 'door') return
dirtyDoorIds.push(id as AnyNodeId)
})
const useProgressiveDoorRebuilds = dirtyDoorIds.length > DOOR_PROGRESSIVE_DIRTY_THRESHOLD
const frameStartedAt = performance.now()
let rebuiltDoorsThisFrame = 0
for (const id of dirtyDoorIds) {
if (useProgressiveDoorRebuilds) {
if (rebuiltDoorsThisFrame >= MAX_DOOR_REBUILDS_PER_FRAME) {
break
}
if (
rebuiltDoorsThisFrame > 0 &&
performance.now() - frameStartedAt >= DOOR_PROGRESSIVE_TIME_BUDGET_MS
) {
break
}
}
const node = nodes[id]
if (!node || node.type !== 'door') continue
const mesh = sceneRegistry.nodes.get(id) as THREE.Mesh
if (!mesh) return // Keep dirty until mesh mounts
if (!mesh) continue // Keep dirty until mesh mounts
// Merge any live override (width / height / position) so the mesh
// rebuild reflects the in-flight drag without zustand churn. When
@@ -89,6 +119,7 @@ export const DoorSystem = () => {
const effectiveNode = getEffectiveNode(node as DoorNode)
updateDoorMesh(effectiveNode, mesh)
clearDirty(id as AnyNodeId)
rebuiltDoorsThisFrame += 1
// Rebuild the parent wall so its cutout reflects the updated door geometry
// Avoid triggering expensive wall CSG rebuilds while the door is being interactively moved/duplicated.
@@ -97,7 +128,7 @@ export const DoorSystem = () => {
if (!isTransient && effectiveNode.parentId) {
useScene.getState().dirtyNodes.add(effectiveNode.parentId as AnyNodeId)
}
})
}
}, 3)
return null