@@ -38,8 +38,49 @@ export interface Placeable {
3838
3939const DUMMY = new Object3D ( )
4040const INSTANCE_MATRIX = new Matrix4 ( )
41+ const ZERO_MATRIX = new Matrix4 ( ) . makeScale ( 0 , 0 , 0 )
4142const NO_RAYCAST = ( ) => { }
4243
44+ // ── Runtime per-instance hiding ──────────────────────────────────────────────
45+ // Lets a GAME layer (e.g. the Boots plugin's destructible-tree mode) hide a
46+ // single plant's instance WITHOUT touching the scene store: hidden nodes get
47+ // a zero-scale matrix at write time and every InstancedSubMesh rewrites when
48+ // the epoch bumps. Exposed on globalThis under a well-known key so consumers
49+ // need no package dependency (feature-detect `__pascalTreesRuntime`). State
50+ // is transient by design — a reload or `restoreAll()` brings every plant back.
51+
52+ const hiddenNodeIds = new Set < string > ( )
53+ let hiddenEpoch = 0
54+
55+ export const treesRuntime = {
56+ /** Zero-scale this node's instance until restored. Returns true if it changed. */
57+ hide ( nodeId : string ) : boolean {
58+ if ( hiddenNodeIds . has ( nodeId ) ) return false
59+ hiddenNodeIds . add ( nodeId )
60+ hiddenEpoch ++
61+ return true
62+ } ,
63+ restore ( nodeId : string ) : boolean {
64+ const changed = hiddenNodeIds . delete ( nodeId )
65+ if ( changed ) hiddenEpoch ++
66+ return changed
67+ } ,
68+ restoreAll ( ) : void {
69+ if ( hiddenNodeIds . size === 0 ) return
70+ hiddenNodeIds . clear ( )
71+ hiddenEpoch ++
72+ } ,
73+ isHidden : ( nodeId : string ) : boolean => hiddenNodeIds . has ( nodeId ) ,
74+ get epoch ( ) : number {
75+ return hiddenEpoch
76+ } ,
77+ }
78+
79+ if ( typeof globalThis !== 'undefined' ) {
80+ ; ( globalThis as { __pascalTreesRuntime ?: typeof treesRuntime } ) . __pascalTreesRuntime =
81+ treesRuntime
82+ }
83+
4384// Wind is a TSL vertex bend baked into the variant materials (see `wind-node.ts`)
4485// — animated on the GPU, so the instance matrices here stay static.
4586
@@ -205,6 +246,10 @@ function InstancedSubMesh<N extends Placeable>({
205246 for ( let i = 0 ; i < nodes . length ; i += 1 ) {
206247 const node = nodes [ i ]
207248 if ( ! node ) continue
249+ if ( hiddenNodeIds . has ( node . id ) ) {
250+ mesh . setMatrixAt ( i , ZERO_MATRIX )
251+ continue
252+ }
208253 const scale = node . height / naturalHeight
209254 DUMMY . position . set ( node . position [ 0 ] , plantElevation ( node , sceneNodes ) , node . position [ 2 ] )
210255 DUMMY . rotation . set ( node . rotation [ 0 ] , node . rotation [ 1 ] , node . rotation [ 2 ] )
@@ -251,8 +296,16 @@ function InstancedSubMesh<N extends Placeable>({
251296 // callback has no explicit priority, so it runs before the priority-2 pass in
252297 // `InstancedKindSystem` that consumes them, whereas a React effect might not
253298 // flush until after the marks were already cleared.
299+ // Rewrites when the runtime hidden set changes (game-mode hide/restore).
300+ const seenEpoch = useRef ( hiddenEpoch )
301+
254302 useFrame ( ( ) => {
255303 if ( ! ref . current ) return
304+ if ( seenEpoch . current !== hiddenEpoch ) {
305+ seenEpoch . current = hiddenEpoch
306+ writeMatrices ( )
307+ return
308+ }
256309 if ( ! localSpace ) {
257310 for ( const [ id , cached ] of parentWorlds . current ) {
258311 const parent = sceneRegistry . nodes . get ( id )
0 commit comments