From 6317006ef3b2c0d3ae1128b86eca430cb3d5413a Mon Sep 17 00:00:00 2001 From: kali <64813012+kali-shade@users.noreply.github.com> Date: Tue, 23 Sep 2025 02:26:00 -0300 Subject: [PATCH] Fix effect switching race: add async disposal guards and scene cleanup to prevent stray meshes (#175) --- examples/splat-transitions/effects/explosion.js | 3 +++ examples/splat-transitions/effects/flow.js | 6 ++++-- examples/splat-transitions/effects/morph.js | 4 +++- examples/splat-transitions/effects/spheric.js | 8 +++++--- examples/splat-transitions/main.js | 17 +++++++++++++++++ 5 files changed, 32 insertions(+), 6 deletions(-) diff --git a/examples/splat-transitions/effects/explosion.js b/examples/splat-transitions/effects/explosion.js index d5ffa73..8b137c9 100644 --- a/examples/splat-transitions/effects/explosion.js +++ b/examples/splat-transitions/effects/explosion.js @@ -6,6 +6,7 @@ import { getAssetFileURL } from "/examples/js/get-asset-url.js"; export async function init({ THREE: _THREE, scene, camera, renderer, spark }) { const group = new THREE.Group(); scene.add(group); + let disposed = false; // Basic lights const ambient = new THREE.AmbientLight(0x404040, 0.6); @@ -329,6 +330,7 @@ export async function init({ THREE: _THREE, scene, camera, renderer, spark }) { } await Promise.all([loadSplats(), loadTable()]); + if (disposed) return { group, update: () => {}, dispose, setupGUI }; // Instructional text const instructionsText = textSplats({ @@ -400,6 +402,7 @@ export async function init({ THREE: _THREE, scene, camera, renderer, spark }) { } function dispose() { + disposed = true; window.removeEventListener("keydown", onKeyDown); // Disable controls to avoid interfering with other effects controls.fpsMovement.enable = false; diff --git a/examples/splat-transitions/effects/flow.js b/examples/splat-transitions/effects/flow.js index 664ccd1..1cbf72d 100644 --- a/examples/splat-transitions/effects/flow.js +++ b/examples/splat-transitions/effects/flow.js @@ -6,6 +6,7 @@ import { getAssetFileURL } from "/examples/js/get-asset-url.js"; export async function init({ THREE: _THREE, scene, camera, renderer, spark }) { const group = new THREE.Group(); scene.add(group); + let disposed = false; const PARAMETERS = { speedMultiplier: 0.5, @@ -148,7 +149,7 @@ export async function init({ THREE: _THREE, scene, camera, renderer, spark }) { const skyFile = "dali-env.glb"; const env = await loadGLB(skyFile, true); - group.add(env); + if (!disposed) group.add(env); const meshes = []; const period = dyno.dynoFloat(splatFiles.length); @@ -164,7 +165,7 @@ export async function init({ THREE: _THREE, scene, camera, renderer, spark }) { await m.initialized; m.position.copy(positions[i]); m.rotateX(Math.PI); - group.add(m); + if (!disposed) group.add(m); meshes.push(m); } @@ -299,6 +300,7 @@ export async function init({ THREE: _THREE, scene, camera, renderer, spark }) { } function dispose() { + disposed = true; scene.remove(group); } diff --git a/examples/splat-transitions/effects/morph.js b/examples/splat-transitions/effects/morph.js index 73718a7..bbd2b60 100644 --- a/examples/splat-transitions/effects/morph.js +++ b/examples/splat-transitions/effects/morph.js @@ -5,6 +5,7 @@ import { getAssetFileURL } from "/examples/js/get-asset-url.js"; export async function init({ THREE: _THREE, scene, camera, renderer, spark }) { const group = new THREE.Group(); scene.add(group); + let disposed = false; // Camera baseline for Morph effect camera.position.set(0, 2.2, 6.5); @@ -169,7 +170,7 @@ export async function init({ THREE: _THREE, scene, camera, renderer, spark }) { mesh.rotateX(Math.PI); mesh.position.set(0, 0, 0); mesh.scale.set(1.5, 1.5, 1.5); - group.add(mesh); + if (!disposed) group.add(mesh); meshes.push(mesh); } @@ -219,6 +220,7 @@ export async function init({ THREE: _THREE, scene, camera, renderer, spark }) { } function dispose() { + disposed = true; scene.remove(group); } diff --git a/examples/splat-transitions/effects/spheric.js b/examples/splat-transitions/effects/spheric.js index aca7ebb..040ba72 100644 --- a/examples/splat-transitions/effects/spheric.js +++ b/examples/splat-transitions/effects/spheric.js @@ -6,6 +6,7 @@ import { getAssetFileURL } from "/examples/js/get-asset-url.js"; export async function init({ THREE: _THREE, scene, camera, renderer, spark }) { const group = new THREE.Group(); scene.add(group); + let disposed = false; // Params and uniforms const PARAMETERS = { @@ -209,12 +210,12 @@ export async function init({ THREE: _THREE, scene, camera, renderer, spark }) { // Load env and assets const sky = await loadDelitGLB(skyFile, true); - group.add(sky); + if (!disposed) group.add(sky); const table = await loadDelitGLB(sceneFile, false); const sceneScale = 3.5; table.scale.set(sceneScale, sceneScale, sceneScale); table.position.set(-1, 0, -0.8); - group.add(table); + if (!disposed) group.add(table); const period = dyno.dynoFloat(splatFiles.length); const spereRadiusDyno = dyno.dynoFloat(PARAMETERS.spereRadius); @@ -233,7 +234,7 @@ export async function init({ THREE: _THREE, scene, camera, renderer, spark }) { spereRadiusDyno, sphereHeightDyno, ); - group.add(mesh); + if (!disposed) group.add(mesh); meshes.push(mesh); } @@ -267,6 +268,7 @@ export async function init({ THREE: _THREE, scene, camera, renderer, spark }) { } function dispose() { + disposed = true; // Remove group scene.remove(group); // No global listeners here; controls are managed by main diff --git a/examples/splat-transitions/main.js b/examples/splat-transitions/main.js index c6be847..f33e97f 100644 --- a/examples/splat-transitions/main.js +++ b/examples/splat-transitions/main.js @@ -46,8 +46,10 @@ const effectFiles = { let active = null; // { api, group } let last = 0; let effectFolder = null; // GUI folder for current effect +let switchCounter = 0; // guards concurrent effect switches async function switchEffect(name) { + const myToken = ++switchCounter; const loading = document.getElementById("loading"); loading.textContent = `Loading ${name}...`; loading.style.display = "block"; @@ -71,10 +73,25 @@ async function switchEffect(name) { const loader = effectFiles[name]; if (!loader) return; + const preChildren = new Set(scene.children); const mod = await loader(); + if (myToken !== switchCounter) { + // A newer switch started; ignore this one + return; + } const context = { THREE, scene, camera, renderer, spark }; const api = await mod.init(context); + if (myToken !== switchCounter) { + try { + api.dispose?.(); + } catch {} + // Remove any children added during this init + for (const child of [...scene.children]) { + if (!preChildren.has(child)) scene.remove(child); + } + return; + } if (api.group) scene.add(api.group); active = { api, group: api.group };