# Viewer API

`Viewer` 是 `u-space` 的核心类，它将 WebGPU 渲染器、场景、相机、控制器以及各种管理器封装为一个易用的接口。

## 构造函数

```typescript
new Viewer(options: ViewerOptions)
```

### `ViewerOptions`

| 属性              | 类型                       | 必填 | 说明                                                                                                           |
| :---------------- | :------------------------- | :--- | :------------------------------------------------------------------------------------------------------------- |
| `el`              | `HTMLElement`              | 是   | WebGPURenderer 的 canvas 将被注入到此 DOM 元素中。                                                             |
| `rendererOptions` | `WebGPURendererParameters` | 否   | 直接传递给底层 `WebGPURenderer` 的选项。默认使用 WebGPU 的高性能配置。                                         |
| `pixelRatio`      | `number`                     | 否   | Drawing buffer 像素比；默认 `Math.min(window.devicePixelRatio, 1.5)`。在 Retina 屏幕上使用 SSR、SSGI 或 TRAA 等重型全屏效果时，可设为 `1` 以降低 GPU 开销。 |

`pixelRatio` 应为有限正数，并会直接传给 Three.js。数值越高，画面越清晰，但完整 drawing buffer 以及依赖它的全屏后处理成本也会提高。

`Viewer` 默认启用 `reversedDepthBuffer`。`RenderPipeline` 内置 SSGI 和 SSR 均兼容该深度模式；只有业务明确需要标准深度时，才需要传入 `rendererOptions: { reversedDepthBuffer: false }`。

```typescript
const viewer = new Viewer({
  el: document.getElementById('app')!,
  pixelRatio: 1,
});
```

## 属性

`Viewer` 实例暴露了若干核心 Three.js 和 `u-space` 组件。

| 属性                 | 类型                                        | 说明                                                                                                              |
| :------------------- | :------------------------------------------ | :---------------------------------------------------------------------------------------------------------------- |
| `el`                 | `HTMLElement`                               | 容器元素。                                                                                                        |
| `renderer`           | `WebGPURenderer`                            | 底层 WebGPU 渲染器实例。                                                                                          |
| `scene`              | `Scene`                                     | 主 Three.js 场景，背景默认为 `0x000000`。                                                                         |
| `camera`             | `PerspectiveCamera` \| `OrthographicCamera` | 当前激活的相机。                                                                                                  |
| `controls`           | `CameraControls`                            | 相机控制器（基于 `camera-controls` 库）。                                                                         |
| `renderPipeline`     | `RenderPipeline`                            | 管理后处理通道和最终渲染调用。                                                                                    |
| `timer`              | `Timer`                                     | Three.js `Timer` 实例，用于每帧精确的 delta time 跟踪。                                                           |
| `roomEnvironment`    | `RoomEnvironment`                           | 为场景提供默认的类室内 IBL 环境贴图。                                                                             |
| `info`               | `Info`                                      | 以叠加层的形式渲染渲染器诊断信息（绘制调用次数、三角面数量等）。                                                   |
| `viewerHelper`       | `ViewerHelper`                              | 提供实用辅助工具（如坐标轴、网格显示），用于开发/调试。                                                           |
| `interactionManager` | `InteractionManager`                        | 管理对象的指针事件和射线检测。                                                                                    |
| `objectManager`      | `ObjectManager`                             | 用于按 ID 或名称注册和检索对象的工具。                                                                            |
| `frameloop`          | `'always'` \| `'demand'`                    | 设置渲染模式。默认为 `'demand'`（仅在需要时渲染）。设置为 `'always'` 可开启持续渲染。                             |
| `cssRenderer`        | `CSSRenderer`                               | CSS 渲染器，支持在 3D 场景中叠加 HTML 元素（CSS2D / CSS2.5D / CSS3D）。                                           |
| `frameCount`         | `number`                                    | 待渲染帧的内部计数器；同一刷新周期内的请求会合并，避免多个动画产生帧积压。                                         |

## 方法

### `init()`

异步初始化渲染器。如果需要确保渲染器（如环境设置或插件）完全就绪，必须调用此方法。

```typescript
await viewer.init();
```

### `render(frame?: number)`

请求渲染一帧。在 `'demand'` 模式下，每当场景发生视觉变化时必须调用此方法来更新画布。`frame` 指定渲染的帧数（默认 `1`）。返回一个在帧渲染完成后 resolve 的 `Promise<void>`。

```typescript
viewer.render();

// 等待帧渲染完成
await viewer.render();
```

### `invalidate(frame?: number)`

请求渲染但不创建 Promise。动画循环、插件更新等不需要等待渲染完成的高频路径应优先使用此方法，避免每帧分配 Promise 和一次性事件监听器。`frame` 指定至少保留的待渲染帧数（默认 `1`）；同一刷新周期内多次调用会合并，不会累加成渲染积压。

```typescript
viewer.invalidate();
```

### `setCamera(camera: PerspectiveCamera | OrthographicCamera)`

为查看器设置自定义相机，并自动更新控制器和交互管理器以使用该相机。

```typescript
const customCamera = new PerspectiveCamera(75, width / height, 0.1, 1000);
viewer.setCamera(customCamera);
```

### `setCameraByType(type: 'perspective' | 'orthographic')`

便捷方法，在保持查看器上下文的同时切换相机类型。

```typescript
viewer.setCameraByType('orthographic');
```

### `createScene()`

创建并返回一个黑色背景的新 `Scene`。由构造函数内部调用，也可用于重置/替换场景。

```typescript
viewer.scene = viewer.createScene();
```

### `createPerspectiveCamera()`

使用合理的默认值创建 `PerspectiveCamera`（视角 50°，近裁 `0.1`，远裁 `1e5`，位置在 `(5, 5, 5)`）。

```typescript
const camera = viewer.createPerspectiveCamera();
viewer.setCamera(camera);
```

### `createOrthographicCamera()`

创建与容器元素等大的 `OrthographicCamera`，近裁 `0.1`，远裁 `1e5`，位置在 `(5, 5, 5)`。

```typescript
const camera = viewer.createOrthographicCamera();
viewer.setCamera(camera);
```

### `resize(width?, height?)`

手动触发容器尺寸更新。可选传入指定宽高。

```typescript
// 手动触发 resize（如容器尺寸变化后）
viewer.resize();

// 指定新尺寸
viewer.resize(800, 600);
```

### `screenshot(options?)`

捕获当前渲染画面并返回 Data URL。

```typescript
const dataUrl = await viewer.screenshot();

// 指定格式和分辨率
const dataUrl = await viewer.screenshot({
  width: 1920,
  height: 1080,
  type: 'image/png',
  quality: 1.0,
});
```

#### `ScreenshotOptions`

| 属性      | 类型                                            | 默认值        | 说明                   |
| :-------- | :---------------------------------------------- | :------------ | :--------------------- |
| `width`   | `number`                                        | 容器宽度      | 截图宽度。             |
| `height`  | `number`                                        | 容器高度      | 截图高度。             |
| `type`    | `'image/png'` \| `'image/jpeg'` \| `'image/webp'` | `'image/png'` | 图片格式。             |
| `quality` | `number`                                        | `1.0`         | 图片质量（0-1）。      |

### `setBackground(background)`

设置场景背景颜色、纹理或清除背景。

```typescript
viewer.setBackground(0x333333);       // 颜色
viewer.setBackground('#87ceeb');      // CSS 颜色字符串
viewer.setBackground(hdrTexture);     // 纹理
viewer.setBackground(null);           // 清除背景
```

### `setEnvironment(envMap)`

设置场景环境贴图（用于反射/IBL）。

```typescript
viewer.setEnvironment(hdrTexture);
viewer.setEnvironment(null); // 清除
```

### `enableShadow()` / `disableShadow()`

开启或关闭阴影渲染。

```typescript
viewer.enableShadow();
viewer.disableShadow();
```

### `enableFog(options?)` / `enableFogExp2(options?)` / `disableFog()`

开启线性雾、指数雾或关闭雾效。

```typescript
// 线性雾
viewer.enableFog({ color: 0xcccccc, near: 10, far: 100 });

// 指数雾
viewer.enableFogExp2({ color: 0xcccccc, density: 0.01 });

// 关闭雾效
viewer.disableFog();
```

#### `FogOptions`

| 属性    | 类型                  | 默认值      | 说明           |
| :------ | :-------------------- | :---------- | :------------- |
| `color` | `ColorRepresentation` | `0xcccccc`  | 雾的颜色。     |
| `near`  | `number`              | `10`        | 雾的起始距离。 |
| `far`   | `number`              | `100`       | 雾的结束距离。 |

#### `FogExp2Options`

| 属性      | 类型                  | 默认值      | 说明           |
| :-------- | :-------------------- | :---------- | :------------- |
| `color`   | `ColorRepresentation` | `0xcccccc`  | 雾的颜色。     |
| `density` | `number`              | `0.01`      | 雾的密度。     |

## 调试工具

### Info

`viewer.info` 是一个轻量的渲染统计叠加层，显示当前帧的 GPU 诊断数据，适合开发调试阶段使用。

```typescript
viewer.info.enable();  // 在画面左下角显示统计信息
viewer.info.disable(); // 隐藏统计信息
```

启用后将在 `viewer.el` 左下角叠加以下数据：

| 指标           | 说明                          |
| :------------- | :---------------------------- |
| `draw calls`   | 当前帧的绘制调用次数          |
| `frame calls`  | 当前帧的帧调用次数            |
| `triangles`    | 当前帧渲染的三角面数量        |
| `points`       | 当前帧渲染的点数量            |
| `lines`        | 当前帧渲染的线段数量          |
| `timestamp`    | GPU 渲染耗时（ms，WebGPU 专属）|

> `timestamp` 指标仅在 WebGPU 后端可用，WebGL 回退模式下显示 `0`。

### ViewerHelper

`viewer.viewerHelper` 是一个方向指示器 gizmo（基于 Three.js `ViewHelper`），显示当前相机朝向的 XYZ 轴，渲染为画面角落的叠加层。

```typescript
viewer.viewerHelper.enable();  // 显示方向 gizmo
viewer.viewerHelper.disable(); // 隐藏方向 gizmo
```

**属性：**

| 属性           | 类型        | 说明                                      |
| :------------- | :---------- | :---------------------------------------- |
| `location`     | `object`    | gizmo 在画面中的位置，支持 `top`、`bottom`、`left`、`right` 偏移（像素）。默认右下角。 |

```typescript
// 调整位置到左下角
viewer.viewerHelper.location.left = 12;
viewer.viewerHelper.location.bottom = 12;
viewer.viewerHelper.location.right = null;
```

### `dispose()`

清理查看器，从 DOM 中移除 canvas，移除事件监听，并释放渲染器和环境贴图，以防止内存泄漏。

```typescript
viewer.dispose();
```

## OffscreenCanvas Worker（实验性）

`u-space/worker` 提供一个独立入口，可将 `Viewer`、Three.js 场景、相机控制和 WebGPU 渲染全部放入 dedicated render Worker。主线程只负责持有可见 canvas、转发输入事件与尺寸，并接收状态或渲染统计。模型默认仍在 render Worker 内加载；大型静态 glTF/SBMX 场景还可以 opt-in 第二个 decode Worker，让网络读取、SBMX 字节还原、JSON/base64 解析和图片解码不阻塞相机与已有场景的渲染。

Worker runtime 入口仅提供 ESM，需使用 `{ type: 'module' }` 创建 Worker。构建工具也必须保留 ES module 输出；Vite 项目需配置 `worker: { format: 'es' }`，否则 IIFE Worker 无法编译 top-level await。

主线程：

```typescript
import { OffscreenViewerHost } from 'u-space/worker';

const worker = new Worker(new URL('./scene.worker.ts', import.meta.url), {
  type: 'module',
});

const host = new OffscreenViewerHost({
  el: document.getElementById('app')!,
  worker,
});

host.addEventListener('ready', (event) => {
  console.log('scene ready', (event as CustomEvent).detail);
});

host.addEventListener('stats', (event) => {
  console.log((event as CustomEvent).detail);
});

await host.init();
```

`await host.init()` 只等待 Worker 创建 `Viewer`、完成 `await viewer.init()` 并发送 `initialized`，此时 WebGPU renderer 和内置 command 已可用，但 HDR、业务模型、editable batch、pipeline 预热和业务首帧可能仍在继续。`initialized` 事件与 `host.init()` resolve 表示同一个边界；`ready` 是业务层边界，只会在 Worker 代码显式调用一次 `worker.ready(detail)` 后触发。因此应在 `host.init()` 之前注册 `ready` / `status` / `stats` 等监听，业务 command 则通常在 `ready` 后调用。

### Worker 超大贴图保护

`createWorkerViewer()` 会在创建 `Viewer` 前安装 Worker 图片加载兼容层，同时覆盖 Three.js `ImageLoader` 和 glTF 常用的 `ImageBitmapLoader`。JPEG、PNG、GIF、WebP 在解码前读取编码尺寸；任一边超过 WebGPU 默认可移植上限 `8192` 时，会在第一次 `createImageBitmap()` 解码时按原宽高比缩小。无法预读尺寸的格式会在首次解码后检查，超限时生成缩放后的 bitmap，并立即 `close()` 临时原图。

该处理保留 `ImageBitmapLoader.setOptions()`、Three.js Cache、request headers、credentials 和 Loader/LoadingManager abort 语义，也不会修改磁盘或服务端源贴图。它避免在默认 `maxTextureDimension2D = 8192` 的 WebGPU device 上触发 `Texture size`，以及随后连续出现的 `Invalid TextureView`、`Invalid BindGroup` 和 `Invalid CommandBuffer`。即使 adapter 支持 `16384`，runtime 也不会强制申请更高 limit，以保持不同 GPU 的可移植性并限制超大贴图的显存占用。

自动缩放只覆盖经 `ImageLoader` / `ImageBitmapLoader` 加载的普通图片；KTX2 等压缩纹理由各自 loader 处理，业务仍应在资产管线中确保其尺寸不超过目标设备 limit。

### 独立模型 Decode Worker

在拥有 Three.js/WebGPU 的 render Worker 中，把一个专用 Worker 交给 `ModelLoaderManager`：

```typescript
import { ModelLoaderManager } from 'u-space';
import { createWorkerViewer } from 'u-space/worker/runtime';

const worker = await createWorkerViewer();
const modelDecodeWorker = new Worker(
  new URL('./model.decode.worker.ts', import.meta.url),
  { type: 'module' },
);

const disposeModelDecodeWorker = ModelLoaderManager.setDecodeWorker(modelDecodeWorker);
worker.onDispose(disposeModelDecodeWorker);
```

专用 decode Worker 入口不创建 `Viewer`，只安装解码协议：

```typescript
import { installModelDecodeWorker } from 'u-space/worker/model-decoder';

installModelDecodeWorker();
```

启用后，`ModelLoaderManager.loadAsync()` 会把受支持的静态 `.gltf`、`.glb` 和 `.sbmx` 请求排入 decode Worker。decode Worker 负责 fetch/Cache Storage、SBMX nibble swap、glTF JSON/base64/buffer 解析、普通图片解码和超大图片等比缩放；完成后以 transferable `ArrayBuffer` / `ImageBitmap` 把纯数据包发送给 render Worker。render Worker 才创建 `BufferGeometry`、`MeshStandardMaterial`、`Texture` 和 `Object3D`，因此 Three.js 对象和 GPU 资源仍只有一个 owner。

协议采用串行队列和 ACK backpressure：render Worker 重建完当前模型后才允许发送下一个大数据包，避免消息队列同时堆积多个模型副本。相同图片会按 SHA-256 内容身份复用已转移的 `ImageBitmap`，保持与 Three.js loader cache 一致的材质签名和 editable batching 数量；身份索引使用有界 LRU，不会随不同贴图持续增长。传输层直接使用浏览器 structured clone/transferable，没有额外二进制序列化依赖。

外部 buffer/贴图 URL 会回到 render Worker 经 `LoadingManager.resolveURL()` 处理，并完整触发 `itemStart` / `itemEnd` / `itemError`。模型请求的 headers 与 credentials 只会传给同源依赖；URL modifier 把依赖改写到其他 origin 时会自动去掉敏感 headers 并使用 `credentials: 'omit'`，避免授权信息泄漏给资产文件中声明的第三方地址。

当前快速路径只接受无 animation、skin、morph target、camera、sparse accessor、glTF extension/压缩扩展且 primitive mode 为 triangles 的 glTF 2 静态模型；节点图还必须满足无重复 child、无多父节点、无环及安全深度限制。其他受支持但不适合快速路径的模型会自动回退现有 `GLTFLoader` / `SBMXLoader`，不会降级功能；decode Worker 致命错误也会摘除失效 client 并回退当前请求。`ModelLoaderManager` 拥有传入的 Worker；再次调用 `setDecodeWorker()` 或传入 `null` 会 dispose 并 terminate 旧 Worker。`setDecodeWorker(worker)` 返回与该实例绑定的 disposer，建议交给 render runtime 的 `onDispose()`，避免旧清理回调误终止后来替换的 Worker。AbortSignal 同时覆盖 decode Worker 快速路径、普通 loader 顶层 fetch 和 glTF/SBMX 外部资源加载。

这个能力也可以在主线程的普通 `Viewer` 中启用，但它的主要收益是在 Offscreen render Worker 中把模型解码进一步拆开，使场景已可交互时仍能继续流式装载。它不减少网络体积、最终 GPU 内存或 draw calls；这些仍由资源压缩、instancing、editable batching、LOD 和裁剪解决。

### `OffscreenViewerHost`

构造选项：

| 选项 | 类型 | 必需 | 说明 |
| :--- | :--- | :--- | :--- |
| `el` | `HTMLElement` | 是 | 主线程容器；默认创建的 canvas 会挂载到这里。 |
| `worker` | `Worker` | 是 | 使用 `{ type: 'module' }` 创建的 dedicated Worker。 |
| `canvas` | `HTMLCanvasElement` | 否 | 自定义可见 canvas；省略时自动创建。 |
| `pixelRatio` | `number` | 否 | 发送给 Worker renderer 的 drawing-buffer pixel ratio；省略时使用 `Math.min(window.devicePixelRatio, 1.5)`。 |
| `terminateWorkerOnDispose` | `boolean` | 否 | `dispose()` 完成后是否终止 Worker，默认 `true`。 |

公开方法：

| 方法 | 说明 |
| :--- | :--- |
| `OffscreenViewerHost.isSupported()` | 检查当前环境是否具备 Worker、OffscreenCanvas 和 `transferControlToOffscreen()`。Worker 内是否暴露 WebGPU 仍会在初始化时检查。 |
| `init(): Promise<void>` | 绑定输入/resize，转移 canvas 并等待 Worker 的 `initialized` 消息；重复调用返回同一个 Promise。 |
| `request<T>(command, payload?)` | 调用 Worker 已注册或内置的 command。必须在 `init()` resolve 后调用；业务 command 通常还应等待 `ready`。 |
| `dispose(): void` | 停止事件与尺寸转发、reject 未完成的 Promise、移除 canvas，并通知 Worker 逆序释放业务资源和 `Viewer`。 |

Host 事件：

| 事件 | `detail` | 说明 |
| :--- | :--- | :--- |
| `initialized` | 无 | 与 `host.init()` resolve 相同的 Viewer 初始化边界。 |
| `ready` | `unknown` | Worker 调用一次 `worker.ready(detail)` 后的业务就绪边界。 |
| `status` | `string` | Worker 通过 `worker.status(message)` 上报的加载状态。 |
| `stats` | `WorkerRendererStats` | renderer 计数和滚动帧间隔统计。 |
| 自定义事件 | `unknown` | Worker 通过 `worker.emit(type, detail)` 上报；事件名为传入的 `type`。 |
| `error` | `Error` | Worker 初始化、未捕获异常或资源清理错误。 |
| `disposed` | 无 | Worker 已完成清理并关闭。 |

`WorkerRendererStats` 包含 `drawCalls`、`frameCalls`、`triangles`、`points`、`lines`、作为滚动平均帧间隔的 `frameTime`，以及 `frameTiming.samples` / `fps` / `average` / `p95` / `max` / `missedFramePercent`。其中 `missedFramePercent` 表示滚动窗口内超过 25ms 的样本占比；demand 模式的静止空闲间隔不会被作为渲染帧计入。

Worker（runtime 使用独立入口和 top-level await，避免主线程打包 Viewer/Three.js）：

```typescript
import { createWorkerViewer } from 'u-space/worker/runtime';

const worker = await createWorkerViewer();
const { viewer } = worker;

worker.status('loading scene');
const scene = await loadScene(viewer);
viewer.scene.add(scene);
await viewer.render();

worker.registerCommand('getObjectCount', () => viewer.scene.children.length);
worker.onDispose(() => disposeScene(scene));
worker.ready({ objects: viewer.scene.children.length });
```

`createWorkerViewer()` 必须在 module Worker 顶层立即调用。函数会先同步注册 host 消息监听，再返回一个等待 OffscreenCanvas 传入和 `viewer.init()` 完成的 Promise，因此可以安全使用 top-level await，不会丢失 `init` 消息。

返回的 `WorkerViewerRuntime` 提供：

| 属性 / 方法 | 说明 |
| :---------- | :--- |
| `viewer` / `canvas` | Worker 内的完整 `Viewer` 和已转移的 `OffscreenCanvas`。 |
| `status(message)` | 向主线程发送加载状态。 |
| `emit(type, detail?)` | 向主线程派发自定义事件；detail 必须可 structured clone。 |
| `registerCommand(name, handler)` | 注册 `host.request()` 可调用的业务命令，返回注销函数；重复命令名会抛错。 |
| `onDispose(handler)` | 注册 Worker 销毁回调，返回注销函数；多个回调按注册的逆序执行。 |
| `ready(detail?)` | 场景准备完成后发送一次 `ready` 事件；重复调用会抛错。 |

主线程应等待 `ready` 事件后，再用 `await host.request('getObjectCount')` 调用业务命令。runtime 还内置 `getStats`、`getViewpoint`、`setViewpoint` 和 `render` 命令，这些命令在 `host.init()` resolve 后即可调用。命令参数和返回值必须可被 structured clone；返回值中重复引用或嵌套在对象、数组、Map、Set、TypedArray / DataView 中的 `ArrayBuffer` 会去重后零拷贝转移，并从 Worker 端 detach。`Viewer`、`Object3D`、材质和 Geometry 等 Three.js 对象始终留在 Worker 内。

`ready()` 前发生的顶层未捕获错误或 Promise rejection 会先发送主线程 `error` 事件，再按逆序执行 `onDispose()`、释放 `Viewer`、发送 `disposed` 并关闭 Worker，避免失败的场景加载继续占用动画循环和 GPU 资源。Host 在初始化完成前被销毁，canvas 转移或初始 `postMessage()` 抛错，或收到 Worker 主动发送的 `disposed` 时，会 reject 尚未完成的 `init()` 和 `request()`，同时解除 DOM/resize 监听、清空待发送 pointer move、移除 canvas，并按 `terminateWorkerOnDispose` 完成 Worker 收尾。Host 一旦进入 disposed 状态，会忽略队列中迟到的 `initialized` / `ready` / `status` / `stats` / 自定义事件和 response，只保留清理阶段的 `error` / `disposed` 通知。

### UManager 与 editable batching

完整示例 `examples/offscreen/test_umanager2_offscreen.html` 会在 Worker 中加载 HDR、建筑语义模型和场景模型，并将 `SceneLoader.setEditableBatching()` 与 OffscreenCanvas 组合：

```typescript
import { SceneLoader } from 'u-space/plugins/u-manager';
import { createWorkerViewer } from 'u-space/worker/runtime';

const worker = await createWorkerViewer();
const { viewer } = worker;
// 在初始静态层级和相机定位完成后冻结每帧场景矩阵遍历。
viewer.scene.updateMatrixWorld(true);
viewer.scene.matrixWorldAutoUpdate = false;
const sceneLoader = new SceneLoader(viewer);
worker.onDispose(() => sceneLoader.dispose());
sceneLoader.setPath(SCENE_PATH);
sceneLoader.setKey(SCENE_KEY);
sceneLoader.setEditableBatching({
  maxVerticesPerBatch: 1_500_000,
  maxIndicesPerBatch: 4_500_000,
  freezeAnimations: true,
});

const scene = await sceneLoader.loadAsync();
const controlsEnabled = viewer.controls.enabled;
viewer.controls.enabled = false;
try {
  viewer.scene.add(scene);
  scene.updateWorldMatrix(true, true);
  viewer.invalidate();
  await viewer.renderer.compileAsync(viewer.scene, viewer.camera);
} finally {
  viewer.controls.enabled = controlsEnabled;
}
await viewer.render();

worker.ready({
  editableBatch: scene.userData.editableBatch,
  semanticOverlayHidden: false,
});
```

Geometry clone、矩阵烘焙、`mergeGeometries()`、TSL batch material 创建和 WebGPU 渲染提交都会在 Worker 中完成，不阻塞主线程 UI。`ready` 事件中的 `detail.editableBatch` 是普通统计对象，可以安全传回主线程；示例 HUD 同时显示 `drawCalls`、`frameTiming.fps`、1 秒滚动窗口的 `average` / `p95` / `max`、超过 25ms 的 `missedFramePercent`，以及 `sourceMeshes` / `batches` / `drawCallsSaved` / `unsupportedInstances` 和 `static scene matrices` 状态。`frameTime` 保留为滚动平均值；demand 模式长时间静止产生的空闲间隔不会纳入统计。

目标场景的压平语义楼层是半透明分析叠加层，并会有意覆盖建筑表面。示例保持 `SemanticGroup.visible = true`，并调用 `showAllFloors().showAllFacilities()`，让楼层、墙体、空间、门窗和设备等全部语义对象默认参与渲染。设备选择、飞向、高亮和清除命令均不改变语义对象显隐，“清除调试”只撤销高亮。业务项目可以按自身展示策略切换语义层，并继续通过跨楼层语义合批或 LOD 降低提交成本。示例保留标准 RenderPipeline scene pass，以维持源模型透明对象的正确离屏合成，不修改 drawing-buffer pixel ratio。

目标场景主要由静态普通 Mesh 组成，因此相机定位完成后先调用 Three.js 原生 `viewer.scene.updateMatrixWorld(true)` 提交当前层级，再设置 `viewer.scene.matrixWorldAutoUpdate = false`，移除每个渲染帧的整树更新；加载完场景模型后只调用 `scene.updateWorldMatrix(true, true)` 提交新增子树。后续 Worker command 修改单个普通 Mesh 或 `InstanceObject` 时调用 `object.updateWorldMatrix(true, false)`，修改 Group 时调用 `group.updateWorldMatrix(true, true)`，随后调用 `viewer.invalidate()` 请求渲染。`InstanceObject` 覆盖了 Three.js 原生 `updateWorldMatrix()`，会在显式提交后检测世界矩阵/祖先显隐变化并触发 dirty callback；`ModelInstancedLayer` 因而能同步 instance buffer，editable batch 中与烘焙矩阵不同的 `SceneInstanceObject` 也会按现有规则 materialize。对 Group 使用 `updateChildren: true` 时，其后代 `InstanceObject` 同样会执行该检测。相机矩阵不受此模式影响。

OffscreenCanvas 不会自动减少 draw calls；`setEditableBatching()` 也不会把主线程 UI 搬入 Worker。组合使用时，两者分别处理主线程 CPU 压力和 draw submission 压力。需要按 ID 显隐、变色、移动或 `materialize()` 时，应使用 `worker.registerCommand()` 注册业务 command，再由主线程调用 `host.request()`，不要尝试跨线程传输 `SceneInstanceObject`。

通过以下命令启动：

```bash
pnpm example:offscreen
```

当前边界：

- 需要同时支持 `transferControlToOffscreen()` 和 Worker WebGPU（`WorkerNavigator.gpu`）；目前应以最新版 Chromium 为主要目标，并保留普通 `Viewer` 回退路径。
- CSS2D/CSS3D、`Info` 等真实 DOM 叠加层不能放进 Worker，应在主线程用 Worker 状态/统计消息重建 UI。
- Worker 能移走场景解析、矩阵更新和渲染提交等主线程 CPU 工作，但不会减少 draw calls、三角形数量或 GPU 时间；GPU 瓶颈仍需用 instancing、LOD、遮挡/视锥裁剪等方式处理。
- `setEditableBatching()` 与 Worker 兼容，但它会展开重复 Geometry，仍需根据 `vertices` / `indices` 统计评估 GPU 内存；透明、蒙皮、morph 和自定义 shader 子集继续走安全 fallback。
- WebGPU OffscreenCanvas 截图尚未作为 Worker 内置命令开放；需要截图时应实现独立的 render-target readback 流程。

## 事件

Viewer 继承自 `EventDispatcher`，会触发以下事件：

- `beforeControlsUpdate`：在 `CameraControls` 更新之前触发，提供 `{ time: number; delta: number }`。
- `afterControlsUpdate`：在 `CameraControls` 更新之后触发，提供 `{ time: number; delta: number }`。
- `beforeRender`：在 `renderer.render` 调用之前立即触发，提供 `{ time: number; delta: number }`。
- `afterRender`：在 `renderer.render` 完成之后立即触发，提供 `{ time: number; delta: number }`。
- `cameraChange`：调用 `setCamera` 时触发，提供 `{ camera: Camera }`。

其中 `delta` 为上一帧到当前帧的时间间隔（秒），可用于动画更新等场景。
