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
@@ -8,7 +8,7 @@ import {
type WindowNode,
} 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,
@@ -32,12 +32,17 @@ export const LOUVERED_WINDOW_SLATS_NAME = 'louvered-window-slats'
export const AWNING_WINDOW_SASH_NAME = 'awning-window-sash'
export const HOPPER_WINDOW_SASH_NAME = 'hopper-window-sash'
const MAX_WINDOW_REBUILDS_PER_FRAME = 16
const WINDOW_PROGRESSIVE_DIRTY_THRESHOLD = MAX_WINDOW_REBUILDS_PER_FRAME
const WINDOW_PROGRESSIVE_TIME_BUDGET_MS = 8
export const WindowSystem = () => {
const dirtyNodes = useScene((state) => state.dirtyNodes)
const clearDirty = useScene((state) => state.clearDirty)
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 override-only updates re-run this component. Mirrors
// WallSystem + DoorSystem.
useLiveNodeOverrides((s) => s.overrides)
@@ -50,13 +55,17 @@ export const WindowSystem = () => {
: createSurfaceRoleMaterial('glazing', colorPreset)
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 === 'window') {
useScene.getState().dirtyNodes.add(node.id as AnyNodeId)
}
}
}, [shading, textures, colorPreset])
})
useFrame(() => {
if (dirtyNodes.size === 0) return
@@ -68,19 +77,43 @@ export const WindowSystem = () => {
: createSurfaceRoleMaterial('glazing', colorPreset)
const nodes = useScene.getState().nodes
const dirtyWindowIds: AnyNodeId[] = []
dirtyNodes.forEach((id) => {
const node = nodes[id]
if (!node || node.type !== 'window') return
dirtyWindowIds.push(id as AnyNodeId)
})
const useProgressiveWindowRebuilds = dirtyWindowIds.length > WINDOW_PROGRESSIVE_DIRTY_THRESHOLD
const frameStartedAt = performance.now()
let rebuiltWindowsThisFrame = 0
for (const id of dirtyWindowIds) {
if (useProgressiveWindowRebuilds) {
if (rebuiltWindowsThisFrame >= MAX_WINDOW_REBUILDS_PER_FRAME) {
break
}
if (
rebuiltWindowsThisFrame > 0 &&
performance.now() - frameStartedAt >= WINDOW_PROGRESSIVE_TIME_BUDGET_MS
) {
break
}
}
const node = nodes[id]
if (!node || node.type !== 'window') 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.
const effectiveNode = getEffectiveNode(node as WindowNode)
updateWindowMesh(effectiveNode, mesh)
clearDirty(id as AnyNodeId)
rebuiltWindowsThisFrame += 1
// Rebuild the parent wall so its cutout reflects the updated window geometry
// Avoid triggering expensive wall CSG rebuilds while the window is being interactively moved/duplicated.
@@ -89,7 +122,7 @@ export const WindowSystem = () => {
if (!isTransient && effectiveNode.parentId) {
useScene.getState().dirtyNodes.add(effectiveNode.parentId as AnyNodeId)
}
})
}
}, 3)
return null