From 58339c64f5bd7cbe4a4a60363f9ef27c52661e54 Mon Sep 17 00:00:00 2001 From: Wassim SAMAD Date: Thu, 9 Jul 2026 07:39:36 -0400 Subject: [PATCH] fix(plugin-trees): consume dirty marks so scene-ready fires for plant scenes (#476) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instanced plant kinds (trees/grass/flowers) never cleared their dirty marks: FloorElevationSystem deliberately leaves the mark for kinds with a def.system, expecting that system to clear it after its own work, but InstancedKindSystem never participated in the dirty protocol. The marks lived forever, hasPendingSceneBuildWork() never went false, and the Viewer's scene-ready signal stalled at SCENE_READY_MAX_WAIT_FRAMES on every plant-containing scene — measured on the headless bake worker as ~190s (180 frames x ~1s SwiftShader frames) of pure cap-wait per bake. Clear the marks in a priority-2 useFrame pass: after the priority-1 floor-elevation lift in the same frame, and only for nodes whose proxy is registered (instances rebuild synchronously from the store, so a rendered node is already built). Validated by baking Wawa House locally: settle 22.5s -> 11.6s (the remainder is genuine asset loading), exported GLB byte-identical with and without the fix. Co-authored-by: Claude Fable 5 --- packages/plugin-trees/src/instanced.tsx | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/plugin-trees/src/instanced.tsx b/packages/plugin-trees/src/instanced.tsx index 511bdf2e..19b6e029 100644 --- a/packages/plugin-trees/src/instanced.tsx +++ b/packages/plugin-trees/src/instanced.tsx @@ -9,6 +9,7 @@ import { useScene, } from '@pascal-app/core' import { useNodeEvents, useViewer } from '@pascal-app/viewer' +import { useFrame } from '@react-three/fiber' import { useLayoutEffect, useMemo, useRef } from 'react' import { type BufferGeometry, type InstancedMesh, type Material, Matrix4, Object3D } from 'three' import { toStaticMaterial } from './wind-node' @@ -73,6 +74,26 @@ export function InstancedKindSystem({ (n) => (n.type as string) === kind && !active.has(n.id as string), ) as unknown as N[] }, [scene, kind, activeKey]) + + // Consume the dirty marks for this kind. Instances rebuild synchronously + // from the store (the memos above), so a rendered node is already "built" — + // but `FloorElevationSystem` deliberately leaves the mark for kinds with a + // `def.system`, expecting that system to clear it. Without this pass the + // marks live forever: `hasPendingSceneBuildWork` never goes false, so the + // scene-ready signal (and every headless bake) stalls at its frame cap. + // Priority 2 = after the priority-1 floor-elevation lift in the same frame; + // clearing only registered nodes leaves unmounted proxies for a later frame. + useFrame(() => { + const { dirtyNodes, nodes: sceneNodes, clearDirty } = useScene.getState() + if (dirtyNodes.size === 0) return + for (const id of dirtyNodes) { + const node = sceneNodes[id] + if (!node || (node.type as string) !== kind) continue + if (!sceneRegistry.nodes.has(id)) continue + clearDirty(id) + } + }, 2) + return }