# 3D performance — measured UI and thumbnail stall fix

## Source of truth

This pass is based on the browser report `asuka-3d-perf-2026-06-14T18-56-06-716Z.json`, not on an assumed Babylon bottleneck.

The report showed:

- GLB preload: **292.1 ms**.
- Reusing the preloaded Babylon container: **33.7 ms**.
- Full Babylon `applyProduct`: **44.9 ms**.
- Element Editor Add completion: **517 ms**.
- Hover ActionManager callback: **0.6–1.6 ms**.
- Hover event propagation: **0.3–0.4 ms**.
- Camera input to first render: **1.4–3.8 ms**.
- Babylon render work normally stayed under **1.5 ms**.

The observed freezes therefore happened outside the measured Babylon operations. The report also showed large heap jumps around UI hover updates and preview captures:

- approximately **147 MB → 807 MB** after the first measured hover state update;
- approximately **431 MB → 1.23 GB → 1.83 GB** around later camera/preview activity;
- approximately **787–922 MB** immediately after Add, while Babylon Add had already completed.

## Causes corrected

### 1. 3D hover no longer rerenders AsukaRoot

The 3D hover listener previously called `setCanvasHover(...)`. Even though the hover state itself was small, that state belonged to `AsukaRoot`, so every part transition rerendered the full application tree while the product catalogue and its 23-million-character model data URL were reachable from application state.

3D hover now updates a dedicated tooltip DOM node through refs:

- opacity feedback remains owned by the Babylon hover implementation;
- tooltip label and position still change for each part;
- duplicate hover signatures are ignored;
- Fabric hover keeps its existing state-based path;
- no 3D hover event schedules a Preact root state update.

### 2. Camera motion no longer persists the complete scene

`engine3d.camera.changed` previously scheduled a full `exportView()` and store write after camera idle. Camera position is viewport state and is deliberately excluded from document history.

Camera changes no longer trigger full scene persistence. The mounted scene is still persisted for structural document mutations, history restoration, view switching and unmounting.

### 3. 3D thumbnails no longer encode the full WebGL canvas

The thumbnail caller passed `scale`, but the Babylon PNG exporter ignored it and called `canvas.toBlob()` on the full-size WebGL canvas before downscaling the resulting image.

The exporter now:

1. reads the requested scale or dimensions;
2. draws the WebGL frame directly into a small 2D canvas;
3. encodes only that small canvas;
4. logs the source size, target size, duration and resulting byte count.

Thumbnail capture is also:

- excluded from camera motion;
- delayed until the 3D interaction has been quiet;
- limited to one in-flight request;
- requested at a bounded low resolution.

### 4. Duplicate 3D facade notifications removed

Babylon already emits `engine3d.changed`. The generic render-engine facade also emitted a synchronous `renderEngine.changed` after the underlying 3D operation returned, causing another set of module refreshes before the caller regained control.

The generic facade is now silent for 3D. Fabric retains its generic notifications. 3D modules use the canonical `engine3d.changed` event.

### 5. Diagnostics now measure missed frames correctly

The old `render.long-frame` metric measured only the duration of `scene.render()`. It could report a 0.2 ms render even when JavaScript blocked the browser for one second before the next frame.

The logger now emits `render.frame-gap` whenever the interval between two render-loop callbacks exceeds 50 ms. Scene persistence, thumbnail capture and PNG export also have dedicated spans.

## Verification

- `tsc --noEmit`: OK.
- Vitest: 74/74 tests passed.
- Engine-neutrality audit: OK.
- Canonical 3D contract audit: OK.
- Functional hover diagnostics audit: OK.
- Add performance audit: OK.
- New UI-stall audit: OK.
- Product Vite build: OK.
- `src/` and `pass/src/` changed files are synchronized.

## Expected diagnostic markers in a new report

A new JSON report should contain:

- `hover-ui.dom-updated` instead of `hover-ui.changed-state-scheduled`;
- `render.frame-gap` for actual main-thread stalls;
- `thumbnail.capture:*`;
- `export.png:*` with small target dimensions;
- `scene-persist.export:*` and `scene-persist.store:*`;
- no scene persistence caused solely by camera movement.
