fix(plugin-trees): consume dirty marks so scene-ready fires for plant scenes (#476)

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 <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-07-09 07:39:36 -04:00
committed by GitHub
co-authored by Claude Fable 5
parent a477c4ade3
commit 58339c64f5
+21
View File
@@ -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 extends Placeable>({
(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 <InstancedNodes getVariant={getVariant} nodes={nodes} variantKeyOf={variantKeyOf} />
}