Skip to content

Commit 56c847f

Browse files
Snoopy147claude
andcommitted
Runtime per-instance hiding for game layers
treesRuntime { hide/restore/restoreAll/isHidden/epoch } — a game mode (Boots' destructible trees) can zero-scale a single plant's instance without touching the scene store; every InstancedSubMesh rewrites on the epoch bump. Exposed on globalThis.__pascalTreesRuntime so consumers feature-detect it with no package dependency. Transient by design. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent f054f88 commit 56c847f

1 file changed

Lines changed: 53 additions & 0 deletions

File tree

src/instanced.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,49 @@ export interface Placeable {
3838

3939
const DUMMY = new Object3D()
4040
const INSTANCE_MATRIX = new Matrix4()
41+
const ZERO_MATRIX = new Matrix4().makeScale(0, 0, 0)
4142
const 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

Comments
 (0)