# Batches API

`u-space` 的 `src/batches` 模块提供两种共享渲染聚合层。`ModelInstancedLayer` 面向重复模板的 `InstancedMesh` 渲染；`EditableGeometryBatchLayer` 面向大量普通、主要静态但仍需按对象控制的 Geometry 烘焙合并。两者都从顶层 `u-space` 导出，并使用 `InstanceObject` 作为逻辑对象协议。

```typescript
import {
  EditableGeometryBatchLayer,
  InstanceObject,
  ModelInstancedLayer,
  type EditableGeometryBatchOptions,
  type EditableGeometryBatchStats,
} from 'u-space';
```

## 职责边界

| 模块 | 职责 |
| :--- | :--- |
| `src/instances` | `InstanceObject` 的身份、transform、样式、bounds、dirty 和 materialize 协议。 |
| `src/batches` | `InstanceObject` 对应的批量渲染、GPU 状态同步、raycast remap 和资源生命周期。 |
| 插件/业务 Loader | 模型加载、URL/业务 ID、unsupported fallback 和场景树装配策略。 |

## `ModelInstancedLayer`

`ModelInstancedLayer<T extends InstanceObject>` 继承自 `BaseGroup`。它按模板 key 创建 `InstancedMesh` batch，保留模板中的独立 Mesh 以维持多材质、透明排序和局部包围体，并在实例 dirty 时同步 matrix、颜色、透明度与可见实例集合。

常用方法：

| 方法 | 说明 |
| :--- | :--- |
| `reserveBatch(key, template, capacity)` | 为模板预分配实例容量。 |
| `addInstance(key, template, instance)` / `addInstances(...)` | 添加一个或多个逻辑实例。 |
| `getInstances()` / `getInstanceById(id)` | 枚举实例或按 `instanceId` 查询。 |
| `removeInstance()` / `removeInstances()` | 按对象或 ID 删除实例。 |
| `removeBatch(key)` / `clearBatches()` | 删除一个 batch 或清空全部 batch。 |
| `setInstanceCulling(options)` | 配置可选的逐实例视锥和屏幕尺寸裁剪。 |

复杂静态模板在射线通过实例包围体后会按需建立 `three-mesh-bvh`；命中会映射回对应 `InstanceObject`。修改实例 transform 时只需操作实例对象；根 Scene 冻结自动矩阵更新时，再调用 `instance.updateWorldMatrix(true, false)` 和 `viewer.invalidate()`。

## `EditableGeometryBatchLayer`

`EditableGeometryBatchLayer<T extends InstanceObject>` 继承自 `BaseGroup`。它把兼容 Mesh 的 transform 烘焙进克隆 Geometry，按材质、Geometry attribute/index 布局、阴影和 `renderOrder` 分组，再通过 `mergeGeometries()` 合并为少量内部 `BaseMesh`。内部 Mesh 是实现细节，不从公共 API 导出。

### 配置与统计

| `EditableGeometryBatchOptions` 字段 | 默认值 | 说明 |
| :--- | :--- | :--- |
| `maxVerticesPerBatch` | `1_500_000` | 单个 merged Geometry 的最大顶点数。 |
| `maxIndicesPerBatch` | `4_500_000` | 单个 merged Geometry 的最大索引数。 |
| `freezeAnimations` | `false` | 是否允许烘焙带 animation clip/mixer 模板的当前姿态；SkinnedMesh 和 morph target 仍 fallback。 |

`stats` / `build().stats` 包含 `instances`、`sourceMeshes`、`batches`、`drawCallsSaved`、`vertices`、`indices`、`unsupportedInstances` 和 `unsupportedByReason`。

### 创建和构建

```typescript
import {
  EditableGeometryBatchLayer,
  InstanceObject,
  Model,
} from 'u-space';

const layer = new EditableGeometryBatchLayer<InstanceObject>({
  maxVerticesPerBatch: 1_500_000,
  maxIndicesPerBatch: 4_500_000,
});

const template = new Model();
const instances = [new InstanceObject(), new InstanceObject()];

layer.addSource({
  key: '/models/building.glb',
  template,
  instances,
});

scene.add(layer, ...instances);
scene.updateMatrixWorld(true);

layer.addEventListener('materialize', ({ instance, object }) => {
  console.log('materialized', instance.instanceId, object);
});

const result = layer.build();
console.table(result.stats);
```

`addSource()` 接收 `key`、`Model` 模板和同一模板对应的实例数组。全部 source 添加完成后调用一次 `build()`；layer 是 one-shot builder，重复 `build()`、build 后继续 `addSource()` 或 dispose 后复用都会抛出错误。同一个 `InstanceObject` 在一个 layer 中只能出现一次，重复 source 会直接抛错，避免生成无法独立隐藏或拾取的重复烘焙几何。`build()` 返回统计信息，以及没有完全进入 merged Geometry 的 `unsupported` 数组。每个 unsupported 项包含 `key`、fallback `template`、`instances` 和 `reasons`，由调用方决定继续 instancing 还是挂载普通 `Model`。

透明材质、SkinnedMesh、morph target、多材质数组、自定义 NodeMaterial / `onBeforeCompile`、不兼容 Geometry 布局、负行列式变换和未冻结动画会进入 unsupported 路径。负行列式会按实例拆分，正 determinant 的同模板实例仍可合并；`negative-scale` 实例必须使用普通 `Model` fallback，不能继续交给不支持负缩放的 `InstancedMesh`。混合模板仍会合并兼容的不透明子集，并返回只保留不支持子 Mesh 的 fallback 模板。

### 对象状态与 materialize

每个逻辑对象在 Float `DataTexture` 中占一个 RGBA texel。内部 NodeMaterial 通过 TSL 和顶点 `batchObjectIndex` attribute 读取显隐、颜色模式和颜色，因此普通显隐、颜色和不引入半透明的高亮只更新 dirty texel，不重新合并 Geometry。兼容 batch 只包含不透明源材质，并保留源材质原有的 opacity 路径；对象显隐使用独立布尔 `maskNode`，避免动态 opacity 同时参与 alpha 输出和 discard 判断而改变楼壳外观。dirty callback 会立即在 CPU 侧同步对应状态，GPU texture 在下一帧上传；transform materialize 不依赖内部 batch 先通过视锥裁剪，因此从屏幕外移动到屏幕内也不会丢失。

实例 transform 偏离烘焙矩阵或有效 opacity 低于 `0.999` 时，layer 会 clone 完整模板、为本次 materialize 创建独立材质并调用 `InstanceObject.setInstanceRenderObject()`，同时 mask merged Geometry 中的旧副本。新挂载对象会立即提交世界矩阵，冻结根 Scene 时也不会错过当前帧；若另一个 layer materialize 了同一逻辑实例，旧 layer 会通过 dirty callback 隐藏自己的烘焙副本。不同 materialized 实例可以安全使用不同 opacity/highlight，不会反向修改模板或其他实例。业务也可以主动调用 `instance.materialize()`。materialize 成功后 layer 会派发类型化的 `materialize` 事件；如果需要观察 build 期间由初始 opacity 触发的 materialize，应像上例一样在 `build()` 前注册监听：

```typescript
const model = instances[0].materialize();
```

`setInstanceMaterializer()` / `clearInstanceMaterializer(materializer?)` 是 batching 实现连接 `InstanceObject` 的低层协议，普通业务不需要直接设置。带 delegate 参数的 clear 只会解除同一个 materializer，避免旧 layer dispose 时清除后来接管实例的新 layer。materialize 是单向操作；恢复原 transform 或 opacity 不会自动重新进入 batch。

### Raycast 与释放

内部 merged Mesh 首次拾取时按需建立 `three-mesh-bvh`，再通过 `faceIndex → vertexIndex → batchObjectIndex` 将命中映射回原始 `InstanceObject`。隐藏 layer 或内部 batch Mesh 会遵循 `ignoreInvisibleWhenRaycast`；已经 materialize 的 state 会从 merged BVH 命中过滤，旧烘焙位置不会残留 ghost picking。

不再使用 layer 时必须调用 `dispose()`。它会取消 dirty 订阅、仅解除仍由当前 layer 持有的 instance materializer、释放状态纹理、Geometry 和克隆材质，并清空内部节点。materialized 普通对象及其独立材质已经交给对应 `InstanceObject`，不由 layer dispose。

## 与 `THREE.BatchedMesh` 的区别

`EditableGeometryBatchLayer` 不是 `THREE.BatchedMesh`。它选择把主要静态 Geometry 真正烘焙合并，并只同步 dirty 对象状态，以减少 draw submission 和逐对象 render-list 工作；代价是展开重复 Geometry、增加 GPU 顶点内存，并在 transform 或半透明变化时 materialize 当前对象。高重复、较大且经常变换的同模板对象通常更适合 `ModelInstancedLayer`。
