Skip to content

fix(viewer): stop isolation and solo from overwriting each other's layer masks - #607

Open
toycenterboss-bot wants to merge 1 commit into
pascalorg:mainfrom
toycenterboss-bot:fix/layer-visibility
Open

fix(viewer): stop isolation and solo from overwriting each other's layer masks#607
toycenterboss-bot wants to merge 1 commit into
pascalorg:mainfrom
toycenterboss-bot:fix/layer-visibility

Conversation

@toycenterboss-bot

@toycenterboss-bot toycenterboss-bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Isolation (lib/isolation.ts) and solo's shadow-caster-only pass (lib/shadow-only.ts)
both hide an object by clearing its SCENE_LAYER bit, and each stashed the object's
previous layers.mask under its own private Symbol, restoring it wholesale on the
way out.

That holds while the two nest. Interleaved, the second to finish writes back a mask the
first has since changed:

  1. Solo a floor. Levels above it go shadow-caster-only; shadow-only stashes the
    original mask.
  2. Isolate a wall. isolation stashes the current mask — which is now the
    shadow-only one.
  3. Leave solo. shadow-only restores what it stashed, so every level gets its scene
    layer back while the isolation filter is still up. Everything the filter was
    hiding reappears.
  4. Clear isolation. isolation restores what it stashed, so the level ends up
    shadow-caster-only with nothing soloed — invisible until reload.

The other order (isolate → solo → clear isolation) breaks the same way, leaving a level
stuck in the shadow pass.

Fix. A small module, lib/scene-visibility.ts, owns Object3D.layers for both
features. Callers name a reason (isolated, shadow-only) rather than handing over a
mask. The snapshot is taken once, when the first reason arrives; the mask is recomputed
from that snapshot while any reason holds; the original is handed back only when the last
reason leaves. Order stops mattering, and the two duplicated stash implementations
collapse into one.

Behaviour is unchanged whenever only one of the two is active.

How to test

Automated — bun run test --filter @pascal-app/viewer:

  • src/lib/isolation.test.ts drives the real applyIsolation / applyShadowOnly pair in
    both interleavings. Both cases fail on main and pass here — I verified by
    restoring just isolation.ts and shadow-only.ts from main and re-running: 2 fail,
    1 pass (the third test covers solo being re-applied every frame, which was already
    correct).
  • src/lib/scene-visibility.test.ts covers the module itself: reason stacking, unwind
    order, idempotent re-hide, dropping a reason that was never held, and an object that was
    already off the scene layer before anything hid it.

Manually, in a scene with more than one level:

  1. Solo a lower level, so the levels above it go shadow-caster-only.
  2. Isolate a single wall on the soloed level.
  3. Leave solo — on main the hidden levels come back while the filter is still on; here
    they stay hidden.
  4. Clear isolation — on main the level stays invisible until reload; here everything
    returns to normal.

Checklist

  • Tested locally with bun dev
  • bun check clean
  • bun check-types clean
  • Targets main

Note

Medium Risk
Touches core viewer rendering visibility (layer masks) for isolation and solo; behavior is well covered by new tests but incorrect logic would show wrong geometry in the 3D view.

Overview
Fixes a bug where isolation and solo (shadow-only) each stashed and restored Object3D.layers.mask independently, so when both were active the feature that cleared second could restore a stale mask—e.g. hidden levels reappearing while isolation was still on, or a level stuck invisible after clearing isolation.

Introduces scene-visibility.ts as the single owner of scene-layer hiding: callers pass a reason (isolated | shadow-only) via hideFromScene / showInScene. The original mask is snapshotted once; the mask is recomputed while any reason remains; the snapshot is restored only when the last reason is dropped. isolation.ts and shadow-only.ts are wired through this API instead of per-feature Symbols.

Adds scene-visibility.test.ts (stacking, unwind order, idempotency) and isolation.test.ts (interleaved solo/isolation scenarios that failed on main).

Reviewed by Cursor Bugbot for commit 8c0d835. Bugbot is set up for automated code reviews on this repo. Configure here.

…yers

Both features hide an object by clearing its scene layer, and each stashed
the previous `layers.mask` under its own private Symbol, restoring it
wholesale on the way out. That only holds while they nest. Interleave them
and the second to finish writes back a mask the first has since changed:

  solo a floor, isolate a wall, leave solo

hands every level its scene layer straight back, so leaving solo un-hides
exactly what the isolation filter was hiding. Clearing the filter afterwards
then restores the mask isolation captured *during* solo, and the level is
stuck shadow-caster-only with nothing soloed — invisible until reload.

`lib/scene-visibility.ts` takes the mask over. Callers name a reason rather
than a mask, the mask is recomputed from the one snapshot taken when the
first reason arrived, and the original is handed back only when the last
reason leaves. Order stops mattering, and two duplicated stash
implementations collapse into one.

The new `isolation.test.ts` drives the real pair in both interleavings; both
cases fail on `main` and pass here.

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant