Skip to content

fix(viewer): UI flicker on camera move from interactive overlays + dirty-mark leaks#401

Merged
wass08 merged 5 commits into
mainfrom
fix/ui-blink-cursor-move
Jun 12, 2026
Merged

fix(viewer): UI flicker on camera move from interactive overlays + dirty-mark leaks#401
wass08 merged 5 commits into
mainfrom
fix/ui-blink-cursor-move

Conversation

@wass08

@wass08 wass08 commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Fixes the whole-UI flicker / frame starvation on camera moves in large scenes (reported on a project with hundreds of interactive recessed lights and ceiling fans — DOM UI blinking while the WebGPU canvas stayed at a healthy GPU time).

  • Interactive overlays (InteractiveSystem): every interactive item mounted a drei <Html occlude> unconditionally — invisible at opacity: 0 with no zone selected, but still raycasting the entire scene per overlay on every camera-move frame (bare occluderaycaster.intersectObjects([scene], true)) and churning z-index/display on hundreds of backdrop-filter DOM elements. Overlays now mount only while a zone is selected and the item is inside its polygon, fade in/out over 300ms, and drop occlude. eps={-1} works around a drei mount bug where freshly mounted overlays stayed mis-scaled until the camera moved (drei's mount path writes the transform without the distanceFactor scale and the eps guard never re-applies it while the camera is static).
  • Dirty-mark leaks: site/building/level/zone/guide have no dirty consumer (no def.geometry, no legacy system, no floorPlaced), so their marks were never cleared — accumulating all session and permanently defeating every system's dirtyNodes.size === 0 early exit. NodeDefinition gains a dirtyTracking?: boolean opt-out (default tracked), markDirty consults the registry, and the five structural kinds opt out. Also fixes deleteNodesAction leaving deleted ids in the dirty set forever.
  • Perf overlay: DIRTY now breaks down by kind — e.g. 29 (12 wall, 9 ceiling, 8 item) — non-zero kinds only, with a missing bucket, so future leaks are attributable at a glance.

Docs: new dirtyTracking section in wiki/architecture/node-definitions.md. Tests: use-scene-dirty-tracking.test.ts (opt-out skipped, default tracked, deletes clear marks).

How to test

  1. bun dev, open a large project with many interactive items (recessed lights / ceiling fans), append ?perf to the URL.
  2. Orbit the camera with no zone selected — no UI flicker, FPS holds at the 50fps cap, and DIRTY sits at 0 when idle (previously stale zone/guide/level/site/building entries).
  3. Select a zone — controls for that zone's items fade in at the correct scale immediately (no mis-scaled flash, no waiting for a camera move); items outside the zone show no overlay.
  4. Deselect the zone (or switch zones) — overlays fade out instead of vanishing abruptly.
  5. Create then delete a wall — DIRTY returns to 0 (no missing entries).
  6. bun test in packages/core and packages/nodes; bun run check-types.

Screenshots / screen recording

N/A — perf/behavior fix; before/after is best seen live via step 2 (reporter confirmed the flicker fix on the affected project).

Checklist

  • I've tested this locally with bun dev
  • My code follows the existing code style (run bun check to verify)
  • I've updated relevant documentation (if applicable)
  • This PR targets the main branch

wass08 and others added 5 commits June 12, 2026 09:57
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ering the UI

Every interactive item mounted a drei <Html occlude> overlay
unconditionally — invisible (opacity 0) when no zone was selected, but
still alive. With `occlude` as a bare boolean, drei raycasts the entire
scene per overlay on every camera-move frame, and rewrites each
element's z-index while toggling display when the occlusion flips. On
scenes with hundreds of interactive items (recessed lights, ceiling
fans) this starved the frame budget and made the whole DOM UI blink
during camera moves while the WebGPU canvas stayed healthy.

Overlays now mount only while a zone is selected and the item sits
inside its polygon, fade in/out over 300ms (the child components stay
rendered so the exit transition can play before the <Html> unmounts),
and drop `occlude` entirely. eps=-1 works around a drei mount bug: its
mount path writes the element transform without the distanceFactor
scale, and with a static camera the eps guard never re-applies it, so
freshly mounted overlays stayed mis-scaled until the camera moved.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DIRTY now reads e.g. "29 (12 wall, 9 ceiling, 8 item)" — sorted by
count, only non-zero kinds, with a "missing" bucket for dirty ids whose
node no longer exists. Makes dirty-mark leaks attributable at a glance.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
dirtyNodes is consumed by GeometrySystem (def.geometry),
FloorElevationSystem (capabilities.floorPlaced), and the legacy
per-kind viewer systems. Site, building, level, zone, and guide match
none of those, so their marks were never cleared: they accumulated for
the whole session (every child create/delete dirties its parent),
permanently defeated every consumer's empty-set early exit each frame,
and polluted the perf overlay's DIRTY readout.

NodeDefinition gains an explicit dirtyTracking?: boolean opt-out
(default tracked — no derivable predicate exists since wall's dirty
consumption lives in the viewer while zone/guide/level declare
def.system for unrelated per-frame work). markDirty consults the
registry; the five structural kinds opt out.

Also fixes a second leak: deleteNodesAction never removed deleted ids
from the dirty set, and every consumer skips missing nodes without
clearing them, so marks on deleted nodes lived forever.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
biome's useIterableCallbackReturn rejects forEach callbacks that
implicitly return a value (Set.add / clearDirty).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@wass08 wass08 merged commit cf24b62 into main Jun 12, 2026
2 of 3 checks passed
@mintlify

mintlify Bot commented Jun 12, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
pascal 🔴 Failed Jun 12, 2026, 2:39 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

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