Merge remote-tracking branch 'origin/main' into feat/paint-slots
# Conflicts: # packages/core/src/store/use-scene.ts # packages/editor/src/components/editor/index.tsx
This commit is contained in:
@@ -2606,3 +2606,13 @@ function syncDoorCutout(node: DoorNode, mesh: THREE.Mesh) {
|
||||
}
|
||||
cutout.visible = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a fresh door mesh for preview/ghost rendering.
|
||||
* Returns a mesh with an invisible hitbox root and visible children (frame, panels, hardware).
|
||||
*/
|
||||
export function buildDoorPreviewMesh(node: DoorNode): THREE.Mesh {
|
||||
const mesh = new THREE.Mesh()
|
||||
updateDoorMesh(node, mesh)
|
||||
return mesh
|
||||
}
|
||||
|
||||
@@ -68,6 +68,14 @@ export const GeometrySystem = () => {
|
||||
// shelf dirties the shelf without altering its boards.
|
||||
const builtGeometryKeyRef = useRef<Map<string, string>>(new Map())
|
||||
|
||||
// Re-mark every geometry-backed node dirty whenever a viewer appearance
|
||||
// value changes, so `def.geometry` builders re-run and pick up the new
|
||||
// shading / texture / preset / theme. These four are deliberate re-run
|
||||
// TRIGGERS, not values read in the body — the effect re-fires on any
|
||||
// change. They're primitives (stable by value), so listing them is safe;
|
||||
// biome flags them as "unnecessary" because the body doesn't reference
|
||||
// them, but dropping them silently breaks appearance-mode switching.
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: shading/textures/colorPreset/sceneTheme are intentional re-run triggers; removing them stops geometry from rebuilding on appearance change.
|
||||
useEffect(() => {
|
||||
const nodes = useScene.getState().nodes
|
||||
for (const node of Object.values(nodes)) {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { type LevelNode, sceneRegistry, useScene } from '@pascal-app/core'
|
||||
import { getLevelHeight, type LevelNode, sceneRegistry, useScene } from '@pascal-app/core'
|
||||
import { useFrame } from '@react-three/fiber'
|
||||
import { lerp } from 'three/src/math/MathUtils.js'
|
||||
import useViewer from '../../store/use-viewer'
|
||||
import { getLevelHeight } from './level-utils'
|
||||
|
||||
const EXPLODED_GAP = 5
|
||||
|
||||
@@ -40,7 +39,11 @@ export const LevelSystem = () => {
|
||||
obj.position.y = lerp(obj.position.y, targetY, delta * 12) // Smoothly animate to new Y position
|
||||
obj.visible = levelMode !== 'solo' || level?.id === selectedLevel || !selectedLevel
|
||||
|
||||
cumulativeY += getLevelHeight(levelId, nodes)
|
||||
cumulativeY += getLevelHeight(
|
||||
levelId,
|
||||
nodes,
|
||||
(wallId) => sceneRegistry.nodes.get(wallId)?.position.y,
|
||||
)
|
||||
}
|
||||
}, 5) // Using a lower priority so it runs after transforms from other systems have settled
|
||||
return null
|
||||
|
||||
@@ -1,53 +1,4 @@
|
||||
import {
|
||||
type CeilingNode,
|
||||
type LevelNode,
|
||||
sceneRegistry,
|
||||
useScene,
|
||||
type WallNode,
|
||||
} from '@pascal-app/core'
|
||||
|
||||
export const DEFAULT_LEVEL_HEIGHT = 2.5
|
||||
|
||||
// Cache: levelId → computed height. Invalidated when the nodes reference changes.
|
||||
// Zustand produces a new `nodes` object on every mutation, so reference equality
|
||||
// is a zero-cost way to detect stale data without any subscription overhead.
|
||||
const heightCache = new Map<string, number>()
|
||||
let lastNodesRef: object | null = null
|
||||
|
||||
export function getLevelHeight(
|
||||
levelId: string,
|
||||
nodes: ReturnType<typeof useScene.getState>['nodes'],
|
||||
): number {
|
||||
if (nodes !== lastNodesRef) {
|
||||
heightCache.clear()
|
||||
lastNodesRef = nodes
|
||||
}
|
||||
|
||||
if (heightCache.has(levelId)) return heightCache.get(levelId)!
|
||||
|
||||
const level = nodes[levelId as LevelNode['id']] as LevelNode | undefined
|
||||
if (!level) return DEFAULT_LEVEL_HEIGHT
|
||||
|
||||
let maxTop = 0
|
||||
|
||||
for (const childId of level.children) {
|
||||
const child = nodes[childId as keyof typeof nodes]
|
||||
if (!child) continue
|
||||
if (child.type === 'ceiling') {
|
||||
const ch = (child as CeilingNode).height ?? DEFAULT_LEVEL_HEIGHT
|
||||
if (ch > maxTop) maxTop = ch
|
||||
} else if (child.type === 'wall') {
|
||||
let meshY = sceneRegistry.nodes.get(childId as any)?.position.y ?? 0
|
||||
if (meshY < 0) meshY = 0
|
||||
const top = meshY + ((child as WallNode).height ?? DEFAULT_LEVEL_HEIGHT)
|
||||
if (top > maxTop) maxTop = top
|
||||
}
|
||||
}
|
||||
|
||||
const height = maxTop > 0 ? maxTop : DEFAULT_LEVEL_HEIGHT
|
||||
heightCache.set(levelId, height)
|
||||
return height
|
||||
}
|
||||
import { getLevelHeight, type LevelNode, sceneRegistry, useScene } from '@pascal-app/core'
|
||||
|
||||
/**
|
||||
* Instantly snaps all level Objects3D to their true stacked Y positions
|
||||
@@ -90,7 +41,11 @@ export function snapLevelsToTruePositions(): () => void {
|
||||
for (const { levelId, obj } of entries) {
|
||||
obj.position.y = cumulativeY
|
||||
obj.visible = true
|
||||
cumulativeY += getLevelHeight(levelId, nodes)
|
||||
cumulativeY += getLevelHeight(
|
||||
levelId,
|
||||
nodes,
|
||||
(wallId) => sceneRegistry.nodes.get(wallId)?.position.y,
|
||||
)
|
||||
}
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -3648,3 +3648,13 @@ function syncWindowCutout(node: WindowNode, mesh: THREE.Mesh) {
|
||||
}
|
||||
cutout.visible = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a fresh window mesh for preview/ghost rendering.
|
||||
* Returns a mesh with an invisible hitbox root and visible children (frame, glass, sash, hardware).
|
||||
*/
|
||||
export function buildWindowPreviewMesh(node: WindowNode): THREE.Mesh {
|
||||
const mesh = new THREE.Mesh()
|
||||
updateWindowMesh(node, mesh)
|
||||
return mesh
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user