u-space-mcp 0.0.13 → 0.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/dist/generated/docs-index.js +323 -33
- package/dist/generated/docs-index.js.map +1 -1
- package/package.json +6 -1
|
@@ -32,6 +32,51 @@ export const docsIndex = [
|
|
|
32
32
|
],
|
|
33
33
|
"content": "# Animations API\n\n`u-space` 提供了 `Tween` 类和 `tweenAnimation` 辅助函数,用于对任意数值属性进行动画处理。两者都是对 [`@tweenjs/tween.js`](https://github.com/tweenjs/tween.js) 的轻量封装,并自动与 `Viewer` 渲染循环集成。\n\n## `tweenAnimation`\n\n对任意对象属性执行动画的最简方式,返回一个在动画完成后 resolve 的 `Promise`。\n\n```typescript\nimport { tweenAnimation } from 'u-space';\n\nconst source = { x: 0, y: 0, z: 0 };\n\nawait tweenAnimation(\n viewer,\n source, // 可变的起始状态(每帧被修改)\n { x: 10, y: 5, z: 10 }, // 目标状态\n {\n duration: 1500, // 毫秒\n delay: 0,\n mode: 'Cubic.InOut',\n repeat: false,\n yoyo: false,\n },\n (current) => {\n // 每帧以插值结果调用\n myObject.position.set(current.x, current.y, current.z);\n },\n);\n```\n\n### `AnimationOptions`\n\n| 属性 | 类型 | 默认值 | 说明 |\n| :--------- | :-------------------------- | :-------------- | :------------------------------------------------- |\n| `duration` | `number` | `1000` | 动画时长(毫秒)。 |\n| `delay` | `number` | `0` | 开始延迟(毫秒)。 |\n| `mode` | `AnimationModeType` | `'Linear.None'` | 缓动函数。 |\n| `repeat` | `number \\| boolean` | `false` | 额外重复次数,或 `true` 表示无限循环。 |\n| `yoyo` | `boolean` | `false` | 在每个循环周期反向播放。 |\n\n### `AnimationModeType`\n\n支持所有标准缓动模式:\n\n`Linear.None` · `Quadratic.In/Out/InOut` · `Cubic.In/Out/InOut` · `Quartic.In/Out/InOut` · `Quintic.In/Out/InOut` · `Sinusoidal.In/Out/InOut` · `Exponential.In/Out/InOut` · `Circular.In/Out/InOut` · `Elastic.In/Out/InOut` · `Back.In/Out/InOut` · `Bounce.In/Out/InOut`\n\n## `Tween`\n\n提供完全控制的底层类。继承自 `tween.js` 的基础 `Tween`,并通过 `addEventListener('afterControlsUpdate', ...)` 挂入 `Viewer` 事件循环。\n\n```typescript\nimport { Tween } from 'u-space';\n\nconst source = { opacity: 1 };\n\nconst tween = new Tween(viewer, source)\n .to({ opacity: 0 }, 800)\n .easingByMode('Sinusoidal.Out')\n .onUpdate((s) => {\n myMaterial.opacity = s.opacity;\n viewer.render();\n })\n .onComplete(() => console.log('完成'));\n\ntween.start();\n// tween.stop();\n```\n\n### 方法\n\n| 方法 | 说明 |\n| :------------------------ | :-------------------------------------------------------------- |\n| `easingByMode(mode)` | 使用 `AnimationModeType` 设置缓动函数的便捷简写。 |\n| `start(time?)` | 启动补间并注册到 viewer 循环中。 |\n| `stop()` | 停止补间并从 viewer 循环中注销。 |\n\n其他方法(`to`、`delay`、`repeat`、`yoyo`、`onUpdate`、`onComplete`、`onStop`、`onStart`)均继承自 `tween.js` 的基础 `Tween` 类。\n"
|
|
34
34
|
},
|
|
35
|
+
{
|
|
36
|
+
"id": "api-batches",
|
|
37
|
+
"path": "api-batches.md",
|
|
38
|
+
"title": "Batches API",
|
|
39
|
+
"summary": "`u-space` 的 `src/batches` 模块提供两种共享渲染聚合层。`ModelInstancedLayer` 面向重复模板的 `InstancedMesh` 渲染;`EditableGeometryBatchLayer` 面向大量普通、主要静态但仍需按对象控制的 Geometry 烘焙合并。两者都从顶层 `u-space` 导出,并使用 `InstanceObject` 作为逻辑对象协议。 ```typescript import {",
|
|
40
|
+
"headings": [
|
|
41
|
+
{
|
|
42
|
+
"level": 1,
|
|
43
|
+
"text": "Batches API"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"level": 2,
|
|
47
|
+
"text": "职责边界"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"level": 2,
|
|
51
|
+
"text": "ModelInstancedLayer"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"level": 2,
|
|
55
|
+
"text": "EditableGeometryBatchLayer"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"level": 3,
|
|
59
|
+
"text": "配置与统计"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"level": 3,
|
|
63
|
+
"text": "创建和构建"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"level": 3,
|
|
67
|
+
"text": "对象状态与 materialize"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"level": 3,
|
|
71
|
+
"text": "Raycast 与释放"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"level": 2,
|
|
75
|
+
"text": "与 THREE.BatchedMesh 的区别"
|
|
76
|
+
}
|
|
77
|
+
],
|
|
78
|
+
"content": "# Batches API\n\n`u-space` 的 `src/batches` 模块提供两种共享渲染聚合层。`ModelInstancedLayer` 面向重复模板的 `InstancedMesh` 渲染;`EditableGeometryBatchLayer` 面向大量普通、主要静态但仍需按对象控制的 Geometry 烘焙合并。两者都从顶层 `u-space` 导出,并使用 `InstanceObject` 作为逻辑对象协议。\n\n```typescript\nimport {\n EditableGeometryBatchLayer,\n InstanceObject,\n ModelInstancedLayer,\n type EditableGeometryBatchOptions,\n type EditableGeometryBatchStats,\n} from 'u-space';\n```\n\n## 职责边界\n\n| 模块 | 职责 |\n| :--- | :--- |\n| `src/instances` | `InstanceObject` 的身份、transform、样式、bounds、dirty 和 materialize 协议。 |\n| `src/batches` | `InstanceObject` 对应的批量渲染、GPU 状态同步、raycast remap 和资源生命周期。 |\n| 插件/业务 Loader | 模型加载、URL/业务 ID、unsupported fallback 和场景树装配策略。 |\n\n## `ModelInstancedLayer`\n\n`ModelInstancedLayer<T extends InstanceObject>` 继承自 `BaseGroup`。它按模板 key 创建 `InstancedMesh` batch,保留模板中的独立 Mesh 以维持多材质、透明排序和局部包围体,并在实例 dirty 时同步 matrix、颜色、透明度与可见实例集合。\n\n常用方法:\n\n| 方法 | 说明 |\n| :--- | :--- |\n| `reserveBatch(key, template, capacity)` | 为模板预分配实例容量。 |\n| `addInstance(key, template, instance)` / `addInstances(...)` | 添加一个或多个逻辑实例。 |\n| `getInstances()` / `getInstanceById(id)` | 枚举实例或按 `instanceId` 查询。 |\n| `removeInstance()` / `removeInstances()` | 按对象或 ID 删除实例。 |\n| `removeBatch(key)` / `clearBatches()` | 删除一个 batch 或清空全部 batch。 |\n| `setInstanceCulling(options)` | 配置可选的逐实例视锥和屏幕尺寸裁剪。 |\n\n复杂静态模板在射线通过实例包围体后会按需建立 `three-mesh-bvh`;命中会映射回对应 `InstanceObject`。修改实例 transform 时只需操作实例对象;根 Scene 冻结自动矩阵更新时,再调用 `instance.updateWorldMatrix(true, false)` 和 `viewer.invalidate()`。\n\n## `EditableGeometryBatchLayer`\n\n`EditableGeometryBatchLayer<T extends InstanceObject>` 继承自 `BaseGroup`。它把兼容 Mesh 的 transform 烘焙进克隆 Geometry,按材质、Geometry attribute/index 布局、阴影和 `renderOrder` 分组,再通过 `mergeGeometries()` 合并为少量内部 `BaseMesh`。内部 Mesh 是实现细节,不从公共 API 导出。\n\n### 配置与统计\n\n| `EditableGeometryBatchOptions` 字段 | 默认值 | 说明 |\n| :--- | :--- | :--- |\n| `maxVerticesPerBatch` | `1_500_000` | 单个 merged Geometry 的最大顶点数。 |\n| `maxIndicesPerBatch` | `4_500_000` | 单个 merged Geometry 的最大索引数。 |\n| `freezeAnimations` | `false` | 是否允许烘焙带 animation clip/mixer 模板的当前姿态;SkinnedMesh 和 morph target 仍 fallback。 |\n\n`stats` / `build().stats` 包含 `instances`、`sourceMeshes`、`batches`、`drawCallsSaved`、`vertices`、`indices`、`unsupportedInstances` 和 `unsupportedByReason`。\n\n### 创建和构建\n\n```typescript\nimport {\n EditableGeometryBatchLayer,\n InstanceObject,\n Model,\n} from 'u-space';\n\nconst layer = new EditableGeometryBatchLayer<InstanceObject>({\n maxVerticesPerBatch: 1_500_000,\n maxIndicesPerBatch: 4_500_000,\n});\n\nconst template = new Model();\nconst instances = [new InstanceObject(), new InstanceObject()];\n\nlayer.addSource({\n key: '/models/building.glb',\n template,\n instances,\n});\n\nscene.add(layer, ...instances);\nscene.updateMatrixWorld(true);\n\nlayer.addEventListener('materialize', ({ instance, object }) => {\n console.log('materialized', instance.instanceId, object);\n});\n\nconst result = layer.build();\nconsole.table(result.stats);\n```\n\n`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`。\n\n透明材质、SkinnedMesh、morph target、多材质数组、自定义 NodeMaterial / `onBeforeCompile`、不兼容 Geometry 布局、负行列式变换和未冻结动画会进入 unsupported 路径。负行列式会按实例拆分,正 determinant 的同模板实例仍可合并;`negative-scale` 实例必须使用普通 `Model` fallback,不能继续交给不支持负缩放的 `InstancedMesh`。混合模板仍会合并兼容的不透明子集,并返回只保留不支持子 Mesh 的 fallback 模板。\n\n### 对象状态与 materialize\n\n每个逻辑对象在 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 先通过视锥裁剪,因此从屏幕外移动到屏幕内也不会丢失。\n\n实例 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()` 前注册监听:\n\n```typescript\nconst model = instances[0].materialize();\n```\n\n`setInstanceMaterializer()` / `clearInstanceMaterializer(materializer?)` 是 batching 实现连接 `InstanceObject` 的低层协议,普通业务不需要直接设置。带 delegate 参数的 clear 只会解除同一个 materializer,避免旧 layer dispose 时清除后来接管实例的新 layer。materialize 是单向操作;恢复原 transform 或 opacity 不会自动重新进入 batch。\n\n### Raycast 与释放\n\n内部 merged Mesh 首次拾取时按需建立 `three-mesh-bvh`,再通过 `faceIndex → vertexIndex → batchObjectIndex` 将命中映射回原始 `InstanceObject`。隐藏 layer 或内部 batch Mesh 会遵循 `ignoreInvisibleWhenRaycast`;已经 materialize 的 state 会从 merged BVH 命中过滤,旧烘焙位置不会残留 ghost picking。\n\n不再使用 layer 时必须调用 `dispose()`。它会取消 dirty 订阅、仅解除仍由当前 layer 持有的 instance materializer、释放状态纹理、Geometry 和克隆材质,并清空内部节点。materialized 普通对象及其独立材质已经交给对应 `InstanceObject`,不由 layer dispose。\n\n## 与 `THREE.BatchedMesh` 的区别\n\n`EditableGeometryBatchLayer` 不是 `THREE.BatchedMesh`。它选择把主要静态 Geometry 真正烘焙合并,并只同步 dirty 对象状态,以减少 draw submission 和逐对象 render-list 工作;代价是展开重复 Geometry、增加 GPU 顶点内存,并在 transform 或半透明变化时 materialize 当前对象。高重复、较大且经常变换的同模板对象通常更适合 `ModelInstancedLayer`。\n"
|
|
79
|
+
},
|
|
35
80
|
{
|
|
36
81
|
"id": "api-camera-controls",
|
|
37
82
|
"path": "api-camera-controls.md",
|
|
@@ -110,7 +155,7 @@ export const docsIndex = [
|
|
|
110
155
|
"id": "api-effects",
|
|
111
156
|
"path": "api-effects.md",
|
|
112
157
|
"title": "Effects API",
|
|
113
|
-
"summary": "`u-space` 提供了两个基于 Three.js 着色语言(TSL/WebGPU 节点)的静态特效工具:`MaterialEffects` 用于为对象应用高亮、呼吸等材质特效,`TSLEffects` 用于生成动态颜色节点模式。 静态工具类,将基于 TSL 的视觉特效直接应用于对象的材质。适用于任何 `Object3D`(支持单个或数组),会自动遍历所有子网格。 当对象是 `
|
|
158
|
+
"summary": "`u-space` 提供了两个基于 Three.js 着色语言(TSL/WebGPU 节点)的静态特效工具:`MaterialEffects` 用于为对象应用高亮、呼吸等材质特效,`TSLEffects` 用于生成动态颜色节点模式。 静态工具类,将基于 TSL 的视觉特效直接应用于对象的材质。适用于任何 `Object3D`(支持单个或数组),会自动遍历所有子网格。 当对象是 `InstanceObject`(例如 `SceneInstanceObject`、`FacilityInstanceObject` 或楼层里的 `FloorSemanticInstanceObject`)时,`highlightColor()` / `removeHighlightColor()` 会通过 `setInstanceHighlight()` / `clearInstanceHighlight()` 改",
|
|
114
159
|
"headings": [
|
|
115
160
|
{
|
|
116
161
|
"level": 1,
|
|
@@ -173,13 +218,13 @@ export const docsIndex = [
|
|
|
173
218
|
"text": "TSLEffects.fluid(parameters?)"
|
|
174
219
|
}
|
|
175
220
|
],
|
|
176
|
-
"content": "# Effects API\n\n`u-space` 提供了两个基于 Three.js 着色语言(TSL/WebGPU 节点)的静态特效工具:`MaterialEffects` 用于为对象应用高亮、呼吸等材质特效,`TSLEffects` 用于生成动态颜色节点模式。\n\n## `MaterialEffects`\n\n静态工具类,将基于 TSL 的视觉特效直接应用于对象的材质。适用于任何 `Object3D`(支持单个或数组),会自动遍历所有子网格。\n当对象是 `
|
|
221
|
+
"content": "# Effects API\n\n`u-space` 提供了两个基于 Three.js 着色语言(TSL/WebGPU 节点)的静态特效工具:`MaterialEffects` 用于为对象应用高亮、呼吸等材质特效,`TSLEffects` 用于生成动态颜色节点模式。\n\n## `MaterialEffects`\n\n静态工具类,将基于 TSL 的视觉特效直接应用于对象的材质。适用于任何 `Object3D`(支持单个或数组),会自动遍历所有子网格。\n当对象是 `InstanceObject`(例如 `SceneInstanceObject`、`FacilityInstanceObject` 或楼层里的 `FloorSemanticInstanceObject`)时,`highlightColor()` / `removeHighlightColor()` 会通过 `setInstanceHighlight()` / `clearInstanceHighlight()` 改写该实例的颜色和透明度,而不是改动共享 batch 材质。\n`InstanceObject` 支持 `overwrite` 的替换/染色语义;`depthWrite` 属于共享 batch 材质状态,不能按单个实例设置,因此对该类对象不会生效。\n\n特性:\n- **效果可叠加**:高亮和呼吸效果可同时作用于同一对象,呼吸在高亮结果之上混合\n- **共享材质安全**:按材质跟踪引用计数,多个 Mesh 共享材质时互不干扰\n- **完整还原**:移除效果时自动恢复材质原始的 `colorNode`、`opacityNode`、`transparent` 和 `depthWrite` 状态\n\n### `MaterialEffects.highlightColor(object, options?)`\n\n为对象中所有网格应用颜色/透明度高亮。\n\n```typescript\nimport { MaterialEffects } from 'u-space';\n\n// 以 50% 透明度高亮为红色(叠加模式)\nMaterialEffects.highlightColor(myModel, {\n color: 0xff0000,\n opacity: 0.5,\n overwrite: false, // false = 叠加(相乘),true = 完全替换颜色\n});\n\n// 半透明效果\nMaterialEffects.highlightColor(myModel, { opacity: 0.3 });\n\n// X 光效果(替换颜色 + 关闭深度写入)\nMaterialEffects.highlightColor(myModel, {\n color: 0x0088ff,\n opacity: 0.25,\n overwrite: true,\n depthWrite: false,\n});\n\n// 支持数组\nMaterialEffects.highlightColor([model1, model2], { color: 0x00ff00 });\n```\n\n#### `HighlightColorOptions`\n\n| 属性 | 类型 | 默认值 | 说明 |\n| :---------- | :-------------------- | :---------- | :--------------------------------------------------------------------------- |\n| `color` | `ColorRepresentation` | `0xff0000` | 高亮颜色。 |\n| `opacity` | `number` | `0.5` | 高亮时材质的透明度。 |\n| `overwrite` | `boolean` | `false` | `false` = 与原始颜色相乘(叠加);`true` = 完全替换颜色。 |\n| `depthWrite` | `boolean` | — | 可选。设为 `false` 可产生 X 光透视效果。不设置时保持原始值。 |\n| `multiplyOpacity` | `boolean` | `false` | 可选。设为 `true` 时高亮透明度会与材质原始透明度相乘,主要用于语义 fallback 模型内部。 |\n\n### `MaterialEffects.removeHighlightColor(object)`\n\n移除高亮效果。当对象上所有效果都被移除后,材质将完整恢复到原始状态。\n\n```typescript\nMaterialEffects.removeHighlightColor(myModel);\n```\n\n### `MaterialEffects.breatheColor(object, options?)`\n\n为对象应用呼吸灯效果,颜色在材质原色与目标颜色之间随时间脉冲变化。需要 `viewer.frameloop = 'always'`。\n\n```typescript\nMaterialEffects.breatheColor(myModel, {\n color: 0x00ff00,\n speed: 1.0,\n intensity: 2.0,\n});\nviewer.frameloop = 'always';\n```\n\n#### `BreatheColorOptions`\n\n| 属性 | 类型 | 默认值 | 说明 |\n| :---------- | :-------------------- | :---------- | :--------------------------------- |\n| `color` | `ColorRepresentation` | `0x00ff00` | 呼吸目标颜色。 |\n| `speed` | `number` | `1.0` | 振荡速度。 |\n| `intensity` | `number` | `2.0` | 控制峰值的锐度。 |\n\n### `MaterialEffects.removeBreatheColor(object)`\n\n移除呼吸效果。\n\n```typescript\nMaterialEffects.removeBreatheColor(myModel);\n```\n\n### `MaterialEffects.wireframe(object, enabled?)`\n\n开启或关闭线框渲染模式。\n\n```typescript\nMaterialEffects.wireframe(myModel); // 开启线框\nMaterialEffects.wireframe(myModel, false); // 关闭线框\nMaterialEffects.removeWireframe(myModel); // 等同于 wireframe(obj, false)\n```\n\n### `MaterialEffects.fadeIn(object, options?)` / `MaterialEffects.fadeOut(object, options?)`\n\n淡入/淡出动画效果。返回 `Promise`,在动画完成后 resolve。淡出后对象材质保持透明状态。\n\n```typescript\n// 淡出\nawait MaterialEffects.fadeOut(myModel, { duration: 1000 });\n\n// 淡入\nawait MaterialEffects.fadeIn(myModel, { duration: 500 });\n```\n\n> 动画期间需要持续渲染,建议配合 `viewer.frameloop = 'always'` 使用。\n\n#### `FadeOptions`\n\n| 属性 | 类型 | 默认值 | 说明 |\n| :--------- | :------- | :----- | :------------------ |\n| `duration` | `number` | `500` | 动画时长(毫秒)。 |\n\n---\n\n## `TSLEffects`\n\n静态工厂类,返回 TSL 颜色节点。将返回值赋给 `NodeMaterial` 的 `material.colorNode` 即可应用动态着色器特效。持续动画需要 `viewer.frameloop = 'always'`。\n\n### `TSLEffects.flow(parameters?)`\n\n沿网格 UV X 轴方向的定向光扫效果,适用于道路、管道和流线。\n\n```typescript\nimport { TSLEffects } from 'u-space';\n\nmyTubeMesh.material.colorNode = TSLEffects.flow({\n baseColor: 0x001133,\n flowColor: 0x00aaff,\n speed: 1.5,\n scale: 4.0,\n intensity: 6.0,\n});\nviewer.frameloop = 'always';\n```\n\n**参数:**\n\n| 属性 | 类型 | 默认值 | 说明 |\n| :---------- | :-------------------- | :---------- | :------------------------------------------------ |\n| `baseColor` | `ColorRepresentation` | `0xffffff` | 背景/底色。 |\n| `flowColor` | `ColorRepresentation` | `0x00ff00` | 扫光高亮颜色。 |\n| `speed` | `number` | `1.0` | 动画速度(越高扫光越快)。 |\n| `scale` | `number` | `3.0` | 图案的空间频率。 |\n| `intensity` | `number` | `4.0` | 峰值锐度,值越高光束越细。 |\n\n### `TSLEffects.breathe(parameters?)`\n\n在两种颜色之间随时间振荡的脉冲发光效果,适合状态指示器和警报。\n\n```typescript\nmyMesh.material.colorNode = TSLEffects.breathe({\n baseColor: 0x333333,\n breathColor: 0x00ff88,\n speed: 2.0,\n intensity: 3.0,\n});\n```\n\n**参数:**\n\n| 属性 | 类型 | 默认值 | 说明 |\n| :------------ | :-------------------- | :---------- | :--------------------------------- |\n| `baseColor` | `ColorRepresentation` | `0xffffff` | 低/静息状态的颜色。 |\n| `breathColor` | `ColorRepresentation` | `0x00ff00` | 峰值亮度时的颜色。 |\n| `speed` | `number` | `1.0` | 振荡速度。 |\n| `intensity` | `number` | `2.0` | 控制峰值的锐度。 |\n\n### `TSLEffects.fluid(parameters?)`\n\n噪声扭曲的流动效果,适用于水面、等离子体或有机流动材质。\n\n```typescript\nmyPlaneMesh.material.colorNode = TSLEffects.fluid({\n baseColor: 0x002244,\n flowColor: 0x0066ff,\n speed: 0.5,\n scale: 2.0,\n intensity: 1.5,\n distortion: 0.3,\n});\n```\n\n**参数:**\n\n| 属性 | 类型 | 默认值 | 说明 |\n| :----------- | :-------------------- | :---------- | :-------------------------------------------- |\n| `baseColor` | `ColorRepresentation` | `0xffffff` | 基础颜色。 |\n| `flowColor` | `ColorRepresentation` | `0x0000ff` | 流体高亮颜色。 |\n| `speed` | `number` | `1.0` | 动画速度。 |\n| `scale` | `number` | `1.0` | 噪声图案的 UV 缩放比例。 |\n| `intensity` | `number` | `1.0` | 流体图案的锐度。 |\n| `distortion` | `number` | `0.5` | 采样前噪声对 UV 的扭曲程度。 |\n"
|
|
177
222
|
},
|
|
178
223
|
{
|
|
179
224
|
"id": "api-interactions",
|
|
180
225
|
"path": "api-interactions.md",
|
|
181
226
|
"title": "Interactions API",
|
|
182
|
-
"summary": "`InteractionManager` 提供了一种统一的方式来处理 3D 场景中的用户输入(鼠标、触摸、指针)。它将屏幕空间的射线检测转换为世界空间坐标,并将对应事件直接分发到被命中的 3D 对象上。 `InteractionManager` 在 `Viewer` 内部自动实例化,可通过 `viewer.interactionManager` 访问。
|
|
227
|
+
"summary": "`InteractionManager` 提供了一种统一的方式来处理 3D 场景中的用户输入(鼠标、触摸、指针)。它将屏幕空间的射线检测转换为世界空间坐标,并将对应事件直接分发到被命中的 3D 对象上。 `InteractionManager` 在 `Viewer` 内部自动实例化,可通过 `viewer.interactionManager` 访问。 对于三角形较多的静态模型,可以在模型加载后调用 `enableMeshBVHRaycast(root)`。该函数不会修改 Three.js 原型;它先保留每个 Mesh 的局部包围体粗筛,只有射线命中包围球后才为至少 10000 个三角形的 geometry 按需建立并复用 `three-mesh-bvh`。小几何、蒙皮、morph target 或自定义 raycast 对象会继续使用原始路径。",
|
|
183
228
|
"headings": [
|
|
184
229
|
{
|
|
185
230
|
"level": 1,
|
|
@@ -290,7 +335,7 @@ export const docsIndex = [
|
|
|
290
335
|
"text": "dispose()"
|
|
291
336
|
}
|
|
292
337
|
],
|
|
293
|
-
"content": "# Interactions API\n\n`InteractionManager` 提供了一种统一的方式来处理 3D 场景中的用户输入(鼠标、触摸、指针)。它将屏幕空间的射线检测转换为世界空间坐标,并将对应事件直接分发到被命中的 3D 对象上。\n\n`InteractionManager` 在 `Viewer` 内部自动实例化,可通过 `viewer.interactionManager` 访问。\n\n## 启用交互事件\n\n出于性能考虑,指针移动事件默认处于禁用状态。如需悬停和拖拽效果,必须显式开启。\n\n```typescript\n// 启用鼠标移动事件(如 pointerenter 和 pointerleave)\nviewer.interactionManager.pointerMoveEventsEnabled = true;\n```\n\n### 目标对象\n\n可以限制射线检测所针对的对象范围。默认值为 `scene.children`(即整个场景的直接子级,含递归嵌套)。\n`BaseMesh` / `BaseGroup` 默认会在自身 `visible === false` 时让 `raycast()` 返回 `false`,从而阻止自身和子级继续参与递归射线检测;如需隐藏但仍可交互,可将对象的 `ignoreInvisibleWhenRaycast` 设为 `false`。`setInteractable(object, false)` / `addIgnore(object)` 会在命中后过滤该对象及其子对象。\n\n```typescript\n// 仅检测指定对象\nviewer.interactionManager.targetObjects = [myBox, myModel];\n\n// 恢复默认:检测整个场景\nviewer.interactionManager.targetObjects = viewer.scene.children;\n```\n\n## 添加事件监听\n\n由于 `u-space` 对象(以及继承自 `BaseMesh`、`BaseGroup` 的对象)支持自定义事件,你可以像操作 DOM 元素一样原生地绑定监听器。\n\n```typescript\nconst myBox = new Mesh(geometry, material);\nviewer.scene.add(myBox);\n\n// 直接在 Mesh 或 Model 上绑定监听器\nmyBox.addEventListener('click', (eventData) => {\n // 底层指针事件和射线检测数据\n const intersect = eventData.event.intersect;\n\n console.log('点击位置:', intersect.point);\n});\n```\n\n## 支持的事件类型\n\n以下事件类型可在可交互对象上监听:\n\n- `click`:指针点击对象时触发。内置智能过滤:长按(>500ms)或移动距离过大时忽略,双击时不重复触发。\n- `dblclick`:快速双击对象时触发。\n- `rightclick`:右键短按松开时触发(长按或拖拽不触发;浏览器原生上下文菜单已被阻止)。在 `pointerup` 时派发,因此可以正确过滤相机拖拽等操作。\n- `pointerdown`:指针按键在对象上按下时触发。\n- `pointerup`:指针按键在对象上释放时触发。\n- `pointermove`:指针在对象上移动时触发,需要 `pointerMoveEventsEnabled = true`。\n- `pointerenter`:光标进入对象范围时触发,需要 `pointerMoveEventsEnabled = true`。\n- `pointerleave`:光标离开对象范围时触发,需要 `pointerMoveEventsEnabled = true`。\n\n### 事件冒泡\n\n事件会沿父级链向上冒泡。若要阻止事件继续传播,可调用事件载荷上的 `stopPropagation()`:\n\n```typescript\nmyObject.addEventListener('click', (e) => {\n e.event.stopPropagation();\n});\n```\n\n## `InteractionEvent`\n\n传递给所有监听回调的事件对象,位于 `event` 键下。\n\n### 属性\n\n| 属性 | 类型 | 说明 |\n| :-------------- | :----------------------------------------- | :-------------------------------------------------------------------------- |\n| `type` | `InteractionEventType` | 事件类型字符串(如 `'click'`)。 |\n| `target` | `Object3D` | 触发事件的原始 3D 对象(第一个命中点)。 |\n| `currentTarget` | `Object3D` | 冒泡链中当前处理事件的对象。 |\n| `intersect` | `Intersection \\| null` | Three.js 射线检测数据:`point`、`face`、`distance`、`uv` 等。 |\n| `originalEvent` | `PointerEvent \\| MouseEvent` | 原始 DOM 指针/鼠标事件。 |\n\n### 方法\n\n#### `stopPropagation()`\n\n阻止事件继续向上冒泡。\n\n## `InteractionManager` API\n\n### 属性\n\n| 属性 | 类型 | 默认值 | 说明 |\n| :------------------------- | :---------------------------- | :---------------- | :--------------------------------------------------------- |\n| `targetObjects` | `Object3D[]` | `scene.children` | 射线检测的目标对象列表,默认为场景的直接子级。 |\n| `pointerMoveEventsEnabled` | `boolean` | `false` | 启用 `pointermove`、`pointerenter`、`pointerleave` 事件。 |\n\n### 方法\n\n#### `enable()` / `disable()`\n\n全局启用或禁用所有交互事件。禁用后不再触发任何交互事件。\n\n```typescript\nviewer.interactionManager.disable(); // 暂停交互\nviewer.interactionManager.enable(); // 恢复交互\n```\n\n#### `setInteractable(object, interactable)`\n\n控制单个对象是否可交互。设为 `false` 后该对象及其所有子对象不再参与射线检测。\n\n```typescript\nviewer.interactionManager.setInteractable(myModel, false); // 禁止交互\nviewer.interactionManager.setInteractable(myModel, true); // 恢复交互\n```\n\n#### `addIgnore(object)` / `removeIgnore(object)`\n\n`setInteractable` 的别名。`addIgnore(obj)` 等同于 `setInteractable(obj, false)`,`removeIgnore(obj)` 等同于 `setInteractable(obj, true)`。\n\n```typescript\nviewer.interactionManager.addIgnore(helperObject); // 忽略辅助对象\nviewer.interactionManager.removeIgnore(helperObject); // 恢复\n```\n\n#### `setCamera(camera)`\n\n更新用于射线检测的相机。由 `viewer.setCamera()` 自动调用。\n\n#### `dispose()`\n\n移除所有 DOM 事件监听,清理内部状态。\n\n```typescript\nviewer.interactionManager.dispose();\n```\n\n---\n\n## Selection\n\n`Selection` 提供对象选择能力,包括单选、多选、toggle 和框选(rubber-band)。\n\n```typescript\nimport { Selection } from 'u-space';\n\nconst selection = new Selection(viewer.renderer.domElement, viewer.scene, viewer.camera);\n```\n\n### 基础选择\n\n```typescript\nselection.select(myObject); // 选中\nselection.select([obj1, obj2]); // 多选\nselection.deselect(myObject); // 取消选择\nselection.toggle(myObject); // 切换选择状态\nselection.clear(); // 清空选择\nselection.isSelected(myObject); // 检查是否已选\n```\n\n### 属性\n\n| 属性 | 类型 | 说明 |\n| :-------------- | :-------------- | :---------------------------------- |\n| `selected` | `Set<Object3D>` | 当前已选对象集合。 |\n| `selectedArray` | `Object3D[]` | 当前已选对象数组。 |\n| `targetObjects` | `Object3D[]` | 框选时的候选对象(默认为场景子级)。 |\n\n### 框选\n\n启用后可用鼠标拖动矩形区域选择多个对象。框选覆盖层会渲染在容器元素上。\n\n```typescript\nselection.enableBoxSelection({ deep: true }); // 开启框选\nselection.disableBoxSelection(); // 关闭框选\n```\n\n#### `BoxSelectionOptions`\n\n| 属性 | 类型 | 默认值 | 说明 |\n| :---------- | :-------- | :----- | :------------------------------ |\n| `className` | `string` | — | 框选矩形的 CSS 类名。 |\n| `deep` | `boolean` | `true` | 是否递归检测子对象。 |\n\n### 事件\n\n```typescript\nselection.addEventListener('select', (e) => {\n console.log('新选中:', e.objects);\n});\n\nselection.addEventListener('deselect', (e) => {\n console.log('取消选中:', e.objects);\n});\n\nselection.addEventListener('change', (e) => {\n console.log('当前选中:', e.selected.size, '个对象');\n});\n```\n\n### 方法\n\n#### `setCamera(camera)`\n\n更新用于框选投影计算的相机。切换相机时需调用。\n\n#### `dispose()`\n\n关闭框选、移除事件监听、清空选择集。\n"
|
|
338
|
+
"content": "# Interactions API\n\n`InteractionManager` 提供了一种统一的方式来处理 3D 场景中的用户输入(鼠标、触摸、指针)。它将屏幕空间的射线检测转换为世界空间坐标,并将对应事件直接分发到被命中的 3D 对象上。\n\n`InteractionManager` 在 `Viewer` 内部自动实例化,可通过 `viewer.interactionManager` 访问。\n\n对于三角形较多的静态模型,可以在模型加载后调用 `enableMeshBVHRaycast(root)`。该函数不会修改 Three.js 原型;它先保留每个 Mesh 的局部包围体粗筛,只有射线命中包围球后才为至少 10000 个三角形的 geometry 按需建立并复用 `three-mesh-bvh`。小几何、蒙皮、morph target 或自定义 raycast 对象会继续使用原始路径。\n\n```ts\nimport { enableMeshBVHRaycast } from 'u-space';\n\nenableMeshBVHRaycast(loadedModel);\n```\n\n## 启用交互事件\n\n出于性能考虑,指针移动事件默认处于禁用状态。如需悬停和拖拽效果,必须显式开启。\n\n```typescript\n// 启用鼠标移动事件(如 pointerenter 和 pointerleave)\nviewer.interactionManager.pointerMoveEventsEnabled = true;\n```\n\n### 目标对象\n\n可以限制射线检测所针对的对象范围。默认值为 `scene.children`(即整个场景的直接子级,含递归嵌套)。\n`BaseMesh` / `BaseGroup` 默认会在自身 `visible === false` 时让 `raycast()` 返回 `false`,从而阻止自身和子级继续参与递归射线检测;如需隐藏但仍可交互,可将对象的 `ignoreInvisibleWhenRaycast` 设为 `false`。`setInteractable(object, false)` / `addIgnore(object)` 会在命中后过滤该对象及其子对象。\n\n```typescript\n// 仅检测指定对象\nviewer.interactionManager.targetObjects = [myBox, myModel];\n\n// 恢复默认:检测整个场景\nviewer.interactionManager.targetObjects = viewer.scene.children;\n```\n\n## 添加事件监听\n\n由于 `u-space` 对象(以及继承自 `BaseMesh`、`BaseGroup` 的对象)支持自定义事件,你可以像操作 DOM 元素一样原生地绑定监听器。\n\n```typescript\nconst myBox = new Mesh(geometry, material);\nviewer.scene.add(myBox);\n\n// 直接在 Mesh 或 Model 上绑定监听器\nmyBox.addEventListener('click', (eventData) => {\n // 底层指针事件和射线检测数据\n const intersect = eventData.event.intersect;\n\n console.log('点击位置:', intersect.point);\n});\n```\n\n## 支持的事件类型\n\n以下事件类型可在可交互对象上监听:\n\n- `click`:指针点击对象时触发。内置智能过滤:长按(>500ms)或移动距离过大时忽略,双击时不重复触发。\n- `dblclick`:快速双击对象时触发。\n- `rightclick`:右键短按松开时触发(长按或拖拽不触发;浏览器原生上下文菜单已被阻止)。在 `pointerup` 时派发,因此可以正确过滤相机拖拽等操作。\n- `pointerdown`:指针按键在对象上按下时触发。\n- `pointerup`:指针按键在对象上释放时触发。\n- `pointermove`:指针在对象上移动时触发,需要 `pointerMoveEventsEnabled = true`。\n- `pointerenter`:光标进入对象范围时触发,需要 `pointerMoveEventsEnabled = true`。\n- `pointerleave`:光标离开对象范围时触发,需要 `pointerMoveEventsEnabled = true`。\n\n### 事件冒泡\n\n事件会沿父级链向上冒泡。若要阻止事件继续传播,可调用事件载荷上的 `stopPropagation()`:\n\n```typescript\nmyObject.addEventListener('click', (e) => {\n e.event.stopPropagation();\n});\n```\n\n## `InteractionEvent`\n\n传递给所有监听回调的事件对象,位于 `event` 键下。\n\n### 属性\n\n| 属性 | 类型 | 说明 |\n| :-------------- | :----------------------------------------- | :-------------------------------------------------------------------------- |\n| `type` | `InteractionEventType` | 事件类型字符串(如 `'click'`)。 |\n| `target` | `Object3D` | 触发事件的原始 3D 对象(第一个命中点)。 |\n| `currentTarget` | `Object3D` | 冒泡链中当前处理事件的对象。 |\n| `intersect` | `Intersection \\| null` | Three.js 射线检测数据:`point`、`face`、`distance`、`uv` 等。 |\n| `originalEvent` | `PointerEvent \\| MouseEvent` | 原始 DOM 指针/鼠标事件。 |\n\n### 方法\n\n#### `stopPropagation()`\n\n阻止事件继续向上冒泡。\n\n## `InteractionManager` API\n\n### 属性\n\n| 属性 | 类型 | 默认值 | 说明 |\n| :------------------------- | :---------------------------- | :---------------- | :--------------------------------------------------------- |\n| `targetObjects` | `Object3D[]` | `scene.children` | 射线检测的目标对象列表,默认为场景的直接子级。 |\n| `pointerMoveEventsEnabled` | `boolean` | `false` | 启用 `pointermove`、`pointerenter`、`pointerleave` 事件。 |\n\n### 方法\n\n#### `enable()` / `disable()`\n\n全局启用或禁用所有交互事件。禁用后不再触发任何交互事件。\n\n```typescript\nviewer.interactionManager.disable(); // 暂停交互\nviewer.interactionManager.enable(); // 恢复交互\n```\n\n#### `setInteractable(object, interactable)`\n\n控制单个对象是否可交互。设为 `false` 后该对象及其所有子对象不再参与射线检测。\n\n```typescript\nviewer.interactionManager.setInteractable(myModel, false); // 禁止交互\nviewer.interactionManager.setInteractable(myModel, true); // 恢复交互\n```\n\n#### `addIgnore(object)` / `removeIgnore(object)`\n\n`setInteractable` 的别名。`addIgnore(obj)` 等同于 `setInteractable(obj, false)`,`removeIgnore(obj)` 等同于 `setInteractable(obj, true)`。\n\n```typescript\nviewer.interactionManager.addIgnore(helperObject); // 忽略辅助对象\nviewer.interactionManager.removeIgnore(helperObject); // 恢复\n```\n\n#### `setCamera(camera)`\n\n更新用于射线检测的相机。由 `viewer.setCamera()` 自动调用。\n\n#### `dispose()`\n\n移除所有 DOM 事件监听,清理内部状态。\n\n```typescript\nviewer.interactionManager.dispose();\n```\n\n---\n\n## Selection\n\n`Selection` 提供对象选择能力,包括单选、多选、toggle 和框选(rubber-band)。\n\n```typescript\nimport { Selection } from 'u-space';\n\nconst selection = new Selection(viewer.renderer.domElement, viewer.scene, viewer.camera);\n```\n\n### 基础选择\n\n```typescript\nselection.select(myObject); // 选中\nselection.select([obj1, obj2]); // 多选\nselection.deselect(myObject); // 取消选择\nselection.toggle(myObject); // 切换选择状态\nselection.clear(); // 清空选择\nselection.isSelected(myObject); // 检查是否已选\n```\n\n### 属性\n\n| 属性 | 类型 | 说明 |\n| :-------------- | :-------------- | :---------------------------------- |\n| `selected` | `Set<Object3D>` | 当前已选对象集合。 |\n| `selectedArray` | `Object3D[]` | 当前已选对象数组。 |\n| `targetObjects` | `Object3D[]` | 框选时的候选对象(默认为场景子级)。 |\n\n### 框选\n\n启用后可用鼠标拖动矩形区域选择多个对象。框选覆盖层会渲染在容器元素上。\n\n```typescript\nselection.enableBoxSelection({ deep: true }); // 开启框选\nselection.disableBoxSelection(); // 关闭框选\n```\n\n#### `BoxSelectionOptions`\n\n| 属性 | 类型 | 默认值 | 说明 |\n| :---------- | :-------- | :----- | :------------------------------ |\n| `className` | `string` | — | 框选矩形的 CSS 类名。 |\n| `deep` | `boolean` | `true` | 是否递归检测子对象。 |\n\n### 事件\n\n```typescript\nselection.addEventListener('select', (e) => {\n console.log('新选中:', e.objects);\n});\n\nselection.addEventListener('deselect', (e) => {\n console.log('取消选中:', e.objects);\n});\n\nselection.addEventListener('change', (e) => {\n console.log('当前选中:', e.selected.size, '个对象');\n});\n```\n\n### 方法\n\n#### `setCamera(camera)`\n\n更新用于框选投影计算的相机。切换相机时需调用。\n\n#### `dispose()`\n\n关闭框选、移除事件监听、清空选择集。\n"
|
|
294
339
|
},
|
|
295
340
|
{
|
|
296
341
|
"id": "api-managers",
|
|
@@ -447,7 +492,7 @@ export const docsIndex = [
|
|
|
447
492
|
"text": "管理"
|
|
448
493
|
}
|
|
449
494
|
],
|
|
450
|
-
"content": "# Managers API\n\n`u-space` 使用专用管理器来处理特定领域的逻辑,例如用于缓存和注册 3D 对象的 `ObjectManager`。\n\n## ObjectManager\n\n`ObjectManager` 提供了一个集中式字典/映射层,让你无需递归遍历 Three.js 场景图,就能通过字符串 ID 或字符串名称轻松检索复杂模型或特定网格。\n\n默认实例可通过 `viewer.objectManager` 访问。\n\n### 添加对象\n\n向管理器添加对象时,必须提供唯一标识符。\n\n```typescript\nconst myModel = new Model();\n// ... 加载逻辑\n\n// 用指定的唯一 ID 添加\nviewer.objectManager.add('my-unique-car-id', myModel);\n\n// 一个对象可以注册多个 ID\nviewer.objectManager.add('player-vehicle', myModel);\n```\n\n### 检索对象\n\n管理器提供快速访问对象的方法。\n\n#### `getById(id: string)`\n\n精确检索与指定 ID 匹配的一个对象(或 `undefined`)。\n\n```typescript\nconst car = viewer.objectManager.getById('my-unique-car-id');\n```\n\n#### `getByName(name: string)`\n\n由于 Three.js 对象可以共享同一个 `.name` 属性,此方法返回一个 `Set<Object3D>`,包含所有与指定名称匹配的已注册对象。\n\n```typescript\n// 假设 myModel.name = 'sedan'\nconst sedans = viewer.objectManager.getByName('sedan');\n\nsedans.forEach((vehicle) => {\n vehicle.position.y += 10;\n});\n```\n\n#### `getByType(type: string)`\n\n返回一个 `Set<Object3D>`,包含所有 `object.type` 与指定类型匹配的已注册对象。\n\n```typescript\nconst models = viewer.objectManager.getByType('Model');\n```\n\n#### `getObjectIds(object: Object3D)`\n\n返回给定对象已注册的所有 ID 的 `Set<string>`。\n\n```typescript\nconst ids = viewer.objectManager.getObjectIds(myModel);\n```\n\n### 移除对象\n\n#### `remove(object: Object3D)`\n\n从所有内部映射中移除该对象及其所有关联 ID。\n\n```typescript\nviewer.objectManager.remove(myModel);\n```\n\n#### `removeById(id: string)`\n\n移除注册在指定 ID 下的对象。\n\n```typescript\nviewer.objectManager.removeById('my-unique-car-id');\n```\n\n#### `removeByName(name: string)`\n\n移除所有具有指定 `.name` 的对象。\n\n```typescript\nviewer.objectManager.removeByName('sedan');\n```\n\n#### `removeByType(type: string)`\n\n移除所有具有指定 `.type` 的对象。\n\n```typescript\nviewer.objectManager.removeByType('Model');\n```\n\n### 显隐控制\n\n#### `show(id: string)` / `hide(id: string)`\n\n控制对象的可见性。\n\n```typescript\nviewer.objectManager.hide('car-01');\nviewer.objectManager.show('car-01');\n```\n\n#### `isolate(ids: string[])`\n\n孤立显示:仅显示指定 ID 的对象,隐藏其余所有。\n\n```typescript\nviewer.objectManager.isolate(['car-01', 'car-02']);\n```\n\n#### `showAll()`\n\n恢复显示所有已管理对象。\n\n```typescript\nviewer.objectManager.showAll();\n```\n\n### 材质操作\n\n#### `setOpacity(id: string, opacity: number)`\n\n设置指定对象所有材质的透明度。对于 `
|
|
495
|
+
"content": "# Managers API\n\n`u-space` 使用专用管理器来处理特定领域的逻辑,例如用于缓存和注册 3D 对象的 `ObjectManager`。\n\n## ObjectManager\n\n`ObjectManager` 提供了一个集中式字典/映射层,让你无需递归遍历 Three.js 场景图,就能通过字符串 ID 或字符串名称轻松检索复杂模型或特定网格。\n\n默认实例可通过 `viewer.objectManager` 访问。\n\n### 添加对象\n\n向管理器添加对象时,必须提供唯一标识符。\n\n```typescript\nconst myModel = new Model();\n// ... 加载逻辑\n\n// 用指定的唯一 ID 添加\nviewer.objectManager.add('my-unique-car-id', myModel);\n\n// 一个对象可以注册多个 ID\nviewer.objectManager.add('player-vehicle', myModel);\n```\n\n### 检索对象\n\n管理器提供快速访问对象的方法。\n\n#### `getById(id: string)`\n\n精确检索与指定 ID 匹配的一个对象(或 `undefined`)。\n\n```typescript\nconst car = viewer.objectManager.getById('my-unique-car-id');\n```\n\n#### `getByName(name: string)`\n\n由于 Three.js 对象可以共享同一个 `.name` 属性,此方法返回一个 `Set<Object3D>`,包含所有与指定名称匹配的已注册对象。\n\n```typescript\n// 假设 myModel.name = 'sedan'\nconst sedans = viewer.objectManager.getByName('sedan');\n\nsedans.forEach((vehicle) => {\n vehicle.position.y += 10;\n});\n```\n\n#### `getByType(type: string)`\n\n返回一个 `Set<Object3D>`,包含所有 `object.type` 与指定类型匹配的已注册对象。\n\n```typescript\nconst models = viewer.objectManager.getByType('Model');\n```\n\n#### `getObjectIds(object: Object3D)`\n\n返回给定对象已注册的所有 ID 的 `Set<string>`。\n\n```typescript\nconst ids = viewer.objectManager.getObjectIds(myModel);\n```\n\n### 移除对象\n\n#### `remove(object: Object3D)`\n\n从所有内部映射中移除该对象及其所有关联 ID。\n\n```typescript\nviewer.objectManager.remove(myModel);\n```\n\n#### `removeById(id: string)`\n\n移除注册在指定 ID 下的对象。\n\n```typescript\nviewer.objectManager.removeById('my-unique-car-id');\n```\n\n#### `removeByName(name: string)`\n\n移除所有具有指定 `.name` 的对象。\n\n```typescript\nviewer.objectManager.removeByName('sedan');\n```\n\n#### `removeByType(type: string)`\n\n移除所有具有指定 `.type` 的对象。\n\n```typescript\nviewer.objectManager.removeByType('Model');\n```\n\n### 显隐控制\n\n#### `show(id: string)` / `hide(id: string)`\n\n控制对象的可见性。\n\n```typescript\nviewer.objectManager.hide('car-01');\nviewer.objectManager.show('car-01');\n```\n\n#### `isolate(ids: string[])`\n\n孤立显示:仅显示指定 ID 的对象,隐藏其余所有。\n\n```typescript\nviewer.objectManager.isolate(['car-01', 'car-02']);\n```\n\n#### `showAll()`\n\n恢复显示所有已管理对象。\n\n```typescript\nviewer.objectManager.showAll();\n```\n\n### 材质操作\n\n#### `setOpacity(id: string, opacity: number)`\n\n设置指定对象所有材质的透明度。对于 `InstanceObject`(例如 `SceneInstanceObject`、`FacilityInstanceObject` 或 `FloorSemanticInstanceObject`),会调用 `setInstanceOpacity()` 写入该实例的透明度,而不会影响同一模型 path 或同一楼层内的其他实例。\n\n```typescript\nviewer.objectManager.setOpacity('building-01', 0.3);\nviewer.objectManager.setOpacity('building-01', 1.0); // 恢复不透明\n```\n\n### 空间查询\n\n#### `getBoundingBox(id?: string)`\n\n获取指定对象的包围盒,不传 `id` 则返回所有已管理对象的包围盒。\n\n```typescript\nconst box = viewer.objectManager.getBoundingBox('car-01');\nconst allBox = viewer.objectManager.getBoundingBox(); // 全部对象\n```\n\n### 遍历与过滤\n\n#### `forEach(callback)`\n\n遍历所有已管理对象。\n\n```typescript\nviewer.objectManager.forEach((object, ids) => {\n console.log(`对象: ${object.name}, IDs: ${Array.from(ids).join(', ')}`);\n});\n```\n\n#### `filter(predicate)`\n\n按条件过滤对象,返回匹配的数组。\n\n```typescript\nconst cars = viewer.objectManager.filter((obj) => obj.name.startsWith('car'));\n```\n\n### 工具方法\n\n- `getAll()`:返回所有已跟踪对象的 `Set<Object3D>`。\n- `clear()`:从所有内部映射中移除所有对象。\n- `size`:返回当前管理器跟踪的唯一对象总数。\n\n---\n\n## SceneManager\n\n`SceneManager` 用于管理多个场景的创建、注册、切换和序列化。\n\n```typescript\nimport { SceneManager } from 'u-space';\n\nconst sceneManager = new SceneManager();\n```\n\n### 方法\n\n#### `create(key, options?)`\n\n创建并注册一个新场景。\n\n```typescript\nconst scene = sceneManager.create('indoor', { background: 0x333333 });\n```\n\n#### `add(key, scene)` / `get(key)` / `remove(key)`\n\n手动注册、获取或移除场景。\n\n```typescript\nsceneManager.add('outdoor', existingScene);\nconst scene = sceneManager.get('outdoor');\nsceneManager.remove('outdoor');\n```\n\n#### `serialize(key)`\n\n将场景元数据序列化为 JSON 快照。\n\n```typescript\nconst snapshot = sceneManager.serialize('indoor');\n// { name, background, environmentRotation, children }\n```\n\n#### `keys()` / `size` / `clear()`\n\n列出所有场景 key、获取场景数量或清空。\n\n---\n\n## LightManager\n\n`LightManager` 提供灯光的便捷创建、预设和 Helper 可视化。\n\n```typescript\nimport { LightManager } from 'u-space';\n\nconst lightManager = new LightManager(viewer.scene);\n```\n\n### 灯光创建\n\n```typescript\nlightManager.addAmbient('ambient', { color: 0xffffff, intensity: 0.5 });\nlightManager.addDirectional('sun', { position: { x: 5, y: 10, z: 5 }, castShadow: true });\nlightManager.addPoint('bulb', { position: { x: 0, y: 3, z: 0 }, intensity: 2 });\nlightManager.addSpot('spot', { position: { x: 0, y: 10, z: 0 }, angle: Math.PI / 6 });\nlightManager.addHemisphere('hemi', { skyColor: 0x87ceeb, groundColor: 0x362907 });\n```\n\n### 灯光预设\n\n一键应用常见场景的灯光配置:\n\n```typescript\nlightManager.applyPreset('indoor'); // 室内:环境光 + 顶部点光源\nlightManager.applyPreset('outdoor'); // 室外:半球光 + 方向光(阳光)\nlightManager.applyPreset('studio'); // 摄影棚:三点布光\nlightManager.applyPreset('warehouse'); // 仓库:多点光源照明\n```\n\n| 预设 | 配置 |\n| :---------- | :-------------------------------------- |\n| `indoor` | 环境光 + 2 个点光源 |\n| `outdoor` | 半球光 + 方向光(带阴影) |\n| `studio` | 环境光 + 主光 + 补光 + 轮廓光 |\n| `warehouse` | 环境光 + 4 个点光源(四角分布) |\n\n### Helper 可视化\n\n```typescript\nlightManager.showHelper('sun'); // 显示单个灯光 Helper(默认 size=5, color=0xff0000)\nlightManager.showHelper('sun', 2, 0x00ff00); // 自定义尺寸和颜色\nlightManager.showAllHelpers(); // 显示所有灯光 Helper\nlightManager.hideHelper('sun'); // 隐藏单个\nlightManager.hideAllHelpers(); // 隐藏所有\n```\n\n#### `showHelper(id, size?, color?)`\n\n| 参数 | 类型 | 默认值 | 说明 |\n| :------ | :-------------------- | :---------- | :---------------- |\n| `id` | `string` | — | 灯光 ID。 |\n| `size` | `number` | `5` | Helper 尺寸。 |\n| `color` | `ColorRepresentation` | `0xff0000` | Helper 颜色。 |\n\n### 管理\n\n```typescript\nlightManager.get('sun'); // 获取灯光\nlightManager.remove('sun'); // 移除灯光\nlightManager.removeAll(); // 移除所有\nlightManager.dispose(); // 清理\n```\n"
|
|
451
496
|
},
|
|
452
497
|
{
|
|
453
498
|
"id": "api-objects",
|
|
@@ -471,6 +516,66 @@ export const docsIndex = [
|
|
|
471
516
|
"level": 3,
|
|
472
517
|
"text": "BaseGroup"
|
|
473
518
|
},
|
|
519
|
+
{
|
|
520
|
+
"level": 2,
|
|
521
|
+
"text": "InstanceObject"
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
"level": 3,
|
|
525
|
+
"text": "类型和常量"
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
"level": 4,
|
|
529
|
+
"text": "InstanceStyle"
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
"level": 4,
|
|
533
|
+
"text": "InstanceObjectOptions"
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
"level": 4,
|
|
537
|
+
"text": "InstanceIdentity"
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
"level": 4,
|
|
541
|
+
"text": "颜色模式常量"
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
"level": 3,
|
|
545
|
+
"text": "字段"
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
"level": 3,
|
|
549
|
+
"text": "构造函数"
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
"level": 4,
|
|
553
|
+
"text": "new InstanceObject(options?)"
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
"level": 3,
|
|
557
|
+
"text": "身份和样式"
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
"level": 3,
|
|
561
|
+
"text": "包围盒和相机定位"
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
"level": 3,
|
|
565
|
+
"text": "Fallback 渲染对象"
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
"level": 3,
|
|
569
|
+
"text": "Dirty callback"
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
"level": 3,
|
|
573
|
+
"text": "类型守卫"
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
"level": 4,
|
|
577
|
+
"text": "isInstanceObject(object)"
|
|
578
|
+
},
|
|
474
579
|
{
|
|
475
580
|
"level": 2,
|
|
476
581
|
"text": "模型"
|
|
@@ -644,7 +749,7 @@ export const docsIndex = [
|
|
|
644
749
|
"text": "dispose()"
|
|
645
750
|
}
|
|
646
751
|
],
|
|
647
|
-
"content": "# Objects API\n\n`u-space` 提供了若干对象封装类,主要扩展自 Three.js 的基础节点,使 3D 资产的操作和加载更加便捷。\n\n## 基础类\n\n### `BaseMesh`\n\n继承自 `THREE.Mesh`,`u-space` 中所有基础网格类都继承于此。\n\n**关键属性:**\n\n| 属性 | 类型 | 默认值 | 说明 |\n| :--------------------------- | :-------- | :----- | :------------------------------------------------------------- |\n| `castShadow` | `boolean` | `true` | 默认参与阴影投射;可在单个对象上改为 `false`。 |\n| `receiveShadow` | `boolean` | `true` | 默认接收阴影;可在不需要阴影的网格上改为 `false`。 |\n| `ignoreInvisibleWhenRaycast` | `boolean` | `true` | 为 `true` 时,不可见的网格在射线检测时会被跳过(不参与碰撞)。 |\n\n### `BaseGroup`\n\n继承自 `THREE.Group`,`Model` 和 `Topology` 均继承于此。\n\n**关键属性:**\n\n| 属性 | 类型 | 默认值 | 说明 |\n| :--------------------------- | :-------- | :----- | :--------------------------------------------------------------- |\n| `ignoreInvisibleWhenRaycast` | `boolean` | `true` | 为 `true` 时,不可见的组在射线检测时会被跳过(不参与碰撞)。 |\n\n## 模型\n\n`Model` 类继承自 `BaseGroup`(进而继承自 `THREE.Group`),简化了异步加载 `glb`、`gltf` 等外部 3D 模型的流程,内置多层缓存支持。\n\n```typescript\nimport { Model } from 'u-space';\n\nconst myModel = new Model();\n```\n\n### 加载资产\n\n`loadAsync` 方法负责从 URL 加载资产并将其添加到组中。\n\n```typescript\nawait myModel.loadAsync(parameters: ModelParameters)\n```\n\n#### `ModelParameters`\n\n| 属性 | 类型 | 默认值 | 说明 |\n| :----------- | :-------- | :----- | :-------------------------------------------------------------------------------------- |\n| `url` | `string` | — | `.gltf` 或 `.glb` 文件的 URL。 |\n| `cache` | `boolean` | `true` | 启用内存缓存。若模型已加载过,则直接从内存中获取并克隆。 |\n| `persistent` | `boolean` | `true` | 使用浏览器 Cache API 启用持久化磁盘缓存,适合较大的模型。 |\n\n#### 缓存示例\n\n```javascript\n// 默认开启双层缓存,直接加载即可\nconst model1 = new Model();\nmodel1.loadAsync({ url: 'model.glb' });\n\n// 禁用内存缓存(每次都从网络或持久化缓存重新加载)\nconst model2 = new Model();\nmodel2.loadAsync({ url: 'model.glb', cache: false });\n\n// 完全禁用缓存\nconst model3 = new Model();\nmodel3.loadAsync({ url: 'model.glb', cache: false, persistent: false });\n```\n\n### 动画\n\n`Model` 在加载含内置动画的模型(如 glTF/FBX)后,会自动初始化 `AnimationMixer` 并收集 `AnimationClip`。\n\n**属性:**\n\n| 属性 | 类型 | 说明 |\n| :----------- | :-------------------------- | :----------------------------------------- |\n| `mixer` | `AnimationMixer \\| null` | 动画混合器,加载含动画模型后自动创建。 |\n| `animations` | `AnimationClip[]` | 模型内置的动画片段列表。 |\n\n#### `playAnimation(nameOrIndex?, options?)`\n\n播放指定动画。不传参时播放第一个动画。\n\n```typescript\n// 按名称播放\nmodel.playAnimation('Walk');\n\n// 按索引播放\nmodel.playAnimation(0);\n\n// 带选项\nmodel.playAnimation('Run', {\n loop: true, // 是否循环(默认 true)\n repetitions: 3, // 循环次数(默认 Infinity)\n timeScale: 1.5, // 播放速度(默认 1)\n clampWhenFinished: true, // 播放结束后停留在最后一帧(默认 false)\n});\n```\n\n#### `playAllAnimations(options?)`\n\n播放模型所有内置动画。参数同 `playAnimation`。\n\n```typescript\nmodel.playAllAnimations({ timeScale: 2 });\n```\n\n#### `stopAnimation(nameOrIndex?)`\n\n停止指定动画。不传参时停止所有动画。\n\n```typescript\nmodel.stopAnimation('Walk'); // 停止指定动画\nmodel.stopAnimation(); // 停止所有动画\n```\n\n#### `updateAnimation(delta)`\n\n推进动画时间,需在渲染循环中调用。`delta` 为帧间隔时间(秒),可从 Viewer 事件的 `delta` 属性获取。\n\n```typescript\nviewer.addEventListener('beforeRender', (e) => {\n model.updateAnimation(e.delta);\n});\n```\n\n### 空间查询\n\n#### `getBoundingBox()`\n\n获取模型的轴对齐包围盒(AABB)。\n\n```typescript\nconst box = myModel.getBoundingBox();\nconst size = box.getSize(new Vector3());\nconsole.log('模型尺寸:', size);\n```\n\n#### `getCenter()`\n\n获取模型包围盒的中心点。\n\n```typescript\nconst center = myModel.getCenter();\nconsole.log('模型中心:', center);\n```\n\n### 材质操作\n\n#### `setMaterial(material)`\n\n将模型内所有 Mesh 的材质统一替换为指定材质。\n\n```typescript\nimport { MeshStandardMaterial } from 'three/webgpu';\n\nmyModel.setMaterial(new MeshStandardMaterial({ color: 0xff0000 }));\n```\n\n### 清除缓存\n\n可通过 `Model` 类的静态方法手动清除内部缓存:\n\n- `Model.clearMemoryCache()`:清除运行时内存缓存。\n- `await Model.clearPersistentCache()`:完全清除浏览器中 `u-space` 模型的持久化缓存。\n\n## 网格\n\n`u-space` 提供了多种精简的网格类,均继承自 `BaseMesh`。所有类默认使用 `MeshStandardNodeMaterial`,构造函数接受 `{ geometryParameters, materialParameters }` 参数,并支持交互事件分发。\n\n### `SphereMesh`\n\n```typescript\nimport { SphereMesh } from 'u-space';\n\nconst sphere = new SphereMesh({\n geometryParameters: { radius: 1, widthSegments: 32, heightSegments: 32 },\n materialParameters: { color: 0x0077ff },\n});\n```\n\n**`SphereMeshParameters`**\n\n| 属性 | 类型 | 说明 |\n| :-------------------- | :----------------------------------- | :----------------------------------------------- |\n| `geometryParameters` | `SphereGeometry` 构造函数参数 | `radius`、`widthSegments`、`heightSegments` 等。 |\n| `materialParameters` | `MeshStandardNodeMaterialParameters` | 标准材质选项(颜色等)。 |\n\n### `PlaneMesh`\n\n水平平面网格。\n\n```typescript\nimport { PlaneMesh } from 'u-space';\n\nconst plane = new PlaneMesh({\n geometryParameters: { width: 10, height: 10 },\n materialParameters: { color: 0x888888 },\n});\n```\n\n### `CircleMesh`\n\n圆形平面网格。\n\n```typescript\nimport { CircleMesh } from 'u-space';\n\nconst circle = new CircleMesh({\n geometryParameters: { radius: 5, segments: 64 },\n});\n```\n\n### `TubeMesh`\n\n沿指定 `Curve<Vector3>` 曲线生成的管道网格。\n\n```typescript\nimport { TubeMesh } from 'u-space';\nimport { CatmullRomCurve3, Vector3 } from 'three/webgpu';\n\nconst path = new CatmullRomCurve3([new Vector3(0,0,0), new Vector3(5,2,5)]);\nconst tube = new TubeMesh({\n geometryParameters: { path, tubularSegments: 20, radius: 0.2, radialSegments: 8 },\n materialParameters: { color: 0x00ff00 },\n});\n```\n\n### `ShapeMesh`\n\n由 `THREE.Shape` 或 2D 点生成的平面网格。\n\n```typescript\nimport { ShapeMesh } from 'u-space';\n\n// 从显式 Shape 创建\nconst mesh = new ShapeMesh({ geometryParameters: { shape: myShape } });\n\n// 静态辅助方法:从 x/z 点数组创建\nconst mesh2 = ShapeMesh.createFromPoints([{ x: 0, z: 0 }, { x: 5, z: 0 }, { x: 5, z: 5 }]);\n```\n\n### `ExtrudeMesh`\n\n由 `THREE.Shape` 或 2D 点拉伸生成的 3D 实体网格。\n\n```typescript\nimport { ExtrudeMesh } from 'u-space';\n\nconst solid = new ExtrudeMesh({\n geometryParameters: {\n shape: myShape,\n options: { depth: 3, bevelEnabled: false },\n },\n materialParameters: { color: 0xff8800 },\n});\n\n// 静态辅助方法\nconst solid2 = ExtrudeMesh.createFromPoints(\n [{ x: 0, z: 0 }, { x: 5, z: 0 }, { x: 5, z: 5 }],\n { geometryParameters: { options: { depth: 2, bevelEnabled: false } } }\n);\n```\n\n## Poi\n\n`Poi` 是一个基于 canvas 渲染的公告板(精灵),用于在 3D 场景中放置图标和标签,继承自 `BaseSprite`。\n\n```typescript\nimport { Poi } from 'u-space';\n\nconst poi = new Poi({\n img: '/icons/marker.png',\n text: '我的位置',\n fontSize: 28,\n color: '#ffffff',\n backgroundColor: 'rgba(0,0,0,0.6)',\n textPosition: 'right',\n});\n// 构造时会自动调用 updateAsync(),无需手动调用\n// 如需更新参数,可再次调用:\n// await poi.updateAsync({ text: '新文字' });\n\npoi.position.set(10, 5, 10);\nviewer.scene.add(poi);\n```\n\n### `PoiParameters`\n\n| 属性 | 类型 | 默认值 | 说明 |\n| :---------------- | :------------------------------------------ | :----------------------- | :--------------------------------- |\n| `img` | `string \\| CanvasImageSource` | `''` | 图标图片 URL 或元素。 |\n| `text` | `string` | `''` | 标签文字。 |\n| `fontSize` | `number` | `32` | 字体大小(像素)。 |\n| `fontFamily` | `string` | `'Arial'` | 字体族。 |\n| `color` | `string` | `'#ffffff'` | 文字颜色。 |\n| `iconSize` | `number` | `64` | 图标大小(像素)。 |\n| `padding` | `number` | `10` | 内容四周的内边距。 |\n| `backgroundColor` | `string` | `'rgba(0, 0, 0, 0)'` | 背景填充颜色。 |\n| `borderRadius` | `number` | `8` | 背景圆角半径。 |\n| `textPosition` | `'top' \\| 'bottom' \\| 'left' \\| 'right'` | `'right'` | 文字相对图标的位置。 |\n| `scaleFactor` | `number` | `0.01` | canvas 像素到世界单位的缩放系数。 |\n\n### 方法\n\n#### `updateAsync(parameters?)`\n\n使用可选的参数覆盖重新渲染 canvas 纹理。构造时会自动调用一次,后续参数未变化时会跳过重绘。\n\n```typescript\nawait poi.updateAsync({ text: '更新后的标签', color: '#ffff00' });\nviewer.render();\n```\n\n#### `dispose()`\n\n释放 canvas 纹理和材质。\n\n## Topology\n\n`Topology` 是一个带有内置 3D 可视化的图数据结构。它存储节点(位置)和带权重的边,实现了 Dijkstra 最短路径算法,并将图渲染为球体和管道。\n\n```typescript\nimport { Topology } from 'u-space';\n\nconst topo = new Topology({\n nodeColor: 0x0000ff,\n nodeRadius: 0.3,\n edgeColor: 0x00ff00,\n edgeRadius: 0.05,\n});\n\ntopo.addNode('A', new Vector3(0, 0, 0));\ntopo.addNode('B', new Vector3(5, 0, 0));\ntopo.addNode('C', new Vector3(5, 0, 5));\n\ntopo.addEdge('A', 'B');\ntopo.addEdge('B', 'C');\n\ntopo.renderGraph(); // 创建球体 + 管道网格\nviewer.scene.add(topo);\n```\n\n### `TopologyParameters`\n\n| 属性 | 类型 | 默认值 | 说明 |\n| :------------ | :-------------------- | :---------- | :----------------------- |\n| `nodeColor` | `ColorRepresentation` | `0x0000ff` | 节点球体颜色。 |\n| `nodeRadius` | `number` | `0.5` | 节点球体半径。 |\n| `edgeColor` | `ColorRepresentation` | `0x00ff00` | 边管道颜色。 |\n| `edgeRadius` | `number` | `0.1` | 边管道半径。 |\n| `pathColor` | `ColorRepresentation` | `0xff00ff` | 路径可视化颜色。 |\n| `pathRadius` | `number` | `0.2` | 路径管道半径。 |\n\n### 方法\n\n#### `addNode(id, position)`\n\n向图中添加一个节点。\n\n#### `removeNode(id)`\n\n移除一个节点及其关联的边。\n\n#### `addEdge(from, to, weight?, bidirectional?)`\n\n在两个节点之间添加一条边。`weight` 默认为欧氏距离,`bidirectional` 默认为 `true`(双向)。\n\n#### `removeEdge(from, to, bidirectional?)`\n\n移除两个节点之间的边。\n\n#### `getShortestPath(startId, endId): Vector3[]`\n\n使用 Dijkstra 算法返回最短路径(世界坐标位置数组)。若不存在路径则返回 `[]`。\n\n#### `renderGraph()`\n\n根据当前节点和边构建球体/管道场景图。修改图后需调用此方法刷新可视化。\n\n#### `clearGraph()`\n\n移除并释放所有图网格。\n\n#### `renderPath(points, color?): TubeMesh`\n\n将给定点通过 CatmullRomCurve3 平滑处理后渲染为管道网格。\n\n```typescript\nconst path = topo.getShortestPath('A', 'C');\ntopo.renderPath(path, 0xff0000);\nviewer.render();\n```\n\n#### `clearPaths()`\n\n移除并释放所有路径网格。\n\n#### `getNeighbors(id): Map<string, number> | undefined`\n\n返回节点的邻接表(邻居 ID → 边权重)。\n\n#### `exportData(): TopologyData`\n\n将当前拓扑图(节点和边)导出为 JSON 可序列化对象。\n\n```typescript\nconst data = topo.exportData();\n// { nodes: Record<string, {x,y,z}>, edges: Array<{from,to,weight}> }\n```\n\n双向边在导出时会去重,只保留一条记录。\n\n#### `importData(data: TopologyData)`\n\n从 `exportData()` 返回的数据中还原拓扑图,替换当前所有节点、边和路径网格,并重新渲染。\n\n```typescript\ntopo.importData(data);\n```\n\n#### `dispose()`\n\n清除图和路径网格。\n"
|
|
752
|
+
"content": "# Objects API\n\n`u-space` 提供了若干对象封装类,主要扩展自 Three.js 的基础节点,使 3D 资产的操作和加载更加便捷。\n\n## 基础类\n\n### `BaseMesh`\n\n继承自 `THREE.Mesh`,`u-space` 中所有基础网格类都继承于此。\n\n**关键属性:**\n\n| 属性 | 类型 | 默认值 | 说明 |\n| :--------------------------- | :-------- | :----- | :------------------------------------------------------------- |\n| `castShadow` | `boolean` | `true` | 默认参与阴影投射;可在单个对象上改为 `false`。 |\n| `receiveShadow` | `boolean` | `true` | 默认接收阴影;可在不需要阴影的网格上改为 `false`。 |\n| `ignoreInvisibleWhenRaycast` | `boolean` | `true` | 为 `true` 时,不可见的网格在射线检测时会被跳过(不参与碰撞)。 |\n\n### `BaseGroup`\n\n继承自 `THREE.Group`,`Model` 和 `Topology` 均继承于此。\n\n**关键属性:**\n\n| 属性 | 类型 | 默认值 | 说明 |\n| :--------------------------- | :-------- | :----- | :--------------------------------------------------------------- |\n| `ignoreInvisibleWhenRaycast` | `boolean` | `true` | 为 `true` 时,不可见的组在射线检测时会被跳过(不参与碰撞)。 |\n\n## `InstanceObject`\n\n`InstanceObject` 是核心包导出的统一实例对象基类。无论实例最终由合并楼层几何、`InstancedMesh`、`EditableGeometryBatchLayer` 还是 fallback `Model` 渲染,业务代码都可以使用相同的身份、显隐、颜色、透明度、高亮、包围盒和 materialize API。\n\n```typescript\nimport {\n InstanceObject,\n isInstanceObject,\n INSTANCE_COLOR_MODE_NONE,\n INSTANCE_COLOR_MODE_OVERRIDE,\n INSTANCE_COLOR_MODE_TINT,\n} from 'u-space';\n```\n\n### 类型和常量\n\n#### `InstanceStyle`\n\n| 属性 | 类型 | 说明 |\n| :---------- | :-------------- | :------------------------------------------------------ |\n| `color` | `Color \\| null` | 当前样式颜色;`null` 表示不使用实例颜色。 |\n| `colorMode` | `number` | 当前颜色模式,值为下表中的颜色模式常量。 |\n| `opacity` | `number` | 当前透明度,实例设置 API 会将其限制在 `0` 到 `1` 之间。 |\n\n#### `InstanceObjectOptions`\n\n| 属性 | 类型 | 必填 | 默认值 | 说明 |\n| :------------ | :------- | :--- | :----------------- | :------------------------- |\n| `boundsName?` | `string` | 否 | `'InstanceBounds'` | 内部手动包围盒节点的名称。 |\n\n#### `InstanceIdentity`\n\n| 属性 | 类型 | 必填 | 说明 |\n| :------ | :--------------- | :--- | :------------------------------------------------------ |\n| `id?` | `string \\| null` | 否 | 实例 ID。省略时保持原值,显式传 `null` 时清为空字符串。 |\n| `kind?` | `string \\| null` | 否 | 实例类型。省略时保持原值,显式传 `null` 时清为空字符串。 |\n| `name?` | `string \\| null` | 否 | 实例名称。省略时保持原值,显式传 `null` 时清为空字符串。 |\n\n`InstanceIdentity` 的三个字段都可省略且可为 `null`:省略的字段保持不变,显式传入 `null` 会将对应身份字段清为空字符串。\n\n#### 颜色模式常量\n\n| 常量 | 值 | 说明 |\n| :----------------------------- | :-: | :--------------------------------------------- |\n| `INSTANCE_COLOR_MODE_NONE` | `0` | 不使用实例颜色。 |\n| `INSTANCE_COLOR_MODE_OVERRIDE` | `1` | 使用指定颜色覆盖原材质颜色。 |\n| `INSTANCE_COLOR_MODE_TINT` | `2` | 将指定颜色作为 tint 与原材质颜色混合。 |\n\n### 字段\n\n| 字段 | 类型 | 默认值 | 说明 |\n| :--------------- | :------------------ | :----------------- | :------------------------------------------------------- |\n| `isInstanceObject` | `boolean` | `true` | 实例对象标识,供 `isInstanceObject()` 类型守卫使用。 |\n| `type` | `string` | `'InstanceObject'` | Three.js 对象类型名称。 |\n| `instanceId` | `string` | `''` | 实例 ID。 |\n| `instanceKind` | `string` | `''` | 实例类型。 |\n| `instanceName` | `string` | `''` | 实例名称。 |\n| `geometry` | `BufferGeometry` | 空几何体 | 实例对象公开的兼容几何体字段。 |\n| `boundingBox` | `Box3 \\| null` | `null` | 本地坐标包围盒缓存。 |\n| `boundingSphere` | `Sphere \\| null` | `null` | 本地坐标包围球缓存。 |\n\n### 构造函数\n\n#### `new InstanceObject(options?)`\n\n```typescript\nconst instance = new InstanceObject({ boundsName: 'FloorBounds' });\n```\n\n`options` 的类型为 `InstanceObjectOptions`;省略时使用默认的内部包围盒节点名称 `InstanceBounds`。\n\n### 身份和样式\n\n身份、显隐和基础样式方法都返回当前实例,可以链式调用:\n\n```typescript\nconst instance = new InstanceObject()\n .setInstanceIdentity({ id: 'room-101', kind: 'room', name: '会议室 101' })\n .setInstanceVisible(true)\n .setInstanceColor('#4f8cff')\n .setInstanceOpacity(0.65);\n\ninstance.setInstanceHighlight('#ffff00', 0.9);\ninstance.clearInstanceHighlight();\ninstance.resetInstanceColor();\n```\n\n| 方法 | 返回值 | 说明 |\n| :--- | :----- | :--- |\n| `setInstanceIdentity(identity: InstanceIdentity)` | `this` | 更新身份字段;省略字段保持不变,`null` 将对应字段清为空字符串。 |\n| `setInstanceVisible(visible: boolean)` | `this` | 设置实例显隐并标记渲染数据需要同步。 |\n| `setInstanceColor(color: ColorRepresentation)` | `this` | 设置基础覆盖色,颜色模式变为 `INSTANCE_COLOR_MODE_OVERRIDE`。 |\n| `resetInstanceColor()` | `this` | 清除基础颜色,颜色模式恢复为 `INSTANCE_COLOR_MODE_NONE`。 |\n| `setInstanceOpacity(opacity: number)` | `this` | 设置基础透明度;传入值会被限制在 `0` 到 `1` 之间。 |\n| `getInstanceColor(target = new Color())` | `Color` | 将当前生效样式的颜色写入 `target` 并返回;未设置颜色时返回白色。 |\n| `hasInstanceColor()` | `boolean` | 当前生效样式是否包含颜色。 |\n| `getInstanceOpacity()` | `number` | 返回当前生效样式的透明度。 |\n| `setInstanceHighlight(color: ColorRepresentation, opacity: number, overwrite = false)` | `this` | 设置临时高亮;默认使用 `INSTANCE_COLOR_MODE_TINT`,`overwrite` 为 `true` 时使用覆盖模式,透明度会被限制在 `0` 到 `1` 之间。 |\n| `clearInstanceHighlight()` | `this` | 清除临时高亮并恢复基础样式。 |\n| `getInstanceColorMode()` | `number` | 返回当前生效样式的颜色模式。 |\n\n临时高亮优先于基础颜色和透明度;`clearInstanceHighlight()` 会恢复基础样式。默认场景矩阵自动更新开启时,实例 transform 的世界矩阵发生变化会自动触发 dirty callback,所属 `ModelInstancedLayer` 会在下一次渲染前同步 instance buffer。业务代码只需修改 `position`、`rotation`、`quaternion` 或 `scale`,不需要直接写内部 instance matrix,也不需要手动调用 `InstanceObject.updateMatrixWorld()`。\n\n如果为大型静态场景显式设置了 `viewer.scene.matrixWorldAutoUpdate = false`,renderer 不会再遍历该根节点,修改后必须用 Three.js 原生方法提交受影响范围并请求新帧:\n\n```typescript\ninstance.position.set(10, 0, 5);\ninstance.updateWorldMatrix(true, false);\nviewer.invalidate();\n\n// 修改父 Group 时更新整个受影响子树。\ngroup.updateWorldMatrix(true, true);\nviewer.invalidate();\n```\n\n`InstanceObject` 覆盖了原生 `updateWorldMatrix(updateParents, updateChildren)` 和 `updateMatrixWorld(force)`,两条路径都会检测世界矩阵及祖先显隐变化并触发 dirty callback。对 Group 使用 `updateChildren: true` 时,后代 `InstanceObject` 也会执行该检测;editable batch 中与烘焙矩阵不同的 `SceneInstanceObject` 会按合批规则 materialize。\n\n### 包围盒和相机定位\n\n`boundingBox` 和 `boundingSphere` 是本地坐标缓存;`computeBoundingBox()` 与 `computeBoundingSphere()` 会按需更新它们。`getInstanceBoundingBox(target, true)` 返回世界坐标包围盒。\n\n| 方法 | 返回值 | 说明 |\n| :--- | :----- | :--- |\n| `setInstanceBounds(bounds: Box3)` | `this` | 克隆并设置手动本地包围盒;空包围盒会被忽略。 |\n| `computeBoundingBox()` | `void` | 计算并缓存本地坐标包围盒。 |\n| `computeBoundingSphere()` | `void` | 基于本地包围盒计算并缓存本地坐标包围球。 |\n| `getInstanceBoundingBox(target = new Box3(), world = false)` | `Box3` | 将包围盒写入 `target`;`world` 为 `true` 时返回世界坐标结果。 |\n\n实例可以直接交给相机控制器定位:\n\n```typescript\nawait viewer.controls.flyToObject(instance);\n```\n\n当实例没有可供计算的渲染几何体,或业务端已经拥有精确范围时,可以手动提供本地包围盒并按需获取世界范围:\n\n```typescript\nimport { Box3, Vector3 } from 'three/webgpu';\n\ninstance\n .setInstanceBounds(new Box3(\n new Vector3(-2, 0, -3),\n new Vector3(2, 4, 3),\n ))\n .position.set(100, 0, 50);\n\nconst worldBounds = instance.getInstanceBoundingBox(new Box3(), true);\n```\n\n### Fallback 渲染对象\n\n`setInstanceRenderObject()` 用于挂载无法进入合并几何或 `InstancedMesh` 管线的 fallback Three.js 对象。`InstanceObject` 会将当前显隐、颜色、透明度和高亮语义统一应用到该对象。\n\n| 方法 | 返回值 | 说明 |\n| :--- | :----- | :--- |\n| `setInstanceRenderObject(object: Object3D \\| null)` | `this` | 替换 fallback 渲染对象;传入 `null` 时解除当前对象。 |\n| `getInstanceRenderObject()` | `Object3D \\| null` | 返回当前 fallback 渲染对象。 |\n| `setInstanceMaterializer(materializer)` | `this` | 设置 batching 实现使用的低层 materialize delegate;传入 `null` 时解除。普通业务通常不直接调用。 |\n| `clearInstanceMaterializer(materializer?)` | `this` | 不传参数时直接解除;传入 delegate 时只在当前 delegate 与其相同时解除,用于安全释放 materializer 所有权。 |\n| `materialize()` | `Object3D \\| null` | 调用当前 materializer;未连接 materializer 时返回已有 fallback render object 或 `null`。 |\n\n```typescript\nimport { Model } from 'u-space';\n\nconst fallbackModel = new Model();\nawait fallbackModel.loadAsync({ url: '/models/special-room.glb' });\n\ninstance\n .setInstanceRenderObject(fallbackModel)\n .setInstanceColor('#ff8a3d')\n .setInstanceOpacity(0.8);\n```\n\n`EditableGeometryBatchLayer` 会自动连接 `setInstanceMaterializer()`,并在 dispose 时通过所有权匹配安全解除。当实例 transform 或半透明状态无法继续由烘焙 Geometry 表达时,layer 会 clone 完整模板、克隆独立材质并挂到当前实例,同时立即提交新对象的世界矩阵并让其他 active layer 隐藏同一实例的旧烘焙副本;业务也可以显式调用 `instance.materialize()`。完整构建、unsupported fallback 和事件语义见 [Batches API](./api-batches#editablegeometrybatchlayer)。\n\n### Dirty callback\n\n| 方法 | 返回值 | 说明 |\n| :--- | :----- | :--- |\n| `onInstanceRenderDirty(callback: () => void)` | `() => boolean` | 注册 dirty callback,并返回用于取消订阅的函数。 |\n| `markInstanceRenderDirty()` | `this` | 立即通知所有已注册的 dirty callback。 |\n\n保存并调用取消订阅函数,避免监听方销毁后遗留回调:\n\n```typescript\nconst dirtyInstances = new Set<InstanceObject>();\nconst unsubscribe = instance.onInstanceRenderDirty(() => {\n dirtyInstances.add(instance);\n});\n\n// 不再需要监听时\nunsubscribe();\ndirtyInstances.delete(instance);\n```\n\n### 类型守卫\n\n#### `isInstanceObject(object)`\n\n`isInstanceObject(object: Object3D): object is InstanceObject` 检查对象的 `isInstanceObject` 标识,并在 TypeScript 中将其收窄为 `InstanceObject`。\n\n```typescript\nif (isInstanceObject(object)) {\n object.setInstanceHighlight('#ffff00', 1);\n}\n```\n\n## 模型\n\n`Model` 类继承自 `BaseGroup`(进而继承自 `THREE.Group`),简化了异步加载 `glb`、`gltf` 等外部 3D 模型的流程,内置多层缓存支持。\n\n```typescript\nimport { Model } from 'u-space';\n\nconst myModel = new Model();\n```\n\n### 加载资产\n\n`loadAsync` 方法负责从 URL 加载资产并将其添加到组中。\n\n```typescript\nawait myModel.loadAsync(parameters: ModelParameters)\n```\n\n#### `ModelParameters`\n\n| 属性 | 类型 | 默认值 | 说明 |\n| :----------- | :-------- | :----- | :-------------------------------------------------------------------------------------- |\n| `url` | `string` | — | `.gltf` 或 `.glb` 文件的 URL。 |\n| `cache` | `boolean` | `true` | 启用内存缓存。若模型已加载过,则直接从内存中获取并克隆。 |\n| `persistent` | `boolean` | `true` | 使用浏览器 Cache API 启用持久化磁盘缓存,适合较大的模型。 |\n\n#### 缓存示例\n\n```javascript\n// 默认开启双层缓存,直接加载即可\nconst model1 = new Model();\nmodel1.loadAsync({ url: 'model.glb' });\n\n// 禁用内存缓存(每次都从网络或持久化缓存重新加载)\nconst model2 = new Model();\nmodel2.loadAsync({ url: 'model.glb', cache: false });\n\n// 完全禁用缓存\nconst model3 = new Model();\nmodel3.loadAsync({ url: 'model.glb', cache: false, persistent: false });\n```\n\n### 动画\n\n`Model` 在加载含内置动画的模型(如 glTF/FBX)后,会自动初始化 `AnimationMixer` 并收集 `AnimationClip`。\n\n**属性:**\n\n| 属性 | 类型 | 说明 |\n| :----------- | :-------------------------- | :----------------------------------------- |\n| `mixer` | `AnimationMixer \\| null` | 动画混合器,加载含动画模型后自动创建。 |\n| `animations` | `AnimationClip[]` | 模型内置的动画片段列表。 |\n\n#### `playAnimation(nameOrIndex?, options?)`\n\n播放指定动画。不传参时播放第一个动画。\n\n```typescript\n// 按名称播放\nmodel.playAnimation('Walk');\n\n// 按索引播放\nmodel.playAnimation(0);\n\n// 带选项\nmodel.playAnimation('Run', {\n loop: true, // 是否循环(默认 true)\n repetitions: 3, // 循环次数(默认 Infinity)\n timeScale: 1.5, // 播放速度(默认 1)\n clampWhenFinished: true, // 播放结束后停留在最后一帧(默认 false)\n});\n```\n\n#### `playAllAnimations(options?)`\n\n播放模型所有内置动画。参数同 `playAnimation`。\n\n```typescript\nmodel.playAllAnimations({ timeScale: 2 });\n```\n\n#### `stopAnimation(nameOrIndex?)`\n\n停止指定动画。不传参时停止所有动画。\n\n```typescript\nmodel.stopAnimation('Walk'); // 停止指定动画\nmodel.stopAnimation(); // 停止所有动画\n```\n\n#### `updateAnimation(delta)`\n\n推进动画时间,需在渲染循环中调用。`delta` 为帧间隔时间(秒),可从 Viewer 事件的 `delta` 属性获取。\n\n```typescript\nviewer.addEventListener('beforeRender', (e) => {\n model.updateAnimation(e.delta);\n});\n```\n\n### 空间查询\n\n#### `getBoundingBox()`\n\n获取模型的轴对齐包围盒(AABB)。\n\n```typescript\nconst box = myModel.getBoundingBox();\nconst size = box.getSize(new Vector3());\nconsole.log('模型尺寸:', size);\n```\n\n#### `getCenter()`\n\n获取模型包围盒的中心点。\n\n```typescript\nconst center = myModel.getCenter();\nconsole.log('模型中心:', center);\n```\n\n### 材质操作\n\n#### `setMaterial(material)`\n\n将模型内所有 Mesh 的材质统一替换为指定材质。\n\n```typescript\nimport { MeshStandardMaterial } from 'three/webgpu';\n\nmyModel.setMaterial(new MeshStandardMaterial({ color: 0xff0000 }));\n```\n\n### 清除缓存\n\n可通过 `Model` 类的静态方法手动清除内部缓存:\n\n- `Model.clearMemoryCache()`:清除运行时内存缓存。\n- `await Model.clearPersistentCache()`:完全清除浏览器中 `u-space` 模型的持久化缓存。\n\n## 网格\n\n`u-space` 提供了多种精简的网格类,均继承自 `BaseMesh`。所有类默认使用 `MeshStandardNodeMaterial`,构造函数接受 `{ geometryParameters, materialParameters }` 参数,并支持交互事件分发。\n\n### `SphereMesh`\n\n```typescript\nimport { SphereMesh } from 'u-space';\n\nconst sphere = new SphereMesh({\n geometryParameters: { radius: 1, widthSegments: 32, heightSegments: 32 },\n materialParameters: { color: 0x0077ff },\n});\n```\n\n**`SphereMeshParameters`**\n\n| 属性 | 类型 | 说明 |\n| :-------------------- | :----------------------------------- | :----------------------------------------------- |\n| `geometryParameters` | `SphereGeometry` 构造函数参数 | `radius`、`widthSegments`、`heightSegments` 等。 |\n| `materialParameters` | `MeshStandardNodeMaterialParameters` | 标准材质选项(颜色等)。 |\n\n### `PlaneMesh`\n\n水平平面网格。\n\n```typescript\nimport { PlaneMesh } from 'u-space';\n\nconst plane = new PlaneMesh({\n geometryParameters: { width: 10, height: 10 },\n materialParameters: { color: 0x888888 },\n});\n```\n\n### `CircleMesh`\n\n圆形平面网格。\n\n```typescript\nimport { CircleMesh } from 'u-space';\n\nconst circle = new CircleMesh({\n geometryParameters: { radius: 5, segments: 64 },\n});\n```\n\n### `TubeMesh`\n\n沿指定 `Curve<Vector3>` 曲线生成的管道网格。\n\n```typescript\nimport { TubeMesh } from 'u-space';\nimport { CatmullRomCurve3, Vector3 } from 'three/webgpu';\n\nconst path = new CatmullRomCurve3([new Vector3(0,0,0), new Vector3(5,2,5)]);\nconst tube = new TubeMesh({\n geometryParameters: { path, tubularSegments: 20, radius: 0.2, radialSegments: 8 },\n materialParameters: { color: 0x00ff00 },\n});\n```\n\n### `ShapeMesh`\n\n由 `THREE.Shape` 或 2D 点生成的平面网格。\n\n```typescript\nimport { ShapeMesh } from 'u-space';\n\n// 从显式 Shape 创建\nconst mesh = new ShapeMesh({ geometryParameters: { shape: myShape } });\n\n// 静态辅助方法:从 x/z 点数组创建\nconst mesh2 = ShapeMesh.createFromPoints([{ x: 0, z: 0 }, { x: 5, z: 0 }, { x: 5, z: 5 }]);\n```\n\n### `ExtrudeMesh`\n\n由 `THREE.Shape` 或 2D 点拉伸生成的 3D 实体网格。\n\n```typescript\nimport { ExtrudeMesh } from 'u-space';\n\nconst solid = new ExtrudeMesh({\n geometryParameters: {\n shape: myShape,\n options: { depth: 3, bevelEnabled: false },\n },\n materialParameters: { color: 0xff8800 },\n});\n\n// 静态辅助方法\nconst solid2 = ExtrudeMesh.createFromPoints(\n [{ x: 0, z: 0 }, { x: 5, z: 0 }, { x: 5, z: 5 }],\n { geometryParameters: { options: { depth: 2, bevelEnabled: false } } }\n);\n```\n\n## Poi\n\n`Poi` 是一个基于 canvas 渲染的公告板(精灵),用于在 3D 场景中放置图标和标签,继承自 `BaseSprite`。\n\n```typescript\nimport { Poi } from 'u-space';\n\nconst poi = new Poi({\n img: '/icons/marker.png',\n text: '我的位置',\n fontSize: 28,\n color: '#ffffff',\n backgroundColor: 'rgba(0,0,0,0.6)',\n textPosition: 'right',\n});\n// 构造时会自动调用 updateAsync(),无需手动调用\n// 如需更新参数,可再次调用:\n// await poi.updateAsync({ text: '新文字' });\n\npoi.position.set(10, 5, 10);\nviewer.scene.add(poi);\n```\n\n### `PoiParameters`\n\n| 属性 | 类型 | 默认值 | 说明 |\n| :---------------- | :------------------------------------------ | :----------------------- | :--------------------------------- |\n| `img` | `string \\| CanvasImageSource` | `''` | 图标图片 URL 或元素。 |\n| `text` | `string` | `''` | 标签文字。 |\n| `fontSize` | `number` | `32` | 字体大小(像素)。 |\n| `fontFamily` | `string` | `'Arial'` | 字体族。 |\n| `color` | `string` | `'#ffffff'` | 文字颜色。 |\n| `iconSize` | `number` | `64` | 图标大小(像素)。 |\n| `padding` | `number` | `10` | 内容四周的内边距。 |\n| `backgroundColor` | `string` | `'rgba(0, 0, 0, 0)'` | 背景填充颜色。 |\n| `borderRadius` | `number` | `8` | 背景圆角半径。 |\n| `textPosition` | `'top' \\| 'bottom' \\| 'left' \\| 'right'` | `'right'` | 文字相对图标的位置。 |\n| `scaleFactor` | `number` | `0.01` | canvas 像素到世界单位的缩放系数。 |\n\n### 方法\n\n#### `updateAsync(parameters?)`\n\n使用可选的参数覆盖重新渲染 canvas 纹理。构造时会自动调用一次,后续参数未变化时会跳过重绘。\n\n```typescript\nawait poi.updateAsync({ text: '更新后的标签', color: '#ffff00' });\nviewer.render();\n```\n\n#### `dispose()`\n\n释放 canvas 纹理和材质。\n\n## Topology\n\n`Topology` 是一个带有内置 3D 可视化的图数据结构。它存储节点(位置)和带权重的边,实现了 Dijkstra 最短路径算法,并将图渲染为球体和管道。\n\n```typescript\nimport { Topology } from 'u-space';\n\nconst topo = new Topology({\n nodeColor: 0x0000ff,\n nodeRadius: 0.3,\n edgeColor: 0x00ff00,\n edgeRadius: 0.05,\n});\n\ntopo.addNode('A', new Vector3(0, 0, 0));\ntopo.addNode('B', new Vector3(5, 0, 0));\ntopo.addNode('C', new Vector3(5, 0, 5));\n\ntopo.addEdge('A', 'B');\ntopo.addEdge('B', 'C');\n\ntopo.renderGraph(); // 创建球体 + 管道网格\nviewer.scene.add(topo);\n```\n\n### `TopologyParameters`\n\n| 属性 | 类型 | 默认值 | 说明 |\n| :------------ | :-------------------- | :---------- | :----------------------- |\n| `nodeColor` | `ColorRepresentation` | `0x0000ff` | 节点球体颜色。 |\n| `nodeRadius` | `number` | `0.5` | 节点球体半径。 |\n| `edgeColor` | `ColorRepresentation` | `0x00ff00` | 边管道颜色。 |\n| `edgeRadius` | `number` | `0.1` | 边管道半径。 |\n| `pathColor` | `ColorRepresentation` | `0xff00ff` | 路径可视化颜色。 |\n| `pathRadius` | `number` | `0.2` | 路径管道半径。 |\n\n### 方法\n\n#### `addNode(id, position)`\n\n向图中添加一个节点。\n\n#### `removeNode(id)`\n\n移除一个节点及其关联的边。\n\n#### `addEdge(from, to, weight?, bidirectional?)`\n\n在两个节点之间添加一条边。`weight` 默认为欧氏距离,`bidirectional` 默认为 `true`(双向)。\n\n#### `removeEdge(from, to, bidirectional?)`\n\n移除两个节点之间的边。\n\n#### `getShortestPath(startId, endId): Vector3[]`\n\n使用 Dijkstra 算法返回最短路径(世界坐标位置数组)。若不存在路径则返回 `[]`。\n\n#### `renderGraph()`\n\n根据当前节点和边构建球体/管道场景图。修改图后需调用此方法刷新可视化。\n\n#### `clearGraph()`\n\n移除并释放所有图网格。\n\n#### `renderPath(points, color?): TubeMesh`\n\n将给定点通过 CatmullRomCurve3 平滑处理后渲染为管道网格。\n\n```typescript\nconst path = topo.getShortestPath('A', 'C');\ntopo.renderPath(path, 0xff0000);\nviewer.render();\n```\n\n#### `clearPaths()`\n\n移除并释放所有路径网格。\n\n#### `getNeighbors(id): Map<string, number> | undefined`\n\n返回节点的邻接表(邻居 ID → 边权重)。\n\n#### `exportData(): TopologyData`\n\n将当前拓扑图(节点和边)导出为 JSON 可序列化对象。\n\n```typescript\nconst data = topo.exportData();\n// { nodes: Record<string, {x,y,z}>, edges: Array<{from,to,weight}> }\n```\n\n双向边在导出时会去重,只保留一条记录。\n\n#### `importData(data: TopologyData)`\n\n从 `exportData()` 返回的数据中还原拓扑图,替换当前所有节点、边和路径网格,并重新渲染。\n\n```typescript\ntopo.importData(data);\n```\n\n#### `dispose()`\n\n清除图和路径网格。\n"
|
|
648
753
|
},
|
|
649
754
|
{
|
|
650
755
|
"id": "api-plugin-atmosphere",
|
|
@@ -915,7 +1020,7 @@ export const docsIndex = [
|
|
|
915
1020
|
},
|
|
916
1021
|
{
|
|
917
1022
|
"level": 3,
|
|
918
|
-
"text": "
|
|
1023
|
+
"text": "ModelInstancedLayer"
|
|
919
1024
|
},
|
|
920
1025
|
{
|
|
921
1026
|
"level": 3,
|
|
@@ -933,6 +1038,30 @@ export const docsIndex = [
|
|
|
933
1038
|
"level": 2,
|
|
934
1039
|
"text": "SceneLoader"
|
|
935
1040
|
},
|
|
1041
|
+
{
|
|
1042
|
+
"level": 3,
|
|
1043
|
+
"text": "可编辑静态合批:setEditableBatching()"
|
|
1044
|
+
},
|
|
1045
|
+
{
|
|
1046
|
+
"level": 4,
|
|
1047
|
+
"text": "合并规则"
|
|
1048
|
+
},
|
|
1049
|
+
{
|
|
1050
|
+
"level": 4,
|
|
1051
|
+
"text": "对象级状态与 materialize"
|
|
1052
|
+
},
|
|
1053
|
+
{
|
|
1054
|
+
"level": 4,
|
|
1055
|
+
"text": "Fallback 与透明材质"
|
|
1056
|
+
},
|
|
1057
|
+
{
|
|
1058
|
+
"level": 4,
|
|
1059
|
+
"text": "Raycast 与统计"
|
|
1060
|
+
},
|
|
1061
|
+
{
|
|
1062
|
+
"level": 3,
|
|
1063
|
+
"text": "SceneGroup"
|
|
1064
|
+
},
|
|
936
1065
|
{
|
|
937
1066
|
"level": 3,
|
|
938
1067
|
"text": "SceneInstanceObject"
|
|
@@ -941,6 +1070,10 @@ export const docsIndex = [
|
|
|
941
1070
|
"level": 3,
|
|
942
1071
|
"text": "SceneInstancedLayer"
|
|
943
1072
|
},
|
|
1073
|
+
{
|
|
1074
|
+
"level": 3,
|
|
1075
|
+
"text": "SceneEditableBatchLayer"
|
|
1076
|
+
},
|
|
944
1077
|
{
|
|
945
1078
|
"level": 2,
|
|
946
1079
|
"text": "TopologiesLoader / TopologyParser"
|
|
@@ -958,13 +1091,13 @@ export const docsIndex = [
|
|
|
958
1091
|
"text": "PropertiesLoader"
|
|
959
1092
|
}
|
|
960
1093
|
],
|
|
961
|
-
"content": "# u-manager\n\n`u-manager` 是一套全面的加载器和解析器,用于从服务器路径流式传输、解密并显示结构化场景数据。支持场景、拓扑、动画、属性、相机视点、楼层、井道和设备语义模型。\n\n所有加载器均继承自 Three.js 的 `Loader`,并提供 `setPath(path)` 方法,在调用 `loadAsync()` 前配置基础数据目录。\n\n## `UManagerLoader`\n\n`UManagerLoader` 是推荐的一体化入口。它会在同一路径下同时加载 `SemanticLoader` 和 `SceneLoader`:语义文件中已经存在的 `id` 会由 `SceneLoader` 自动跳过,避免建筑、楼层、设备被重复创建;非语义场景树节点仍会正常加载。返回值是一个 `UManagerSceneGroup`,可直接通过 `semanticGroup` 访问 `SemanticGroup`,通过 `sceneGroup` 访问非语义场景组。\n\n```typescript\nimport { UManagerLoader } from 'u-space/plugins/u-manager';\n\nconst loader = new UManagerLoader(viewer);\nloader.setPath('./scenes/my-scene');\nloader.setKey('YOUR_LICENSE_KEY');\n\nconst group = await loader.loadAsync();\nviewer.scene.add(group);\n\nconst semanticGroup = group.semanticGroup;\nconst sceneGroup = group.sceneGroup;\n```\n\n完整可运行示例见 [`examples/test_umanager_loader.html`](https://u-space-phi.vercel.app/examples/test_umanager_loader.html)。该示例展示了如何从返回根组中直接读取 `semanticGroup` / `sceneGroup`,并通过 `viewer.objectManager` 获取 `SceneInstanceObject` / `FacilityInstanceObject` 后调用统一 `getSemanticBoundingBox()`、`setSemanticHighlight()`、`setSemanticVisible()` 和 `setSemanticOpacity()`。\n\n## `SemanticLoader`\n\n加载 `db/semantic_model.json` 楼层语义数据。`loadAsync()` 返回顶层 `SemanticGroup`,用于组织整次语义解析产生的建筑、楼层以及 scene-level facility layer。每个建筑包含多个 `FloorMesh`,每个楼层把墙、柱、空间、楼梯等语义对象合并为一个 TSL 材质驱动的网格;建筑级电梯井和通风井会使用 `ExtrudeMesh` 独立渲染。语义模型包含 `Facilities` 时,`SemanticLoader` 会额外读取 `SceneMetadata.json` 指向的 `tree_models`,按场景授权配置解密,再把对应设备模型挂载到所属楼层。\n\n```typescript\nimport { Vector3, Vector4 } from 'three';\nimport { SemanticLoader } from 'u-space/plugins/u-manager';\n\nconst loader = new SemanticLoader(viewer);\nloader.setPath('./scenes/my-scene');\nloader.setKey('YOUR_LICENSE_KEY'); // 官方授权场景解析 tree_models 时必需\n\nconst semanticGroup = await loader.loadAsync(); // SemanticGroup\nviewer.scene.add(semanticGroup);\n\nconst building = semanticGroup.getBuildingAt(0);\nconst floor = building.getFloorAt(0);\n\nbuilding.isolateFloor(floor);\nawait viewer.controls.flyToObject(floor);\n\nfloor.addEventListener('click', ({ event }) => {\n const { currentTarget, intersect } = event;\n if (!intersect) return;\n\n const index = intersect.semanticIndex;\n\n currentTarget.setColorAt(index, new Vector4(1, 0, 0, 0.5));\n currentTarget.setScaleAt(index, new Vector3(1, 0.1, 1));\n viewer.render();\n});\n```\n\n所有可按 ID 操作的实例都会尽量暴露统一的 `SemanticInstanceObject` API。楼层多边形语义使用轻量 `FloorSemanticInstanceObject` 代理合并几何中的一个实例;Facilities 和 SceneLoader path instancing 使用同一套模型实例化底层。业务代码优先使用 `setSemanticVisible()`、`setSemanticColor()`、`setSemanticOpacity()` 和 `getSemanticBoundingBox()`,而不是关心底层是合并几何、`InstancedMesh` 还是 fallback `Model`。\n\n### `SemanticGroup`\n\n`SemanticGroup` 继承自 `BaseGroup`,是单次 `SemanticParser` 解析结果的顶层容器。它管理所有 `BuildingGroup`,并把跨楼层共享的 Facilities `InstancedMesh` 渲染层作为建筑的同级对象挂在 `facilityLayer`,而不是挂到某个 `BuildingGroup` 内。\n\n建筑 ID 和设备 ID 查询使用内部 `Map` 索引维护;`getBuildingById()` 和 `getFacilityById()` 不会通过数组扫描查找语义对象。\n\n| 属性 / 方法 | 说明 |\n| :---------- | :--- |\n| `buildings` | 当前语义场景内的 `BuildingGroup[]`。 |\n| `facilityLayer` | scene-level 设备渲染层;同模型 URL 的静态设备会合并为 `InstancedMesh` batch。 |\n| `addBuilding(group)` / `removeBuilding(group)` | 添加或移除建筑,并同步更新 group 子节点。 |\n| `getBuildingAt(index)` / `getBuildingById(id)` / `getBuildings()` | 按下标、`Building.ID` 或整体列表获取建筑。 |\n| `floorMeshes` | 聚合返回所有建筑内的 `FloorMesh[]`。 |\n| `setFacilityLayer(layer)` | 设置跨楼层设备渲染层;传入 `null` 会移除已有 layer。 |\n| `getFacilityById(id)` | 在整次语义解析结果中按 `Facility.ID` 或显式别名查找设备。 |\n| `showAllFacilities()` / `hideAllFacilities()` | 显示或隐藏整次语义解析结果中的全部设备。 |\n| `showAllFloors()` | 显示所有建筑的所有楼层。 |\n| `planishFloors(kind?)` / `unplanishFloors(kind?)` | 压扁或恢复所有楼层;默认覆盖所有合并语义实例和 Facilities,并按包围盒贴到楼层平面附近,同时添加稳定的轻微 Y 偏移以减少共面闪烁;传入 `kind` 时只作用于该语义类型,`kind` 可包含 `Facilities`。 |\n\n### `BuildingGroup`\n\n`BuildingGroup` 继承自 `BaseGroup`,用于组织一个建筑下的所有楼层、电梯井和通风井。设备语义对象不会直接挂到 `BuildingGroup`;能 instancing 的设备会渲染到 `SemanticGroup.facilityLayer`,同时在对应 `FloorMesh` 下保留可检索、可控制的轻量引用。\n\n建筑内设备 ID 到楼层的关系由内部 `Map` 索引维护,并通过楼层设备索引版本自动同步。\n\n| 属性 / 方法 | 说明 |\n| :---------- | :--- |\n| `floorMeshes` | 当前建筑内的 `FloorMesh[]`。 |\n| `elevatorMeshes` | 当前建筑内由 `Elevators` 语义生成的 `ExtrudeMesh[]`。 |\n| `ventMeshes` | 当前建筑内由 `Vents` 语义生成的 `ExtrudeMesh[]`。 |\n| `addFloor(floor)` | 添加楼层,并同步加入 group。 |\n| `removeFloor(floor)` | 移除楼层,并同步从 group 移除。 |\n| `getFloorAt(index)` | 按楼层下标获取 `FloorMesh`。 |\n| `getFloorById(id)` | 按楼层语义 ID 获取 `FloorMesh`,内部使用 `Map` 索引。 |\n| `addElevator(elevator)` / `removeElevator(elevator)` | 添加或移除电梯井,并同步更新 group。 |\n| `getElevatorAt(index)` | 按下标获取电梯井 `ExtrudeMesh`。 |\n| `getElevatorById(id)` | 按电梯井语义 ID 获取电梯井,内部使用 `Map` 索引。 |\n| `addVent(vent)` / `removeVent(vent)` | 添加或移除通风井,并同步更新 group。 |\n| `getVentAt(index)` | 按下标获取通风井 `ExtrudeMesh`。 |\n| `getVentById(id)` | 按通风井语义 ID 获取通风井,内部使用 `Map` 索引。 |\n| `isolateFloor(floor \\| index)` | 只显示指定楼层,隐藏其他楼层;也兼容传入楼层数组。 |\n| `isolateFloors(floors \\| indexes)` | 只显示指定多个楼层,隐藏其他楼层。 |\n| `showAllFloors()` | 显示全部楼层。 |\n| `getFacilityById(id)` | 在当前建筑的楼层中按 `Facility.ID` 或显式别名查找设备。 |\n| `showAllFacilities()` | 显示整栋建筑内全部楼层设备。 |\n| `hideAllFacilities()` | 隐藏整栋建筑内全部楼层设备。 |\n| `planishFloors(kind?)` | 压扁所有楼层;默认覆盖所有合并语义实例和 Facilities,并按包围盒贴到楼层平面附近,同时添加稳定的轻微 Y 偏移以减少共面闪烁;传入 `kind` 时只压扁该语义类型,`kind` 可包含 `Facilities`。 |\n| `unplanishFloors(kind?)` | 恢复所有楼层;默认恢复所有合并语义实例和 Facilities;传入 `kind` 时只恢复该语义类型,`kind` 可包含 `Facilities`。 |\n\n### 电梯井和通风井\n\n`SemanticLoader` 会读取 `semantic_model.json` 中建筑引用的 `Elevators` 和 `Vents`。每个井道使用 `Polygon.shape` 作为底面轮廓,`Elevation` 作为底面高度,`TotalHeight` 作为拉伸高度,并以 building 级 `ExtrudeMesh` 加入 `BuildingGroup`。\n\n```typescript\nconst elevator = building.getElevatorAt(0);\nconst elevatorById = building.getElevatorById('ELEVATOR_001');\nconst vent = building.getVentAt(0);\nconst ventById = building.getVentById('VENT_001');\n\nif (elevator) {\n await viewer.controls.flyToObject(elevator);\n}\n\nif (vent) {\n building.removeVent(vent);\n}\n```\n\n电梯井和通风井会在渲染阶段沿法线轻微内收,减少与墙体共面时的闪面。`Spaces` 实例也会使用稳定的 per-instance 渲染偏移抖动,缓解完全重叠空间之间的 z-fighting;这些偏移只影响渲染顶点,不改变语义 ID、实例矩阵和包围体查询的使用方式。\n\n### 设施设备\n\n`SemanticLoader` 会从 `Stories[].Facilities` 读取设备索引,再从顶层 `Facilities` 数组取得 `Facility` 数据,并使用 `Facility.ID` 在解密后的 `tree_models` 中查找同 ID 节点。只有带 `renderType: '3D'` 和模型 `path` 的节点会被加载。\n\n`tree_models.matrix` 是相对父节点的本地矩阵;`SemanticLoader` 会沿树父级累乘得到设备世界矩阵,再转换为对应 `FloorMesh` 的局部矩阵。因此楼层移动、隔离或显示隐藏时设备会跟随楼层一起工作。即使某个 story 只有 `Facilities`、没有墙柱空间等合并几何,也会创建一个空的 `FloorMesh` 作为设备容器。\n\nFacilities 会在单次 `SemanticParser` 解析范围内按模型 URL 自动分组。静态模型会被转换为 scene-level `InstancedMesh` batch 并挂到 `SemanticGroup.facilityLayer`;每个设备仍会在所属 `FloorMesh` 下保留一个 `FacilityInstanceObject` 语义实例引用,用于 `getFacilityById()`、显隐、颜色、透明度、包围盒和 `viewer.objectManager` 检索。设备 batch 使用普通 `Group` 包裹,以保证每帧渲染前都能执行相机实例裁剪和 hide/show 后的实例同步;带动画、骨骼、morph target 或缺少合法 `geometry.groups` 的多材质 Mesh 会自动回退为普通 `Model`,但普通 `Model` 也会挂到同一个 `FacilityInstanceObject` wrapper 下,对外 API 不变。\n\n`facilityLayer` 使用 `FacilityInstancedLayer`,并继承自 `BaseGroup`。当 `facilityLayer.visible = false` 时,layer 会阻止内部 `InstancedMesh` 继续参与射线检测,避免隐藏的批处理设备仍被点击命中;单个设备的显隐应先通过 `getFacilityById()` 或 `viewer.objectManager.getById()` 取回实例,再调用 `setSemanticVisible()` 控制。\n\nInstanced 设备通过 per-instance opacity attribute 控制单个设备透明度。batch 材质默认沿用源材质的透明状态;只有源材质本身透明,或当前参与渲染的设备存在 `opacity < 1` 时,材质才会切到 `transparent: true`,`setSemanticOpacity(1)` 可在没有其他半透明实例时回到不透明渲染队列。`resetSemanticColor()` 只清除颜色 override,不影响透明度 override。`FacilityInstancedLayer` 会在设备显隐、颜色、透明度、floor planish/unplanish 改变设备 transform,或相机矩阵、投影矩阵、viewport 高度变化时,在下一次渲染前同步实例矩阵、颜色、透明度和当前 active instance count。普通 fallback `Model` 的颜色和透明度控制由 wrapper 委托到 `MaterialEffects`,单材质和多材质 Mesh 都会应用 override。\n\n加载成功后,设备引用会写入 `userData.facilityId`、`semanticId`、`semanticKind: 'Facilities'`、`semanticName`、`spaces`、`twinsIdentifier`、`storyId` 和 `floorIndex`,并按 `Facility.ID` 注册到 `viewer.objectManager`。自动 instancing 的引用还会带有 `userData.instanced: true` 和 `userData.modelUrl`。\n\n```typescript\nimport { Box3 } from 'three';\n\nconst floor = building.getFloorAt(0);\nconst box = new Box3();\n\nconst facility = floor.getFacilityById('FACILITY_001') ?? viewer.objectManager.getById('FACILITY_001');\nconst facilityEntity = floor.getSemanticById('FACILITY_001');\n\nif (facility && facilityEntity?.type === 'object') {\n const facilityBox = facility.getSemanticBoundingBox(box, true);\n\n await viewer.controls.flyToBox(facilityBox, {\n viewpoint: 'rightFrontTop',\n padding: 0.2,\n });\n}\n\nconst facilities = floor.getSemanticsByKind('Facilities');\n\nfacility\n .setSemanticColor('#ff5533')\n .setSemanticOpacity(0.45)\n .resetSemanticColor()\n .setSemanticVisible(true);\n```\n\n对 Facilities 推荐使用 `controls.flyToBox()` 飞向设备包围盒。无论设备实际渲染来自 `InstancedMesh` 还是 fallback `Model`,都可以通过 `getSemanticBoundingBox(box, true)` 取得完整世界包围盒。\n\n### `FacilityInstanceObject`\n\n`FacilityInstanceObject` 继承自统一的 `SemanticInstanceObject`,表示一个设备语义实例。它会挂在所属 `FloorMesh` 下,用于 ID 检索、事件派发、显隐、颜色、透明度和包围盒查询;如果设备可以 instancing,真正几何由 `SemanticGroup.facilityLayer` 里的 `InstancedMesh` 统一渲染;如果不能 instancing,fallback `Model` 会作为它的子对象挂载。\n\n通常不需要手动创建 `FacilityInstanceObject`。`SemanticParser` 在解析 `Facilities` 时会自动创建它,并写入 `userData.facilityId`、`semanticId`、`semanticKind: 'Facilities'`、`semanticName`、`spaces`、`twinsIdentifier`、`storyId`、`floorIndex`、`instanced` 和 `modelUrl`。`userData.instanced` 表示当前设备是否由 `InstancedMesh` 渲染;fallback 时仍然保留同一套语义实例 API。\n\n加载完成后,设备可以按 `Facility.ID` 从全局 `objectManager` 取回。推荐使用 `getSemanticBoundingBox(box, true)` 取得完整世界包围盒,再调用 `viewer.controls.flyToBox()`。\n\n```typescript\nimport { Box3 } from 'three';\nimport { FacilityInstanceObject } from 'u-space/plugins/u-manager';\n\nconst facility = viewer.objectManager.getById<FacilityInstanceObject>('FACILITY_001');\n\nif (facility?.isFacilityInstanceObject) {\n const box = facility.getSemanticBoundingBox(new Box3(), true);\n\n await viewer.controls.flyToBox(box, {\n viewpoint: 'rightFrontTop',\n padding: 0.2,\n enableTransition: true,\n });\n}\n```\n\n| 属性 / 方法 | 说明 |\n| :---------- | :--- |\n| `isFacilityInstanceObject` | 类型标记,值为 `true`。 |\n| `type` | Three.js 对象类型名,值为 `'FacilityInstanceObject'`。 |\n| `setSemanticBounds(bounds)` | 设置模板在引用本地空间内的包围盒,并返回自身;主要由 `SemanticModelInstancedLayer.addSemanticBatch()` 在建 batch 时写入。 |\n| `getSemanticBoundingBox(target?, world?)` | 获取完整包围盒。`world = true` 时会应用 `matrixWorld`,适合传给 `viewer.controls.flyToBox()`;没有 bounds 时返回 empty `Box3`。 |\n| `setSemanticVisible(visible)` | 设置单个语义实例的 `visible`,并返回自身;渲染和射线检测都会跳过不可见引用。 |\n| `setSemanticColor(color)` | 设置单个语义实例的颜色 override,并返回自身。`color` 支持 Three.js `ColorRepresentation`。 |\n| `resetSemanticColor()` | 清除颜色 override,恢复使用源材质颜色,并返回自身;不会清除透明度 override。 |\n| `setSemanticOpacity(opacity)` | 设置单个语义实例透明度,并返回自身;值会被限制在 `0..1`。 |\n| `getSemanticColor(target?)` | 读取当前颜色 override;没有 override 时返回白色 `(1, 1, 1)`。 |\n| `hasSemanticColor()` | 判断当前实例是否存在颜色 override。 |\n| `getSemanticOpacity()` | 读取当前透明度 override。 |\n\n```typescript\nimport { Box3 } from 'three';\nimport { FacilityInstanceObject } from 'u-space/plugins/u-manager';\n\nconst box = new Box3();\nconst facility = floor.getFacilityById('FACILITY_001');\n\nif (facility instanceof FacilityInstanceObject) {\n facility\n .setSemanticColor('#29ccff')\n .setSemanticOpacity(0.6)\n .setSemanticVisible(true);\n\n await viewer.controls.flyToBox(facility.getSemanticBoundingBox(box, true), {\n viewpoint: 'rightFrontTop',\n padding: 0.2,\n });\n}\n```\n\n### `FacilityInstancedLayer`\n\n`FacilityInstancedLayer` 继承自 `SemanticModelInstancedLayer`,是 scene-level Facilities 的批量渲染层。`SemanticGroup.facilityLayer` 默认就是该类型;它会按模型 URL 把同一模板的静态设备合并为一个或多个 `InstancedMesh`,并用普通 `Group` 包裹每个模型 URL batch,保证设备显隐、颜色、透明度和相机实例裁剪能在每帧渲染前同步,同时保留每个楼层下的 `FacilityInstanceObject` 引用用于业务 API 和事件冒泡。\n\n使用 `SemanticLoader` 时通常不需要直接调用 `addSemanticBatch()`;只有自定义语义解析或自建设备 batch 时才需要手动创建 layer,然后通过 `semanticGroup.setFacilityLayer(layer)` 挂回语义场景。\n\n| 属性 / 方法 | 说明 |\n| :---------- | :--- |\n| `isFacilityInstancedLayer` | 类型标记,值为 `true`。 |\n| `type` | Three.js 对象类型名,值为 `'FacilityInstancedLayer'`。 |\n| `name` | 构造函数默认设为 `'Facilities'`。 |\n| `getInstanceCulling()` | 返回当前实例裁剪配置:`enabled`、`frustum`、`minScreenRadius`。 |\n| `setInstanceCulling(options)` | 设置实例裁剪配置,并返回自身。默认 `enabled: false`、`frustum: true`、`minScreenRadius: 0`;默认不按相机裁剪设备,避免相机距离影响设备显隐。需要性能压缩时可设置 `enabled: true` 开启视锥裁剪,或再把 `minScreenRadius` 设为大于 0 的值开启屏幕尺寸裁剪。 |\n| `addSemanticBatch(url, template, instances)` | 尝试把同一模型 URL 的语义实例引用合并为 instanced batch。成功返回 `true`,失败返回 `false`,失败时调用方应回退为普通模型渲染。 |\n\n`addSemanticBatch(url, template, instances)` 会从 `template` 收集可 instancing 的可见 `Mesh`,克隆源材质,并为每个 batch 建立 instance matrix、颜色和透明度 attribute。没有动画、没有骨骼、没有 morph target,且每个多材质 `Mesh` 都有合法 `geometry.groups` 的模板会进入 instancing;不满足条件、模板没有可用 Mesh 或 `instances` 为空时会返回 `false`。\n\n渲染前,layer 会同步每个 `FacilityInstanceObject` 的世界矩阵、颜色和透明度,并只在数据变化时上传对应 buffer。默认情况下,相机移动不会改变设备 active instance 集合;显式调用 `setInstanceCulling({ enabled: true })` 后,layer 才会按当前渲染相机逐实例压缩 `InstancedMesh.count`,视锥外设备不会进入本帧实例 buffer;只有同时设置 `minScreenRadius > 0` 时,屏幕半径小于该阈值的设备才会被跳过。相机矩阵、投影矩阵或 viewport 高度未变化时不会重复压缩;设备显隐、透明度为 0、颜色/透明度变化或 `planish('Facilities')` / `unplanish('Facilities')` 改变 transform 时会标记 batch dirty。全部设备不可渲染时,内部 `InstancedMesh.visible` 会被关闭;只有被当前相机裁掉时,mesh 会保留可见并把 `count` 设为 0,以便相机移动后自动恢复。射线检测命中 batch 内实例时,`hit.object` 会被改写为对应的 `FacilityInstanceObject`,并附加 `semanticObject`、`semanticId`、`semanticKind: 'Facilities'`、`semanticName` 和 `semanticIndex`,事件随后会按楼层引用对象继续冒泡。\n\n```typescript\nimport { FacilityInstancedLayer, FacilityInstanceObject } from 'u-space/plugins/u-manager';\n\nconst layer = new FacilityInstancedLayer();\nconst refs = facilityItems.map(() => new FacilityInstanceObject());\n\nif (layer.addSemanticBatch('/models/camera.glb', templateObject, refs)) {\n semanticGroup.setFacilityLayer(layer);\n}\n```\n\n如果只想整体隐藏或禁用 scene-level batch,可以设置 `semanticGroup.facilityLayer.visible = false`;如果只想控制单个设备,推荐从 `floor.getFacilityById(id)` 或 `viewer.objectManager.getById(id)` 取回 `FacilityInstanceObject` 后调用 `setSemanticVisible()` / `setSemanticOpacity()`。\n\n### `SemanticModelInstancedLayer`\n\n`SemanticModelInstancedLayer` 是 `SceneInstancedLayer` 和 `FacilityInstancedLayer` 的共享实现。它负责模板 mesh 收集、材质克隆、`InstancedMesh` 创建、instance matrix/color/opacity buffer 同步、raycast hit remap、fallback 判定和可选相机实例裁剪。通常业务代码不需要直接使用它,除非要为新的语义实例类型复用同一套模型 instancing 能力。\n\n### `FloorMesh`\n\n`FloorMesh` 继承自 `BaseMesh`,表示一个楼层。它不是 `BatchedMesh`,而是把同一楼层内的语义对象合并为一份几何,并通过 TSL + `DataTexture` 在 GPU 侧按 `semanticIndex` 控制颜色、透明度、显示隐藏和实例矩阵。楼层语义网格会按实例透明度拆分为不透明和透明两个材质通道;墙、柱、门等默认不透明实例会进入 opaque pipeline,`Spaces`、`Windows` 或通过 `setOpacityAt()` 改成半透明的实例会进入 transparent pipeline。Facilities 通过楼层下的引用对象进行检索和控制;实际渲染可能来自 `SemanticGroup.facilityLayer` 的 `InstancedMesh` batch,也可能是 fallback 的普通 `Model`。\n\n这种结构的目标是让每个楼层通常只占一个主渲染 draw call,同时仍保留实例级控制能力。`xxxAt` 方法全部作用于单个语义实例;没有 `At` 后缀的方法作用于整个楼层 mesh。\n\n| 属性 / 方法 | 说明 |\n| :---------- | :--- |\n| `semanticCount` / `instanceCount` | 当前楼层语义实例数量。 |\n| `semanticInstances` | `Map<string, number>`,从语义对象 ID 映射到实例下标。 |\n| `wallsInstances` / `columnsInstances` / `spacesInstances` / `doorsInstances` / `windowsInstances` / `staircasesInstances` | 按语义类型维护的 ID 到实例下标映射。 |\n| `semanticEntities` | `Map<string, FloorSemanticEntity>`,统一索引楼层内合并几何语义和 `Facilities` 对象语义。 |\n| `facilityObjectsById` | `Map<string, FacilityInstanceObject>`,按 `Facility.ID` 或显式别名查找设备引用。 |\n| `facilityObjects` | 当前楼层挂载的设备引用数组。 |\n| `getSemanticIndexById(id)` | 根据语义 ID 获取实例下标。 |\n| `getSemanticById(id)` | 根据语义 ID 或设备别名获取统一语义实体。 |\n| `getSemantics()` | 获取楼层内全部语义实体,包含合并几何和设备对象。 |\n| `getSemanticsByKind(kind)` | 按语义类型过滤实体;`kind` 可以是 `Walls`、`Columns`、`Spaces`、`Doors`、`Windows`、`Staircases` 或 `Facilities`。 |\n| `getSemanticIdAt(index)` | 获取实例语义 ID。 |\n| `getSemanticKindAt(index)` | 获取合并几何实例类型:`'Walls' \\| 'Columns' \\| 'Spaces' \\| 'Doors' \\| 'Windows' \\| 'Staircases'`。 |\n| `getSemanticNameAt(index)` | 获取实例名称;当前 `Spaces` 会从语义数据的 `Name` 写入,其他类型可能为 `undefined`。 |\n| `getSemanticObjectAt(index)` | 获取合并几何实例对应的 `FloorSemanticInstanceObject` 轻量对象。 |\n| `showAllFacilities()` | 显示当前楼层的全部设备对象。 |\n| `hideAllFacilities()` | 隐藏当前楼层的全部设备对象。 |\n| `addFacility(object, ids?)` | 把设备对象挂到楼层,并按显式 ID、`facilityId`、`semanticId`、`id` 建立检索别名。 |\n| `getFacilityById(id)` | 根据 `Facility.ID` 或别名获取设备对象。 |\n| `getFacilityAt(index)` | 按楼层设备数组下标获取设备对象。 |\n| `getFacilities()` | 获取当前楼层全部设备对象。 |\n| `removeFacility(facility)` | 按设备对象或 ID 从楼层移除设备,并同步清理检索索引。 |\n| `setColorAt(index, color)` / `getColorAt(index, target?)` | 修改或读取实例颜色,支持 `Color`、`Vector3`、`Vector4`。 |\n| `setOpacityAt(index, opacity)` / `getOpacityAt(index)` | 修改或读取实例透明度。 |\n| `setVisibleAt(index, visible)` / `getVisibleAt(index)` | 修改或读取实例显隐状态。 |\n| `setMatrixAt(index, matrix)` / `getMatrixAt(index, target?)` | 设置或读取实例附加矩阵。 |\n| `setScaleAt(index, scale, y?, z?)` | 修改实例缩放,支持 `number` 或 `Vector3`。 |\n| `setScaleYAt(index, y)` | 只修改实例 Y 轴缩放,保留 X/Z 缩放和已有位移、旋转。 |\n| `getFinalMatrixAt(index, target?)` | 读取实例最终矩阵,包含语义对象原始基点矩阵。 |\n| `getBoundingBoxAt(index, target?, world?)` | 获取实例包围盒;`world = true` 时包含 `FloorMesh.matrixWorld`。 |\n| `getBoundingSphereAt(index, target?, world?)` | 获取实例包围球;`world = true` 时包含 `FloorMesh.matrixWorld`。 |\n| `computeBoundingBox()` / `computeBoundingSphere()` | 计算整个楼层的包围体。 |\n| `planish(kind?)` / `unplanish(kind?)` | 压扁或恢复楼层内所有合并语义实例和 Facilities;压平时按各自包围盒贴到楼层平面附近,并加入稳定的轻微 Y 偏移;传入 `kind` 时只作用于该语义类型,`kind` 可包含 `Facilities`。 |\n\n### 飞向语义实例\n\n点击、射线检测命中 `FloorMesh` 里的语义几何或设备时,`intersect` 上会附加语义字段:`semanticIndex`、`semanticId`、`semanticKind` 和 `semanticObject`。普通楼层语义几何的事件目标是 `FloorSemanticInstanceObject`;instanced Facilities 的事件目标是楼层下的 `FacilityInstanceObject` 引用,事件会继续冒泡到所属 `FloorMesh`、`BuildingGroup` 和 `SemanticGroup`。因此楼层监听器中应使用 `event.currentTarget` 操作楼层,用 `intersect.semanticObject` 或 `event.target` 取得具体语义实例。\n\n```typescript\nimport { Box3 } from 'three';\n\nconst box = new Box3();\n\nfloor.addEventListener('click', async ({ event }) => {\n const { currentTarget, intersect } = event;\n if (!intersect) return;\n\n if (intersect.semanticKind === 'Facilities') {\n const facility = intersect.semanticObject ?? event.target;\n const facilityBox = facility.getSemanticBoundingBox(box, true);\n\n await viewer.controls.flyToBox(facilityBox, {\n viewpoint: 'rightFrontTop',\n padding: 0.2,\n });\n return;\n }\n\n const semanticBox = intersect.semanticObject?.getSemanticBoundingBox(box, true)\n ?? currentTarget.getBoundingBoxAt(intersect.semanticIndex, box, true);\n\n if (semanticBox) {\n await viewer.controls.flyToBox(semanticBox, {\n viewpoint: 'rightFrontTop',\n padding: 0.2,\n });\n }\n});\n```\n\n如果要根据 ID 操作对象,先通过 `getSemanticIndexById(id)` 转为实例下标。\n\n```typescript\nconst index = floor.getSemanticIndexById('SPACE_001');\n\nif (index !== undefined) {\n floor.setVisibleAt(index, false);\n viewer.render();\n}\n```\n\n### 楼层压扁\n\n`planish()` / `unplanish()` 默认作用于当前楼层的全部合并语义实例,包含墙、柱、空间、门窗、楼梯等语义几何,也包含楼层下的 Facilities 引用;传入 `kind` 时只处理该类型,例如 `floor.planish('Walls')` 或 `floor.planish('Facilities')`。所有被压平的实例都会按自身包围盒把压平后的薄片贴到楼层平面 `FloorMesh.pivot.y` 附近,而不是围绕实例自身基点压缩;压平时还会按语义类型和实例 ID 加入稳定的轻微 Y 偏移,减少门、墙或其他重叠实例之间的共面闪烁。`unplanish()` 会恢复合并几何实例的原始矩阵,`unplanish('Facilities')` 会恢复设备原始 `position.y` 和 `scale.y`。instanced Facilities 会通过楼层下的 `FacilityInstanceObject` transform 同步到 `SemanticGroup.facilityLayer`。\n\n如果只想调整单个合并几何实例,可以使用 `setScaleAt()` 或 `setScaleYAt()`,它们会保留该语义对象自己的基点矩阵,因此压扁墙、柱、房间时不会被压到世界原点。\n\n## `SceneLoader`\n\n从服务端场景包加载完整场景树(模型、组、基础形状),并自动将所有已加载对象按 `id` 和 `sid` 注册到 `viewer.objectManager`。\n默认会尝试读取同一路径下的 `db/semantic_model.json`,如果场景树节点的 `id` 已存在于语义文件中,则跳过该节点,方便和 `SemanticLoader` 同时加载而不重复创建建筑或设备。\n对语义去重后仍保留的 3D 节点,`SceneLoader` 默认会按模型 `path` 自动合并重复静态模型:同一 `path` 的多个实例只加载一次模板,并由 `SceneInstancedLayer` 通过共享的 `SemanticModelInstancedLayer` 批量渲染。原场景树中仍会保留每个 `id` / `sid` 对应的 `SceneInstanceObject` 逻辑对象,用于业务检索、显隐、颜色、透明度、包围盒和事件派发。\n\n模板包含动画、骨骼、morph target、非法多材质 groups 或没有可实例化网格时,会自动回退为普通 `Model` 渲染;fallback `Model` 会挂在同一个 `SceneInstanceObject` 下,因此调用侧不需要额外配置。\n\n```typescript\nimport { SceneLoader } from 'u-space/plugins/u-manager';\n\nconst sceneLoader = new SceneLoader(viewer);\nsceneLoader.setPath('./scenes/my-scene');\nsceneLoader.setKey('YOUR_LICENSE_KEY'); // 官方授权场景必需\nconst group = await sceneLoader.loadAsync();\nviewer.scene.add(group);\n```\n\n**方法:**\n\n| 方法 | 说明 |\n| :------------------ | :------------------------------------------------------------------- |\n| `setKey(key)` | 设置授权场景的 RSA 解密密钥。 |\n| `loadAsync()` | 加载并解析场景,返回去除语义重复节点后的 `Group`。 |\n| `clearCache()` | 从 `objectManager` 中移除此加载器注册的所有 ID。 |\n| `dispose()` | 清除缓存并释放水印叠加层。 |\n\n### `SceneInstanceObject`\n\n`SceneInstanceObject` 继承自统一的 `SemanticInstanceObject`,表示一个由 `SceneInstancedLayer` 批量渲染或 fallback 普通渲染的场景模型引用。它会保留在原场景树层级中,并注册到 `viewer.objectManager`,因此 `getById()`、`show()` / `hide()`、`isolate()`、`showAll()`、`setOpacity()` 和 `getBoundingBox()` 可以继续按单个模型 ID 使用。\n\n`SceneInstanceObject` 本身只增加场景模型语义:`isSceneInstanceObject = true`、`type = 'SceneInstanceObject'`,并把 `userData.semanticKind` 设为 `SceneInstances`。加载器会把场景树节点的 `id` 和 `sid` 都注册到 `viewer.objectManager`,因此同一个对象可以通过任一 ID 取回。对重复模型的 instanced 路径,`userData.instanced = true`,并带有 `modelPath`、`modelUrl`、`semanticId` 和 `semanticName`;如果模板不支持 instancing,会退回普通 `Model`,但仍挂在同一个 `SceneInstanceObject` 下,对外 API 不变。\n\n**识别字段:**\n\n| 字段 | 说明 |\n| :--- | :--- |\n| `isSceneInstanceObject` | 固定为 `true`,用于判断对象是否来自 `SceneLoader` 的场景实例。 |\n| `isSemanticInstanceObject` | 固定为 `true`,表示它支持统一语义实例 API。 |\n| `type` | 固定为 `SceneInstanceObject`。 |\n| `userData.id` / `userData.sid` | 原始场景树节点 ID;两个 ID 都会注册到 `viewer.objectManager`。 |\n| `userData.semanticId` | 语义事件和 raycast remap 使用的实例 ID,默认等于场景树节点 `id`。 |\n| `userData.semanticKind` | 固定为 `SceneInstances`。 |\n| `userData.semanticName` | 原始场景树节点名称。 |\n| `userData.instanced` | `true` 表示实际渲染来自 `SceneInstancedLayer`;`false` 表示 fallback `Model` 挂在当前对象下。 |\n| `userData.modelPath` / `userData.modelUrl` | instanced 场景实例对应的模型相对路径和完整 URL。 |\n\n**方法:**\n\n| 方法 | 说明 |\n| :--- | :--- |\n| `setSemanticBounds(bounds)` | 设置本地包围盒。`SceneLoader` 会在创建 instanced batch 时根据模板模型自动设置;业务一般不需要手动调用。 |\n| `setSemanticRenderObject(object)` | 设置 fallback 渲染对象并作为子对象挂载。模板无法 instancing 时由 `SceneLoader` 自动调用;自定义语义实例渲染时可手动使用。 |\n| `getSemanticRenderObject()` | 返回 fallback 渲染对象;instanced 渲染路径通常返回 `null`,因为真实 Mesh 在 `SceneInstancedLayer` 中。 |\n| `getSemanticBoundingBox(target?, world?)` | 返回实例包围盒。`world = true` 时会应用 `matrixWorld`,适合直接传给 `viewer.controls.flyToBox()`;无 bounds 和 render object 时返回 empty `Box3`。 |\n| `setSemanticVisible(visible)` | 设置单个实例显隐并标记 batch dirty;隐藏后该实例不会继续渲染,也不会被 instanced raycast 命中。 |\n| `setSemanticColor(color)` | 设置单个实例颜色 override。instanced 路径写入 per-instance color;fallback `Model` 路径会委托到材质高亮逻辑。 |\n| `resetSemanticColor()` | 清除颜色 override,恢复使用源模型颜色;不会清除透明度 override。 |\n| `setSemanticOpacity(opacity)` | 设置单个实例透明度,值会限制在 `0..1`。instanced 路径写入 per-instance opacity;fallback 路径会按原始材质透明度相乘。 |\n| `getSemanticColor(target?)` | 获取当前生效颜色。存在颜色或高亮 override 时返回该颜色,否则返回白色。传入 `Color` 可复用对象。 |\n| `hasSemanticColor()` | 返回当前是否存在颜色或高亮颜色 override。 |\n| `getSemanticOpacity()` | 获取当前生效透明度;高亮存在时返回高亮透明度,否则返回基础透明度。 |\n| `setSemanticHighlight(color, opacity, overwrite?)` | 设置临时高亮样式。`overwrite = false` 时按 tint 模式与原色混合,`true` 时直接替换颜色;高亮会覆盖基础颜色/透明度直到调用 `clearSemanticHighlight()`。 |\n| `clearSemanticHighlight()` | 清除临时高亮,并恢复到基础颜色/透明度状态。 |\n| `getSemanticColorMode()` | 返回当前颜色模式:`SEMANTIC_COLOR_MODE_NONE`、`SEMANTIC_COLOR_MODE_OVERRIDE` 或 `SEMANTIC_COLOR_MODE_TINT`。 |\n| `onSemanticRenderDirty(callback)` | 监听实例渲染状态变化,返回取消监听函数。实例显隐、颜色、透明度或世界矩阵变化时会触发。 |\n| `markSemanticRenderDirty()` | 手动标记实例渲染状态已变化,通知所属 batch 在下一帧同步 instance buffer。 |\n\n`MaterialEffects.highlightColor()` / `removeHighlightColor()` 会识别 `SceneInstanceObject` 并调用 `setSemanticHighlight()` / `clearSemanticHighlight()`,不会直接修改共享 batch 材质。`ObjectManager.setOpacity()`、`ObjectManager.hide()`、`ObjectManager.show()` 也会通过统一 API 作用到单个实例。\n\n加载完成后可以按场景树节点 `id` 从 `objectManager` 取回 `SceneInstanceObject`,再用 `getSemanticBoundingBox(box, true)` 取得世界包围盒并传给 `viewer.controls.flyToBox()`。`SceneInstanceObject` 内部带有不可见的本地 bounds,因此不需要加载真实 Mesh 副本也能计算飞行包围盒。\n\n```typescript\nimport { Box3 } from 'three/webgpu';\nimport { SceneInstanceObject } from 'u-space/plugins/u-manager';\n\nconst object = viewer.objectManager.getById<SceneInstanceObject>('SCENE_NODE_ID');\n\nif (object?.isSceneInstanceObject) {\n const box = object.getSemanticBoundingBox(new Box3(), true);\n await viewer.controls.flyToBox(box, {\n viewpoint: 'current',\n padding: 0.2,\n enableTransition: true,\n });\n}\n```\n\n颜色、透明度和显隐可以直接链式调用:\n\n```typescript\nobject\n ?.setSemanticColor('#29ccff')\n .setSemanticOpacity(0.55)\n .setSemanticVisible(true);\n\nobject?.setSemanticHighlight('#ffb703', 0.75, true);\nobject?.clearSemanticHighlight();\nobject?.resetSemanticColor();\n```\n\n如果需要自己控制包围盒,也可以取世界包围盒后调用 `flyToBox()`:\n\n```typescript\nimport { Box3 } from 'three/webgpu';\n\nconst box = object.getSemanticBoundingBox(new Box3(), true);\nawait viewer.controls.flyToBox(box, { viewpoint: 'frontTop', padding: 0.2 });\n```\n\n射线检测命中批处理实例时,`intersect.object` 会被改写为对应的 `SceneInstanceObject`,并附加 `semanticObject`、`semanticId`、`semanticKind: 'SceneInstances'` 和 `semanticIndex`,事件会按逻辑对象所在的原场景树继续冒泡。\n\n### `SceneInstancedLayer`\n\n`SceneInstancedLayer` 是 `SceneLoader` 内部使用的批量渲染层,默认 `name = 'SceneInstances'`。它继承自 `SemanticModelInstancedLayer`,每个重复模型 URL 会生成一个 batch,batch 内按模板的可实例化 Mesh 创建 `InstancedMesh`。渲染前只在实例矩阵、显隐、颜色或透明度变化时同步 instance buffer;普通相机移动不会重新上传所有实例数据。\n\n## `TopologiesLoader` / `TopologyParser`\n\n加载拓扑图数据并将其转换为 `Topology` 对象。\n\n```typescript\nimport { TopologiesLoader } from 'u-space/plugins/u-manager';\n\nconst loader = new TopologiesLoader();\nloader.setPath('./scenes/my-scene');\nconst topologies = await loader.loadAsync(); // Topology[]\nviewer.scene.add(...topologies);\n```\n\n## `VisionsLoader` / `VisionsParser`\n\n加载命名相机视点,并将相机飞行到指定视点。\n\n```typescript\nimport { VisionsLoader, VisionsParser } from 'u-space/plugins/u-manager';\n\nconst visionsLoader = new VisionsLoader();\nvisionsLoader.setPath('./scenes/my-scene');\nconst visionsData = await visionsLoader.loadAsync();\n// visionsData 是 Record<string, IVisions[]>\n\nconst visionsParser = new VisionsParser(viewer);\n\n// 飞行到指定视点\nawait visionsParser.flyTo(visionsData['HOME'][0]);\n\n// 飞行到主(默认)视点\nawait visionsParser.flyToPrimary(visionsData['HOME']);\n```\n\n**`IVisions` 字段:**\n\n| 字段 | 类型 | 说明 |\n| :--------- | :------------ | :------------------------------ |\n| `camera` | `'P' \\| 'O'` | 相机类型:透视或正交。 |\n| `position` | `IVector3` | 相机世界位置。 |\n| `target` | `IVector3` | 相机朝向目标点。 |\n| `zoom` | `number` | 相机缩放级别。 |\n| `primary` | `boolean` | 是否为默认视点。 |\n\n## `AnimationsLoader` / `AnimationsParser`\n\n加载关键帧动画数据,并通过 `tweenAnimation` 驱动 `Object3D` 的变换。\n\n```typescript\nimport { AnimationsLoader, AnimationsParser } from 'u-space/plugins/u-manager';\n\nconst animLoader = new AnimationsLoader();\nanimLoader.setPath('./scenes/my-scene');\nconst animationsData = await animLoader.loadAsync(); // IAnimations[]\n\n// 查找特定对象的动画\nconst data = animationsData.find((a) => a.modelId === myModel.userData.id);\n\nconst parser = new AnimationsParser(viewer, myModel);\nparser.initTransform(); // 保存初始位置/旋转/缩放\n\nawait parser.play(data.keyframes); // 播放动画序列\n\n// 中途停止\nparser.stop();\n\n// 重置到初始变换\nparser.reset();\n```\n\n## `PropertiesLoader`\n\n加载与模型关联的结构化属性元数据(如 BIM 属性)。\n\n```typescript\nimport { PropertiesLoader } from 'u-space/plugins/u-manager';\n\nconst propsLoader = new PropertiesLoader();\npropsLoader.setPath('./scenes/my-scene');\nconst properties = await propsLoader.loadAsync(); // IProperties[]\n\n// 查找模型的属性\nconst modelProps = properties.filter(p => p.modelId === myModel.userData.id);\n```\n\n**`IProperties` 字段:**\n\n| 字段 | 类型 | 说明 |\n| :-------- | :--------------- | :----------------------- |\n| `modelId` | `string` | 关联模型的 ID。 |\n| `group` | `string` | 属性分组/类别名称。 |\n| `key` | `string` | 属性键名。 |\n| `value` | `string \\| null` | 属性值。 |\n| `label` | `string \\| null` | 属性的显示标签。 |\n"
|
|
1094
|
+
"content": "# u-manager\n\n`u-manager` 是一套全面的加载器和解析器,用于从服务器路径流式传输、解密并显示结构化场景数据。支持场景、拓扑、动画、属性、相机视点、楼层、井道和设备语义模型。\n\n所有加载器均继承自 Three.js 的 `Loader`,并提供 `setPath(path)` 方法,在调用 `loadAsync()` 前配置基础数据目录。\n\n## `UManagerLoader`\n\n`UManagerLoader` 是推荐的一体化入口。它会在同一路径下同时加载 `SemanticLoader` 和 `SceneLoader`:语义文件中已经存在的 `id` 会由 `SceneLoader` 自动跳过,避免建筑、楼层、设备被重复创建;非语义场景树节点仍会正常加载。返回值是一个 `UManagerSceneGroup`,可直接通过 `semanticGroup` 访问 `SemanticGroup`,通过 `sceneGroup` 访问非语义场景组。\n\n```typescript\nimport { UManagerLoader } from 'u-space/plugins/u-manager';\n\nconst loader = new UManagerLoader(viewer);\nloader.setPath('./scenes/my-scene');\nloader.setKey('YOUR_LICENSE_KEY');\n\nconst group = await loader.loadAsync();\nviewer.scene.add(group);\n\nconst semanticGroup = group.semanticGroup;\nconst sceneGroup = group.sceneGroup;\nconst facilityLayer = semanticGroup.getDefaultFacilityLayer();\nconst sceneLayer = sceneGroup.getDefaultSceneLayer();\n```\n\n完整可运行示例见 [`examples/test_umanager_loader.html`](https://u-space-phi.vercel.app/examples/test_umanager_loader.html)。该示例展示了如何从返回根组中直接读取 `semanticGroup` / `sceneGroup`,通过 `getDefaultFacilityLayer()` / `getDefaultSceneLayer()` 获取默认合批层,并通过 `viewer.objectManager` 获取 `SceneInstanceObject` / `FacilityInstanceObject` 后调用统一 `viewer.controls.flyToObject()`、`setInstanceHighlight()`、`setInstanceVisible()` 和 `setInstanceOpacity()`。运行时动态新增/删除 batch 实例的示例见 [`examples/test_umanager_dynamic_instances.html`](https://u-space-phi.vercel.app/examples/test_umanager_dynamic_instances.html)。\n\n## `SemanticLoader`\n\n加载 `db/semantic_model.json` 楼层语义数据。`loadAsync()` 返回顶层 `SemanticGroup`,用于组织整次语义解析产生的建筑、楼层以及 scene-level facility layer。每个建筑包含多个 `FloorMesh`,每个楼层把墙、柱、空间、楼梯等语义对象合并为一个 TSL 材质驱动的网格;建筑级电梯井和通风井会使用 `ExtrudeMesh` 独立渲染。语义模型包含 `Facilities` 时,`SemanticLoader` 会额外读取 `SceneMetadata.json` 指向的 `tree_models`,按场景授权配置解密,再把对应设备模型挂载到所属楼层。\n\n```typescript\nimport { Vector3, Vector4 } from 'three';\nimport { SemanticLoader } from 'u-space/plugins/u-manager';\n\nconst loader = new SemanticLoader(viewer);\nloader.setPath('./scenes/my-scene');\nloader.setKey('YOUR_LICENSE_KEY'); // 官方授权场景解析 tree_models 时必需\n\nconst semanticGroup = await loader.loadAsync(); // SemanticGroup\nviewer.scene.add(semanticGroup);\n\nconst building = semanticGroup.getBuildingAt(0);\nconst floor = building.getFloorAt(0);\n\nbuilding.isolateFloor(floor);\nawait viewer.controls.flyToObject(floor);\n\nfloor.addEventListener('click', ({ event }) => {\n const { currentTarget, intersect } = event;\n if (!intersect) return;\n\n const index = intersect.semanticIndex;\n\n currentTarget.setColorAt(index, new Vector4(1, 0, 0, 0.5));\n currentTarget.setScaleAt(index, new Vector3(1, 0.1, 1));\n viewer.render();\n});\n```\n\n所有可按 ID 操作的实例都会尽量暴露统一的 `InstanceObject` API。楼层多边形语义使用轻量 `FloorSemanticInstanceObject` 代理合并几何中的一个实例;Facilities 和 SceneLoader path instancing 使用同一套模型实例化底层。业务代码优先使用 `setInstanceVisible()`、`setInstanceColor()`、`setInstanceOpacity()` 和 `viewer.controls.flyToObject(instance)`,而不是关心底层是合并几何、`InstancedMesh` 还是 fallback `Model`。\n\n完整的继承字段、方法、颜色模式、包围盒和 dirty 同步语义见 [核心 `InstanceObject` API](./api-objects.md#instanceobject)。\n\n`InstanceObject` 也按 Three.js 对象级包围盒约定提供 `boundingBox`、`boundingSphere`、`computeBoundingBox()` 和 `computeBoundingSphere()`。它们缓存的是实例本地坐标包围体,`Box3.setFromObject()` 会自动调用 `computeBoundingBox()` 并应用 `matrixWorld`,因此 `viewer.controls.flyToObject()` 可以直接飞向 `SceneInstanceObject`、`FacilityInstanceObject` 和楼层实例代理。需要手动处理世界包围盒时,仍可使用 `getInstanceBoundingBox(target, true)`。\n\n### `SemanticGroup`\n\n`SemanticGroup` 继承自 `BaseGroup`,是单次 `SemanticParser` 解析结果的顶层容器。它管理所有 `BuildingGroup`,并把跨楼层共享的 Facilities `InstancedMesh` 渲染层作为建筑的同级对象挂在 `facilityLayer`,而不是挂到某个 `BuildingGroup` 内。\n\n建筑 ID 和设备 ID 查询使用内部 `Map` 索引维护;`getBuildingById()` 和 `getFacilityById()` 不会通过数组扫描查找语义对象。\n\n| 属性 / 方法 | 说明 |\n| :---------- | :--- |\n| `buildings` | 当前语义场景内的 `BuildingGroup[]`。 |\n| `facilityLayer` | scene-level 设备渲染层;同模型 URL 的静态设备会合并为 `InstancedMesh` batch。 |\n| `addBuilding(group)` / `removeBuilding(group)` | 添加或移除建筑,并同步更新 group 子节点。 |\n| `getBuildingAt(index)` / `getBuildingById(id)` / `getBuildings()` | 按下标、`Building.ID` 或整体列表获取建筑。 |\n| `floorMeshes` | 聚合返回所有建筑内的 `FloorMesh[]`。 |\n| `setFacilityLayer(layer)` | 设置跨楼层设备渲染层;传入 `null` 会移除已有 layer。 |\n| `getDefaultFacilityLayer()` | 返回当前默认 `FacilityInstancedLayer`;没有可实例化 Facilities 时返回 `null`。 |\n| `getFacilityById(id)` | 在整次语义解析结果中按 `Facility.ID` 或显式别名查找设备。 |\n| `showAllFacilities()` / `hideAllFacilities()` | 显示或隐藏整次语义解析结果中的全部设备。 |\n| `showAllFloors()` | 显示所有建筑的所有楼层。 |\n| `planishFloors(kind?)` / `unplanishFloors(kind?)` | 压扁或恢复所有楼层;默认覆盖所有合并语义实例和 Facilities,并按包围盒贴到楼层平面附近,同时添加稳定的轻微 Y 偏移以减少共面闪烁;传入 `kind` 时只作用于该语义类型,`kind` 可包含 `Facilities`。 |\n\n### `BuildingGroup`\n\n`BuildingGroup` 继承自 `BaseGroup`,用于组织一个建筑下的所有楼层、电梯井和通风井。设备语义对象不会直接挂到 `BuildingGroup`;能 instancing 的设备会渲染到 `SemanticGroup.facilityLayer`,同时在对应 `FloorMesh` 下保留可检索、可控制的轻量引用。\n\n建筑内设备 ID 到楼层的关系由内部 `Map` 索引维护,并通过楼层设备索引版本自动同步。\n\n| 属性 / 方法 | 说明 |\n| :---------- | :--- |\n| `floorMeshes` | 当前建筑内的 `FloorMesh[]`。 |\n| `elevatorMeshes` | 当前建筑内由 `Elevators` 语义生成的 `ExtrudeMesh[]`。 |\n| `ventMeshes` | 当前建筑内由 `Vents` 语义生成的 `ExtrudeMesh[]`。 |\n| `addFloor(floor)` | 添加楼层,并同步加入 group。 |\n| `removeFloor(floor)` | 移除楼层,并同步从 group 移除。 |\n| `getFloorAt(index)` | 按楼层下标获取 `FloorMesh`。 |\n| `getFloorById(id)` | 按楼层语义 ID 获取 `FloorMesh`,内部使用 `Map` 索引。 |\n| `addElevator(elevator)` / `removeElevator(elevator)` | 添加或移除电梯井,并同步更新 group。 |\n| `getElevatorAt(index)` | 按下标获取电梯井 `ExtrudeMesh`。 |\n| `getElevatorById(id)` | 按电梯井语义 ID 获取电梯井,内部使用 `Map` 索引。 |\n| `addVent(vent)` / `removeVent(vent)` | 添加或移除通风井,并同步更新 group。 |\n| `getVentAt(index)` | 按下标获取通风井 `ExtrudeMesh`。 |\n| `getVentById(id)` | 按通风井语义 ID 获取通风井,内部使用 `Map` 索引。 |\n| `isolateFloor(floor \\| index)` | 只显示指定楼层,隐藏其他楼层;也兼容传入楼层数组。 |\n| `isolateFloors(floors \\| indexes)` | 只显示指定多个楼层,隐藏其他楼层。 |\n| `showAllFloors()` | 显示全部楼层。 |\n| `getFacilityById(id)` | 在当前建筑的楼层中按 `Facility.ID` 或 `FloorMesh.addFacility()` 显式传入的别名查找设备。 |\n| `showAllFacilities()` | 显示整栋建筑内全部楼层设备。 |\n| `hideAllFacilities()` | 隐藏整栋建筑内全部楼层设备。 |\n| `planishFloors(kind?)` | 压扁所有楼层;默认覆盖所有合并语义实例和 Facilities,并按包围盒贴到楼层平面附近,同时添加稳定的轻微 Y 偏移以减少共面闪烁;传入 `kind` 时只压扁该语义类型,`kind` 可包含 `Facilities`。 |\n| `unplanishFloors(kind?)` | 恢复所有楼层;默认恢复所有合并语义实例和 Facilities;传入 `kind` 时只恢复该语义类型,`kind` 可包含 `Facilities`。 |\n\n### 电梯井和通风井\n\n`SemanticLoader` 会读取 `semantic_model.json` 中建筑引用的 `Elevators` 和 `Vents`。每个井道使用 `Polygon.shape` 作为底面轮廓,`Elevation` 作为底面高度,`TotalHeight` 作为拉伸高度,并以 building 级 `ExtrudeMesh` 加入 `BuildingGroup`。\n\n```typescript\nconst elevator = building.getElevatorAt(0);\nconst elevatorById = building.getElevatorById('ELEVATOR_001');\nconst vent = building.getVentAt(0);\nconst ventById = building.getVentById('VENT_001');\n\nif (elevator) {\n await viewer.controls.flyToObject(elevator);\n}\n\nif (vent) {\n building.removeVent(vent);\n}\n```\n\n电梯井和通风井会在渲染阶段沿法线轻微内收,减少与墙体共面时的闪面。`Spaces` 实例也会使用稳定的 per-instance 渲染偏移抖动,缓解完全重叠空间之间的 z-fighting;这些偏移只影响渲染顶点,不改变语义 ID、实例矩阵和包围体查询的使用方式。\n\n### 设施设备\n\n`SemanticLoader` 会从 `Stories[].Facilities` 读取设备索引,再从顶层 `Facilities` 数组取得 `Facility` 数据,并使用 `Facility.ID` 在解密后的 `tree_models` 中查找同 ID 节点。只有带 `renderType: '3D'` 和模型 `path` 的节点会被加载。\n\n`tree_models.matrix` 是相对父节点的本地矩阵;`SemanticLoader` 会沿树父级累乘得到设备世界矩阵,再转换为对应 `FloorMesh` 的局部矩阵。因此楼层移动、隔离或显示隐藏时设备会跟随楼层一起工作。即使某个 story 只有 `Facilities`、没有墙柱空间等合并几何,也会创建一个空的 `FloorMesh` 作为设备容器。\n\nFacilities 会在单次 `SemanticParser` 解析范围内按模型 URL 自动分组。静态模型会被转换为 scene-level `InstancedMesh` batch 并挂到 `SemanticGroup.facilityLayer`;每个设备仍会在所属 `FloorMesh` 下保留一个 `FacilityInstanceObject` 实例引用,用于 `getFacilityById()`、显隐、颜色、透明度、包围盒和 `viewer.objectManager` 检索。设备 batch 使用普通 `Group` 包裹,以保证每帧渲染前都能执行相机实例裁剪和 hide/show 后的实例同步;带动画、骨骼、morph target 或缺少合法 `geometry.groups` 的多材质 Mesh 会自动回退为普通 `Model`,但普通 `Model` 也会挂到同一个 `FacilityInstanceObject` wrapper 下,对外 API 不变。\n\n`facilityLayer` 使用 `FacilityInstancedLayer`,并继承自 `BaseGroup`。当 `facilityLayer.visible = false` 时,layer 会阻止内部 `InstancedMesh` 继续参与射线检测,避免隐藏的批处理设备仍被点击命中;单个设备的显隐应先通过 `getFacilityById()` 或 `viewer.objectManager.getById()` 取回实例,再调用 `setInstanceVisible()` 控制。\n\nInstanced 设备通过 per-instance opacity attribute 控制单个设备透明度。batch 材质默认沿用源材质的透明状态;只有源材质本身透明,或当前参与渲染的设备存在 `opacity < 1` 时,材质才会切到 `transparent: true`,`setInstanceOpacity(1)` 可在没有其他半透明实例时回到不透明渲染队列。`resetInstanceColor()` 只清除颜色 override,不影响透明度 override。`FacilityInstancedLayer` 会在设备显隐、颜色、透明度、floor planish/unplanish 改变设备 transform,或相机矩阵、投影矩阵、viewport 高度变化时,在下一次渲染前同步实例矩阵、颜色、透明度和当前 active instance count。普通 fallback `Model` 的颜色和透明度控制由 wrapper 委托到 `MaterialEffects`,单材质和多材质 Mesh 都会应用 override。\n\n加载成功后,`BuildingGroup`、`FloorMesh` 仍会保留自身的 `semanticId`、`semanticKind` 和 `semanticName`;设备引用则使用通用 `InstanceObject` 身份字段 `instanceId`、`instanceKind` 和 `instanceName`。`SemanticGroup.getBuildingById()`、`BuildingGroup.getFloorById()`、`BuildingGroup.getFacilityById()`、`FloorMesh.getFacilityById()` 以及 `ModelInstancedLayer` 的查询、删除和 raycast remap 都使用这些对象字段或显式传入的别名,不再从 `userData.id`、`userData.semanticId`、`userData.facilityId` 里扫描 ID。`userData` 只保留原始业务元数据,例如 `facilityId`、`spaces`、`twinsIdentifier`、`storyId`、`floorIndex`、`instanced` 和 `modelUrl`。\n\n```typescript\nconst floor = building.getFloorAt(0);\n\nconst facility = floor.getFacilityById('FACILITY_001') ?? viewer.objectManager.getById('FACILITY_001');\nconst facilityEntity = floor.getSemanticById('FACILITY_001');\n\nif (facility && facilityEntity?.type === 'object') {\n await viewer.controls.flyToObject(facility, {\n viewpoint: 'rightFrontTop',\n padding: 0.2,\n });\n}\n\nconst facilities = floor.getSemanticsByKind('Facilities');\n\nfacility\n .setInstanceColor('#ff5533')\n .setInstanceOpacity(0.45)\n .resetInstanceColor()\n .setInstanceVisible(true);\n```\n\n对 Facilities 推荐直接使用 `controls.flyToObject()` 飞向设备。无论设备实际渲染来自 `InstancedMesh` 还是 fallback `Model`,`FacilityInstanceObject` 都会暴露本地 `boundingBox` / `boundingSphere`,让 Three.js 的 `Box3.setFromObject()` 能计算完整世界包围盒。\n\n### `FacilityInstanceObject`\n\n`FacilityInstanceObject` 继承自统一的 `InstanceObject`,表示一个设备实例引用。它会挂在所属 `FloorMesh` 下,用于 ID 检索、事件派发、显隐、颜色、透明度和包围盒查询;如果设备可以 instancing,真正几何由 `SemanticGroup.facilityLayer` 里的 `InstancedMesh` 统一渲染;如果不能 instancing,fallback `Model` 会作为它的子对象挂载。\n\n完整的继承字段、方法、颜色模式、包围盒和 dirty 同步语义见 [核心 `InstanceObject` API](./api-objects.md#instanceobject)。\n\n通常不需要手动创建 `FacilityInstanceObject`。`SemanticParser` 在解析 `Facilities` 时会自动创建它,并通过 `setInstanceIdentity()` 写入 `instanceId`、`instanceKind` 和 `instanceName`。`userData.instanced` 表示当前设备是否由 `InstancedMesh` 渲染;fallback 时仍然保留同一套实例 API。\n\n加载完成后,设备可以按 `Facility.ID` 从全局 `objectManager` 取回,并直接传给 `viewer.controls.flyToObject()`。如果需要自己合并多个对象或调整包围盒,也可以继续调用 `getInstanceBoundingBox(box, true)`。\n\n```typescript\nimport { FacilityInstanceObject } from 'u-space/plugins/u-manager';\n\nconst facility = viewer.objectManager.getById<FacilityInstanceObject>('FACILITY_001');\n\nif (facility?.isFacilityInstanceObject) {\n await viewer.controls.flyToObject(facility, {\n viewpoint: 'rightFrontTop',\n padding: 0.2,\n enableTransition: true,\n });\n}\n```\n\n| 属性 / 方法 | 说明 |\n| :---------- | :--- |\n| `isFacilityInstanceObject` | 类型标记,值为 `true`。 |\n| `type` | Three.js 对象类型名,值为 `'FacilityInstanceObject'`。 |\n| `instanceId` | 实例独立 ID,设备默认等于 `Facility.ID`;layer 的按 ID 查询、删除和 raycast remap 都使用该字段。 |\n| `instanceKind` | 实例类型,设备为 `'Facilities'`。 |\n| `instanceName` | 实例显示名称,设备默认使用 `Facility.Name`。 |\n| loader-created bounds | `ModelInstancedLayer.addInstances()` / `reserveBatch()` 会在建 batch 时通过继承的 `setInstanceBounds()` 写入模板包围盒,因此实例可直接传给 `viewer.controls.flyToObject()`。 |\n| `userData.instanced` | `true` 表示由 `FacilityInstancedLayer` 批量渲染;`false` 表示 fallback `Model` 挂在当前对象下。 |\n\n```typescript\nimport { FacilityInstanceObject } from 'u-space/plugins/u-manager';\n\nconst facility = floor.getFacilityById('FACILITY_001');\n\nif (facility instanceof FacilityInstanceObject) {\n facility\n .setInstanceColor('#29ccff')\n .setInstanceOpacity(0.6)\n .setInstanceVisible(true);\n\n await viewer.controls.flyToObject(facility, {\n viewpoint: 'rightFrontTop',\n padding: 0.2,\n });\n}\n```\n\n### `FacilityInstancedLayer`\n\n`FacilityInstancedLayer` 继承自 `ModelInstancedLayer`,是 scene-level Facilities 的批量渲染层。`SemanticGroup.facilityLayer` 默认就是该类型;它会按模型 URL 把同一模板的静态设备合并为一个或多个 `InstancedMesh`,并用普通 `Group` 包裹每个模型 URL batch,保证设备显隐、颜色、透明度和相机实例裁剪能在每帧渲染前同步,同时保留每个楼层下的 `FacilityInstanceObject` 引用用于业务 API 和事件冒泡。\n\n使用 `SemanticLoader` 时通常不需要直接调用 `addInstances()`;只有自定义语义解析、运行时新增设备或自建设备 batch 时才需要手动创建 layer,然后通过 `semanticGroup.setFacilityLayer(layer)` 挂回语义场景。\n\n| 属性 / 方法 | 说明 |\n| :---------- | :--- |\n| `isFacilityInstancedLayer` | 类型标记,值为 `true`。 |\n| `type` | Three.js 对象类型名,值为 `'FacilityInstancedLayer'`。 |\n| `name` | 构造函数默认设为 `'Facilities'`。 |\n| `getInstanceCulling()` | 返回当前实例裁剪配置:`enabled`、`frustum`、`minScreenRadius`。 |\n| `setInstanceCulling(options)` | 设置实例裁剪配置,并返回自身。默认 `enabled: false`、`frustum: true`、`minScreenRadius: 0`;默认不按相机裁剪设备,避免相机距离影响设备显隐。需要性能压缩时可设置 `enabled: true` 开启视锥裁剪,或再把 `minScreenRadius` 设为大于 0 的值开启屏幕尺寸裁剪。 |\n| `getInstances()` | 返回当前 layer 内全部实例对象数组。 |\n| `getInstanceById(id)` | 根据实例对象的 `instanceId` 快速获取实例;找不到时返回 `undefined`。 |\n| `hasInstance(id)` | 判断当前 layer 是否包含对应 `instanceId` 的实例。 |\n| `reserveBatch(url, template, capacity)` | 为某个模型 URL 预分配 batch 容量。适合接下来会多次运行时新增实例的场景,成功返回 `true`,模板不支持 instancing 时返回 `false`。 |\n| `addInstance(url, template, instance)` | 向某个模型 URL 的 batch 新增一个实例。成功返回 `true`,失败时调用方应回退为普通模型渲染。 |\n| `addInstances(url, template, instances)` | 向某个模型 URL 的 batch 批量新增实例。推荐运行时新增多个实例时优先使用,避免连续单个新增造成重复扩容检查。 |\n| `removeInstance(instanceOrId)` | 删除一个实例,可传实例对象或完整 `instanceId`。删除成功返回 `true`。 |\n| `removeInstances(instancesOrIds)` | 删除一个或多个实例。可传单个实例对象、单个字符串 `instanceId`,或混合实例对象和 ID 的 iterable;返回实际删除数量。 |\n| `removeBatch(url)` | 删除某个模型 URL 的完整 batch,释放对应内部 `InstancedMesh` 和 cloned materials。 |\n| `clearBatches()` | 清空当前 layer 的所有模型 batch,并返回自身。 |\n\n`addInstances(url, template, instances)` 会按模型 URL 复用或创建 batch,从 `template` 收集可 instancing 的可见 `Mesh`,克隆源材质,并为每个 batch 建立 instance matrix、颜色和透明度 attribute。没有动画、没有骨骼、没有 morph target,且每个多材质 `Mesh` 都有合法 `geometry.groups` 的模板会进入 instancing;不满足条件、模板没有可用 Mesh、`instances` 为空,或待加入实例的 `instanceId` 与当前 layer 内已有实例重复时会返回 `false`。\n\n动态新增时,layer 会维护每个 URL batch 的 `capacity`。容量足够时,新增实例只会写入逻辑数组、注册 dirty callback 并标记下一帧同步;容量不足时才按 2 倍增长策略重建该 URL 下的内部 `InstancedMesh` 和 attribute buffer。运行时需要连续新增大量实例时,推荐先调用 `reserveBatch(url, template, expectedCount)`,再调用 `addInstances()`。删除实例不会立即收缩 GPU buffer;只有 batch 为空、调用 `removeBatch()` 或 `clearBatches()` 时才释放内部渲染资源。\n\n外部代码不需要直接写内部 `InstancedMesh.instanceMatrix`。要移动、旋转或缩放实例时,直接修改对应 `InstanceObject` 的 transform 即可;transform 的世界矩阵发生变化时会自动标记实例 dirty,`ModelInstancedLayer` 会在下一次渲染前把世界矩阵同步到内部 instance matrix 并设置 `instanceMatrix.needsUpdate = true`。因此即使底层使用 WebGPU storage instanced buffer,公开更新方式仍然是面向实例对象:\n\n```typescript\nconst instance = layer.getInstanceById('FACILITY_001');\n\nif (instance) {\n instance.position.set(10, 0, 5);\n viewer.render();\n}\n```\n\n如果应用为静态大场景关闭了根 Scene 的 `matrixWorldAutoUpdate`,修改单个实例后还需显式调用 `instance.updateWorldMatrix(true, false)` 和 `viewer.invalidate()`;修改包含实例的父 Group 时调用 `group.updateWorldMatrix(true, true)`。这会执行 `InstanceObject` 的原生方法覆盖并触发 dirty callback,不需要直接调用 layer 内部同步方法。\n\n删除 API 支持单个字符串 ID,不需要为了删除一个实例额外包一层数组。下面两个调用等价,都会按完整 ID 删除实例:\n\n```typescript\nlayer.removeInstance('FACILITY_001');\nlayer.removeInstances('FACILITY_001');\n```\n\n渲染前,layer 会同步每个 `FacilityInstanceObject` 的世界矩阵、颜色和透明度,并只在数据变化时上传对应 buffer。默认情况下,相机移动不会改变设备 active instance 集合;显式调用 `setInstanceCulling({ enabled: true })` 后,layer 才会按当前渲染相机逐实例压缩 `InstancedMesh.count`,视锥外设备不会进入本帧实例 buffer;只有同时设置 `minScreenRadius > 0` 时,屏幕半径小于该阈值的设备才会被跳过。相机矩阵、投影矩阵或 viewport 高度未变化时不会重复压缩;设备显隐、透明度为 0、颜色/透明度变化或 `planish('Facilities')` / `unplanish('Facilities')` 改变 transform 时会标记 batch dirty。全部设备不可渲染时,内部 `InstancedMesh.visible` 会被关闭;只有被当前相机裁掉时,mesh 会保留可见并把 `count` 设为 0,以便相机移动后自动恢复。射线检测命中 batch 内实例时,`hit.object` 会被改写为对应的 `FacilityInstanceObject`,并从实例对象字段附加 `instanceObject`、`instanceObjectId`、`instanceKind`、`instanceName` 和 `instanceIndex`,事件随后会按楼层引用对象继续冒泡。\n\n```typescript\nimport { FacilityInstancedLayer, FacilityInstanceObject } from 'u-space/plugins/u-manager';\n\nconst layer = new FacilityInstancedLayer();\nconst refs = facilityItems.map(() => new FacilityInstanceObject());\n\nlayer.reserveBatch('/models/camera.glb', templateObject, refs.length + 100);\n\nif (layer.addInstances('/models/camera.glb', templateObject, refs)) {\n semanticGroup.setFacilityLayer(layer);\n}\n\n// 运行时按完整字符串 ID 删除一个实例;返回实际删除数量。\nlayer.removeInstances('FACILITY_001');\n```\n\n如果只想整体隐藏或禁用 scene-level batch,可以设置 `semanticGroup.facilityLayer.visible = false`;如果只想控制单个设备,推荐从 `floor.getFacilityById(id)` 或 `viewer.objectManager.getById(id)` 取回 `FacilityInstanceObject` 后调用 `setInstanceVisible()` / `setInstanceOpacity()`。\n\n### `ModelInstancedLayer`\n\n`ModelInstancedLayer` 是核心 `src/batches` 模块导出的公共类,也是 `SceneInstancedLayer` 和 `FacilityInstancedLayer` 的共享实现。它负责模板 mesh 收集、材质克隆、`InstancedMesh` 创建、运行时实例新增/删除、capacity 扩容、instance matrix/color/opacity buffer 同步、raycast hit remap、fallback 判定和可选相机实例裁剪。模板 mesh 会保持独立,避免跨楼壳合并后破坏透明排序和局部包围体;射线通过实例包围体后,三角形数量较高的静态模板几何会按需建立并复用 `three-mesh-bvh`,小几何或不受支持的几何布局自动保留 Three.js 默认路径。普通静态模型可以由调用方对明确的热点 root 使用 `enableMeshBVHRaycast(root)`,避免对整个大型场景自动建树造成首击卡顿。layer 内部只依赖 `InstanceObject` 的 `instanceId`、`instanceKind`、`instanceName` 独立字段,不再扫描 `instance.userData` 的 `facilityId`、`id` 或 `sid`。`getInstanceById()` / `removeInstances(id)` 会按需刷新 id 索引;新增实例时如果发现重复 `instanceId` 会拒绝加入,避免同一个 layer 内出现不确定查询结果。通用 API 见 [Batches API](./api-batches#modelinstancedlayer)。\n\n### `FloorMesh`\n\n`FloorMesh` 继承自 `BaseMesh`,表示一个楼层。它不是 `BatchedMesh`,而是把同一楼层内的语义对象合并为一份几何,并通过 TSL + `DataTexture` 在 GPU 侧按 `semanticIndex` 控制颜色、透明度、显示隐藏和实例矩阵。楼层语义网格会按实例透明度拆分为不透明和透明两个材质通道;墙、柱、门等默认不透明实例会进入 opaque pipeline,`Spaces`、`Windows` 或通过 `setOpacityAt()` 改成半透明的实例会进入 transparent pipeline。Facilities 通过楼层下的引用对象进行检索和控制;实际渲染可能来自 `SemanticGroup.facilityLayer` 的 `InstancedMesh` batch,也可能是 fallback 的普通 `Model`。\n\n这种结构的目标是让每个楼层通常只占一个主渲染 draw call,同时仍保留实例级控制能力。`xxxAt` 方法全部作用于单个语义实例;没有 `At` 后缀的方法作用于整个楼层 mesh。\n\n| 属性 / 方法 | 说明 |\n| :---------- | :--- |\n| `semanticCount` / `instanceCount` | 当前楼层语义实例数量。 |\n| `semanticInstances` | `Map<string, number>`,从语义对象 ID 映射到实例下标。 |\n| `wallsInstances` / `columnsInstances` / `spacesInstances` / `doorsInstances` / `windowsInstances` / `staircasesInstances` | 按语义类型维护的 ID 到实例下标映射。 |\n| `semanticEntities` | `Map<string, FloorSemanticEntity>`,统一索引楼层内合并几何语义和 `Facilities` 对象语义。 |\n| `facilityObjectsById` | `Map<string, FacilityInstanceObject>`,按 `Facility.ID` 或显式别名查找设备引用。 |\n| `facilityObjects` | 当前楼层挂载的设备引用数组。 |\n| `getSemanticIndexById(id)` | 根据语义 ID 获取实例下标。 |\n| `getSemanticById(id)` | 根据语义 ID 或设备别名获取统一语义实体。 |\n| `getSemantics()` | 获取楼层内全部语义实体,包含合并几何和设备对象。 |\n| `getSemanticsByKind(kind)` | 按语义类型过滤实体;`kind` 可以是 `Walls`、`Columns`、`Spaces`、`Doors`、`Windows`、`Staircases` 或 `Facilities`。 |\n| `getSemanticIdAt(index)` | 获取实例语义 ID。 |\n| `getSemanticKindAt(index)` | 获取合并几何实例类型:`'Walls' \\| 'Columns' \\| 'Spaces' \\| 'Doors' \\| 'Windows' \\| 'Staircases'`。 |\n| `getSemanticNameAt(index)` | 获取实例名称;当前 `Spaces` 会从语义数据的 `Name` 写入,其他类型可能为 `undefined`。 |\n| `getSemanticObjectAt(index)` | 获取合并几何实例对应的 `FloorSemanticInstanceObject` 轻量对象。 |\n| `showAllFacilities()` | 显示当前楼层的全部设备对象。 |\n| `hideAllFacilities()` | 隐藏当前楼层的全部设备对象。 |\n| `addFacility(object, ids?)` | 把设备对象挂到楼层,并按对象 `instanceId` 和显式 `ids` 建立检索别名。不会从 `object.userData` 自动扫描 ID。 |\n| `getFacilityById(id)` | 根据 `Facility.ID` 或别名获取设备对象。 |\n| `getFacilityAt(index)` | 按楼层设备数组下标获取设备对象。 |\n| `getFacilities()` | 获取当前楼层全部设备对象。 |\n| `removeFacility(facility)` | 按设备对象或 ID 从楼层移除设备,并同步清理检索索引。 |\n| `setColorAt(index, color)` / `getColorAt(index, target?)` | 修改或读取实例颜色,支持 `Color`、`Vector3`、`Vector4`。 |\n| `setOpacityAt(index, opacity)` / `getOpacityAt(index)` | 修改或读取实例透明度。 |\n| `setVisibleAt(index, visible)` / `getVisibleAt(index)` | 修改或读取实例显隐状态。 |\n| `setMatrixAt(index, matrix)` / `getMatrixAt(index, target?)` | 设置或读取实例附加矩阵。 |\n| `setScaleAt(index, scale, y?, z?)` | 修改实例缩放,支持 `number` 或 `Vector3`。 |\n| `setScaleYAt(index, y)` | 只修改实例 Y 轴缩放,保留 X/Z 缩放和已有位移、旋转。 |\n| `getFinalMatrixAt(index, target?)` | 读取实例最终矩阵,包含语义对象原始基点矩阵。 |\n| `getBoundingBoxAt(index, target?, world?)` | 获取实例包围盒;`world = true` 时包含 `FloorMesh.matrixWorld`。 |\n| `getBoundingSphereAt(index, target?, world?)` | 获取实例包围球;`world = true` 时包含 `FloorMesh.matrixWorld`。 |\n| `computeBoundingBox()` / `computeBoundingSphere()` | 计算整个楼层的包围体。 |\n| `planish(kind?)` / `unplanish(kind?)` | 压扁或恢复楼层内所有合并语义实例和 Facilities;压平时按各自包围盒贴到楼层平面附近,并加入稳定的轻微 Y 偏移;传入 `kind` 时只作用于该语义类型,`kind` 可包含 `Facilities`。 |\n\n### 飞向语义实例\n\n点击、射线检测命中 `FloorMesh` 里的语义几何或设备时,`intersect` 上会附加语义字段:`semanticIndex`、`semanticId`、`semanticKind` 和 `semanticObject`。普通楼层语义几何的事件目标是 `FloorSemanticInstanceObject`;instanced Facilities 的事件目标是楼层下的 `FacilityInstanceObject` 引用,事件会继续冒泡到所属 `FloorMesh`、`BuildingGroup` 和 `SemanticGroup`。因此楼层监听器中应使用 `event.currentTarget` 操作楼层,用 `intersect.semanticObject` 或 `event.target` 取得具体语义实例。\n\n```typescript\nimport { Box3 } from 'three';\n\nconst box = new Box3();\n\nfloor.addEventListener('click', async ({ event }) => {\n const { currentTarget, intersect } = event;\n if (!intersect) return;\n\n if (intersect.semanticKind === 'Facilities') {\n const facility = intersect.semanticObject ?? event.target;\n\n await viewer.controls.flyToObject(facility, {\n viewpoint: 'rightFrontTop',\n padding: 0.2,\n });\n return;\n }\n\n if (intersect.semanticObject) {\n await viewer.controls.flyToObject(intersect.semanticObject, {\n viewpoint: 'rightFrontTop',\n padding: 0.2,\n });\n return;\n }\n\n const semanticBox = currentTarget.getBoundingBoxAt(intersect.semanticIndex, box, true);\n\n if (semanticBox) {\n await viewer.controls.flyToBox(semanticBox, {\n viewpoint: 'rightFrontTop',\n padding: 0.2,\n });\n }\n});\n```\n\n如果要根据 ID 操作对象,先通过 `getSemanticIndexById(id)` 转为实例下标。\n\n```typescript\nconst index = floor.getSemanticIndexById('SPACE_001');\n\nif (index !== undefined) {\n floor.setVisibleAt(index, false);\n viewer.render();\n}\n```\n\n### 楼层压扁\n\n`planish()` / `unplanish()` 默认作用于当前楼层的全部合并语义实例,包含墙、柱、空间、门窗、楼梯等语义几何,也包含楼层下的 Facilities 引用;传入 `kind` 时只处理该类型,例如 `floor.planish('Walls')` 或 `floor.planish('Facilities')`。所有被压平的实例都会按自身包围盒把压平后的薄片贴到楼层平面 `FloorMesh.pivot.y` 附近,而不是围绕实例自身基点压缩;压平时还会按语义类型和实例 ID 加入稳定的轻微 Y 偏移,减少门、墙或其他重叠实例之间的共面闪烁。`unplanish()` 会恢复合并几何实例的原始矩阵,`unplanish('Facilities')` 会恢复设备原始 `position.y` 和 `scale.y`。instanced Facilities 会通过楼层下的 `FacilityInstanceObject` transform 同步到 `SemanticGroup.facilityLayer`。\n\n如果只想调整单个合并几何实例,可以使用 `setScaleAt()` 或 `setScaleYAt()`,它们会保留该语义对象自己的基点矩阵,因此压扁墙、柱、房间时不会被压到世界原点。\n\n## `SceneLoader`\n\n从服务端场景包加载完整场景树(模型、组、基础形状),并自动将所有已加载对象按 `id` 和 `sid` 注册到 `viewer.objectManager`。\n默认会尝试读取同一路径下的 `db/semantic_model.json`,如果场景树节点的 `id` 已存在于语义文件中,则跳过该节点,方便和 `SemanticLoader` 同时加载而不重复创建建筑或设备。\n对语义去重后仍保留的 3D 节点,`SceneLoader` 默认会按模型 `path` 自动合并重复静态模型:同一 `path` 的多个实例只加载一次模板,并由 `SceneInstancedLayer` 通过共享的 `ModelInstancedLayer` 批量渲染。原场景树中仍会保留每个 `id` / `sid` 对应的 `SceneInstanceObject` 逻辑对象,用于业务检索、显隐、颜色、透明度、包围盒和事件派发。\n\n如果场景主要由大量普通 Mesh 组成,并且比默认的同 URL instancing 更重视 draw call 和可渲染节点数量,可以显式调用 `setEditableBatching()`。该模式会把兼容 Mesh 的变换烘焙到顶点,再按材质和 Geometry 布局合并为少量 `SceneEditableBatchLayer` Mesh;逻辑 `SceneInstanceObject` 仍留在原场景树中。它是静态 Geometry batching,不是 `InstancedMesh`、WebGPU indirect draw 或 compute rasterizer。\n\n两种 layer 是 `SceneLoader` 的可替换主渲染策略,不存在继承关系:默认模式把 `SceneInstancedLayer` 设为 `SceneGroup.sceneLayer`;启用 editable batching 且至少生成一个 merged batch 时改为 `SceneEditableBatchLayer`。后者只接收兼容的不透明静态 Mesh;不兼容模板或子 Mesh 会继续尝试放入一个名为 `SceneEditableBatchFallback` 的独立 `SceneInstancedLayer`,重复 fallback 无法 instancing 时才退回普通 `Model`。因此 `SceneGroup.sceneLayer` / `getDefaultSceneLayer()` 只暴露主 `SceneEditableBatchLayer`,fallback `SceneInstancedLayer` 是同一 `SceneGroup` 下的辅助渲染层,不会替换主 layer;如果没有生成任何 merged Mesh,主 layer 会被移除并返回 `null`,即使同级 fallback 仍然存在。\n\n模板包含动画、骨骼、morph target、非法多材质 groups 或没有可实例化网格时,会自动回退为普通 `Model` 渲染;fallback `Model` 会挂在同一个 `SceneInstanceObject` 下,因此调用侧不需要额外配置。\n\n```typescript\nimport { SceneLoader } from 'u-space/plugins/u-manager';\n\nconst sceneLoader = new SceneLoader(viewer);\nsceneLoader.setPath('./scenes/my-scene');\nsceneLoader.setKey('YOUR_LICENSE_KEY'); // 官方授权场景必需\nsceneLoader.setEditableBatching({\n maxVerticesPerBatch: 1_500_000,\n maxIndicesPerBatch: 4_500_000,\n freezeAnimations: true,\n});\nconst group = await sceneLoader.loadAsync(); // SceneGroup\nviewer.scene.add(group);\n\nconst sceneLayer = group.getDefaultSceneLayer();\n```\n\n**方法:**\n\n| 方法 | 说明 |\n| :------------------ | :------------------------------------------------------------------- |\n| `setKey(key)` | 设置授权场景的 RSA 解密密钥。 |\n| `setEditableBatching(options?)` | 开启可编辑静态 Geometry batching。传 `true` 使用默认选项,传 `false` 关闭;默认关闭。 |\n| `loadAsync()` | 加载并解析场景,返回去除语义重复节点后的 `SceneGroup`。 |\n| `clearCache()` | 从 `objectManager` 中移除此加载器注册的所有 ID。 |\n| `dispose()` | 清除缓存并释放水印叠加层。 |\n\n### 可编辑静态合批:`setEditableBatching()`\n\n`setEditableBatching(options)` 是面向大型、主要静态但仍需按对象操作的场景的 opt-in 路径。开启后,`SceneLoader` 会把所有保留的 3D 节点创建为 `SceneInstanceObject`,不再只处理重复 `path`;每个唯一模型 URL 仍只加载一次模板。\n\n```typescript\nimport {\n SceneLoader,\n type SceneEditableBatchOptions,\n type SceneEditableBatchStats,\n} from 'u-space/plugins/u-manager';\n\nconst options: SceneEditableBatchOptions = {\n maxVerticesPerBatch: 1_500_000,\n maxIndicesPerBatch: 4_500_000,\n freezeAnimations: true,\n};\n\nconst sceneLoader = new SceneLoader(viewer).setEditableBatching(options);\nconst sceneGroup = await sceneLoader.loadAsync();\nviewer.scene.add(sceneGroup);\n\nconst stats = sceneGroup.userData.editableBatch as SceneEditableBatchStats | undefined;\nconsole.table(stats);\n```\n\n**配置:**\n\n| 字段 | 默认值 | 说明 |\n| :--- | :--- | :--- |\n| `maxVerticesPerBatch` | `1_500_000` | 单个 merged Geometry 的最大顶点数;超过后开始新 chunk。 |\n| `maxIndicesPerBatch` | `4_500_000` | 单个 merged Geometry 的最大索引数;超过后开始新 chunk。 |\n| `freezeAnimations` | `false` | `false` 时含 animation clip 或 mixer 的模板整体 fallback;`true` 时烘焙当前姿态,后续播放动画前必须先 materialize。SkinnedMesh 和 morph target 始终 fallback。 |\n\n#### 合并规则\n\n每个候选 Mesh 会生成稳定的 material key 和 geometry-layout key。只有以下状态完全兼容的 Mesh 才进入同一组:\n\n- 材质类型、颜色/粗糙度等序列化参数,以及贴图源和 sampler/transform 配置一致。\n- Geometry 的 indexed/non-indexed 类型、index 数组类型、attribute 名称、`itemSize`、`normalized`、数组类型和 `gpuType` 一致。Geometry 内容和顶点数量可以不同。\n- `castShadow`、`receiveShadow` 和 `renderOrder` 一致。\n\n合并前会为每个源 Mesh 克隆 Geometry,并应用以下矩阵:\n\n```text\ninverse(batchLayer.matrixWorld) × instance.matrixWorld × meshLocalMatrix\n```\n\n随后为全部顶点写入 `batchObjectIndex` attribute,再通过 `mergeGeometries()` 生成 merged Mesh。批次达到顶点或索引上限时自动切分,因此每个兼容组可能生成多个 draw call。\n\n该方案会把重复模板的顶点展开到 merged Geometry,通常以更高的 GPU 顶点内存换取更少的 draw calls 和可渲染 Mesh 遍历;重复率极高、几何较大的同模板对象仍可能更适合原生 `SceneInstancedLayer`。\n\n#### 对象级状态与 materialize\n\n合批后,每个逻辑对象在 Float `DataTexture` 中占一个 RGBA texel,batch NodeMaterial 通过 TSL `colorNode` 和独立布尔 `maskNode` 读取 `batchObjectIndex` 对应的颜色与显隐状态;不透明 batch 保留源材质原有的 opacity 路径。因此以下操作只会把对象标记为 dirty,并立即更新 CPU 状态纹理数据,GPU 在下一帧上传,不会重新合并 Geometry:\n\n- `setInstanceVisible()` 和父级显隐变化。\n- `setInstanceColor()` / `resetInstanceColor()`。\n- 不引入半透明的 `setInstanceHighlight()` / `clearInstanceHighlight()`。\n\n如果对象的世界 transform 与烘焙矩阵不同,或者有效 opacity 低于 `0.999`,该对象会自动 `materialize()`:原始完整模板被 clone 为普通 `Model`,其中每个材质为当前实例独立克隆,然后挂到当前 `SceneInstanceObject` 下,同时 merged batch 中的旧副本被 mask 掉并从 BVH 命中过滤。根 Scene 使用 `matrixWorldAutoUpdate = false` 时,transform 修改后必须调用该对象的 `updateWorldMatrix(true, false)`,或在父 Group 上调用 `updateWorldMatrix(true, true)`,才能提交新世界矩阵并立即触发 dirty/materialize 检测。该同步发生在内部 batch 视锥裁剪前,从屏幕外移动到屏幕内也不会丢失。materialize 是单向操作;对象不会在恢复原矩阵或不透明度后自动重新进入 batch,需要重新加载场景。也可以提前显式调用:\n\n```typescript\nconst object = viewer.objectManager.getById<SceneInstanceObject>('SCENE_NODE_ID');\nconst model = object?.materialize();\n\nif (model) {\n object.position.x += 2;\n object.setInstanceOpacity(0.5);\n await viewer.render();\n}\n```\n\n#### Fallback 与透明材质\n\n透明材质、`opacity < 0.999`、SkinnedMesh、morph target、多材质数组、自定义 NodeMaterial / `onBeforeCompile`、非标准 Geometry 布局、负行列式变换和未冻结动画不会进入 merged batch。负 determinant 会按实例拆分;同模板的正 determinant 实例仍可合并,而负缩放实例直接使用普通 `Model`,不会继续进入同样不支持负缩放的 `SceneInstancedLayer`。混合模板会只合并兼容的不透明 Mesh,并生成一个隐藏这些已合并 Mesh 的 fallback 模板来保留透明或其他不支持的子 Mesh;build 期间已经因初始 opacity materialize 的实例不会再次加入透明 fallback。其余同 URL fallback 有多个实例时仍由 `SceneInstancedLayer` 渲染,单例则使用普通 `Model`。\n\n这条边界用于保留透明排序、蒙皮/变形和自定义 shader 行为。不要通过强制改成不透明材质来提高合批率。\n\n#### Raycast 与统计\n\nmerged Mesh 首次实际拾取时会按需建立 `three-mesh-bvh`。命中结果通过 `faceIndex → vertexIndex → batchObjectIndex` 映射回原始 `SceneInstanceObject`,因此 `intersect.object` 仍是业务逻辑对象;已经 materialize 的对象不会在旧烘焙位置继续产生命中。\n\n`SceneEditableBatchStats` 写入 `sceneGroup.userData.editableBatch`:\n\n| 字段 | 说明 |\n| :--- | :--- |\n| `instances` | 进入 editable batch 状态表的逻辑对象数。 |\n| `sourceMeshes` | 合并前的兼容源 Mesh 数。 |\n| `batches` | 最终 merged Mesh 数。 |\n| `drawCallsSaved` | `sourceMeshes - batches` 的理论 draw-call 减少量,不包含多 pass 放大。 |\n| `vertices` / `indices` | 烘焙后的总顶点数和索引数,用于评估显存代价。 |\n| `unsupportedInstances` | 需要完整或部分 fallback 的逻辑对象数。 |\n| `unsupportedByReason` | 按 `transparent`、`animation`、`skinned`、`morph`、`material-array`、`geometry-layout`、`negative-scale` 等原因统计。 |\n\nWebGPU 首次创建大量 batch material pipeline 时可能出现短暂的 shader 编译开销。如果产品不希望首个可见帧展示未完成的材质,可在需求渲染模式下暂时停止相机控制产生的 render invalidation,完成 `compileAsync()` 后再提交首帧;`examples/test_umanager2.html` 展示了这一流程。\n\n### `SceneGroup`\n\n`SceneGroup` 继承自 `BaseGroup`,是 `SceneLoader.loadAsync()` 的返回根组。它保留 `tree_models.json` 解析后的原始父子层级,并把重复静态模型的默认合批渲染层挂在 `sceneLayer`,方便外部直接访问,而不需要遍历 `children`。\n\n| 属性 / 方法 | 说明 |\n| :---------- | :--- |\n| `isSceneGroup` | 固定为 `true`,用于判断对象是否为 `SceneLoader` 根组。 |\n| `sceneLayer` | 默认 path instancing 下为 `SceneInstancedLayer`;启用 editable batching 后为 `SceneEditableBatchLayer`;没有可合批模型时为 `null`。 |\n| `setSceneLayer(layer)` | 设置 `SceneInstancedLayer` 或 `SceneEditableBatchLayer`;传入 `null` 会移除已有 layer。 |\n| `getDefaultSceneLayer()` | 返回当前默认场景渲染层;没有可合批模型时返回 `null`。 |\n\n### `SceneInstanceObject`\n\n`SceneInstanceObject` 继承自统一的 `InstanceObject`,表示一个由 `SceneInstancedLayer`、`SceneEditableBatchLayer` 或 fallback 普通模型渲染的场景模型引用。它会保留在原场景树层级中,并注册到 `viewer.objectManager`,因此 `getById()`、`show()` / `hide()`、`isolate()`、`showAll()`、`setOpacity()` 和 `getBoundingBox()` 可以继续按单个模型 ID 使用。\n\n完整的继承字段、方法、颜色模式、包围盒和 dirty 同步语义见 [核心 `InstanceObject` API](./api-objects.md#instanceobject)。\n\n`SceneInstanceObject` 本身只增加场景模型标记:`isSceneInstanceObject = true`、`type = 'SceneInstanceObject'`、`instanceKind = 'SceneInstances'`。加载器会把场景树节点的 `id` 和 `sid` 都注册到 `viewer.objectManager`,因此同一个对象可以通过任一 ID 取回。对重复模型的 instanced 路径,`instanceId` 默认等于场景树节点 `id`,`instanceName` 默认等于节点名称;`userData.instanced = true`,并保留 `modelPath` 和 `modelUrl` 等原始加载元数据。如果模板不支持 instancing,会退回普通 `Model`,但仍挂在同一个 `SceneInstanceObject` 下,对外 API 不变。\n\n**识别字段:**\n\n| 字段 | 说明 |\n| :--- | :--- |\n| `isSceneInstanceObject` | 固定为 `true`,用于判断对象是否来自 `SceneLoader` 的场景实例。 |\n| `type` | 固定为 `SceneInstanceObject`。 |\n| `instanceId` | 实例独立 ID,默认等于场景树节点 `id`;layer 的按 ID 查询、删除和 raycast remap 都使用该字段。 |\n| `instanceKind` | 实例类型,场景实例为 `'SceneInstances'`。 |\n| `instanceName` | 实例显示名称,默认等于场景树节点名称。 |\n| `userData.id` / `userData.sid` | 原始场景树节点 ID;两个 ID 都会注册到 `viewer.objectManager`。 |\n| `userData.instanced` | 加载时选择 batch 路径的元数据。editable batch 对象 materialize 后该字段不作为实时状态;使用 `getInstanceRenderObject()` 是否非空判断当前是否已有普通渲染对象。 |\n| `userData.modelPath` / `userData.modelUrl` | instanced 场景实例对应的模型相对路径和完整 URL。 |\n\n**方法:**\n\n| 方法 | 说明 |\n| :--- | :--- |\n| 继承的实例 API | 完整定义见 [核心 `InstanceObject` API](./api-objects.md#instanceobject);包含身份、bounds、fallback render object、显隐、颜色、透明度、高亮和 dirty callback。 |\n| `materialize()` | 如果对象由 `SceneEditableBatchLayer` 渲染,clone 原始完整模板并将当前对象从 merged/fallback batch 中移出;默认 path instancing 或普通 fallback 下返回已有 render object 或 `null`。 |\n\n`MaterialEffects.highlightColor()` / `removeHighlightColor()` 会识别 `SceneInstanceObject` 并调用 `setInstanceHighlight()` / `clearInstanceHighlight()`,不会直接修改共享 batch 材质。`ObjectManager.setOpacity()`、`ObjectManager.hide()`、`ObjectManager.show()` 也会通过统一 API 作用到单个实例。\n\n加载完成后可以按场景树节点 `id` 从 `objectManager` 取回 `SceneInstanceObject`,再直接传给 `viewer.controls.flyToObject()`。`SceneInstanceObject` 内部带有本地 `boundingBox` 缓存能力,因此不需要加载真实 Mesh 副本也能计算飞行包围盒。\n\n```typescript\nimport { SceneInstanceObject } from 'u-space/plugins/u-manager';\n\nconst object = viewer.objectManager.getById<SceneInstanceObject>('SCENE_NODE_ID');\n\nif (object?.isSceneInstanceObject) {\n await viewer.controls.flyToObject(object, {\n viewpoint: 'current',\n padding: 0.2,\n enableTransition: true,\n });\n}\n```\n\n颜色、透明度和显隐可以直接链式调用:\n\n```typescript\nobject\n ?.setInstanceColor('#29ccff')\n .setInstanceOpacity(0.55)\n .setInstanceVisible(true);\n\nobject?.setInstanceHighlight('#ffb703', 0.75, true);\nobject?.clearInstanceHighlight();\nobject?.resetInstanceColor();\n```\n\n如果需要自己控制包围盒,也可以取世界包围盒后调用 `flyToBox()`:\n\n```typescript\nimport { Box3 } from 'three/webgpu';\n\nconst box = object.getInstanceBoundingBox(new Box3(), true);\nawait viewer.controls.flyToBox(box, { viewpoint: 'frontTop', padding: 0.2 });\n```\n\n射线检测命中批处理实例时,`intersect.object` 会被改写为对应的 `SceneInstanceObject`,并附加 `instanceObject`、`instanceObjectId`、`instanceKind: 'SceneInstances'`、`instanceName` 和 `instanceIndex`。Three.js 原生的 `intersect.instanceId` 仍保留为内部 `InstancedMesh` 的数字实例下标,事件会按逻辑对象所在的原场景树继续冒泡。\n\n### `SceneInstancedLayer`\n\n`SceneInstancedLayer` 是 `SceneLoader` 内部使用的批量渲染层,默认 `name = 'SceneInstances'`。它继承自 `ModelInstancedLayer`,每个重复模型 URL 会生成一个 batch,batch 内按模板的可实例化 Mesh 创建 `InstancedMesh`。渲染前只在实例矩阵、显隐、颜色或透明度变化时同步 instance buffer;普通相机移动不会重新上传所有实例数据。\n\n### `SceneEditableBatchLayer`\n\n`SceneEditableBatchLayer` 继承自核心 `EditableGeometryBatchLayer<SceneInstanceObject>`,后者继承自 `BaseGroup`。它是 `setEditableBatching()` 创建的 scene-specific 静态 Geometry 合并层,默认 `name = 'SceneEditableBatches'`;通用的材质/Geometry 分组、变换烘焙、chunk 切分、TSL 状态纹理、materialize、lazy BVH 和资源释放都由核心层实现。内部未导出的 `EditableGeometryBatchMesh` 继承自 `BaseMesh`;隐藏整个 layer 或单个 batch Mesh 时,`ignoreInvisibleWhenRaycast` 会在进入自定义 BVH raycast 前停止命中,阴影属性仍由对应源 Mesh 的 `castShadow` / `receiveShadow` 覆盖。\n\n它不继承 `SceneInstancedLayer`。开启 editable batching 且生成 merged Mesh 时,`SceneEditableBatchLayer` 是 `SceneGroup.sceneLayer` 的主层;不兼容的重复 fallback 由同级、名称为 `SceneEditableBatchFallback` 的 `SceneInstancedLayer` 辅助渲染,单例或仍不支持 instancing 的对象使用挂在 `SceneInstanceObject` 下的普通 `Model`。没有 merged Mesh 时主 layer 为 `null`,辅助 fallback 不会被 `getDefaultSceneLayer()` 返回。\n\n通常由 `SceneLoader` 管理,不需要业务代码直接调用 scene adapter 的 `addTemplate()` / `build()`,也不需要自行处理 materialize 后的 fallback 移除。公开的 `stats` 和 `options` 可用于诊断;materialize 通知使用继承的类型化 `materialize` 事件,释放独立创建的 layer 时应调用 `dispose()`。如果要在其他插件或应用中直接复用同一机制,应使用核心 [EditableGeometryBatchLayer](./api-batches#editablegeometrybatchlayer) 的 `addSource()` API。\n\n## `TopologiesLoader` / `TopologyParser`\n\n加载拓扑图数据并将其转换为 `Topology` 对象。\n\n```typescript\nimport { TopologiesLoader } from 'u-space/plugins/u-manager';\n\nconst loader = new TopologiesLoader();\nloader.setPath('./scenes/my-scene');\nconst topologies = await loader.loadAsync(); // Topology[]\nviewer.scene.add(...topologies);\n```\n\n## `VisionsLoader` / `VisionsParser`\n\n加载命名相机视点,并将相机飞行到指定视点。\n\n```typescript\nimport { VisionsLoader, VisionsParser } from 'u-space/plugins/u-manager';\n\nconst visionsLoader = new VisionsLoader();\nvisionsLoader.setPath('./scenes/my-scene');\nconst visionsData = await visionsLoader.loadAsync();\n// visionsData 是 Record<string, IVisions[]>\n\nconst visionsParser = new VisionsParser(viewer);\n\n// 飞行到指定视点\nawait visionsParser.flyTo(visionsData['HOME'][0]);\n\n// 飞行到主(默认)视点\nawait visionsParser.flyToPrimary(visionsData['HOME']);\n```\n\n**`IVisions` 字段:**\n\n| 字段 | 类型 | 说明 |\n| :--------- | :------------ | :------------------------------ |\n| `camera` | `'P' \\| 'O'` | 相机类型:透视或正交。 |\n| `position` | `IVector3` | 相机世界位置。 |\n| `target` | `IVector3` | 相机朝向目标点。 |\n| `zoom` | `number` | 相机缩放级别。 |\n| `primary` | `boolean` | 是否为默认视点。 |\n\n## `AnimationsLoader` / `AnimationsParser`\n\n加载关键帧动画数据,并通过 `tweenAnimation` 驱动 `Object3D` 的变换。\n\n```typescript\nimport { AnimationsLoader, AnimationsParser } from 'u-space/plugins/u-manager';\n\nconst animLoader = new AnimationsLoader();\nanimLoader.setPath('./scenes/my-scene');\nconst animationsData = await animLoader.loadAsync(); // IAnimations[]\n\n// 查找特定对象的动画\nconst data = animationsData.find((a) => a.modelId === myModel.userData.id);\n\nconst parser = new AnimationsParser(viewer, myModel);\nparser.initTransform(); // 保存初始位置/旋转/缩放\n\nawait parser.play(data.keyframes); // 播放动画序列\n\n// 中途停止\nparser.stop();\n\n// 重置到初始变换\nparser.reset();\n```\n\n## `PropertiesLoader`\n\n加载与模型关联的结构化属性元数据(如 BIM 属性)。\n\n```typescript\nimport { PropertiesLoader } from 'u-space/plugins/u-manager';\n\nconst propsLoader = new PropertiesLoader();\npropsLoader.setPath('./scenes/my-scene');\nconst properties = await propsLoader.loadAsync(); // IProperties[]\n\n// 查找模型的属性\nconst modelProps = properties.filter(p => p.modelId === myModel.userData.id);\n```\n\n**`IProperties` 字段:**\n\n| 字段 | 类型 | 说明 |\n| :-------- | :--------------- | :----------------------- |\n| `modelId` | `string` | 关联模型的 ID。 |\n| `group` | `string` | 属性分组/类别名称。 |\n| `key` | `string` | 属性键名。 |\n| `value` | `string \\| null` | 属性值。 |\n| `label` | `string \\| null` | 属性的显示标签。 |\n"
|
|
962
1095
|
},
|
|
963
1096
|
{
|
|
964
1097
|
"id": "api-render-pipeline",
|
|
965
1098
|
"path": "api-render-pipeline.md",
|
|
966
1099
|
"title": "RenderPipeline API(后处理管线)",
|
|
967
|
-
"summary": "`viewer.renderPipeline` 管理渲染后处理链,内置 Bloom、SSGI、TRAA 支持,并提供自定义后处理覆盖机制。 ``` Stage 1: pass(scene, camera) + MRT(按需配置 output/emissive/normal/velocity)",
|
|
1100
|
+
"summary": "`viewer.renderPipeline` 管理渲染后处理链,内置 Bloom、SSGI、SSR、TRAA 支持,并提供自定义后处理覆盖机制。 ``` Stage 1: pass(scene, camera) + MRT(按需配置 output/emissive/normal/SSR G-buffer/velocity)",
|
|
968
1101
|
"headings": [
|
|
969
1102
|
{
|
|
970
1103
|
"level": 1,
|
|
@@ -994,6 +1127,30 @@ export const docsIndex = [
|
|
|
994
1127
|
"level": 3,
|
|
995
1128
|
"text": "SSGIConfig"
|
|
996
1129
|
},
|
|
1130
|
+
{
|
|
1131
|
+
"level": 2,
|
|
1132
|
+
"text": "SSR(屏幕空间反射)"
|
|
1133
|
+
},
|
|
1134
|
+
{
|
|
1135
|
+
"level": 3,
|
|
1136
|
+
"text": "SSRConfig"
|
|
1137
|
+
},
|
|
1138
|
+
{
|
|
1139
|
+
"level": 3,
|
|
1140
|
+
"text": "SSRTemporalReprojectConfig"
|
|
1141
|
+
},
|
|
1142
|
+
{
|
|
1143
|
+
"level": 3,
|
|
1144
|
+
"text": "SSRDenoiseConfig"
|
|
1145
|
+
},
|
|
1146
|
+
{
|
|
1147
|
+
"level": 3,
|
|
1148
|
+
"text": "Reversed depth 兼容"
|
|
1149
|
+
},
|
|
1150
|
+
{
|
|
1151
|
+
"level": 3,
|
|
1152
|
+
"text": "性能建议"
|
|
1153
|
+
},
|
|
997
1154
|
{
|
|
998
1155
|
"level": 2,
|
|
999
1156
|
"text": "TRAA(时间抗锯齿)"
|
|
@@ -1011,7 +1168,7 @@ export const docsIndex = [
|
|
|
1011
1168
|
"text": "Overlay Passes"
|
|
1012
1169
|
}
|
|
1013
1170
|
],
|
|
1014
|
-
"content": "# RenderPipeline API(后处理管线)\n\n`viewer.renderPipeline` 管理渲染后处理链,内置 Bloom、SSGI、TRAA 支持,并提供自定义后处理覆盖机制。\n\n## 渲染节点图\n\n```\nStage 1: pass(scene, camera) + MRT(按需配置 output/emissive/normal/velocity)\n ↓\nStage 2: 后处理链\n ├─ 内置模式: SSGI → Bloom
|
|
1171
|
+
"content": "# RenderPipeline API(后处理管线)\n\n`viewer.renderPipeline` 管理渲染后处理链,内置 Bloom、SSGI、SSR、TRAA 支持,并提供自定义后处理覆盖机制。\n\n## 渲染节点图\n\n```\nStage 1: pass(scene, camera) + MRT(按需配置 output/emissive/normal/SSR G-buffer/velocity)\n ↓\nStage 2: 后处理链\n ├─ 内置模式: SSGI → SSR + Temporal Reproject + Denoise → Bloom\n └─ 自定义模式: outputComposer(scenePass) → Node\n ↓\nStage 3: Output Effects(仅默认/内置模式,插件/扩展追加效果)\n ↓\nStage 4: TRAA 时间抗锯齿\n ↓\nStage 5: Overlay Passes(blendColor 叠加 ViewerHelper 等)\n ↓\noutputNode = 最终结果\n```\n\n## 属性\n\n| 属性 | 类型 | 说明 |\n| :--- | :--- | :--- |\n| `scenePass` | `PassNode` | 场景渲染通道节点,`pass(scene, camera)` 的返回值。 |\n| `needsUpdateOutputNode` | `boolean` | 标记输出节点需要在下一帧重建。 |\n\n## Bloom\n\n基于 emissive MRT 通道的泛光效果。\n\n```typescript\n// 开启 Bloom\nviewer.renderPipeline.enableBloom({ strength: 1.5, radius: 0.4, threshold: 0.2 });\n\n// 运行时更新参数\nviewer.renderPipeline.updateBloom({ strength: 2.0 });\n\n// 关闭 Bloom\nviewer.renderPipeline.disableBloom();\n```\n\n### `BloomConfig`\n\n| 属性 | 类型 | 默认值 | 说明 |\n| :--- | :--- | :----- | :--- |\n| `strength` | `number` | `1` | 泛光强度 |\n| `radius` | `number` | `0` | 泛光扩散半径 |\n| `threshold` | `number` | `0` | 亮度阈值,低于此值不产生泛光 |\n\n## SSGI(屏幕空间全局光照)\n\n> 仅支持 `PerspectiveCamera`,使用 `OrthographicCamera` 时会自动跳过并输出警告。\n\nSSGI 分别生成 AO 与 GI 纹理,并按 `scene.rgb × AO + diffuse.rgb × GI` 合成最终颜色。AO 只压暗遮蔽区域,GI 只增加间接漫反射;背景不参与 SSGI。`Viewer` 默认的 reversed depth 和显式配置的标准深度均受支持,应用侧无需转换 depth texture。\n\n[在线示例:默认开启的纯 AO](https://u-space-phi.vercel.app/examples/test_ao.html)\n\n```typescript\n// 开启 SSGI\nviewer.renderPipeline.enableSSGI({ aoIntensity: 1, giIntensity: 10 });\n\n// 运行时更新参数(直接修改 uniform,不重建节点图)\nviewer.renderPipeline.updateSSGI({ giIntensity: 20, radius: 8 });\n\n// 关闭 SSGI\nviewer.renderPipeline.disableSSGI();\n```\n\n### `SSGIConfig`\n\n| 属性 | 类型 | 默认值 | 范围 | 说明 |\n| :--- | :--- | :----- | :--- | :--- |\n| `sliceCount` | `number` | `1` | 1–4 | 半球切片数 |\n| `stepCount` | `number` | `12` | 1–32 | 每切片采样步数 |\n| `aoIntensity` | `number` | `1` | 0–4 | AO 对遮蔽项的对比度/压暗强度 |\n| `giIntensity` | `number` | `10` | 0–100 | 加到漫反射项上的间接光强度 |\n| `radius` | `number` | `12` | 1–25 | 世界空间采样半径 |\n| `thickness` | `number` | `1` | 0.01–10 | 物体厚度(世界单位) |\n\n## SSR(屏幕空间反射)\n\nSSR 使用 stochastic ray marching 生成反射,随后通过运动向量执行 Temporal Reproject,并使用 Recurrent Denoise 进行时空降噪。完整链路需要连续帧积累,启用后应将 `viewer.frameloop` 设为 `'always'`。\n\n> 仅支持 `PerspectiveCamera`,使用 `OrthographicCamera` 时会自动跳过并输出警告。\n\n[在线示例:SSR 时空降噪](https://u-space-phi.vercel.app/examples/test_ssr.html)\n\n```typescript\nviewer.frameloop = 'always';\n\nviewer.renderPipeline.enableSSR({\n quality: 0.25,\n maxDistance: 0.4,\n temporal: { maxFrames: 16 },\n denoise: { radius: 1.5, strength: 0.725 },\n});\n\nviewer.renderPipeline.updateSSR({\n intensity: 1.2,\n denoise: { radius: 1 },\n});\n\nviewer.renderPipeline.disableSSR();\n```\n\n### `SSRConfig`\n\n| 属性 | 类型 | 默认值 | 说明 |\n| :--- | :--- | :----- | :--- |\n| `resolutionScale` | `number` | `1` | SSR 计算分辨率比例,范围 0–1 |\n| `quality` | `number` | `0.25` | Ray marching 采样质量,范围 0–1 |\n| `mirrorBias` | `number` | `0.5` | 将 stochastic GGX 射线收束到镜面主瓣 |\n| `maxDistance` | `number` | `0.4` | 最大反射追踪距离 |\n| `intensity` | `number` | `1` | 反射强度 |\n| `thickness` | `number` | `0.1` | 命中厚度容差 |\n| `maxLuminance` | `number` | `35` | HDR 反射亮度上限 |\n| `stepExponent` | `number` | `3` | Ray marching 步长分布指数;修改会重编译 SSR 材质 |\n| `binaryRefine` | `boolean` | `false` | 是否使用二分子步细化命中;修改会重编译 SSR 材质 |\n| `screenEdgeFade` | `number` | `0.2` | 屏幕边缘淡出宽度 |\n| `screenEdgeFadeBlack` | `boolean` | `true` | 边缘和未命中区域是否淡出到黑色;修改会重编译 SSR 材质 |\n| `temporal` | `SSRTemporalReprojectConfig` | 见下表 | 时域重投影参数 |\n| `denoise` | `SSRDenoiseConfig` | 见下表 | 循环降噪参数 |\n\n### `SSRTemporalReprojectConfig`\n\n| 属性 | 类型 | 默认值 | 说明 |\n| :--- | :--- | :----- | :--- |\n| `maxFrames` | `number` | `16` | 历史累积最大帧数 |\n| `clampIntensity` | `number` | `0.25` | 历史颜色方差裁剪强度 |\n| `flickerSuppression` | `number` | `1` | 时域闪烁抑制强度 |\n| `hitPointReprojection` | `boolean` | `true` | 是否启用镜面命中点视差重投影 |\n\n### `SSRDenoiseConfig`\n\n| 属性 | 类型 | 默认值 | 说明 |\n| :--- | :--- | :----- | :--- |\n| `lumaPhi` | `number` | `0.75` | 亮度边缘停止权重 |\n| `depthPhi` | `number` | `20` | 深度边缘停止权重 |\n| `normalPhi` | `number` | `0.3` | 法线边缘停止权重 |\n| `roughnessPhi` | `number` | `100` | 粗糙度边缘停止权重 |\n| `radius` | `number` | `1.5` | 空间降噪半径 |\n| `alphaPhi` | `number` | `5` | SSR 射线长度边缘停止权重 |\n| `strength` | `number` | `0.725` | 历史融合强度 |\n| `adapt` | `number` | `0.5` | 自适应历史融合系数 |\n| `smoothDisocclusions` | `boolean` | `true` | 是否平滑遮挡解除区域 |\n| `flickerSuppression` | `number` | `1` | 降噪阶段闪烁抑制强度 |\n| `adaptiveTrust` | `number` | `1` | 历史可信度自适应强度 |\n\nSSR 会增加 `ssrDiffuse`、`ssrNormal` 和 `velocity` MRT 通道;其中法线/粗糙度与漫反射/金属度分别打包到两个 8-bit 附件中。SSGI 单独启用时使用 `ssgiDiffuse`,与 SSR 同时启用时则复用 `ssrDiffuse.rgb`,避免为相同的漫反射数据重复分配 MRT 附件。它可以和 SSGI、Bloom、TRAA 组合,但同时启用仍会增加 MRT 带宽和时域 pass 数量。\n\n默认的 `scene.environment` 通常是 PMREM 纹理,不能直接作为 SSRNode 的等距柱状 HDR 输入。本封装只叠加屏幕空间命中结果,不修改全局 `PhysicalLightingModel`。\n\n### Reversed depth 兼容\n\n`Viewer` 默认启用 Three.js `reversedDepthBuffer`。SSGI 会在 clear depth 为 `0` 时跳过背景像素;SSR 封装会为 SSR、Temporal Reproject 和 Recurrent Denoise 三个内部全屏 pass 补充相同的 reversed-depth 背景像素早退。两者都保留原始 reversed depth 供位置重建、ray marching 和历史深度比较使用。应用侧无需关闭 reversed depth,也不需要转换 depth texture。\n\n如果通过 `rendererOptions: { reversedDepthBuffer: false }` 使用标准深度,兼容层不会改写 Three.js 原有节点图。\n\n`stepExponent`、`binaryRefine` 和 `screenEdgeFadeBlack` 属于编译期 SSR 参数,运行时修改会重编译内部 SSR 材质;其余数值参数直接更新 uniform。\n\n### 性能建议\n\n`resolutionScale` 只缩放 SSR ray-marching target;Temporal Reproject 和 Recurrent Denoise 仍跟随 renderer 的完整 drawing-buffer 尺寸。在 Retina 屏幕或重型场景中,可在创建 Viewer 时使用 `pixelRatio: 1`:\n\n```typescript\nconst viewer = new Viewer({\n el: document.getElementById('app')!,\n pixelRatio: 1,\n});\n```\n\n建议从默认 `maxDistance: 0.4` 开始调节,并谨慎启用每帧更新的大尺寸动态阴影。`frameloop = 'always'` 仍是时域重投影和循环降噪正确收敛的必要条件。\n\n## TRAA(时间抗锯齿)\n\n基于运动向量的时间分辨率抗锯齿。\n\n```typescript\n// 开启 TRAA\nviewer.renderPipeline.enableTRAA();\n\n// 关闭 TRAA\nviewer.renderPipeline.disableTRAA();\n```\n\n## 自定义后处理(OutputComposer)\n\n使用 `setOutputComposer` 可完全覆盖内置后处理链,接收 `scenePass` 返回自定义的 TSL 节点。自定义 composer 会保持最终输出语义,不再叠加插件注册的 output effects。\n\n```typescript\nimport { luminance, vec4 } from 'three/tsl';\n\n// 灰度滤镜\nviewer.renderPipeline.setOutputComposer((scenePass) => {\n const color = scenePass.getTextureNode();\n const gray = luminance(color.rgb);\n return vec4(gray, gray, gray, 1.0);\n});\n\n// 恢复默认(清除自定义后处理)\nviewer.renderPipeline.setOutputComposer(null);\n```\n\n## Output Effects\n\n插件可使用 `addOutputEffect` 在默认/内置后处理链之后追加效果,而不占用业务侧的 `setOutputComposer()`。如果业务设置了自定义 composer,output effects 会被跳过,由 composer 自己负责完整输出。\n\n```typescript\nconst effect = (scenePass, inputNode) => inputNode;\nviewer.renderPipeline.addOutputEffect(effect);\nviewer.renderPipeline.removeOutputEffect(effect);\n```\n\n如果 effect 已经调用了 `renderOutput()`,可设置 `effect.includesOutputTransform = true`,管线只会在该 effect 实际生效时关闭默认输出变换。\n\n如需额外 MRT 输出通道,可用 `addMRTChannel` / `removeMRTChannel` 注册,不会污染核心依赖。\n\n## Overlay Passes\n\n用于叠加辅助渲染层(如 ViewerHelper),始终在后处理链之后执行。\n\n```typescript\nviewer.renderPipeline.addOverlayPass(overlayNode);\nviewer.renderPipeline.removeOverlayPass(overlayNode);\n```\n"
|
|
1015
1172
|
},
|
|
1016
1173
|
{
|
|
1017
1174
|
"id": "api-tools",
|
|
@@ -1148,6 +1305,10 @@ export const docsIndex = [
|
|
|
1148
1305
|
"level": 3,
|
|
1149
1306
|
"text": "render(frame?: number)"
|
|
1150
1307
|
},
|
|
1308
|
+
{
|
|
1309
|
+
"level": 3,
|
|
1310
|
+
"text": "invalidate(frame?: number)"
|
|
1311
|
+
},
|
|
1151
1312
|
{
|
|
1152
1313
|
"level": 3,
|
|
1153
1314
|
"text": "setCamera(camera: PerspectiveCamera | OrthographicCamera)"
|
|
@@ -1220,18 +1381,30 @@ export const docsIndex = [
|
|
|
1220
1381
|
"level": 3,
|
|
1221
1382
|
"text": "dispose()"
|
|
1222
1383
|
},
|
|
1384
|
+
{
|
|
1385
|
+
"level": 2,
|
|
1386
|
+
"text": "OffscreenCanvas Worker(实验性)"
|
|
1387
|
+
},
|
|
1388
|
+
{
|
|
1389
|
+
"level": 3,
|
|
1390
|
+
"text": "OffscreenViewerHost"
|
|
1391
|
+
},
|
|
1392
|
+
{
|
|
1393
|
+
"level": 3,
|
|
1394
|
+
"text": "UManager 与 editable batching"
|
|
1395
|
+
},
|
|
1223
1396
|
{
|
|
1224
1397
|
"level": 2,
|
|
1225
1398
|
"text": "事件"
|
|
1226
1399
|
}
|
|
1227
1400
|
],
|
|
1228
|
-
"content": "# Viewer API\n\n`Viewer` 是 `u-space` 的核心类,它将 WebGPU 渲染器、场景、相机、控制器以及各种管理器封装为一个易用的接口。\n\n## 构造函数\n\n```typescript\nnew Viewer(options: ViewerOptions)\n```\n\n### `ViewerOptions`\n\n| 属性 | 类型 | 必填 | 说明 |\n| :---------------- | :------------------------- | :--- | :------------------------------------------------------------------------------------------------------------- |\n| `el` | `HTMLElement` | 是 | WebGPURenderer 的 canvas 将被注入到此 DOM 元素中。 |\n| `rendererOptions` | `WebGPURendererParameters` | 否 | 直接传递给底层 `WebGPURenderer` 的选项。默认使用 WebGPU 的高性能配置。 |\n\n## 属性\n\n`Viewer` 实例暴露了若干核心 Three.js 和 `u-space` 组件。\n\n| 属性 | 类型 | 说明 |\n| :------------------- | :------------------------------------------ | :---------------------------------------------------------------------------------------------------------------- |\n| `el` | `HTMLElement` | 容器元素。 |\n| `renderer` | `WebGPURenderer` | 底层 WebGPU 渲染器实例。 |\n| `scene` | `Scene` | 主 Three.js 场景,背景默认为 `0x000000`。 |\n| `camera` | `PerspectiveCamera` \\| `OrthographicCamera` | 当前激活的相机。 |\n| `controls` | `CameraControls` | 相机控制器(基于 `camera-controls` 库)。 |\n| `renderPipeline` | `RenderPipeline` | 管理后处理通道和最终渲染调用。 |\n| `timer` | `Timer` | Three.js `Timer` 实例,用于每帧精确的 delta time 跟踪。 |\n| `roomEnvironment` | `RoomEnvironment` | 为场景提供默认的类室内 IBL 环境贴图。 |\n| `info` | `Info` | 以叠加层的形式渲染渲染器诊断信息(绘制调用次数、三角面数量等)。 |\n| `viewerHelper` | `ViewerHelper` | 提供实用辅助工具(如坐标轴、网格显示),用于开发/调试。 |\n| `interactionManager` | `InteractionManager` | 管理对象的指针事件和射线检测。 |\n| `objectManager` | `ObjectManager` | 用于按 ID 或名称注册和检索对象的工具。 |\n| `frameloop` | `'always'` \\| `'demand'` | 设置渲染模式。默认为 `'demand'`(仅在需要时渲染)。设置为 `'always'` 可开启持续渲染。 |\n| `cssRenderer` | `CSSRenderer` | CSS 渲染器,支持在 3D 场景中叠加 HTML 元素(CSS2D / CSS2.5D / CSS3D)。 |\n| `frameCount` | `number` | 待渲染帧的内部计数器,调用 `render()` 时递增。 |\n\n## 方法\n\n### `init()`\n\n异步初始化渲染器。如果需要确保渲染器(如环境设置或插件)完全就绪,必须调用此方法。\n\n```typescript\nawait viewer.init();\n```\n\n### `render(frame?: number)`\n\n请求渲染一帧。在 `'demand'` 模式下,每当场景发生视觉变化时必须调用此方法来更新画布。`frame` 指定渲染的帧数(默认 `1`)。返回一个在帧渲染完成后 resolve 的 `Promise<void>`。\n\n```typescript\nviewer.render();\n\n// 等待帧渲染完成\nawait viewer.render();\n```\n\n### `setCamera(camera: PerspectiveCamera | OrthographicCamera)`\n\n为查看器设置自定义相机,并自动更新控制器和交互管理器以使用该相机。\n\n```typescript\nconst customCamera = new PerspectiveCamera(75, width / height, 0.1, 1000);\nviewer.setCamera(customCamera);\n```\n\n### `setCameraByType(type: 'perspective' | 'orthographic')`\n\n便捷方法,在保持查看器上下文的同时切换相机类型。\n\n```typescript\nviewer.setCameraByType('orthographic');\n```\n\n### `createScene()`\n\n创建并返回一个黑色背景的新 `Scene`。由构造函数内部调用,也可用于重置/替换场景。\n\n```typescript\nviewer.scene = viewer.createScene();\n```\n\n### `createPerspectiveCamera()`\n\n使用合理的默认值创建 `PerspectiveCamera`(视角 50°,近裁 `0.1`,远裁 `1e5`,位置在 `(5, 5, 5)`)。\n\n```typescript\nconst camera = viewer.createPerspectiveCamera();\nviewer.setCamera(camera);\n```\n\n### `createOrthographicCamera()`\n\n创建与容器元素等大的 `OrthographicCamera`,近裁 `0.1`,远裁 `1e5`,位置在 `(5, 5, 5)`。\n\n```typescript\nconst camera = viewer.createOrthographicCamera();\nviewer.setCamera(camera);\n```\n\n### `resize(width?, height?)`\n\n手动触发容器尺寸更新。可选传入指定宽高。\n\n```typescript\n// 手动触发 resize(如容器尺寸变化后)\nviewer.resize();\n\n// 指定新尺寸\nviewer.resize(800, 600);\n```\n\n### `screenshot(options?)`\n\n捕获当前渲染画面并返回 Data URL。\n\n```typescript\nconst dataUrl = await viewer.screenshot();\n\n// 指定格式和分辨率\nconst dataUrl = await viewer.screenshot({\n width: 1920,\n height: 1080,\n type: 'image/png',\n quality: 1.0,\n});\n```\n\n#### `ScreenshotOptions`\n\n| 属性 | 类型 | 默认值 | 说明 |\n| :-------- | :---------------------------------------------- | :------------ | :--------------------- |\n| `width` | `number` | 容器宽度 | 截图宽度。 |\n| `height` | `number` | 容器高度 | 截图高度。 |\n| `type` | `'image/png'` \\| `'image/jpeg'` \\| `'image/webp'` | `'image/png'` | 图片格式。 |\n| `quality` | `number` | `1.0` | 图片质量(0-1)。 |\n\n### `setBackground(background)`\n\n设置场景背景颜色、纹理或清除背景。\n\n```typescript\nviewer.setBackground(0x333333); // 颜色\nviewer.setBackground('#87ceeb'); // CSS 颜色字符串\nviewer.setBackground(hdrTexture); // 纹理\nviewer.setBackground(null); // 清除背景\n```\n\n### `setEnvironment(envMap)`\n\n设置场景环境贴图(用于反射/IBL)。\n\n```typescript\nviewer.setEnvironment(hdrTexture);\nviewer.setEnvironment(null); // 清除\n```\n\n### `enableShadow()` / `disableShadow()`\n\n开启或关闭阴影渲染。\n\n```typescript\nviewer.enableShadow();\nviewer.disableShadow();\n```\n\n### `enableFog(options?)` / `enableFogExp2(options?)` / `disableFog()`\n\n开启线性雾、指数雾或关闭雾效。\n\n```typescript\n// 线性雾\nviewer.enableFog({ color: 0xcccccc, near: 10, far: 100 });\n\n// 指数雾\nviewer.enableFogExp2({ color: 0xcccccc, density: 0.01 });\n\n// 关闭雾效\nviewer.disableFog();\n```\n\n#### `FogOptions`\n\n| 属性 | 类型 | 默认值 | 说明 |\n| :------ | :-------------------- | :---------- | :------------- |\n| `color` | `ColorRepresentation` | `0xcccccc` | 雾的颜色。 |\n| `near` | `number` | `10` | 雾的起始距离。 |\n| `far` | `number` | `100` | 雾的结束距离。 |\n\n#### `FogExp2Options`\n\n| 属性 | 类型 | 默认值 | 说明 |\n| :-------- | :-------------------- | :---------- | :------------- |\n| `color` | `ColorRepresentation` | `0xcccccc` | 雾的颜色。 |\n| `density` | `number` | `0.01` | 雾的密度。 |\n\n## 调试工具\n\n### Info\n\n`viewer.info` 是一个轻量的渲染统计叠加层,显示当前帧的 GPU 诊断数据,适合开发调试阶段使用。\n\n```typescript\nviewer.info.enable(); // 在画面左下角显示统计信息\nviewer.info.disable(); // 隐藏统计信息\n```\n\n启用后将在 `viewer.el` 左下角叠加以下数据:\n\n| 指标 | 说明 |\n| :------------- | :---------------------------- |\n| `draw calls` | 当前帧的绘制调用次数 |\n| `frame calls` | 当前帧的帧调用次数 |\n| `triangles` | 当前帧渲染的三角面数量 |\n| `points` | 当前帧渲染的点数量 |\n| `lines` | 当前帧渲染的线段数量 |\n| `timestamp` | GPU 渲染耗时(ms,WebGPU 专属)|\n\n> `timestamp` 指标仅在 WebGPU 后端可用,WebGL 回退模式下显示 `0`。\n\n### ViewerHelper\n\n`viewer.viewerHelper` 是一个方向指示器 gizmo(基于 Three.js `ViewHelper`),显示当前相机朝向的 XYZ 轴,渲染为画面角落的叠加层。\n\n```typescript\nviewer.viewerHelper.enable(); // 显示方向 gizmo\nviewer.viewerHelper.disable(); // 隐藏方向 gizmo\n```\n\n**属性:**\n\n| 属性 | 类型 | 说明 |\n| :------------- | :---------- | :---------------------------------------- |\n| `location` | `object` | gizmo 在画面中的位置,支持 `top`、`bottom`、`left`、`right` 偏移(像素)。默认右下角。 |\n\n```typescript\n// 调整位置到左下角\nviewer.viewerHelper.location.left = 12;\nviewer.viewerHelper.location.bottom = 12;\nviewer.viewerHelper.location.right = null;\n```\n\n### `dispose()`\n\n清理查看器,从 DOM 中移除 canvas,移除事件监听,并释放渲染器和环境贴图,以防止内存泄漏。\n\n```typescript\nviewer.dispose();\n```\n\n## 事件\n\nViewer 继承自 `EventDispatcher`,会触发以下事件:\n\n- `beforeControlsUpdate`:在 `CameraControls` 更新之前触发,提供 `{ time: number; delta: number }`。\n- `afterControlsUpdate`:在 `CameraControls` 更新之后触发,提供 `{ time: number; delta: number }`。\n- `beforeRender`:在 `renderer.render` 调用之前立即触发,提供 `{ time: number; delta: number }`。\n- `afterRender`:在 `renderer.render` 完成之后立即触发,提供 `{ time: number; delta: number }`。\n- `cameraChange`:调用 `setCamera` 时触发,提供 `{ camera: Camera }`。\n\n其中 `delta` 为上一帧到当前帧的时间间隔(秒),可用于动画更新等场景。\n"
|
|
1401
|
+
"content": "# Viewer API\n\n`Viewer` 是 `u-space` 的核心类,它将 WebGPU 渲染器、场景、相机、控制器以及各种管理器封装为一个易用的接口。\n\n## 构造函数\n\n```typescript\nnew Viewer(options: ViewerOptions)\n```\n\n### `ViewerOptions`\n\n| 属性 | 类型 | 必填 | 说明 |\n| :---------------- | :------------------------- | :--- | :------------------------------------------------------------------------------------------------------------- |\n| `el` | `HTMLElement` | 是 | WebGPURenderer 的 canvas 将被注入到此 DOM 元素中。 |\n| `rendererOptions` | `WebGPURendererParameters` | 否 | 直接传递给底层 `WebGPURenderer` 的选项。默认使用 WebGPU 的高性能配置。 |\n| `pixelRatio` | `number` | 否 | Drawing buffer 像素比;默认 `Math.min(window.devicePixelRatio, 1.5)`。在 Retina 屏幕上使用 SSR、SSGI 或 TRAA 等重型全屏效果时,可设为 `1` 以降低 GPU 开销。 |\n\n`pixelRatio` 应为有限正数,并会直接传给 Three.js。数值越高,画面越清晰,但完整 drawing buffer 以及依赖它的全屏后处理成本也会提高。\n\n`Viewer` 默认启用 `reversedDepthBuffer`。`RenderPipeline` 内置 SSGI 和 SSR 均兼容该深度模式;只有业务明确需要标准深度时,才需要传入 `rendererOptions: { reversedDepthBuffer: false }`。\n\n```typescript\nconst viewer = new Viewer({\n el: document.getElementById('app')!,\n pixelRatio: 1,\n});\n```\n\n## 属性\n\n`Viewer` 实例暴露了若干核心 Three.js 和 `u-space` 组件。\n\n| 属性 | 类型 | 说明 |\n| :------------------- | :------------------------------------------ | :---------------------------------------------------------------------------------------------------------------- |\n| `el` | `HTMLElement` | 容器元素。 |\n| `renderer` | `WebGPURenderer` | 底层 WebGPU 渲染器实例。 |\n| `scene` | `Scene` | 主 Three.js 场景,背景默认为 `0x000000`。 |\n| `camera` | `PerspectiveCamera` \\| `OrthographicCamera` | 当前激活的相机。 |\n| `controls` | `CameraControls` | 相机控制器(基于 `camera-controls` 库)。 |\n| `renderPipeline` | `RenderPipeline` | 管理后处理通道和最终渲染调用。 |\n| `timer` | `Timer` | Three.js `Timer` 实例,用于每帧精确的 delta time 跟踪。 |\n| `roomEnvironment` | `RoomEnvironment` | 为场景提供默认的类室内 IBL 环境贴图。 |\n| `info` | `Info` | 以叠加层的形式渲染渲染器诊断信息(绘制调用次数、三角面数量等)。 |\n| `viewerHelper` | `ViewerHelper` | 提供实用辅助工具(如坐标轴、网格显示),用于开发/调试。 |\n| `interactionManager` | `InteractionManager` | 管理对象的指针事件和射线检测。 |\n| `objectManager` | `ObjectManager` | 用于按 ID 或名称注册和检索对象的工具。 |\n| `frameloop` | `'always'` \\| `'demand'` | 设置渲染模式。默认为 `'demand'`(仅在需要时渲染)。设置为 `'always'` 可开启持续渲染。 |\n| `cssRenderer` | `CSSRenderer` | CSS 渲染器,支持在 3D 场景中叠加 HTML 元素(CSS2D / CSS2.5D / CSS3D)。 |\n| `frameCount` | `number` | 待渲染帧的内部计数器;同一刷新周期内的请求会合并,避免多个动画产生帧积压。 |\n\n## 方法\n\n### `init()`\n\n异步初始化渲染器。如果需要确保渲染器(如环境设置或插件)完全就绪,必须调用此方法。\n\n```typescript\nawait viewer.init();\n```\n\n### `render(frame?: number)`\n\n请求渲染一帧。在 `'demand'` 模式下,每当场景发生视觉变化时必须调用此方法来更新画布。`frame` 指定渲染的帧数(默认 `1`)。返回一个在帧渲染完成后 resolve 的 `Promise<void>`。\n\n```typescript\nviewer.render();\n\n// 等待帧渲染完成\nawait viewer.render();\n```\n\n### `invalidate(frame?: number)`\n\n请求渲染但不创建 Promise。动画循环、插件更新等不需要等待渲染完成的高频路径应优先使用此方法,避免每帧分配 Promise 和一次性事件监听器。`frame` 指定至少保留的待渲染帧数(默认 `1`);同一刷新周期内多次调用会合并,不会累加成渲染积压。\n\n```typescript\nviewer.invalidate();\n```\n\n### `setCamera(camera: PerspectiveCamera | OrthographicCamera)`\n\n为查看器设置自定义相机,并自动更新控制器和交互管理器以使用该相机。\n\n```typescript\nconst customCamera = new PerspectiveCamera(75, width / height, 0.1, 1000);\nviewer.setCamera(customCamera);\n```\n\n### `setCameraByType(type: 'perspective' | 'orthographic')`\n\n便捷方法,在保持查看器上下文的同时切换相机类型。\n\n```typescript\nviewer.setCameraByType('orthographic');\n```\n\n### `createScene()`\n\n创建并返回一个黑色背景的新 `Scene`。由构造函数内部调用,也可用于重置/替换场景。\n\n```typescript\nviewer.scene = viewer.createScene();\n```\n\n### `createPerspectiveCamera()`\n\n使用合理的默认值创建 `PerspectiveCamera`(视角 50°,近裁 `0.1`,远裁 `1e5`,位置在 `(5, 5, 5)`)。\n\n```typescript\nconst camera = viewer.createPerspectiveCamera();\nviewer.setCamera(camera);\n```\n\n### `createOrthographicCamera()`\n\n创建与容器元素等大的 `OrthographicCamera`,近裁 `0.1`,远裁 `1e5`,位置在 `(5, 5, 5)`。\n\n```typescript\nconst camera = viewer.createOrthographicCamera();\nviewer.setCamera(camera);\n```\n\n### `resize(width?, height?)`\n\n手动触发容器尺寸更新。可选传入指定宽高。\n\n```typescript\n// 手动触发 resize(如容器尺寸变化后)\nviewer.resize();\n\n// 指定新尺寸\nviewer.resize(800, 600);\n```\n\n### `screenshot(options?)`\n\n捕获当前渲染画面并返回 Data URL。\n\n```typescript\nconst dataUrl = await viewer.screenshot();\n\n// 指定格式和分辨率\nconst dataUrl = await viewer.screenshot({\n width: 1920,\n height: 1080,\n type: 'image/png',\n quality: 1.0,\n});\n```\n\n#### `ScreenshotOptions`\n\n| 属性 | 类型 | 默认值 | 说明 |\n| :-------- | :---------------------------------------------- | :------------ | :--------------------- |\n| `width` | `number` | 容器宽度 | 截图宽度。 |\n| `height` | `number` | 容器高度 | 截图高度。 |\n| `type` | `'image/png'` \\| `'image/jpeg'` \\| `'image/webp'` | `'image/png'` | 图片格式。 |\n| `quality` | `number` | `1.0` | 图片质量(0-1)。 |\n\n### `setBackground(background)`\n\n设置场景背景颜色、纹理或清除背景。\n\n```typescript\nviewer.setBackground(0x333333); // 颜色\nviewer.setBackground('#87ceeb'); // CSS 颜色字符串\nviewer.setBackground(hdrTexture); // 纹理\nviewer.setBackground(null); // 清除背景\n```\n\n### `setEnvironment(envMap)`\n\n设置场景环境贴图(用于反射/IBL)。\n\n```typescript\nviewer.setEnvironment(hdrTexture);\nviewer.setEnvironment(null); // 清除\n```\n\n### `enableShadow()` / `disableShadow()`\n\n开启或关闭阴影渲染。\n\n```typescript\nviewer.enableShadow();\nviewer.disableShadow();\n```\n\n### `enableFog(options?)` / `enableFogExp2(options?)` / `disableFog()`\n\n开启线性雾、指数雾或关闭雾效。\n\n```typescript\n// 线性雾\nviewer.enableFog({ color: 0xcccccc, near: 10, far: 100 });\n\n// 指数雾\nviewer.enableFogExp2({ color: 0xcccccc, density: 0.01 });\n\n// 关闭雾效\nviewer.disableFog();\n```\n\n#### `FogOptions`\n\n| 属性 | 类型 | 默认值 | 说明 |\n| :------ | :-------------------- | :---------- | :------------- |\n| `color` | `ColorRepresentation` | `0xcccccc` | 雾的颜色。 |\n| `near` | `number` | `10` | 雾的起始距离。 |\n| `far` | `number` | `100` | 雾的结束距离。 |\n\n#### `FogExp2Options`\n\n| 属性 | 类型 | 默认值 | 说明 |\n| :-------- | :-------------------- | :---------- | :------------- |\n| `color` | `ColorRepresentation` | `0xcccccc` | 雾的颜色。 |\n| `density` | `number` | `0.01` | 雾的密度。 |\n\n## 调试工具\n\n### Info\n\n`viewer.info` 是一个轻量的渲染统计叠加层,显示当前帧的 GPU 诊断数据,适合开发调试阶段使用。\n\n```typescript\nviewer.info.enable(); // 在画面左下角显示统计信息\nviewer.info.disable(); // 隐藏统计信息\n```\n\n启用后将在 `viewer.el` 左下角叠加以下数据:\n\n| 指标 | 说明 |\n| :------------- | :---------------------------- |\n| `draw calls` | 当前帧的绘制调用次数 |\n| `frame calls` | 当前帧的帧调用次数 |\n| `triangles` | 当前帧渲染的三角面数量 |\n| `points` | 当前帧渲染的点数量 |\n| `lines` | 当前帧渲染的线段数量 |\n| `timestamp` | GPU 渲染耗时(ms,WebGPU 专属)|\n\n> `timestamp` 指标仅在 WebGPU 后端可用,WebGL 回退模式下显示 `0`。\n\n### ViewerHelper\n\n`viewer.viewerHelper` 是一个方向指示器 gizmo(基于 Three.js `ViewHelper`),显示当前相机朝向的 XYZ 轴,渲染为画面角落的叠加层。\n\n```typescript\nviewer.viewerHelper.enable(); // 显示方向 gizmo\nviewer.viewerHelper.disable(); // 隐藏方向 gizmo\n```\n\n**属性:**\n\n| 属性 | 类型 | 说明 |\n| :------------- | :---------- | :---------------------------------------- |\n| `location` | `object` | gizmo 在画面中的位置,支持 `top`、`bottom`、`left`、`right` 偏移(像素)。默认右下角。 |\n\n```typescript\n// 调整位置到左下角\nviewer.viewerHelper.location.left = 12;\nviewer.viewerHelper.location.bottom = 12;\nviewer.viewerHelper.location.right = null;\n```\n\n### `dispose()`\n\n清理查看器,从 DOM 中移除 canvas,移除事件监听,并释放渲染器和环境贴图,以防止内存泄漏。\n\n```typescript\nviewer.dispose();\n```\n\n## OffscreenCanvas Worker(实验性)\n\n`u-space/worker` 提供一个独立入口,可将 `Viewer`、Three.js 场景、模型解析、相机控制和 WebGPU 渲染全部放入 dedicated Worker。主线程只负责持有可见 canvas、转发输入事件与尺寸,并接收状态或渲染统计。\n\nWorker runtime 入口仅提供 ESM,需使用 `{ type: 'module' }` 创建 Worker。构建工具也必须保留 ES module 输出;Vite 项目需配置 `worker: { format: 'es' }`,否则 IIFE Worker 无法编译 top-level await。\n\n主线程:\n\n```typescript\nimport { OffscreenViewerHost } from 'u-space/worker';\n\nconst worker = new Worker(new URL('./scene.worker.ts', import.meta.url), {\n type: 'module',\n});\n\nconst host = new OffscreenViewerHost({\n el: document.getElementById('app')!,\n worker,\n});\n\nhost.addEventListener('ready', (event) => {\n console.log('scene ready', (event as CustomEvent).detail);\n});\n\nhost.addEventListener('stats', (event) => {\n console.log((event as CustomEvent).detail);\n});\n\nawait host.init();\n```\n\n`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` 后调用。\n\n### `OffscreenViewerHost`\n\n构造选项:\n\n| 选项 | 类型 | 必需 | 说明 |\n| :--- | :--- | :--- | :--- |\n| `el` | `HTMLElement` | 是 | 主线程容器;默认创建的 canvas 会挂载到这里。 |\n| `worker` | `Worker` | 是 | 使用 `{ type: 'module' }` 创建的 dedicated Worker。 |\n| `canvas` | `HTMLCanvasElement` | 否 | 自定义可见 canvas;省略时自动创建。 |\n| `pixelRatio` | `number` | 否 | 发送给 Worker renderer 的 drawing-buffer pixel ratio;省略时使用 `Math.min(window.devicePixelRatio, 1.5)`。 |\n| `terminateWorkerOnDispose` | `boolean` | 否 | `dispose()` 完成后是否终止 Worker,默认 `true`。 |\n\n公开方法:\n\n| 方法 | 说明 |\n| :--- | :--- |\n| `OffscreenViewerHost.isSupported()` | 检查当前环境是否具备 Worker、OffscreenCanvas 和 `transferControlToOffscreen()`。Worker 内是否暴露 WebGPU 仍会在初始化时检查。 |\n| `init(): Promise<void>` | 绑定输入/resize,转移 canvas 并等待 Worker 的 `initialized` 消息;重复调用返回同一个 Promise。 |\n| `request<T>(command, payload?)` | 调用 Worker 已注册或内置的 command。必须在 `init()` resolve 后调用;业务 command 通常还应等待 `ready`。 |\n| `dispose(): void` | 停止事件与尺寸转发、reject 未完成的 Promise、移除 canvas,并通知 Worker 逆序释放业务资源和 `Viewer`。 |\n\nHost 事件:\n\n| 事件 | `detail` | 说明 |\n| :--- | :--- | :--- |\n| `initialized` | 无 | 与 `host.init()` resolve 相同的 Viewer 初始化边界。 |\n| `ready` | `unknown` | Worker 调用一次 `worker.ready(detail)` 后的业务就绪边界。 |\n| `status` | `string` | Worker 通过 `worker.status(message)` 上报的加载状态。 |\n| `stats` | `WorkerRendererStats` | renderer 计数和滚动帧间隔统计。 |\n| 自定义事件 | `unknown` | Worker 通过 `worker.emit(type, detail)` 上报;事件名为传入的 `type`。 |\n| `error` | `Error` | Worker 初始化、未捕获异常或资源清理错误。 |\n| `disposed` | 无 | Worker 已完成清理并关闭。 |\n\n`WorkerRendererStats` 包含 `drawCalls`、`frameCalls`、`triangles`、`points`、`lines`、作为滚动平均帧间隔的 `frameTime`,以及 `frameTiming.samples` / `fps` / `average` / `p95` / `max` / `missedFramePercent`。其中 `missedFramePercent` 表示滚动窗口内超过 25ms 的样本占比;demand 模式的静止空闲间隔不会被作为渲染帧计入。\n\nWorker(runtime 使用独立入口和 top-level await,避免主线程打包 Viewer/Three.js):\n\n```typescript\nimport { createWorkerViewer } from 'u-space/worker/runtime';\n\nconst worker = await createWorkerViewer();\nconst { viewer } = worker;\n\nworker.status('loading scene');\nconst scene = await loadScene(viewer);\nviewer.scene.add(scene);\nawait viewer.render();\n\nworker.registerCommand('getObjectCount', () => viewer.scene.children.length);\nworker.onDispose(() => disposeScene(scene));\nworker.ready({ objects: viewer.scene.children.length });\n```\n\n`createWorkerViewer()` 必须在 module Worker 顶层立即调用。函数会先同步注册 host 消息监听,再返回一个等待 OffscreenCanvas 传入和 `viewer.init()` 完成的 Promise,因此可以安全使用 top-level await,不会丢失 `init` 消息。\n\n返回的 `WorkerViewerRuntime` 提供:\n\n| 属性 / 方法 | 说明 |\n| :---------- | :--- |\n| `viewer` / `canvas` | Worker 内的完整 `Viewer` 和已转移的 `OffscreenCanvas`。 |\n| `status(message)` | 向主线程发送加载状态。 |\n| `emit(type, detail?)` | 向主线程派发自定义事件;detail 必须可 structured clone。 |\n| `registerCommand(name, handler)` | 注册 `host.request()` 可调用的业务命令,返回注销函数;重复命令名会抛错。 |\n| `onDispose(handler)` | 注册 Worker 销毁回调,返回注销函数;多个回调按注册的逆序执行。 |\n| `ready(detail?)` | 场景准备完成后发送一次 `ready` 事件;重复调用会抛错。 |\n\n主线程应等待 `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 内。\n\n`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` 通知。\n\n### UManager 与 editable batching\n\n完整示例 `examples/offscreen/test_umanager2_offscreen.html` 会在 Worker 中加载 HDR、建筑语义模型和场景模型,并将 `SceneLoader.setEditableBatching()` 与 OffscreenCanvas 组合:\n\n```typescript\nimport { SceneLoader } from 'u-space/plugins/u-manager';\nimport { createWorkerViewer } from 'u-space/worker/runtime';\n\nconst worker = await createWorkerViewer();\nconst { viewer } = worker;\n// 在初始静态层级和相机定位完成后冻结每帧场景矩阵遍历。\nviewer.scene.updateMatrixWorld(true);\nviewer.scene.matrixWorldAutoUpdate = false;\nconst sceneLoader = new SceneLoader(viewer);\nworker.onDispose(() => sceneLoader.dispose());\nsceneLoader.setPath(SCENE_PATH);\nsceneLoader.setKey(SCENE_KEY);\nsceneLoader.setEditableBatching({\n maxVerticesPerBatch: 1_500_000,\n maxIndicesPerBatch: 4_500_000,\n freezeAnimations: true,\n});\n\nconst scene = await sceneLoader.loadAsync();\nconst controlsEnabled = viewer.controls.enabled;\nviewer.controls.enabled = false;\ntry {\n viewer.scene.add(scene);\n scene.updateWorldMatrix(true, true);\n viewer.invalidate();\n await viewer.renderer.compileAsync(viewer.scene, viewer.camera);\n} finally {\n viewer.controls.enabled = controlsEnabled;\n}\nawait viewer.render();\n\nworker.ready({\n editableBatch: scene.userData.editableBatch,\n semanticOverlayHidden: false,\n});\n```\n\nGeometry 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 模式长时间静止产生的空闲间隔不会纳入统计。\n\n目标场景的压平语义楼层是半透明分析叠加层,并会有意覆盖建筑表面。示例保持 `SemanticGroup.visible = true`,并调用 `showAllFloors().showAllFacilities()`,让楼层、墙体、空间、门窗和设备等全部语义对象默认参与渲染。设备选择、飞向、高亮和清除命令均不改变语义对象显隐,“清除调试”只撤销高亮。业务项目可以按自身展示策略切换语义层,并继续通过跨楼层语义合批或 LOD 降低提交成本。示例保留标准 RenderPipeline scene pass,以维持源模型透明对象的正确离屏合成,不修改 drawing-buffer pixel ratio。\n\n目标场景主要由静态普通 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` 同样会执行该检测。相机矩阵不受此模式影响。\n\nOffscreenCanvas 不会自动减少 draw calls;`setEditableBatching()` 也不会把主线程 UI 搬入 Worker。组合使用时,两者分别处理主线程 CPU 压力和 draw submission 压力。需要按 ID 显隐、变色、移动或 `materialize()` 时,应使用 `worker.registerCommand()` 注册业务 command,再由主线程调用 `host.request()`,不要尝试跨线程传输 `SceneInstanceObject`。\n\n通过以下命令启动:\n\n```bash\npnpm example:offscreen\n```\n\n当前边界:\n\n- 需要同时支持 `transferControlToOffscreen()` 和 Worker WebGPU(`WorkerNavigator.gpu`);目前应以最新版 Chromium 为主要目标,并保留普通 `Viewer` 回退路径。\n- CSS2D/CSS3D、`Info` 等真实 DOM 叠加层不能放进 Worker,应在主线程用 Worker 状态/统计消息重建 UI。\n- Worker 能移走场景解析、矩阵更新和渲染提交等主线程 CPU 工作,但不会减少 draw calls、三角形数量或 GPU 时间;GPU 瓶颈仍需用 instancing、LOD、遮挡/视锥裁剪等方式处理。\n- `setEditableBatching()` 与 Worker 兼容,但它会展开重复 Geometry,仍需根据 `vertices` / `indices` 统计评估 GPU 内存;透明、蒙皮、morph 和自定义 shader 子集继续走安全 fallback。\n- WebGPU OffscreenCanvas 截图尚未作为 Worker 内置命令开放;需要截图时应实现独立的 render-target readback 流程。\n\n## 事件\n\nViewer 继承自 `EventDispatcher`,会触发以下事件:\n\n- `beforeControlsUpdate`:在 `CameraControls` 更新之前触发,提供 `{ time: number; delta: number }`。\n- `afterControlsUpdate`:在 `CameraControls` 更新之后触发,提供 `{ time: number; delta: number }`。\n- `beforeRender`:在 `renderer.render` 调用之前立即触发,提供 `{ time: number; delta: number }`。\n- `afterRender`:在 `renderer.render` 完成之后立即触发,提供 `{ time: number; delta: number }`。\n- `cameraChange`:调用 `setCamera` 时触发,提供 `{ camera: Camera }`。\n\n其中 `delta` 为上一帧到当前帧的时间间隔(秒),可用于动画更新等场景。\n"
|
|
1229
1402
|
},
|
|
1230
1403
|
{
|
|
1231
1404
|
"id": "changelog",
|
|
1232
1405
|
"path": "changelog.md",
|
|
1233
1406
|
"title": "更新日志",
|
|
1234
|
-
"summary": "- **
|
|
1407
|
+
"summary": "- **AO 示例** — 新增 `examples/test_ao.html`,通过 SSGI 的 `giIntensity: 0` 默认开启纯 AO,并提供遮蔽强度、半径和厚度实时调节。 - **RenderPipeline SSR** — 新增 `enableSSR()`、`updateSSR()` 和 `disableSSR()`,封装 Stochastic SSR、Temporal Reproject、Recurrent Denoise、velocity 以及双向历史反馈;提供完整 SSR 与降噪参数配置。 - **Viewer** — 新增可选 `pixelRatio` 构造参数,省略时继续使用 `Math.min(window.devicePixelRatio, 1.5)`。",
|
|
1235
1408
|
"headings": [
|
|
1236
1409
|
{
|
|
1237
1410
|
"level": 1,
|
|
@@ -1241,6 +1414,34 @@ export const docsIndex = [
|
|
|
1241
1414
|
"level": 2,
|
|
1242
1415
|
"text": "未发布"
|
|
1243
1416
|
},
|
|
1417
|
+
{
|
|
1418
|
+
"level": 3,
|
|
1419
|
+
"text": "新增"
|
|
1420
|
+
},
|
|
1421
|
+
{
|
|
1422
|
+
"level": 3,
|
|
1423
|
+
"text": "变更"
|
|
1424
|
+
},
|
|
1425
|
+
{
|
|
1426
|
+
"level": 3,
|
|
1427
|
+
"text": "修复与优化"
|
|
1428
|
+
},
|
|
1429
|
+
{
|
|
1430
|
+
"level": 3,
|
|
1431
|
+
"text": "文档"
|
|
1432
|
+
},
|
|
1433
|
+
{
|
|
1434
|
+
"level": 2,
|
|
1435
|
+
"text": "0.0.29"
|
|
1436
|
+
},
|
|
1437
|
+
{
|
|
1438
|
+
"level": 3,
|
|
1439
|
+
"text": "变更"
|
|
1440
|
+
},
|
|
1441
|
+
{
|
|
1442
|
+
"level": 3,
|
|
1443
|
+
"text": "示例与文档"
|
|
1444
|
+
},
|
|
1244
1445
|
{
|
|
1245
1446
|
"level": 2,
|
|
1246
1447
|
"text": "0.0.28"
|
|
@@ -1430,13 +1631,13 @@ export const docsIndex = [
|
|
|
1430
1631
|
"text": "新增"
|
|
1431
1632
|
}
|
|
1432
1633
|
],
|
|
1433
|
-
"content": "# 更新日志\n\n## 未发布\n\n## 0.0.28\n\n### 新增\n\n- **u-manager** — 新增 `UManagerLoader` 一体化入口,可同时加载 `SemanticLoader` 和 `SceneLoader`,返回带 `semanticGroup` / `sceneGroup` 直接属性的 `UManagerSceneGroup`,并复用 semantic id 去重避免重复建筑/设备。\n- **examples/docs/mcp** — 新增 `examples/test_umanager_loader.html`,演示 `UManagerLoader` 一次加载语义楼层、Facilities 和非语义场景树,并补充 Examples Guide 与 MCP 检索入口。\n\n### 变更\n\n- **u-manager breaking** — `SceneInstanceObject`、`FacilityInstanceObject` 和楼层多边形语义统一到 `SemanticInstanceObject` API:使用 `setSemanticVisible()`、`setSemanticColor()`、`setSemanticOpacity()`、`getSemanticBoundingBox()` 等方法,不再使用 scene/facility 专属实例方法。\n- **u-manager** — 新增共享 `SemanticModelInstancedLayer`,`SceneInstancedLayer` 和 `FacilityInstancedLayer` 共用同一套模型模板 instancing、material groups、多材质、dirty sync、raycast remap 和 fallback wrapper 逻辑。\n- **ObjectManager / MaterialEffects** — 对语义实例的显隐、透明度和高亮控制统一识别 `SemanticInstanceObject`,不再维护 SceneInstance/FacilityInstance 两套特殊分支。\n- **u-manager loader** — `SceneLoader` 不再解析场景树里的 `AREA`、`FLOOR`、`ROOM` 语义面;楼层、区域和房间统一由 `SemanticLoader` / `SemanticGroup` 处理,避免同一语义对象在组合加载时重复出现。\n\n### 示例与文档\n\n- **在线示例** — `examples/importmap.js` 的 CDN fallback 版本更新为 `0.0.28`,Vercel 部署会继续按 `package.json` 注入当前版本。\n\n## 0.0.27\n\n### 变更\n\n- **u-manager semantics** — `FacilityInstancedLayer` 的相机逐实例裁剪改为显式开启:默认不再按当前相机视锥或 `minScreenRadius` 压缩 active instances,避免相机距离影响设备显隐;需要性能压缩时可通过 `setInstanceCulling({ enabled: true })` 开启。\n- **u-manager semantics** — `FacilityInstancedLayer` 使用动态 `Group` 包裹 Facilities `InstancedMesh` batch,避免 Three.js WebGPU render bundle 缓存跳过子 mesh 的渲染前同步;`hideAllFacilities()` 后再 `showAllFacilities()` 不会复用过期 active instance 状态。\n- **u-manager semantics** — `FacilityInstancedLayer` 支持合法 `material[]` + `geometry.groups` 的设备模板进入 instancing;只有 groups 缺失或材质索引非法的多材质 Mesh 才回退为普通 `Model`。\n- **u-manager semantics** — Facilities instancing 会在相机矩阵、投影矩阵或 viewport 高度变化时重新压缩当前帧实例;设备显隐、颜色、透明度和 `planish('Facilities')` / `unplanish('Facilities')` 改变 transform 时刷新 instance data。\n- **u-manager semantics** — 全部设备隐藏或透明度为 0 时,`FacilityInstancedLayer` 会关闭空的内部 `InstancedMesh`;通过 `showAllFacilities()` 或单设备 API 恢复时会重新加入渲染同步。\n- **fire 插件** — 新增 `FireEffect`,封装 Three.js 官方 `webgpu_volume_fire` 体积火焰示例,支持 WebGPU 3D 流体模拟、烟雾散射、动态光照、体积阴影、output effect 叠加和 `cameraChange` 相机切换同步。\n- **u-manager loader** — `SceneLoader` 默认读取同路径 `db/semantic_model.json` 并按语义 `ID` 跳过重复场景树节点,支持与 `SemanticLoader` 同时加载。\n- **u-manager loader** — `SceneLoader` 默认按模型 `path` 合并重复静态 3D 节点,通过 `SceneInstancedLayer` 批量渲染,同时保留 `SceneInstanceObject` 逻辑对象用于单模型 ID 检索、显隐、颜色、透明度、包围盒和事件派发。\n- **docs/mcp** — 补充 `SceneInstanceObject` 和 `FacilityInstanceObject` 通过 `objectManager.getById()` + `getSemanticBoundingBox()` + `controls.flyToBox()` 飞向单个实例的示例与 MCP 检索说明。\n- **ObjectManager / MaterialEffects** — `setOpacity()`、`show()` / `hide()`、`isolate()`、`showAll()` 以及 `highlightColor()` / `removeHighlightColor()` 支持 `SceneInstanceObject` 的 per-instance 控制。\n- **u-manager loader** — `SceneLoader` 创建的根组、`GROUP` 和 `STUB` 节点改为 `BaseGroup`,隐藏后可通过对象自身 `raycast()` 返回值阻止子级继续参与递归射线检测。\n- **Viewer** — 默认使用 Three.js `reversedDepthBuffer` 替代 `logarithmicDepthBuffer`,减少 WebGPU 顶视角多层透明场景的 fragment 压力,同时保留远距离深度精度。\n\n### 示例与文档\n\n- **在线示例** — `examples/importmap.js` 的 CDN fallback 版本更新为 `0.0.27`,Vercel 部署会继续按 `package.json` 注入当前版本。\n\n## 0.0.26\n\n### 修复\n\n- **u-manager semantics** — `FloorMesh.planish()` 会在压平后的语义实例和 Facilities 上加入稳定的轻微 Y 偏移,减少门、墙等重叠薄片之间的共面闪烁。\n- **u-manager semantics** — `FloorMesh` 会按实例透明度把合并语义网格拆分到不透明和透明材质通道,避免墙、柱、门等完全不透明实例也走透明渲染队列,降低近距离压平场景的 overdraw 成本。\n- **u-manager semantics** — `BuildingGroup` 新增 `getFloorById()`、`getElevatorById()` 和 `getVentById()`,内部通过 `Map` 维护楼层、电梯井和通风井语义 ID 索引。\n- **u-manager semantics** — `FacilityInstancedLayer` 只在实例矩阵、颜色或透明度变化时上传对应 instance buffer,静态 Facilities 不再每帧重复提交全部实例数据;渲染前会按每个 Facility 实例的世界包围盒和当前相机视锥压缩本帧 active instance count,不再通过相机距离阈值裁剪设备。\n- **u-manager semantics** — 修复 Facilities 视锥裁剪在视角切换后可能沿用上一帧 active subset 的 `visible` / `boundingSphere` 状态,导致某些视角下设备整批隐藏的问题;instanced batch 会保留完整包围球,同时用 `count` 控制本帧 active 实例。\n- **u-manager semantics** — Instanced Facilities 的 batch 材质默认保持不透明,只有源材质本身透明或当前渲染实例存在半透明 opacity 时才切到透明队列,减少完全不透明设备在近距离视角下的 overdraw。\n- **MaterialEffects** — 高亮、呼吸色、线框和淡入淡出效果支持材质数组,确保多材质 Mesh 和 `FloorMesh` 双材质通道都能正确应用并恢复材质状态。\n- **docs/mcp** — 补充 Facilities auto instancing 的动态材质透明度、包围盒视锥裁剪、`controls.flyToBox()` 飞向设备,以及多材质 fallback 对颜色/透明度 override 的支持说明。\n\n### 示例与文档\n\n- **在线示例** — `examples/importmap.js` 的 CDN fallback 版本更新为 `0.0.26`,Vercel 部署会继续按 `package.json` 注入当前版本。\n\n## 0.0.25\n\n### 新增\n\n- **u-manager semantics** — 新增 `SemanticGroup`,作为单次 `SemanticParser` 解析结果的顶层容器;`SemanticLoader.loadAsync()` / `SemanticParser.parseAsync()` 直接返回完整语义场景。\n- **u-manager semantics** — `SemanticGroup` 新增 `getBuildingById(id)`,支持按语义数据里的 `Building.ID` 获取建筑。\n- **u-manager semantics** — `SemanticGroup` 和 `BuildingGroup` 的建筑/设备 ID 查询改为内部 `Map` 索引,并通过楼层设备索引版本自动同步,避免每次查询扫描数组。\n- **u-manager semantics** — `FloorMesh.planish()` / `unplanish()`、`BuildingGroup.planishFloors()` / `unplanishFloors()` 和 `SemanticGroup.planishFloors()` / `unplanishFloors()` 会把所有合并语义实例和 Facilities 都按包围盒贴到楼层平面,而不是围绕实例自身基点压缩。\n- **u-manager semantics** — 新增 scene-level Facilities auto instancing:同模型 URL 的静态设备会合并到 `SemanticGroup.facilityLayer` 的 `InstancedMesh` batch,同时保留楼层下的设备引用用于检索、显隐、颜色和透明度控制;带动画、骨骼、morph target 或多材质 Mesh 的设备自动回退为普通 `Model`。\n- **u-manager semantics** — `SemanticGroup`、`BuildingGroup` 和 `FloorMesh` 新增 `getFacilityById()` 设备检索,返回的 `FacilityInstanceObject` 可通过统一 `setSemanticVisible()`、`setSemanticColor()`、`resetSemanticColor()` 和 `setSemanticOpacity()` 控制普通设备模型或 scene-level instanced 设备引用。\n- **docs/mcp** — 补充 Facilities auto instancing 的材质透明度行为、`resetSemanticColor()` 语义,以及多材质 fallback 对颜色/透明度 override 的限制说明。\n\n### 修复\n\n- **u-manager semantics** — 修复 scene-level instanced Facilities 的点击和指针事件派发:射线命中会映射回楼层下的 `FacilityInstanceObject`,并继续冒泡到所属 `FloorMesh`、`BuildingGroup` 和 `SemanticGroup`。\n- **u-manager semantics** — `FacilityInstancedLayer` 继承 `BaseGroup`,避免 `facilityLayer.visible = false` 后内部 `InstancedMesh` 仍参与射线检测并触发隐藏设备点击事件。\n- **u-manager semantics** — 移除语义设备解析里的 `sid` 检索和注册逻辑;Facilities 只按 `Facility.ID` 以及显式传入的别名进入楼层索引和 `viewer.objectManager`。\n\n### 示例与文档\n\n- **在线示例** — `examples/importmap.js` 的 CDN fallback 版本更新为 `0.0.25`,Vercel 部署会继续按 `package.json` 注入当前版本。\n\n## 0.0.23\n\n### 新增\n\n- **u-manager semantics** — `BuildingGroup` 新增 `isolateFloors(floors)`,支持按多个 `FloorMesh` 或楼层下标同时隔离显示多个楼层;`isolateFloor()` 继续兼容单楼层调用,并可接收数组作为便捷写法。\n- **u-manager semantics** — `SemanticLoader` 新增 `Facilities` 解析:从 `Stories[].Facilities` 索引读取顶层设备语义,用 `Facility.ID` 匹配解密后的 `tree_models` 设备节点,沿父级累乘本地矩阵得到设备世界矩阵,加载 3D 设备模型并转换为楼层局部矩阵后挂载到对应 `FloorMesh`。\n- **u-manager semantics** — `FloorMesh` 新增楼层设备检索与统一语义实体 API:`getFacilityById()`、`getFacilityAt()`、`getFacilities()`、`removeFacility()`、`showAllFacilities()`、`hideAllFacilities()`、`getSemanticById()`、`getSemantics()` 和 `getSemanticsByKind('Facilities')`;`BuildingGroup.showAllFacilities()/hideAllFacilities()` 支持批量显示或隐藏整栋建筑内的楼层设备。\n- **u-space MCP** — 文档索引同步楼层 `Facilities` 解析和检索 API,MCP 客户端可检索设备按楼层挂载、`tree_models` 解密匹配、本地矩阵父级累乘和世界坐标转换规则。\n\n### 示例与文档\n\n- **在线示例** — `examples/importmap.js` 的 CDN fallback 版本更新为 `0.0.23`,部署流程仍会优先按 `package.json` 注入当前版本。\n\n## 0.0.21\n\n### 新增\n\n- **u-manager semantics** — `SemanticLoader` 支持解析建筑级 `Elevators` 和 `Vents`,以 `Polygon.shape` 作为底面轮廓、`Elevation` 作为底面高度、`TotalHeight` 作为拉伸高度生成 `ExtrudeMesh`,并通过 `BuildingGroup.elevatorMeshes`、`BuildingGroup.ventMeshes` 及 add/get/remove 方法管理。\n\n### 修复\n\n- **透明语义体块渲染** — 电梯井/通风井在渲染阶段沿法线轻微内收,`Spaces` 实例使用稳定 per-instance 渲染偏移抖动,缓解与墙体或完全重叠空间共面时的 z-fighting。\n- **atmosphere 插件** — 后处理链移除 aerial perspective 节点,保留 lens flare、tone mapping、temporal antialias 和 dithering;`scene.backgroundNode` 使用 `skyBackground()`,`scene.environmentNode` 使用 `skyEnvironment()`,并继续在关闭插件时恢复启用前的背景和环境节点。\n\n### 示例与文档\n\n- **在线示例** — `examples/importmap.js` 的 CDN fallback 版本更新为 `0.0.21`,部署流程仍会优先按 `package.json` 注入当前版本。\n\n## 0.0.18\n\n### 变更\n\n- **版本发布** — 同步发布包含新版大气插件实现、RenderPipeline output effects 和一键 MCP 发布脚本的 npm 包。\n\n## 0.0.17\n\n### 新增\n\n- **atmosphere 插件** — `Atmosphere.enable(options)` 从占位实现升级为可用的大气渲染:支持 WGS84 经纬高定位、太阳/月亮方向、天空背景、天空环境、aerial perspective、lens flare、tone mapping、temporal antialias、dithering 和 `AtmosphereLight` 阴影。\n- **RenderPipeline** — 新增 `addOutputEffect()` / `removeOutputEffect()` 与 `addMRTChannel()` / `removeMRTChannel()`,插件可追加输出后处理和独立 MRT 通道,不再占用业务侧 `setOutputComposer()`。\n\n### 变更\n\n- **大气状态更新** — 大气插件在 `beforeRender` 中刷新相机 ECEF 位置、太阳/月亮方向和阴影 target,相机控制器驱动的移动也能逐帧更新大气状态。\n- **MCP 发布** — 根目录 MCP 发布入口合并为 `pnpm publish:mcp`,命令会先递增 `u-space-mcp` 的 patch 版本,再构建文档索引并发布 npm 包。\n\n## 0.0.16\n\n### 新增\n\n- **u-manager semantics** — `FloorMesh.planish(kind?)`、`FloorMesh.unplanish(kind?)`、`BuildingGroup.planishFloors(kind?)` 和 `BuildingGroup.unplanishFloors(kind?)` 支持按 `Walls`、`Columns`、`Spaces`、`Doors`、`Windows` 语义类型压平或恢复;`setScaleYAt(index, y)` 可只调整单个语义实例的 Y 轴缩放。\n- **BaseMesh** — 默认开启 `castShadow` 与 `receiveShadow`,基础网格和继承自 `BaseMesh` 的对象会直接参与阴影投射与接收。\n\n### 变更\n\n- **语义类型命名** — `SemanticKind` 统一为数据源字段风格:`Walls`、`Columns`、`Spaces`、`Doors`、`Windows`。\n- **示例与依赖** — 更新 `load_tiles.html` 示例依赖,新增 `@takram/three-atmosphere`、`postprocessing` peer dependency,并将 Vite 外部依赖边界扩展到所有 `@takram/*` 包。\n\n## 0.0.12\n\n### 新增\n\n- **u-manager semantics** — 新增 `SemanticLoader`、`BuildingGroup`、`FloorMesh` 语义楼层 API,支持楼层隔离、实例级颜色/透明度/显隐/matrix/scale 控制,以及实例包围盒/包围球查询。\n- **u-space MCP** — 内置文档索引同步语义楼层 API,MCP 客户端可检索 `SemanticLoader`、`BuildingGroup` 和 `FloorMesh` 用法。\n\n## 0.0.8\n\n### 新增\n\n- **Viewer** — `resize()`、`screenshot()`、`setBackground()`、`setEnvironment()`、`enableShadow()`/`disableShadow()`、`enableFog()`/`enableFogExp2()`/`disableFog()`\n- **CameraControls** — `flyTo()`、`getCameraViewpoint()`、`lock()`/`unlock()`、`setViewMode('2d' | '3d')`\n- **ObjectManager** — `show()`/`hide()`、`isolate()`、`showAll()`、`setOpacity()`、`getBoundingBox()`、`forEach()`、`filter()`\n- **InteractionManager** — `enable()`/`disable()`、`setInteractable()`、`addIgnore()`/`removeIgnore()`\n- **Selection** — 新增框选(rubber-band)和单选/多选/切换模块,支持 `select`/`deselect`/`change` 事件\n- **MaterialEffects** — `wireframe()`/`removeWireframe()`、`transparent()`/`removeTransparent()`、`xray()`/`removeXray()`、`fadeIn()`/`fadeOut()`\n- **Model** — `getBoundingBox()`、`getCenter()`、`setMaterial()`\n- **SceneManager** — 新模块,多场景创建、注册、切换与序列化\n- **LightManager** — 新模块,灯光便捷创建、4 种预设(indoor/outdoor/studio/warehouse)、Helper 可视化\n- **MeasureTool** — 新工具,距离/面积/角度测量,结果可视化渲染到场景\n- **ClippingTool** — 新工具,基于 WebGPU `ClippingGroup` 的剖切面与剖切盒\n- **AnnotationManager** — 新工具,3D 引线标注 + HTML 浮动标签\n- **示例** — 新增 8 个交互式示例页面\n\n### 修复\n\n- **Selection** — 框选覆盖层改为挂载到容器元素(而非 canvas),修复框选矩形不可见的问题\n- **ClippingTool** — 从 `renderer.clippingPlanes`(WebGL API)重写为 `ClippingGroup`(WebGPU API),修复剖切无效的问题\n- **MaterialEffects** — fade 动画在对象移除场景后自动停止,避免内存泄漏\n- **InteractionManager** — `dispose()` 中清除 `clickTimer`,避免定时器泄漏\n- **LightManager** — 修复 `removeAll()`/`hideAllHelpers()` 中遍历 Map 同时删除的问题\n- **ObjectManager** — `setOpacity()` 先检查 `isMesh` 再类型转换\n- **ClippingTool** — `PlaneHelper` 颜色使用 `new Color(color).getHex()` 替代直接 `as number`\n- **MeasureTool** — 移除 WebGPU 不支持的 `linewidth` 属性\n\n---\n\n## 0.0.7\n\n### 变更\n\n- 文档拆分:将插件文档拆分为独立文件\n- 新增 CameraControls 独立文档页\n- 修复 getting-started 中的死链\n- 修复 sbmx loader 示例使用新的 MaterialEffects API\n\n---\n\n## 0.0.6\n\n### 新增\n\n- **MaterialEffects** — 可组合的材质特效系统,支持安全共享材质和 remove API\n- **RenderPipeline** — 内置 Bloom、SSGI、TRAA 后处理及 OutputComposer API\n- **CSSRenderer** — CSS2D / CSS2.5D / CSS3D 叠加渲染 HTML 元素\n- 从 `package.json` 导出 `version`,统一示例 importmap\n\n---\n\n## 0.0.5\n\n### 变更\n\n- 版本发布(无重大功能变更)\n\n---\n\n## 0.0.4\n\n### 新增\n\n- **Model** — 动画播放支持(`playAnimation`、`stopAnimation`、`updateAnimation`)\n- **ViewerEventMap** — 事件回调新增 `delta` 时间参数\n\n---\n\n## 0.0.3\n\n### 新增\n\n- **ObjectControls** 插件 — 交互式移动、旋转、缩放 3D 对象\n- **TopologyDrawer** 插件 — 在场景任意表面交互式绘制拓扑路径图\n- **Topology** — `exportData()` / `importData()` 数据序列化\n\n### 变更\n\n- **Interactions** — `contextmenu` 事件替换为 `rightclick`\n- **Poi** — 优化渲染性能,新增 `scaleFactor` 参数\n- 文档全面完善:本地搜索、侧边栏优化、各插件文档页\n\n---\n\n## 0.0.0\n\n### 新增\n\n- 项目初始化:Viewer 核心、WebGPU 渲染器、场景/相机/控制器\n- 模型加载系统:双层缓存(内存 + 持久化)、Draco/KTX2/Meshopt 支持\n- 交互系统:射线检测、事件冒泡、自定义 3D 对象事件\n- 对象系统:Model、Poi、Topology、基础网格(Sphere/Plane/Circle/Tube/Extrude/Shape)\n- ObjectManager 集中式对象注册\n- 补间动画系统\n- u-manager 插件:场景加载、动画、属性、视点管理\n- tiles 插件:ArcGIS 3D 瓦片 / GIS 地球渲染\n- minimap 插件:2D 小地图\n- keyboard-controls 插件:WASD 键盘控制\n- tracking-controls 插件:相机跟随\n- atmosphere 插件:天空/大气渲染\n- curve-movement 插件:曲线路径动画\n"
|
|
1634
|
+
"content": "# 更新日志\n\n## 未发布\n\n### 新增\n\n- **AO 示例** — 新增 `examples/test_ao.html`,通过 SSGI 的 `giIntensity: 0` 默认开启纯 AO,并提供遮蔽强度、半径和厚度实时调节。\n- **RenderPipeline SSR** — 新增 `enableSSR()`、`updateSSR()` 和 `disableSSR()`,封装 Stochastic SSR、Temporal Reproject、Recurrent Denoise、velocity 以及双向历史反馈;提供完整 SSR 与降噪参数配置。\n- **Viewer** — 新增可选 `pixelRatio` 构造参数,省略时继续使用 `Math.min(window.devicePixelRatio, 1.5)`。\n- **Offscreen UManager 示例** — `examples/offscreen/test_umanager2_offscreen.html` 现在在 Worker 内启用 `SceneLoader.setEditableBatching()`、预热 WebGPU batch pipeline,并将渲染与 editable batch 统计传回主线程 HUD。\n- **u-manager editable batching** — `SceneLoader.setEditableBatching()` 可把大型、主要静态的普通 Mesh 场景按材质和 Geometry 布局烘焙合并到 `SceneEditableBatchLayer`,同时保留 `SceneInstanceObject` 的显隐、颜色、高亮、bounds 和 raycast 映射;transform 或半透明修改会只 materialize 当前对象,透明/蒙皮/morph/自定义材质等不支持子集保持安全 fallback。\n- **核心 Batches API** — 新增公共 `src/batches` 模块和 `EditableGeometryBatchLayer<T extends InstanceObject>`,提供通用 source/build/unsupported 结果、TSL dirty 状态、typed materialize 事件、lazy BVH hit remap、统计与资源释放;`ModelInstancedLayer` 同步归入该模块,顶层 `u-space` 导入方式保持不变。\n\n### 变更\n\n- **Offscreen Worker breaking** — 移除 callback 风格的 `defineWorkerViewer()`、`WorkerViewerSetup`、`WorkerViewerSetupResult` 和 `WorkerViewerContext`;改为在 module Worker 顶层 `await createWorkerViewer()`,并通过返回的 `WorkerViewerRuntime` 顺序调用 `status()`、`registerCommand()`、`onDispose()` 和 `ready()`。\n- **InstanceObject materialize** — 将 `SceneInstanceObject` 私有的 materializer delegate 提升为核心 `InstanceObject.setInstanceMaterializer()` / `clearInstanceMaterializer()` / `materialize()` 协议;`SceneEditableBatchLayer` 改为核心 `EditableGeometryBatchLayer` 的 scene-specific 薄适配层,fallback 移除改用 typed `materialize` 事件。\n\n### 修复与优化\n\n- **Offscreen UManager 帧率与楼壳显示** — 示例保留语义数据用于相机定位,但默认隐藏会半透明覆盖楼壳的高 draw-call 分析层;同时通过 Three.js 原生 `Scene.matrixWorldAutoUpdate` / `Object3D.updateWorldMatrix()` 关闭静态场景每帧整树矩阵遍历,并保留标准 RenderPipeline scene pass 以确保源模型透明对象正确合成。在不调整 pixel ratio 的情况下,目标场景持续拖拽可维持约 59–60 FPS。\n- **Editable Geometry Batch 不透明材质** — 不透明 batch 不再复用动态 opacity 节点同时驱动 alpha 输出和 `maskNode`;源材质 opacity 路径保持不变,对象显隐改用独立布尔遮罩,修复建筑楼壳被错误显示为半透明的问题。\n- **InstanceObject 静态矩阵更新** — 覆盖 Three.js 原生 `updateWorldMatrix()`,与 `updateMatrixWorld()` 共享世界矩阵/祖先显隐 dirty 检测;根 Scene 冻结自动矩阵更新后,显式更新单个实例或父 Group 仍会同步 instanced buffer,并让 editable batch 的变换差异正确触发 materialize。\n- **Editable Geometry Batch 拾取** — 公共 `EditableGeometryBatchLayer` 继承 `BaseGroup`,内部未导出的 `EditableGeometryBatchMesh` 继承 `BaseMesh` 并在自定义 BVH raycast 前保留不可见短路;隐藏整个 editable layer 或单个 merged batch 后不会继续命中逻辑对象。\n- **Editable Geometry Batch 生命周期** — 修复多个 layer 共用 inverse matrix、dirty transform 因旧 bounds 先被视锥裁剪、冻结根 Scene 下新挂载模型错过当帧世界矩阵、跨 layer materialize 后旧副本仍显示、materialize 对象共享模板材质、旧烘焙位置残留 BVH 命中、负缩放实例错误进入 merged/instanced path、重复实例生成幽灵几何、重复 build 累积资源、旧 layer dispose 清除新 materializer,以及 build 期间已 materialize 对象重复进入透明 fallback;`EditableGeometryBatchLayer` 现在是 one-shot builder。\n- **Offscreen HUD** — renderer stats 改为 1 秒滚动窗口的 FPS、平均/p95/最大帧间隔、超过 25ms 的占比和样本数,并排除 demand 模式从静止恢复时的空闲间隔。\n- **Offscreen Worker lifecycle** — 修复初始化期间销毁导致 `host.init()` 永久 pending、canvas 转移/初始 `postMessage()` 失败后未完成本地清理、dispose 后迟到的业务消息仍被派发、待发送 pointer move rAF 未取消、`ready()` 前加载失败未释放 Worker/GPU 资源,以及重复 `ArrayBuffer` transfer list 抛出 `DataCloneError`。\n- **RenderPipeline SSGI** — 修正 AO/GI 合成,改为 `scene × AO + diffuse × GI`,确保 `aoIntensity` 与 `giIntensity` 分别作用于遮蔽和间接漫反射;同时兼容 Viewer 默认的 reversed depth 背景判断,并在与 SSR 同时启用时复用 diffuse MRT 附件。\n- **RenderPipeline SSR** — 兼容 Viewer 默认的 `reversedDepthBuffer`:三个内部全屏 pass 会正确跳过 clear-depth 背景,同时保留 raw reversed depth 用于位置重建、ray marching 和 temporal history。\n- **SSR 示例** — 新增 `examples/test_ssr.html`,并使用 1× drawing buffer、`maxDistance: 0.4`、静态 2048² 阴影和关闭默认性能面板的配置降低 GPU 开销。\n\n### 文档\n\n- **docs/mcp** — 将核心 `InstanceObject` 的完整 API 归入 Objects 文档,并同步 u-manager 子类引用、LLM 摘要和 MCP 检索索引;运行时 API 未发生变化。\n- **u-manager editable batching** — 补充 `setEditableBatching()` 配置、合并键、TSL 状态纹理、materialize/fallback 边界、统计字段、显存权衡、`test_umanager2.html` 示例和 MCP 检索范围。\n- **OffscreenCanvas Worker** — 补充 `OffscreenViewerHost` 构造选项、方法、事件和统计字段,明确 `host.init()` / `initialized` 与一次性业务 `ready` 的边界,以及 `examples/offscreen/test_umanager2_offscreen.html` 的 editable batching、`compileAsync()` 预热、HUD 统计、静态根节点下 `InstanceObject` 显式矩阵提交、structured-clone/command 边界和 MCP 检索入口。\n- **SSR** — 补充完整 RenderPipeline SSR API、时域/降噪配置、reversed-depth 兼容说明、性能建议与在线示例入口。\n\n## 0.0.29\n\n### 变更\n\n- **u-manager breaking** — 通用实例基类和模型合批层从 `plugins/u-manager/instances` 移入核心;`InstanceObject` 位于 `src/instances`,`ModelInstancedLayer` / `InstanceCullingOptions` 位于 `src/batches`,均继续通过顶层 `u-space` 导出;旧 `SemanticInstanceObject` / `SemanticModelInstancedLayer` 命名不再保留。\n- **u-manager breaking** — `ModelInstancedLayer` 移除旧的 `addSemanticBatch()` 公开入口,改为 `reserveBatch()`、`addInstance()` / `addInstances()`、`getInstanceById()`、`removeInstance()` / `removeInstances()`、`removeBatch()` 和 `clearBatches()`,支持运行时实例新增/删除、按 URL batch 的 capacity 预分配与扩容;`removeInstances()` 支持直接传单个字符串 `instanceId`,并且 layer 内部不再从 `instance.userData` 扫描 ID。\n- **u-manager breaking** — `InstanceObject` 使用独立身份字段 `instanceId`、`instanceKind`、`instanceName` 和 `setInstanceIdentity()`;Scene / Facility instancing 的查询、删除和 raycast remap 都使用这些字段,`userData` 只保留原始业务元数据。\n- **u-manager breaking** — `SemanticGroup`、`BuildingGroup` 和 `FloorMesh` 的 building/floor/facility 索引统一读取语义对象 ID、实例对象 `instanceId` 或显式传入的别名,不再把 `userData.id`、`userData.semanticId`、`userData.facilityId` 自动注册成查询 ID;`ModelInstancedLayer` 会拒绝重复 `instanceId` 的新增实例,并在身份字段变化后按需刷新 id 索引。\n- **u-manager** — `InstanceObject` 新增对象级 `boundingBox`、`boundingSphere`、`computeBoundingBox()` 和 `computeBoundingSphere()`,`SceneInstanceObject`、`FacilityInstanceObject` 以及楼层语义代理可直接传给 `viewer.controls.flyToObject()`。\n- **u-manager** — `ModelInstancedLayer` 的内部实例矩阵同步改为 WebGPU storage instanced buffer 路径,运行时新增或移动实例后会在下一帧立即同步 matrix;外部仍通过修改 `InstanceObject` transform 和 dirty sync 使用,不需要直接写内部 `InstancedMesh.instanceMatrix`。\n- **u-manager** — `SemanticGroup` 新增 `getDefaultFacilityLayer()`,`SceneLoader.loadAsync()` 返回的 `SceneGroup` 新增 `sceneLayer` / `getDefaultSceneLayer()`,外部可直接获取默认 Facilities 和场景模型合批层。\n- **docs/mcp** — 更新 u-manager 文档、Examples Guide、LLM 索引和 MCP 检索说明,推荐通过 `viewer.controls.flyToObject()` 飞向实例;`getInstanceBoundingBox(target, true)` 保留给手动合并或调整世界包围盒的场景。\n\n### 示例与文档\n\n- **在线示例** — `examples/importmap.js` 的 CDN fallback 版本更新为 `0.0.29`,Vercel 部署会继续按 `package.json` 注入当前版本。\n\n## 0.0.28\n\n### 新增\n\n- **u-manager** — 新增 `UManagerLoader` 一体化入口,可同时加载 `SemanticLoader` 和 `SceneLoader`,返回带 `semanticGroup` / `sceneGroup` 直接属性的 `UManagerSceneGroup`,并复用 semantic id 去重避免重复建筑/设备。\n- **examples/docs/mcp** — 新增 `examples/test_umanager_loader.html`,演示 `UManagerLoader` 一次加载语义楼层、Facilities 和非语义场景树,并补充 Examples Guide 与 MCP 检索入口。\n\n### 变更\n\n- **u-manager breaking** — `SceneInstanceObject`、`FacilityInstanceObject` 和楼层多边形语义统一到 `InstanceObject` API:使用 `setInstanceVisible()`、`setInstanceColor()`、`setInstanceOpacity()`、`getInstanceBoundingBox()` 等方法,不再使用 scene/facility 专属实例方法。\n- **u-manager** — 新增共享 `ModelInstancedLayer`,`SceneInstancedLayer` 和 `FacilityInstancedLayer` 共用同一套模型模板 instancing、material groups、多材质、dirty sync、raycast remap 和 fallback wrapper 逻辑。\n- **ObjectManager / MaterialEffects** — 对语义实例的显隐、透明度和高亮控制统一识别 `InstanceObject`,不再维护 SceneInstance/FacilityInstance 两套特殊分支。\n- **u-manager loader** — `SceneLoader` 不再解析场景树里的 `AREA`、`FLOOR`、`ROOM` 语义面;楼层、区域和房间统一由 `SemanticLoader` / `SemanticGroup` 处理,避免同一语义对象在组合加载时重复出现。\n\n### 示例与文档\n\n- **在线示例** — `examples/importmap.js` 的 CDN fallback 版本更新为 `0.0.28`,Vercel 部署会继续按 `package.json` 注入当前版本。\n\n## 0.0.27\n\n### 变更\n\n- **u-manager semantics** — `FacilityInstancedLayer` 的相机逐实例裁剪改为显式开启:默认不再按当前相机视锥或 `minScreenRadius` 压缩 active instances,避免相机距离影响设备显隐;需要性能压缩时可通过 `setInstanceCulling({ enabled: true })` 开启。\n- **u-manager semantics** — `FacilityInstancedLayer` 使用动态 `Group` 包裹 Facilities `InstancedMesh` batch,避免 Three.js WebGPU render bundle 缓存跳过子 mesh 的渲染前同步;`hideAllFacilities()` 后再 `showAllFacilities()` 不会复用过期 active instance 状态。\n- **u-manager semantics** — `FacilityInstancedLayer` 支持合法 `material[]` + `geometry.groups` 的设备模板进入 instancing;只有 groups 缺失或材质索引非法的多材质 Mesh 才回退为普通 `Model`。\n- **u-manager semantics** — Facilities instancing 会在相机矩阵、投影矩阵或 viewport 高度变化时重新压缩当前帧实例;设备显隐、颜色、透明度和 `planish('Facilities')` / `unplanish('Facilities')` 改变 transform 时刷新 instance data。\n- **u-manager semantics** — 全部设备隐藏或透明度为 0 时,`FacilityInstancedLayer` 会关闭空的内部 `InstancedMesh`;通过 `showAllFacilities()` 或单设备 API 恢复时会重新加入渲染同步。\n- **fire 插件** — 新增 `FireEffect`,封装 Three.js 官方 `webgpu_volume_fire` 体积火焰示例,支持 WebGPU 3D 流体模拟、烟雾散射、动态光照、体积阴影、output effect 叠加和 `cameraChange` 相机切换同步。\n- **u-manager loader** — `SceneLoader` 默认读取同路径 `db/semantic_model.json` 并按语义 `ID` 跳过重复场景树节点,支持与 `SemanticLoader` 同时加载。\n- **u-manager loader** — `SceneLoader` 默认按模型 `path` 合并重复静态 3D 节点,通过 `SceneInstancedLayer` 批量渲染,同时保留 `SceneInstanceObject` 逻辑对象用于单模型 ID 检索、显隐、颜色、透明度、包围盒和事件派发。\n- **docs/mcp** — 补充 `SceneInstanceObject` 和 `FacilityInstanceObject` 通过 `objectManager.getById()` + `getInstanceBoundingBox()` + `controls.flyToBox()` 飞向单个实例的示例与 MCP 检索说明。\n- **ObjectManager / MaterialEffects** — `setOpacity()`、`show()` / `hide()`、`isolate()`、`showAll()` 以及 `highlightColor()` / `removeHighlightColor()` 支持 `SceneInstanceObject` 的 per-instance 控制。\n- **u-manager loader** — `SceneLoader` 创建的根组、`GROUP` 和 `STUB` 节点改为 `BaseGroup`,隐藏后可通过对象自身 `raycast()` 返回值阻止子级继续参与递归射线检测。\n- **Viewer** — 默认使用 Three.js `reversedDepthBuffer` 替代 `logarithmicDepthBuffer`,减少 WebGPU 顶视角多层透明场景的 fragment 压力,同时保留远距离深度精度。\n\n### 示例与文档\n\n- **在线示例** — `examples/importmap.js` 的 CDN fallback 版本更新为 `0.0.27`,Vercel 部署会继续按 `package.json` 注入当前版本。\n\n## 0.0.26\n\n### 修复\n\n- **u-manager semantics** — `FloorMesh.planish()` 会在压平后的语义实例和 Facilities 上加入稳定的轻微 Y 偏移,减少门、墙等重叠薄片之间的共面闪烁。\n- **u-manager semantics** — `FloorMesh` 会按实例透明度把合并语义网格拆分到不透明和透明材质通道,避免墙、柱、门等完全不透明实例也走透明渲染队列,降低近距离压平场景的 overdraw 成本。\n- **u-manager semantics** — `BuildingGroup` 新增 `getFloorById()`、`getElevatorById()` 和 `getVentById()`,内部通过 `Map` 维护楼层、电梯井和通风井语义 ID 索引。\n- **u-manager semantics** — `FacilityInstancedLayer` 只在实例矩阵、颜色或透明度变化时上传对应 instance buffer,静态 Facilities 不再每帧重复提交全部实例数据;渲染前会按每个 Facility 实例的世界包围盒和当前相机视锥压缩本帧 active instance count,不再通过相机距离阈值裁剪设备。\n- **u-manager semantics** — 修复 Facilities 视锥裁剪在视角切换后可能沿用上一帧 active subset 的 `visible` / `boundingSphere` 状态,导致某些视角下设备整批隐藏的问题;instanced batch 会保留完整包围球,同时用 `count` 控制本帧 active 实例。\n- **u-manager semantics** — Instanced Facilities 的 batch 材质默认保持不透明,只有源材质本身透明或当前渲染实例存在半透明 opacity 时才切到透明队列,减少完全不透明设备在近距离视角下的 overdraw。\n- **MaterialEffects** — 高亮、呼吸色、线框和淡入淡出效果支持材质数组,确保多材质 Mesh 和 `FloorMesh` 双材质通道都能正确应用并恢复材质状态。\n- **docs/mcp** — 补充 Facilities auto instancing 的动态材质透明度、包围盒视锥裁剪、`controls.flyToBox()` 飞向设备,以及多材质 fallback 对颜色/透明度 override 的支持说明。\n\n### 示例与文档\n\n- **在线示例** — `examples/importmap.js` 的 CDN fallback 版本更新为 `0.0.26`,Vercel 部署会继续按 `package.json` 注入当前版本。\n\n## 0.0.25\n\n### 新增\n\n- **u-manager semantics** — 新增 `SemanticGroup`,作为单次 `SemanticParser` 解析结果的顶层容器;`SemanticLoader.loadAsync()` / `SemanticParser.parseAsync()` 直接返回完整语义场景。\n- **u-manager semantics** — `SemanticGroup` 新增 `getBuildingById(id)`,支持按语义数据里的 `Building.ID` 获取建筑。\n- **u-manager semantics** — `SemanticGroup` 和 `BuildingGroup` 的建筑/设备 ID 查询改为内部 `Map` 索引,并通过楼层设备索引版本自动同步,避免每次查询扫描数组。\n- **u-manager semantics** — `FloorMesh.planish()` / `unplanish()`、`BuildingGroup.planishFloors()` / `unplanishFloors()` 和 `SemanticGroup.planishFloors()` / `unplanishFloors()` 会把所有合并语义实例和 Facilities 都按包围盒贴到楼层平面,而不是围绕实例自身基点压缩。\n- **u-manager semantics** — 新增 scene-level Facilities auto instancing:同模型 URL 的静态设备会合并到 `SemanticGroup.facilityLayer` 的 `InstancedMesh` batch,同时保留楼层下的设备引用用于检索、显隐、颜色和透明度控制;带动画、骨骼、morph target 或多材质 Mesh 的设备自动回退为普通 `Model`。\n- **u-manager semantics** — `SemanticGroup`、`BuildingGroup` 和 `FloorMesh` 新增 `getFacilityById()` 设备检索,返回的 `FacilityInstanceObject` 可通过统一 `setInstanceVisible()`、`setInstanceColor()`、`resetInstanceColor()` 和 `setInstanceOpacity()` 控制普通设备模型或 scene-level instanced 设备引用。\n- **docs/mcp** — 补充 Facilities auto instancing 的材质透明度行为、`resetInstanceColor()` 语义,以及多材质 fallback 对颜色/透明度 override 的限制说明。\n\n### 修复\n\n- **u-manager semantics** — 修复 scene-level instanced Facilities 的点击和指针事件派发:射线命中会映射回楼层下的 `FacilityInstanceObject`,并继续冒泡到所属 `FloorMesh`、`BuildingGroup` 和 `SemanticGroup`。\n- **u-manager semantics** — `FacilityInstancedLayer` 继承 `BaseGroup`,避免 `facilityLayer.visible = false` 后内部 `InstancedMesh` 仍参与射线检测并触发隐藏设备点击事件。\n- **u-manager semantics** — 移除语义设备解析里的 `sid` 检索和注册逻辑;Facilities 只按 `Facility.ID` 以及显式传入的别名进入楼层索引和 `viewer.objectManager`。\n\n### 示例与文档\n\n- **在线示例** — `examples/importmap.js` 的 CDN fallback 版本更新为 `0.0.25`,Vercel 部署会继续按 `package.json` 注入当前版本。\n\n## 0.0.23\n\n### 新增\n\n- **u-manager semantics** — `BuildingGroup` 新增 `isolateFloors(floors)`,支持按多个 `FloorMesh` 或楼层下标同时隔离显示多个楼层;`isolateFloor()` 继续兼容单楼层调用,并可接收数组作为便捷写法。\n- **u-manager semantics** — `SemanticLoader` 新增 `Facilities` 解析:从 `Stories[].Facilities` 索引读取顶层设备语义,用 `Facility.ID` 匹配解密后的 `tree_models` 设备节点,沿父级累乘本地矩阵得到设备世界矩阵,加载 3D 设备模型并转换为楼层局部矩阵后挂载到对应 `FloorMesh`。\n- **u-manager semantics** — `FloorMesh` 新增楼层设备检索与统一语义实体 API:`getFacilityById()`、`getFacilityAt()`、`getFacilities()`、`removeFacility()`、`showAllFacilities()`、`hideAllFacilities()`、`getSemanticById()`、`getSemantics()` 和 `getSemanticsByKind('Facilities')`;`BuildingGroup.showAllFacilities()/hideAllFacilities()` 支持批量显示或隐藏整栋建筑内的楼层设备。\n- **u-space MCP** — 文档索引同步楼层 `Facilities` 解析和检索 API,MCP 客户端可检索设备按楼层挂载、`tree_models` 解密匹配、本地矩阵父级累乘和世界坐标转换规则。\n\n### 示例与文档\n\n- **在线示例** — `examples/importmap.js` 的 CDN fallback 版本更新为 `0.0.23`,部署流程仍会优先按 `package.json` 注入当前版本。\n\n## 0.0.21\n\n### 新增\n\n- **u-manager semantics** — `SemanticLoader` 支持解析建筑级 `Elevators` 和 `Vents`,以 `Polygon.shape` 作为底面轮廓、`Elevation` 作为底面高度、`TotalHeight` 作为拉伸高度生成 `ExtrudeMesh`,并通过 `BuildingGroup.elevatorMeshes`、`BuildingGroup.ventMeshes` 及 add/get/remove 方法管理。\n\n### 修复\n\n- **透明语义体块渲染** — 电梯井/通风井在渲染阶段沿法线轻微内收,`Spaces` 实例使用稳定 per-instance 渲染偏移抖动,缓解与墙体或完全重叠空间共面时的 z-fighting。\n- **atmosphere 插件** — 后处理链移除 aerial perspective 节点,保留 lens flare、tone mapping、temporal antialias 和 dithering;`scene.backgroundNode` 使用 `skyBackground()`,`scene.environmentNode` 使用 `skyEnvironment()`,并继续在关闭插件时恢复启用前的背景和环境节点。\n\n### 示例与文档\n\n- **在线示例** — `examples/importmap.js` 的 CDN fallback 版本更新为 `0.0.21`,部署流程仍会优先按 `package.json` 注入当前版本。\n\n## 0.0.18\n\n### 变更\n\n- **版本发布** — 同步发布包含新版大气插件实现、RenderPipeline output effects 和一键 MCP 发布脚本的 npm 包。\n\n## 0.0.17\n\n### 新增\n\n- **atmosphere 插件** — `Atmosphere.enable(options)` 从占位实现升级为可用的大气渲染:支持 WGS84 经纬高定位、太阳/月亮方向、天空背景、天空环境、aerial perspective、lens flare、tone mapping、temporal antialias、dithering 和 `AtmosphereLight` 阴影。\n- **RenderPipeline** — 新增 `addOutputEffect()` / `removeOutputEffect()` 与 `addMRTChannel()` / `removeMRTChannel()`,插件可追加输出后处理和独立 MRT 通道,不再占用业务侧 `setOutputComposer()`。\n\n### 变更\n\n- **大气状态更新** — 大气插件在 `beforeRender` 中刷新相机 ECEF 位置、太阳/月亮方向和阴影 target,相机控制器驱动的移动也能逐帧更新大气状态。\n- **MCP 发布** — 根目录 MCP 发布入口合并为 `pnpm publish:mcp`,命令会先递增 `u-space-mcp` 的 patch 版本,再构建文档索引并发布 npm 包。\n\n## 0.0.16\n\n### 新增\n\n- **u-manager semantics** — `FloorMesh.planish(kind?)`、`FloorMesh.unplanish(kind?)`、`BuildingGroup.planishFloors(kind?)` 和 `BuildingGroup.unplanishFloors(kind?)` 支持按 `Walls`、`Columns`、`Spaces`、`Doors`、`Windows` 语义类型压平或恢复;`setScaleYAt(index, y)` 可只调整单个语义实例的 Y 轴缩放。\n- **BaseMesh** — 默认开启 `castShadow` 与 `receiveShadow`,基础网格和继承自 `BaseMesh` 的对象会直接参与阴影投射与接收。\n\n### 变更\n\n- **语义类型命名** — `SemanticKind` 统一为数据源字段风格:`Walls`、`Columns`、`Spaces`、`Doors`、`Windows`。\n- **示例与依赖** — 更新 `load_tiles.html` 示例依赖,新增 `@takram/three-atmosphere`、`postprocessing` peer dependency,并将 Vite 外部依赖边界扩展到所有 `@takram/*` 包。\n\n## 0.0.12\n\n### 新增\n\n- **u-manager semantics** — 新增 `SemanticLoader`、`BuildingGroup`、`FloorMesh` 语义楼层 API,支持楼层隔离、实例级颜色/透明度/显隐/matrix/scale 控制,以及实例包围盒/包围球查询。\n- **u-space MCP** — 内置文档索引同步语义楼层 API,MCP 客户端可检索 `SemanticLoader`、`BuildingGroup` 和 `FloorMesh` 用法。\n\n## 0.0.8\n\n### 新增\n\n- **Viewer** — `resize()`、`screenshot()`、`setBackground()`、`setEnvironment()`、`enableShadow()`/`disableShadow()`、`enableFog()`/`enableFogExp2()`/`disableFog()`\n- **CameraControls** — `flyTo()`、`getCameraViewpoint()`、`lock()`/`unlock()`、`setViewMode('2d' | '3d')`\n- **ObjectManager** — `show()`/`hide()`、`isolate()`、`showAll()`、`setOpacity()`、`getBoundingBox()`、`forEach()`、`filter()`\n- **InteractionManager** — `enable()`/`disable()`、`setInteractable()`、`addIgnore()`/`removeIgnore()`\n- **Selection** — 新增框选(rubber-band)和单选/多选/切换模块,支持 `select`/`deselect`/`change` 事件\n- **MaterialEffects** — `wireframe()`/`removeWireframe()`、`transparent()`/`removeTransparent()`、`xray()`/`removeXray()`、`fadeIn()`/`fadeOut()`\n- **Model** — `getBoundingBox()`、`getCenter()`、`setMaterial()`\n- **SceneManager** — 新模块,多场景创建、注册、切换与序列化\n- **LightManager** — 新模块,灯光便捷创建、4 种预设(indoor/outdoor/studio/warehouse)、Helper 可视化\n- **MeasureTool** — 新工具,距离/面积/角度测量,结果可视化渲染到场景\n- **ClippingTool** — 新工具,基于 WebGPU `ClippingGroup` 的剖切面与剖切盒\n- **AnnotationManager** — 新工具,3D 引线标注 + HTML 浮动标签\n- **示例** — 新增 8 个交互式示例页面\n\n### 修复\n\n- **Selection** — 框选覆盖层改为挂载到容器元素(而非 canvas),修复框选矩形不可见的问题\n- **ClippingTool** — 从 `renderer.clippingPlanes`(WebGL API)重写为 `ClippingGroup`(WebGPU API),修复剖切无效的问题\n- **MaterialEffects** — fade 动画在对象移除场景后自动停止,避免内存泄漏\n- **InteractionManager** — `dispose()` 中清除 `clickTimer`,避免定时器泄漏\n- **LightManager** — 修复 `removeAll()`/`hideAllHelpers()` 中遍历 Map 同时删除的问题\n- **ObjectManager** — `setOpacity()` 先检查 `isMesh` 再类型转换\n- **ClippingTool** — `PlaneHelper` 颜色使用 `new Color(color).getHex()` 替代直接 `as number`\n- **MeasureTool** — 移除 WebGPU 不支持的 `linewidth` 属性\n\n---\n\n## 0.0.7\n\n### 变更\n\n- 文档拆分:将插件文档拆分为独立文件\n- 新增 CameraControls 独立文档页\n- 修复 getting-started 中的死链\n- 修复 sbmx loader 示例使用新的 MaterialEffects API\n\n---\n\n## 0.0.6\n\n### 新增\n\n- **MaterialEffects** — 可组合的材质特效系统,支持安全共享材质和 remove API\n- **RenderPipeline** — 内置 Bloom、SSGI、TRAA 后处理及 OutputComposer API\n- **CSSRenderer** — CSS2D / CSS2.5D / CSS3D 叠加渲染 HTML 元素\n- 从 `package.json` 导出 `version`,统一示例 importmap\n\n---\n\n## 0.0.5\n\n### 变更\n\n- 版本发布(无重大功能变更)\n\n---\n\n## 0.0.4\n\n### 新增\n\n- **Model** — 动画播放支持(`playAnimation`、`stopAnimation`、`updateAnimation`)\n- **ViewerEventMap** — 事件回调新增 `delta` 时间参数\n\n---\n\n## 0.0.3\n\n### 新增\n\n- **ObjectControls** 插件 — 交互式移动、旋转、缩放 3D 对象\n- **TopologyDrawer** 插件 — 在场景任意表面交互式绘制拓扑路径图\n- **Topology** — `exportData()` / `importData()` 数据序列化\n\n### 变更\n\n- **Interactions** — `contextmenu` 事件替换为 `rightclick`\n- **Poi** — 优化渲染性能,新增 `scaleFactor` 参数\n- 文档全面完善:本地搜索、侧边栏优化、各插件文档页\n\n---\n\n## 0.0.0\n\n### 新增\n\n- 项目初始化:Viewer 核心、WebGPU 渲染器、场景/相机/控制器\n- 模型加载系统:双层缓存(内存 + 持久化)、Draco/KTX2/Meshopt 支持\n- 交互系统:射线检测、事件冒泡、自定义 3D 对象事件\n- 对象系统:Model、Poi、Topology、基础网格(Sphere/Plane/Circle/Tube/Extrude/Shape)\n- ObjectManager 集中式对象注册\n- 补间动画系统\n- u-manager 插件:场景加载、动画、属性、视点管理\n- tiles 插件:ArcGIS 3D 瓦片 / GIS 地球渲染\n- minimap 插件:2D 小地图\n- keyboard-controls 插件:WASD 键盘控制\n- tracking-controls 插件:相机跟随\n- atmosphere 插件:天空/大气渲染\n- curve-movement 插件:曲线路径动画\n"
|
|
1434
1635
|
},
|
|
1435
1636
|
{
|
|
1436
1637
|
"id": "examples-guide",
|
|
1437
1638
|
"path": "examples-guide.md",
|
|
1438
1639
|
"title": "示例指南",
|
|
1439
|
-
"summary": "`u-space` 的 `examples/` 目录中包含了大量演示引擎功能的示例。以下是几个关键示例及其所展示的系统特性。 示例统一通过 `examples/importmap.js` 注入 Import Map。本地通过 `localhost`、`127.0.0.1`、`0.0.0.0` 或 `192.168.x.x` 访问时会加载仓库里的 `../dist/` 构建产物;在线部署或非本地域名访问时会从 jsDelivr 加载当前发布版本 `u-space@0.0.
|
|
1640
|
+
"summary": "`u-space` 的 `examples/` 目录中包含了大量演示引擎功能的示例。以下是几个关键示例及其所展示的系统特性。 示例统一通过 `examples/importmap.js` 注入 Import Map。本地通过 `localhost`、`127.0.0.1`、`0.0.0.0` 或 `192.168.x.x` 访问时会加载仓库里的 `../dist/` 构建产物;在线部署或非本地域名访问时会从 jsDelivr 加载当前发布版本 `u-space@0.0.30`。Vercel 文档部署会继续把 `__VERSION__` 占位符替换为 `package.json` 中的版本号,源码里的 `0.0.30` 作为直接托管 `examples/` 时的 fallback。 插件示例可以在页面加载 `importmap.js` 前通过 `window.__IMPORTS__` 声明额",
|
|
1440
1641
|
"headings": [
|
|
1441
1642
|
{
|
|
1442
1643
|
"level": 1,
|
|
@@ -1488,7 +1689,23 @@ export const docsIndex = [
|
|
|
1488
1689
|
},
|
|
1489
1690
|
{
|
|
1490
1691
|
"level": 2,
|
|
1491
|
-
"text": "6.
|
|
1692
|
+
"text": "6. test_umanager_dynamic_instances.html:运行时动态实例"
|
|
1693
|
+
},
|
|
1694
|
+
{
|
|
1695
|
+
"level": 3,
|
|
1696
|
+
"text": "核心要点:"
|
|
1697
|
+
},
|
|
1698
|
+
{
|
|
1699
|
+
"level": 2,
|
|
1700
|
+
"text": "7. test_umanager2.html:大型场景可编辑静态合批"
|
|
1701
|
+
},
|
|
1702
|
+
{
|
|
1703
|
+
"level": 3,
|
|
1704
|
+
"text": "核心要点:"
|
|
1705
|
+
},
|
|
1706
|
+
{
|
|
1707
|
+
"level": 2,
|
|
1708
|
+
"text": "8. test_umanager2_offscreen.html:OffscreenCanvas Worker + editable batching"
|
|
1492
1709
|
},
|
|
1493
1710
|
{
|
|
1494
1711
|
"level": 3,
|
|
@@ -1496,7 +1713,7 @@ export const docsIndex = [
|
|
|
1496
1713
|
},
|
|
1497
1714
|
{
|
|
1498
1715
|
"level": 2,
|
|
1499
|
-
"text": "
|
|
1716
|
+
"text": "9. test_model_animation.html:模型内置动画"
|
|
1500
1717
|
},
|
|
1501
1718
|
{
|
|
1502
1719
|
"level": 3,
|
|
@@ -1504,7 +1721,7 @@ export const docsIndex = [
|
|
|
1504
1721
|
},
|
|
1505
1722
|
{
|
|
1506
1723
|
"level": 2,
|
|
1507
|
-
"text": "
|
|
1724
|
+
"text": "10. test_css_renderer.html:CSS2D / CSS2.5D / CSS3D 渲染器"
|
|
1508
1725
|
},
|
|
1509
1726
|
{
|
|
1510
1727
|
"level": 3,
|
|
@@ -1512,7 +1729,7 @@ export const docsIndex = [
|
|
|
1512
1729
|
},
|
|
1513
1730
|
{
|
|
1514
1731
|
"level": 2,
|
|
1515
|
-
"text": "
|
|
1732
|
+
"text": "11. test_postprocessing.html:后处理效果"
|
|
1516
1733
|
},
|
|
1517
1734
|
{
|
|
1518
1735
|
"level": 3,
|
|
@@ -1520,7 +1737,7 @@ export const docsIndex = [
|
|
|
1520
1737
|
},
|
|
1521
1738
|
{
|
|
1522
1739
|
"level": 2,
|
|
1523
|
-
"text": "
|
|
1740
|
+
"text": "12. test_viewer_utils.html:Viewer 工具方法"
|
|
1524
1741
|
},
|
|
1525
1742
|
{
|
|
1526
1743
|
"level": 3,
|
|
@@ -1528,7 +1745,7 @@ export const docsIndex = [
|
|
|
1528
1745
|
},
|
|
1529
1746
|
{
|
|
1530
1747
|
"level": 2,
|
|
1531
|
-
"text": "
|
|
1748
|
+
"text": "13. test_camera_controls.html:CameraControls 增强方法"
|
|
1532
1749
|
},
|
|
1533
1750
|
{
|
|
1534
1751
|
"level": 3,
|
|
@@ -1536,7 +1753,7 @@ export const docsIndex = [
|
|
|
1536
1753
|
},
|
|
1537
1754
|
{
|
|
1538
1755
|
"level": 2,
|
|
1539
|
-
"text": "
|
|
1756
|
+
"text": "14. test_object_manager.html:ObjectManager 增强方法"
|
|
1540
1757
|
},
|
|
1541
1758
|
{
|
|
1542
1759
|
"level": 3,
|
|
@@ -1544,7 +1761,7 @@ export const docsIndex = [
|
|
|
1544
1761
|
},
|
|
1545
1762
|
{
|
|
1546
1763
|
"level": 2,
|
|
1547
|
-
"text": "
|
|
1764
|
+
"text": "15. test_material_effects.html:MaterialEffects 增强效果"
|
|
1548
1765
|
},
|
|
1549
1766
|
{
|
|
1550
1767
|
"level": 3,
|
|
@@ -1552,7 +1769,7 @@ export const docsIndex = [
|
|
|
1552
1769
|
},
|
|
1553
1770
|
{
|
|
1554
1771
|
"level": 2,
|
|
1555
|
-
"text": "
|
|
1772
|
+
"text": "16. test_selection.html:Selection 选择系统"
|
|
1556
1773
|
},
|
|
1557
1774
|
{
|
|
1558
1775
|
"level": 3,
|
|
@@ -1560,7 +1777,7 @@ export const docsIndex = [
|
|
|
1560
1777
|
},
|
|
1561
1778
|
{
|
|
1562
1779
|
"level": 2,
|
|
1563
|
-
"text": "
|
|
1780
|
+
"text": "17. test_measure.html:MeasureTool 测量工具"
|
|
1564
1781
|
},
|
|
1565
1782
|
{
|
|
1566
1783
|
"level": 3,
|
|
@@ -1568,7 +1785,7 @@ export const docsIndex = [
|
|
|
1568
1785
|
},
|
|
1569
1786
|
{
|
|
1570
1787
|
"level": 2,
|
|
1571
|
-
"text": "
|
|
1788
|
+
"text": "18. test_annotation.html:AnnotationManager 标注管理"
|
|
1572
1789
|
},
|
|
1573
1790
|
{
|
|
1574
1791
|
"level": 3,
|
|
@@ -1576,7 +1793,7 @@ export const docsIndex = [
|
|
|
1576
1793
|
},
|
|
1577
1794
|
{
|
|
1578
1795
|
"level": 2,
|
|
1579
|
-
"text": "
|
|
1796
|
+
"text": "19. test_clipping.html:ClippingTool 剖切工具"
|
|
1580
1797
|
},
|
|
1581
1798
|
{
|
|
1582
1799
|
"level": 3,
|
|
@@ -1584,14 +1801,38 @@ export const docsIndex = [
|
|
|
1584
1801
|
},
|
|
1585
1802
|
{
|
|
1586
1803
|
"level": 2,
|
|
1587
|
-
"text": "
|
|
1804
|
+
"text": "20. test_lights.html:LightManager 灯光管理"
|
|
1805
|
+
},
|
|
1806
|
+
{
|
|
1807
|
+
"level": 3,
|
|
1808
|
+
"text": "核心要点:"
|
|
1809
|
+
},
|
|
1810
|
+
{
|
|
1811
|
+
"level": 2,
|
|
1812
|
+
"text": "21. test_fire.html:WebGPU 体积火焰"
|
|
1813
|
+
},
|
|
1814
|
+
{
|
|
1815
|
+
"level": 3,
|
|
1816
|
+
"text": "核心要点:"
|
|
1817
|
+
},
|
|
1818
|
+
{
|
|
1819
|
+
"level": 2,
|
|
1820
|
+
"text": "22. test_ssr.html:SSR 时空降噪"
|
|
1821
|
+
},
|
|
1822
|
+
{
|
|
1823
|
+
"level": 3,
|
|
1824
|
+
"text": "核心要点:"
|
|
1825
|
+
},
|
|
1826
|
+
{
|
|
1827
|
+
"level": 2,
|
|
1828
|
+
"text": "23. test_ao.html:环境光遮蔽"
|
|
1588
1829
|
},
|
|
1589
1830
|
{
|
|
1590
1831
|
"level": 3,
|
|
1591
1832
|
"text": "核心要点:"
|
|
1592
1833
|
}
|
|
1593
1834
|
],
|
|
1594
|
-
"content": "# 示例指南\n\n`u-space` 的 `examples/` 目录中包含了大量演示引擎功能的示例。以下是几个关键示例及其所展示的系统特性。\n\n## 运行方式与版本\n\n示例统一通过 `examples/importmap.js` 注入 Import Map。本地通过 `localhost`、`127.0.0.1`、`0.0.0.0` 或 `192.168.x.x` 访问时会加载仓库里的 `../dist/` 构建产物;在线部署或非本地域名访问时会从 jsDelivr 加载当前发布版本 `u-space@0.0.28`。Vercel 文档部署会继续把 `__VERSION__` 占位符替换为 `package.json` 中的版本号,源码里的 `0.0.28` 作为直接托管 `examples/` 时的 fallback。\n\n插件示例可以在页面加载 `importmap.js` 前通过 `window.__IMPORTS__` 声明额外依赖。将 `u-space/plugins/<name>` 的值设为 `true` 时,`importmap.js` 会自动在本地和 CDN 路径之间切换。\n\n## 1. `demo01.html`:基础配置与交互\n\n该示例演示了通过 WebGPU 渲染 3D 场景所需的最少配置,与 [快速上手](./getting-started.md) 指南中的内容一致。\n\n### 核心要点:\n\n- 将 `Viewer` 注入 DOM 元素。\n- 使用 WebGPU `Color` 设置场景背景。\n- 添加基础几何体(如 `BoxGeometry`)。\n- 通过 `viewer.interactionManager.pointerMoveEventsEnabled` 启用全局交互。\n- 像操作 DOM 元素一样,为 3D 网格原生绑定事件监听(`click`、`pointerenter`、`pointerleave`)。\n\n## 2. `test_loader.html`:模型缓存机制\n\n该示例深入解析了 `u-space` 的模型加载管线,阐明运行时缓存与浏览器持久化缓存之间的区别。\n\n### 核心要点:\n\n- 使用全局 `Model` 类通过 `Model.prototype.loadAsync()` 异步加载 `.glb` 文件。\n- 理解 `cache: true`(将模型节点保留在内存中,可实现即时克隆/实例化)。\n- 理解 `persistent: true`(使用 Service Worker / Cache Storage API 对实际网络请求数据进行持久化缓存)。\n- 演示如何将这些异步加载的资源安全地注册到 `viewer.objectManager`。\n- 提供通过 `Model.clearMemoryCache()` 和 `Model.clearPersistentCache()` 手动清除缓存的示例。\n\n## 3. `load_tiles.html`:GIS 与地球渲染\n\n该示例使用外部对等依赖 `3d-tiles-renderer`,通过 `u-space` 插件安全封装后,渲染大规模 LOD GIS 地球。\n\n### 核心要点:\n\n- 动态实例化复杂插件(`ArcgisTilesRenderer`),并传入核心 viewer 引用。\n- 由于瓦片是异步流式加载并持续更新视口的,需使用 `viewer.frameloop = 'always'`。\n- 通过插件的 `invalidate(lon, lat, alt)` 方法定位到指定的经纬高位置。\n- 使用 `lonLatHeightToPosition()` / `positionToLonLatHeight()` 在 WGS84 经纬高和场景世界坐标之间互转。\n\n## 4. `test_umanager.html`:场景组合与解析器\n\n该综合示例融合了多种工具,展示如何构建完整的应用程序,而不仅仅是放置单个模型。\n\n### 核心要点:\n\n- 同时初始化多个插件(minimap、tracking、keyboard-controls),并通过 `.enable()` / `.disable()` 切换其状态。\n- 流式传输结构化场景布局,解析场景描述(`SceneLoader`、`TopologiesLoader`)。\n- 使用相机动画(`VisionsLoader` 和 `VisionsParser`)在场景中编排预定义的飞行路径。\n- 获取复杂的节点材质,并自定义 WebGPU 专属的 TSL(Three Shading Language)节点,如 `TSLEffects.fluid()`。\n\n## 5. `test_umanager_loader.html`:UManagerLoader 一体化加载\n\n[在线演示](https://u-space-phi.vercel.app/examples/test_umanager_loader.html)\n\n该示例演示通过 `UManagerLoader` 在同一个路径下同时加载语义楼层、Facilities 和非语义场景树内容。加载结果会返回一个 `UManagerSceneGroup`,其中 `semanticGroup` 指向 `SemanticGroup`,`sceneGroup` 指向非语义场景组。\n\n### 核心要点:\n\n- 使用 `UManagerLoader.setPath()` 配置场景根目录,并通过一次 `loadAsync()` 完成 `SemanticLoader` + `SceneLoader` 组合加载。\n- `SceneLoader` 会自动跳过语义文件中已存在的 `id`,避免建筑、楼层或设备重复加载。\n- 通过 `viewer.objectManager` 获取 `SceneInstanceObject` 和 `FacilityInstanceObject`,再使用统一 `getSemanticBoundingBox()` + `viewer.controls.flyToBox()` 飞向单个实例。\n- 使用 `setSemanticHighlight()`、`setSemanticVisible()`、`setSemanticOpacity()` 等统一 API 控制实例,不需要关心底层是合并几何、`InstancedMesh` 还是 fallback `Model`。\n- 示例把 `viewer`、`loader`、`root`、`semanticGroup`、`sceneGroup`、`sceneInstances` 和 `facilities` 暴露到 `window.uManagerExample`,方便在控制台调试。\n\n## 6. `test_model_animation.html`:模型内置动画\n\n[在线演示](https://u-space-phi.vercel.app/examples/test_model_animation.html)\n\n该示例演示了如何加载含内置动画的 glTF 模型,并通过 `Model` 类的动画 API 控制播放。\n\n### 核心要点:\n\n- 使用 `Model.loadAsync()` 加载带动画的 `.glb` 文件,动画片段自动初始化。\n- 使用 `viewer.frameloop = 'always'` 开启持续渲染以驱动动画。\n- 监听 `beforeRender` 事件,通过 `model.updateAnimation(e.delta)` 推进动画时间。\n- 使用 `model.playAnimation(name)` 按名称播放单个动画,`model.playAllAnimations()` 播放全部。\n- 使用 `model.stopAnimation()` 停止动画播放。\n\n## 7. `test_css_renderer.html`:CSS2D / CSS2.5D / CSS3D 渲染器\n\n该示例演示了如何在 3D 场景中叠加渲染 HTML 元素,展示三种 CSS 渲染模式的效果差异。\n\n### 核心要点:\n\n- 使用 `viewer.cssRenderer.createCSS2DObject()` 创建始终面向相机、固定屏幕尺寸的 2D 标签。\n- 使用 `viewer.cssRenderer.createCSS25DObject()` 创建面向相机但随距离缩放的 2.5D 精灵。\n- 使用 `viewer.cssRenderer.createCSS3DObject()` 创建具有完整 3D 变换的面板。\n- CSS2.5D 和 CSS3D 对象需要设置 `scale`(通常 `0.01`),因为 HTML 元素的像素尺寸远大于 3D 世界单位。\n- 渲染器采用懒加载,仅在首次创建对应类型对象时初始化。\n\n## 8. `test_postprocessing.html`:后处理效果\n\n该示例演示了 `RenderPipeline` 的内置后处理功能,包括 Bloom、SSGI、TRAA 以及自定义后处理组合器。\n\n### 核心要点:\n\n- 使用 `viewer.renderPipeline.enableBloom()` / `disableBloom()` 运行时切换 Bloom 效果。\n- 使用 `viewer.renderPipeline.updateBloom()` 实时调整 strength、radius、threshold 参数。\n- 使用 `viewer.renderPipeline.enableSSGI()` / `disableSSGI()` 切换屏幕空间全局光照。\n- 使用 `viewer.renderPipeline.updateSSGI()` 实时调整 SSGI 参数(直接修改 uniform,无需重建节点图)。\n- 使用 `viewer.renderPipeline.enableTRAA()` / `disableTRAA()` 切换时间抗锯齿。\n- 使用 `viewer.renderPipeline.setOutputComposer()` 自定义后处理链(示例中实现了灰度滤镜)。\n- 使用 `setOutputComposer(null)` 恢复默认渲染管线。\n\n## 9. `test_viewer_utils.html`:Viewer 工具方法\n\n该示例演示了 `Viewer` 新增的便捷方法:截图导出、背景切换、雾效控制和阴影开关。\n\n### 核心要点:\n\n- 使用 `viewer.screenshot()` 导出当前渲染画面为 PNG 文件。\n- 使用 `viewer.setBackground(color)` 快速切换场景背景颜色。\n- 使用 `viewer.enableFog()` / `viewer.disableFog()` 控制雾效。\n- 使用 `viewer.enableShadow()` / `viewer.disableShadow()` 控制阴影渲染。\n- 使用 `viewer.resize(width, height)` 手动触发尺寸更新。\n\n## 10. `test_camera_controls.html`:CameraControls 增强方法\n\n该示例演示了 `CameraControls` 的飞行定位、视角存取、锁定和二三维切换功能。\n\n### 核心要点:\n\n- 使用 `viewer.controls.flyTo(position, target)` 将相机飞行到指定坐标。\n- 使用 `viewer.controls.getCameraViewpoint()` 获取当前视角数据。\n- 使用 `viewer.controls.setCameraViewpoint(data)` 恢复保存的视角。\n- 使用 `viewer.controls.lock()` / `unlock()` 锁定或解锁相机控制。\n- 使用 `viewer.controls.setViewMode('2d')` / `setViewMode('3d')` 在二三维视图间切换。\n\n## 11. `test_object_manager.html`:ObjectManager 增强方法\n\n该示例演示了 `ObjectManager` 的显隐控制、孤立显示、透明度设置、包围盒查询和过滤功能。\n\n### 核心要点:\n\n- 使用 `viewer.objectManager.show(id)` / `hide(id)` 控制对象可见性。\n- 使用 `viewer.objectManager.isolate(ids)` 孤立显示指定对象。\n- 使用 `viewer.objectManager.setOpacity(id, value)` 设置材质透明度。\n- 使用 `viewer.objectManager.getBoundingBox()` 查询所有对象的包围盒。\n- 使用 `viewer.objectManager.filter(predicate)` 按条件过滤对象。\n\n## 12. `test_material_effects.html`:MaterialEffects 增强效果\n\n该示例演示了材质效果系统的线框、半透明、X 光和淡入淡出功能。\n\n### 核心要点:\n\n- 使用 `MaterialEffects.wireframe(object)` 开启线框渲染模式。\n- 使用 `MaterialEffects.highlightColor(object, { opacity })` 设置半透明效果。\n- 使用 `MaterialEffects.highlightColor(object, { color, opacity, overwrite: true, depthWrite: false })` 实现 X 光透视效果。\n- 使用 `MaterialEffects.fadeOut(object)` / `fadeIn(object)` 执行淡入淡出动画。\n- 使用 `MaterialEffects.removeHighlightColor(object)` 恢复原始材质状态。\n\n## 13. `test_selection.html`:Selection 选择系统\n\n该示例演示了对象选择系统的单选和框选功能。\n\n### 核心要点:\n\n- 创建 `Selection` 实例并监听 `select`、`deselect`、`change` 事件。\n- 使用 `selection.select()` / `deselect()` / `toggle()` / `clear()` 进行程序化选择操作。\n- 使用 `selection.enableBoxSelection()` 开启鼠标框选模式。\n- 框选模式下可配合 `viewer.controls.lock()` 避免框选与相机旋转冲突。\n\n## 14. `test_measure.html`:MeasureTool 测量工具\n\n该示例演示了 3D 场景中的距离、面积和角度测量。\n\n### 核心要点:\n\n- 使用 `measureTool.measureDistance(pointA, pointB)` 测量两点距离。\n- 使用 `measureTool.measureArea(points)` 测量多边形面积。\n- 使用 `measureTool.measureAngle(pointA, vertex, pointC)` 测量夹角。\n- 测量结果以可视化线条渲染到场景中,返回的 `MeasureResult` 包含数值和单位。\n\n## 15. `test_annotation.html`:AnnotationManager 标注管理\n\n该示例演示了 3D 标注的创建、更新和管理,配合 `CSSRenderer` 实现 HTML 浮动标签。\n\n### 核心要点:\n\n- 创建 `AnnotationManager` 实例并通过 `setCSSObjectFactory()` 绑定 `CSSRenderer`。\n- 使用 `annotationManager.add(id, options)` 添加带引线和自定义样式的标注。\n- 使用 `annotationManager.addText(text, position)` 快捷添加纯文本标注。\n- 使用 `updateContent()` 动态更新标注内容,`updatePosition()` 移动标注位置。\n- 使用 `hideAll()` / `showAll()` 批量控制标注显隐。\n\n## 16. `test_clipping.html`:ClippingTool 剖切工具\n\n该示例演示了剖切面的创建和实时调整。\n\n### 核心要点:\n\n- 创建 `ClippingTool` 实例并调用 `attach()` 将场景物体移入 ClippingGroup。\n- 使用 `clippingTool.addPlane(id, options)` 添加剖切面。\n- 使用滑块通过 `clippingTool.setPlaneConstant()` 实时调整剖切位置。\n- 使用 `showHelper()` / `hideHelper()` 切换剖切面可视化辅助。\n\n## 17. `test_lights.html`:LightManager 灯光管理\n\n该示例演示了灯光管理器的灯光预设和 Helper 可视化功能。\n\n### 核心要点:\n\n- 创建 `LightManager` 实例并使用 `applyPreset()` 一键应用灯光预设。\n- 支持 4 种预设:`indoor`(室内)、`outdoor`(室外)、`studio`(摄影棚)、`warehouse`(仓库)。\n- 使用 `showAllHelpers()` / `hideAllHelpers()` 显示或隐藏灯光辅助可视化。\n- 每种预设会自动清除旧灯光并创建新的灯光组合。\n\n## 18. `test_fire.html`:WebGPU 体积火焰\n\n该示例演示了 `u-space/plugins/fire` 的 `FireEffect` 插件,封装 Three.js 官方体积火焰示例,在独立 volumetric pass 中模拟火焰和烟雾后叠加到主场景。\n\n### 核心要点:\n\n- 使用 `FireEffect` 创建 WebGPU 体积火焰、烟雾、扰动、动态点光源和体积阴影。\n- 通过 `position` 和 `size` 设置体积盒底部中心和烟雾扩散约束范围。\n- 默认使用 `TeapotGeometry(0.8, 28)` 作为 emitter;也可以传入业务 `Object3D` 和自定义 `emitterGeometry`。\n- 通过 `fire.update()` 实时调整 `fireIntensity`、`smokeLifespan`、`emitterRadius` 等参数。\n- 插件使用 RenderPipeline output effect 接入,不占用业务侧的 `setOutputComposer()`。\n"
|
|
1835
|
+
"content": "# 示例指南\n\n`u-space` 的 `examples/` 目录中包含了大量演示引擎功能的示例。以下是几个关键示例及其所展示的系统特性。\n\n## 运行方式与版本\n\n示例统一通过 `examples/importmap.js` 注入 Import Map。本地通过 `localhost`、`127.0.0.1`、`0.0.0.0` 或 `192.168.x.x` 访问时会加载仓库里的 `../dist/` 构建产物;在线部署或非本地域名访问时会从 jsDelivr 加载当前发布版本 `u-space@0.0.30`。Vercel 文档部署会继续把 `__VERSION__` 占位符替换为 `package.json` 中的版本号,源码里的 `0.0.30` 作为直接托管 `examples/` 时的 fallback。\n\n插件示例可以在页面加载 `importmap.js` 前通过 `window.__IMPORTS__` 声明额外依赖。将 `u-space/plugins/<name>` 的值设为 `true` 时,`importmap.js` 会自动在本地和 CDN 路径之间切换。\n\n## 1. `demo01.html`:基础配置与交互\n\n该示例演示了通过 WebGPU 渲染 3D 场景所需的最少配置,与 [快速上手](./getting-started.md) 指南中的内容一致。\n\n### 核心要点:\n\n- 将 `Viewer` 注入 DOM 元素。\n- 使用 WebGPU `Color` 设置场景背景。\n- 添加基础几何体(如 `BoxGeometry`)。\n- 通过 `viewer.interactionManager.pointerMoveEventsEnabled` 启用全局交互。\n- 像操作 DOM 元素一样,为 3D 网格原生绑定事件监听(`click`、`pointerenter`、`pointerleave`)。\n\n## 2. `test_loader.html`:模型缓存机制\n\n该示例深入解析了 `u-space` 的模型加载管线,阐明运行时缓存与浏览器持久化缓存之间的区别。\n\n### 核心要点:\n\n- 使用全局 `Model` 类通过 `Model.prototype.loadAsync()` 异步加载 `.glb` 文件。\n- 理解 `cache: true`(将模型节点保留在内存中,可实现即时克隆/实例化)。\n- 理解 `persistent: true`(使用 Service Worker / Cache Storage API 对实际网络请求数据进行持久化缓存)。\n- 演示如何将这些异步加载的资源安全地注册到 `viewer.objectManager`。\n- 提供通过 `Model.clearMemoryCache()` 和 `Model.clearPersistentCache()` 手动清除缓存的示例。\n\n## 3. `load_tiles.html`:GIS 与地球渲染\n\n该示例使用外部对等依赖 `3d-tiles-renderer`,通过 `u-space` 插件安全封装后,渲染大规模 LOD GIS 地球。\n\n### 核心要点:\n\n- 动态实例化复杂插件(`ArcgisTilesRenderer`),并传入核心 viewer 引用。\n- 由于瓦片是异步流式加载并持续更新视口的,需使用 `viewer.frameloop = 'always'`。\n- 通过插件的 `invalidate(lon, lat, alt)` 方法定位到指定的经纬高位置。\n- 使用 `lonLatHeightToPosition()` / `positionToLonLatHeight()` 在 WGS84 经纬高和场景世界坐标之间互转。\n\n## 4. `test_umanager.html`:场景组合与解析器\n\n该综合示例融合了多种工具,展示如何构建完整的应用程序,而不仅仅是放置单个模型。\n\n### 核心要点:\n\n- 同时初始化多个插件(minimap、tracking、keyboard-controls),并通过 `.enable()` / `.disable()` 切换其状态。\n- 流式传输结构化场景布局,解析场景描述(`SceneLoader`、`TopologiesLoader`)。\n- 使用相机动画(`VisionsLoader` 和 `VisionsParser`)在场景中编排预定义的飞行路径。\n- 获取复杂的节点材质,并自定义 WebGPU 专属的 TSL(Three Shading Language)节点,如 `TSLEffects.fluid()`。\n\n## 5. `test_umanager_loader.html`:UManagerLoader 一体化加载\n\n[在线演示](https://u-space-phi.vercel.app/examples/test_umanager_loader.html)\n\n该示例演示通过 `UManagerLoader` 在同一个路径下同时加载语义楼层、Facilities 和非语义场景树内容。加载结果会返回一个 `UManagerSceneGroup`,其中 `semanticGroup` 指向 `SemanticGroup`,`sceneGroup` 指向非语义场景组。\n\n### 核心要点:\n\n- 使用 `UManagerLoader.setPath()` 配置场景根目录,并通过一次 `loadAsync()` 完成 `SemanticLoader` + `SceneLoader` 组合加载。\n- `SceneLoader` 会自动跳过语义文件中已存在的 `id`,避免建筑、楼层或设备重复加载。\n- 通过 `root.semanticGroup.getDefaultFacilityLayer()` 和 `root.sceneGroup.getDefaultSceneLayer()` 直接获取默认设备合批层和场景模型合批层。\n- 通过 `viewer.objectManager` 获取 `SceneInstanceObject` 和 `FacilityInstanceObject`,再直接使用 `viewer.controls.flyToObject()` 飞向单个实例。\n- 使用 `setInstanceHighlight()`、`setInstanceVisible()`、`setInstanceOpacity()` 等统一 API 控制实例,不需要关心底层是合并几何、`InstancedMesh` 还是 fallback `Model`。\n- 示例把 `viewer`、`loader`、`root`、`semanticGroup`、`sceneGroup`、`facilityLayer`、`sceneLayer`、`sceneInstances` 和 `facilities` 暴露到 `window.uManagerExample`,方便在控制台调试。\n\n## 6. `test_umanager_dynamic_instances.html`:运行时动态实例\n\n[在线演示](https://u-space-phi.vercel.app/examples/test_umanager_dynamic_instances.html)\n\n该示例把运行时实例新增、删除、恢复和按 ID 查询拆到独立场景中,避免和 `UManagerLoader` 的一体化加载流程混在一起。\n\n### 核心要点:\n\n- 创建 `SceneInstancedLayer` 并调用 `reserveBatch(url, template, capacity)` 预分配容量。\n- 通过 `SceneInstanceObject.setInstanceIdentity({ id, kind, name })` 设置实例独立身份字段,不依赖 `userData` 参与 layer 内部索引。\n- 同一个 layer 内 `instanceId` 必须唯一;重复 ID 的新增会返回 `false`,加入后修改身份字段会在下一次按 ID 查询时刷新索引。\n- 使用 `addInstances()` 批量新增实例,使用 `removeInstance(id)` / `removeInstances(id)` 按完整 `instanceId` 删除实例。\n- 使用 `getInstanceById(id)` 快速获取实例,返回对象可直接用于 `setInstanceHighlight()` 和 `viewer.controls.flyToObject()`。\n- 示例把 `viewer`、`layer`、`template`、`allInstances` 和 `removedInstances` 暴露到 `window.dynamicInstanceExample`。\n\n## 7. `test_umanager2.html`:大型场景可编辑静态合批\n\n该性能目标场景同时加载语义对象和完整场景树,并通过 `SceneLoader.setEditableBatching()` 把兼容的普通 Mesh 烘焙合并到 `SceneEditableBatchLayer`。它适合 draw calls 和可渲染 Mesh 数量成为主要 CPU/GPU 提交瓶颈、但业务仍需要按对象 ID 显隐、高亮或偶尔移动对象的场景。\n\n### 核心要点:\n\n- 使用 `setEditableBatching({ maxVerticesPerBatch, maxIndicesPerBatch, freezeAnimations })` 显式开启;默认 `SceneLoader` 行为不变。\n- 兼容的不透明 Mesh 按材质、Geometry attribute 布局、阴影状态和 `renderOrder` 合并;透明、蒙皮、morph、自定义 shader 和不兼容 Geometry 保留 fallback。\n- 合批对象仍保留 `SceneInstanceObject`,显隐、颜色和不透明高亮通过 TSL 状态纹理更新;transform 或半透明修改只 materialize 当前对象。\n- 页面使用 `pixelRatio: 1` 控制高 DPI drawing buffer 成本,并关闭不需要的交互 raycast。\n- 首帧前通过 `renderer.compileAsync()` 预热 WebGPU material pipeline,避免把编译中的中间画面展示给用户。\n- 使用 `?editableBatching=false` 可关闭该路径,与默认 path-based `SceneInstancedLayer`/普通模型加载结果做性能和画面对比。\n- `sceneGroup.userData.editableBatch` 提供 `sourceMeshes`、`batches`、`drawCallsSaved`、`vertices`、`indices` 和 fallback 原因统计。\n\n该模式通过展开重复顶点换取更少 draw calls;高重复、大 Geometry 的同模板对象可能仍更适合 `SceneInstancedLayer`。详见 [u-manager `setEditableBatching()` API](./api-plugin-u-manager.md#可编辑静态合批seteditablebatching)。\n\n## 8. `test_umanager2_offscreen.html`:OffscreenCanvas Worker + editable batching\n\n该示例是 `test_umanager2.html` 的 Worker 版本。完整 `Viewer`、模型解析、editable batch 构建、相机控制和 WebGPU 渲染运行在 dedicated Worker;主线程只持有可见 canvas、转发输入/resize,并显示 Worker 返回的状态与统计。\n\n通过专用 Vite 配置启动:\n\n```bash\npnpm example:offscreen\n```\n\n然后访问 `http://127.0.0.1:5510/offscreen/test_umanager2_offscreen.html`。该示例的 HTML、主线程入口、Worker 入口和专用 Vite 配置集中在 `examples/offscreen/` 目录。\n\n### 核心要点:\n\n- 主线程使用 `OffscreenViewerHost` 调用 `transferControlToOffscreen()`,以 structured-clone 消息转发 pointer、wheel、resize 和业务 command。\n- Worker 在模块顶层通过 `const worker = await createWorkerViewer()` 创建真正的 `Viewer`,随后以顺序式代码加载 HDR、`SemanticLoader` 和 `SceneLoader`,无需把整个入口包进 setup callback。\n- `await host.init()` / `initialized` 只表示 Worker 内 `viewer.init()` 完成;HDR、模型、editable batch、pipeline 预热和业务首帧完成后,Worker 才显式调用一次 `worker.ready(detail)`。主线程应先注册 `ready` 监听,再执行 `host.init()`,并在 `ready` 后调用业务 command。\n- `SceneLoader.setEditableBatching()` 在 Worker 中完成 Geometry 克隆、变换烘焙、合并和 TSL 状态纹理创建;透明、蒙皮、morph、自定义 shader 等不支持子集保持 fallback。\n- 首帧前关闭 controls invalidation 并等待 `renderer.compileAsync()`,预热 WebGPU 合批管线后再显示完整画面。\n- HUD 显示实际 `drawCalls`、三角形、`static scene matrices` 状态,以及 1 秒滚动窗口的 FPS、平均/p95/最大帧间隔、超过 25ms 的占比和样本数;demand 模式恢复时的空闲间隔不会被误算成渲染帧。\n- 示例调用 `semanticGroup.showAllFloors().showAllFacilities()` 并保持 `SemanticGroup.visible = true`,楼层、墙体、空间、门窗和设备等所有语义对象均保留且默认显示。HUD 的设备调试区通过 Worker command 选择、飞向、高亮或清除当前 Facility;这些操作都不修改任何语义对象或建筑场景的显隐状态,“清除调试”只撤销高亮。\n- `SemanticLoader` 的压平语义 Geometry 属于分析叠加层,本示例按调试需求默认显示完整语义模型,因此其半透明颜色会与建筑楼壳叠加;业务项目可以按自身展示策略单独控制该层。\n- 标准 RenderPipeline scene pass 保持启用,editable batch 只接收源模型中的不透明 Mesh;透明子 Mesh 保留标准 fallback,以兼顾透明合成正确性和不降低 pixel ratio 的运镜帧率。\n- 相机拖拽继续由主线程事件桥接到 Worker;业务对象不能跨线程传输,按 ID 显隐、变色、transform 或 `materialize()` 应包装成 Worker command,通过 `host.request()` 调用。\n- 相机定位后先调用 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` 的原生方法覆盖会继续触发 instance buffer dirty 或 editable-batch materialize。\n- `ready()` 前的加载或管线预编译异常会触发 error、逆序资源清理和 Worker 关闭;初始化期间销毁 Host、canvas 转移/初始消息失败或 Worker 主动关闭会 reject 尚未完成的 `host.init()` / `request()` 并清理 DOM、resize 和待发送 pointer move。Host dispose 后会忽略队列中迟到的业务事件与 response。\n- Offscreen 负责释放主线程,editable batching 负责减少 draw submission;两者都不会自动减少三角形或透明 overdraw。\n\n浏览器必须同时支持 `transferControlToOffscreen()` 和 Worker WebGPU。CSS2D/CSS3D、`Info` 等真实 DOM 叠加层仍应保留在主线程。完整接口和边界见 [Viewer OffscreenCanvas Worker API](./api-viewer.md#offscreencanvas-worker实验性)。\n\n## 9. `test_model_animation.html`:模型内置动画\n\n[在线演示](https://u-space-phi.vercel.app/examples/test_model_animation.html)\n\n该示例演示了如何加载含内置动画的 glTF 模型,并通过 `Model` 类的动画 API 控制播放。\n\n### 核心要点:\n\n- 使用 `Model.loadAsync()` 加载带动画的 `.glb` 文件,动画片段自动初始化。\n- 使用 `viewer.frameloop = 'always'` 开启持续渲染以驱动动画。\n- 监听 `beforeRender` 事件,通过 `model.updateAnimation(e.delta)` 推进动画时间。\n- 使用 `model.playAnimation(name)` 按名称播放单个动画,`model.playAllAnimations()` 播放全部。\n- 使用 `model.stopAnimation()` 停止动画播放。\n\n## 10. `test_css_renderer.html`:CSS2D / CSS2.5D / CSS3D 渲染器\n\n该示例演示了如何在 3D 场景中叠加渲染 HTML 元素,展示三种 CSS 渲染模式的效果差异。\n\n### 核心要点:\n\n- 使用 `viewer.cssRenderer.createCSS2DObject()` 创建始终面向相机、固定屏幕尺寸的 2D 标签。\n- 使用 `viewer.cssRenderer.createCSS25DObject()` 创建面向相机但随距离缩放的 2.5D 精灵。\n- 使用 `viewer.cssRenderer.createCSS3DObject()` 创建具有完整 3D 变换的面板。\n- CSS2.5D 和 CSS3D 对象需要设置 `scale`(通常 `0.01`),因为 HTML 元素的像素尺寸远大于 3D 世界单位。\n- 渲染器采用懒加载,仅在首次创建对应类型对象时初始化。\n\n## 11. `test_postprocessing.html`:后处理效果\n\n该示例演示了 `RenderPipeline` 的内置后处理功能,包括 Bloom、SSGI、TRAA 以及自定义后处理组合器。\n\n### 核心要点:\n\n- 使用 `viewer.renderPipeline.enableBloom()` / `disableBloom()` 运行时切换 Bloom 效果。\n- 使用 `viewer.renderPipeline.updateBloom()` 实时调整 strength、radius、threshold 参数。\n- 使用 `viewer.renderPipeline.enableSSGI()` / `disableSSGI()` 切换屏幕空间全局光照。\n- 使用 `viewer.renderPipeline.updateSSGI()` 实时调整 SSGI 参数(直接修改 uniform,无需重建节点图)。\n- 使用 `viewer.renderPipeline.enableTRAA()` / `disableTRAA()` 切换时间抗锯齿。\n- 使用 `viewer.renderPipeline.setOutputComposer()` 自定义后处理链(示例中实现了灰度滤镜)。\n- 使用 `setOutputComposer(null)` 恢复默认渲染管线。\n\n## 12. `test_viewer_utils.html`:Viewer 工具方法\n\n该示例演示了 `Viewer` 新增的便捷方法:截图导出、背景切换、雾效控制和阴影开关。\n\n### 核心要点:\n\n- 使用 `viewer.screenshot()` 导出当前渲染画面为 PNG 文件。\n- 使用 `viewer.setBackground(color)` 快速切换场景背景颜色。\n- 使用 `viewer.enableFog()` / `viewer.disableFog()` 控制雾效。\n- 使用 `viewer.enableShadow()` / `viewer.disableShadow()` 控制阴影渲染。\n- 使用 `viewer.resize(width, height)` 手动触发尺寸更新。\n\n## 13. `test_camera_controls.html`:CameraControls 增强方法\n\n该示例演示了 `CameraControls` 的飞行定位、视角存取、锁定和二三维切换功能。\n\n### 核心要点:\n\n- 使用 `viewer.controls.flyTo(position, target)` 将相机飞行到指定坐标。\n- 使用 `viewer.controls.getCameraViewpoint()` 获取当前视角数据。\n- 使用 `viewer.controls.setCameraViewpoint(data)` 恢复保存的视角。\n- 使用 `viewer.controls.lock()` / `unlock()` 锁定或解锁相机控制。\n- 使用 `viewer.controls.setViewMode('2d')` / `setViewMode('3d')` 在二三维视图间切换。\n\n## 14. `test_object_manager.html`:ObjectManager 增强方法\n\n该示例演示了 `ObjectManager` 的显隐控制、孤立显示、透明度设置、包围盒查询和过滤功能。\n\n### 核心要点:\n\n- 使用 `viewer.objectManager.show(id)` / `hide(id)` 控制对象可见性。\n- 使用 `viewer.objectManager.isolate(ids)` 孤立显示指定对象。\n- 使用 `viewer.objectManager.setOpacity(id, value)` 设置材质透明度。\n- 使用 `viewer.objectManager.getBoundingBox()` 查询所有对象的包围盒。\n- 使用 `viewer.objectManager.filter(predicate)` 按条件过滤对象。\n\n## 15. `test_material_effects.html`:MaterialEffects 增强效果\n\n该示例演示了材质效果系统的线框、半透明、X 光和淡入淡出功能。\n\n### 核心要点:\n\n- 使用 `MaterialEffects.wireframe(object)` 开启线框渲染模式。\n- 使用 `MaterialEffects.highlightColor(object, { opacity })` 设置半透明效果。\n- 使用 `MaterialEffects.highlightColor(object, { color, opacity, overwrite: true, depthWrite: false })` 实现 X 光透视效果。\n- 使用 `MaterialEffects.fadeOut(object)` / `fadeIn(object)` 执行淡入淡出动画。\n- 使用 `MaterialEffects.removeHighlightColor(object)` 恢复原始材质状态。\n\n## 16. `test_selection.html`:Selection 选择系统\n\n该示例演示了对象选择系统的单选和框选功能。\n\n### 核心要点:\n\n- 创建 `Selection` 实例并监听 `select`、`deselect`、`change` 事件。\n- 使用 `selection.select()` / `deselect()` / `toggle()` / `clear()` 进行程序化选择操作。\n- 使用 `selection.enableBoxSelection()` 开启鼠标框选模式。\n- 框选模式下可配合 `viewer.controls.lock()` 避免框选与相机旋转冲突。\n\n## 17. `test_measure.html`:MeasureTool 测量工具\n\n该示例演示了 3D 场景中的距离、面积和角度测量。\n\n### 核心要点:\n\n- 使用 `measureTool.measureDistance(pointA, pointB)` 测量两点距离。\n- 使用 `measureTool.measureArea(points)` 测量多边形面积。\n- 使用 `measureTool.measureAngle(pointA, vertex, pointC)` 测量夹角。\n- 测量结果以可视化线条渲染到场景中,返回的 `MeasureResult` 包含数值和单位。\n\n## 18. `test_annotation.html`:AnnotationManager 标注管理\n\n该示例演示了 3D 标注的创建、更新和管理,配合 `CSSRenderer` 实现 HTML 浮动标签。\n\n### 核心要点:\n\n- 创建 `AnnotationManager` 实例并通过 `setCSSObjectFactory()` 绑定 `CSSRenderer`。\n- 使用 `annotationManager.add(id, options)` 添加带引线和自定义样式的标注。\n- 使用 `annotationManager.addText(text, position)` 快捷添加纯文本标注。\n- 使用 `updateContent()` 动态更新标注内容,`updatePosition()` 移动标注位置。\n- 使用 `hideAll()` / `showAll()` 批量控制标注显隐。\n\n## 19. `test_clipping.html`:ClippingTool 剖切工具\n\n该示例演示了剖切面的创建和实时调整。\n\n### 核心要点:\n\n- 创建 `ClippingTool` 实例并调用 `attach()` 将场景物体移入 ClippingGroup。\n- 使用 `clippingTool.addPlane(id, options)` 添加剖切面。\n- 使用滑块通过 `clippingTool.setPlaneConstant()` 实时调整剖切位置。\n- 使用 `showHelper()` / `hideHelper()` 切换剖切面可视化辅助。\n\n## 20. `test_lights.html`:LightManager 灯光管理\n\n该示例演示了灯光管理器的灯光预设和 Helper 可视化功能。\n\n### 核心要点:\n\n- 创建 `LightManager` 实例并使用 `applyPreset()` 一键应用灯光预设。\n- 支持 4 种预设:`indoor`(室内)、`outdoor`(室外)、`studio`(摄影棚)、`warehouse`(仓库)。\n- 使用 `showAllHelpers()` / `hideAllHelpers()` 显示或隐藏灯光辅助可视化。\n- 每种预设会自动清除旧灯光并创建新的灯光组合。\n\n## 21. `test_fire.html`:WebGPU 体积火焰\n\n该示例演示了 `u-space/plugins/fire` 的 `FireEffect` 插件,封装 Three.js 官方体积火焰示例,在独立 volumetric pass 中模拟火焰和烟雾后叠加到主场景。\n\n### 核心要点:\n\n- 使用 `FireEffect` 创建 WebGPU 体积火焰、烟雾、扰动、动态点光源和体积阴影。\n- 通过 `position` 和 `size` 设置体积盒底部中心和烟雾扩散约束范围。\n- 默认使用 `TeapotGeometry(0.8, 28)` 作为 emitter;也可以传入业务 `Object3D` 和自定义 `emitterGeometry`。\n- 通过 `fire.update()` 实时调整 `fireIntensity`、`smokeLifespan`、`emitterRadius` 等参数。\n- 插件使用 RenderPipeline output effect 接入,不占用业务侧的 `setOutputComposer()`。\n\n## 22. `test_ssr.html`:SSR 时空降噪\n\n[在线演示](https://u-space-phi.vercel.app/examples/test_ssr.html)\n\n该示例使用程序化室内场景演示完整的屏幕空间反射链路:Stochastic SSR、Temporal Reproject 和 Recurrent Denoise。\n\n### 核心要点:\n\n- 使用 `viewer.renderPipeline.enableSSR()` / `disableSSR()` 开关完整 SSR 后处理链。\n- 使用 `viewer.renderPipeline.updateSSR()` 实时调整 ray marching、历史帧数和降噪参数。\n- SSR 自动写入打包的法线/粗糙度、漫反射/金属度及 velocity MRT 通道。\n- 默认 `Viewer` 的 reversed depth 可直接使用;RenderPipeline 会在三个内部 pass 中正确跳过背景,同时保持原始深度用于位置与历史重建。\n- 使用 `viewer.frameloop = 'always'` 为时域重投影和循环降噪提供连续帧。\n- 场景包含移动发光球体,可直接观察运动向量重投影与残影收敛效果。\n- 示例通过 `pixelRatio: 1` 使用 1× drawing buffer,避免 Retina 屏幕把 Temporal Reproject 和 Recurrent Denoise 放大到 2.25 倍像素量。\n- `maxDistance` 从默认值 `0.4` 开始,保留滑块用于观察追踪距离对质量和性能的影响。\n- 移动物体继续生成 velocity,但不投射动态阴影;2048² 方向光阴影只更新一次。\n\n## 23. `test_ao.html`:环境光遮蔽\n\n[在线演示](https://u-space-phi.vercel.app/examples/test_ao.html)\n\n该示例使用程序化墙角与紧密摆放的几何体,独立展示 RenderPipeline 的 SSGI AO 分量。页面默认开启 AO,并固定 `giIntensity: 0`,因此画面只保留 `scene.rgb × AO` 遮蔽效果。\n\n### 核心要点:\n\n- 使用 `viewer.renderPipeline.enableSSGI()` 开启 AO,并通过 `giIntensity: 0` 关闭间接漫反射。\n- 使用 `disableSSGI()` 对比无 AO 画面,再使用当前参数重新开启。\n- 使用 `updateSSGI()` 实时调整 `aoIntensity`、`radius` 和 `thickness`。\n- 使用 `pixelRatio: 1` 控制全屏 SSGI 在高 DPI 屏幕上的成本。\n- 场景不加载外部资源,墙角、堆叠物体和窄缝可以直接观察接触遮蔽。\n"
|
|
1595
1836
|
},
|
|
1596
1837
|
{
|
|
1597
1838
|
"id": "getting-started",
|
|
@@ -1636,7 +1877,7 @@ export const docsIndex = [
|
|
|
1636
1877
|
"text": "下一步"
|
|
1637
1878
|
}
|
|
1638
1879
|
],
|
|
1639
|
-
"content": "# 快速上手 `u-space`\n\n`u-space` 是一个基于 Three.js 的 WebGPU 3D 引擎。本指南将向你展示如何搭建一个基础的 `u-space` 应用。\n\n## 安装\n\n确保已将 `@types/three` 和 `three` 安装为对等依赖。如果你计划使用交互功能,还需要安装 `camera-controls`。\n\n```bash\npnpm install three camera-controls\n```\n\n## 基础配置\n\n以下是一个最小示例,展示如何初始化 `u-space` 的 `Viewer` 并向场景中添加一个可交互对象。\n\n### 1. HTML 结构\n\n创建一个容纳 3D 查看器的元素。\n\n```html\n<!DOCTYPE html>\n<html lang=\"zh\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>u-space App</title>\n </head>\n <body style=\"margin: 0\">\n <div id=\"app\" style=\"width: 100vw; height: 100vh\"></div>\n <!-- 脚本放这里 -->\n </body>\n</html>\n```\n\n### 2. Import Maps\n\n由于 `u-space` 依赖 Three.js 的 WebGPU 特性,建议配置 Import Map 以解析 Three.js 的 WebGPU 构建版本。\n\n```html\n<script type=\"importmap\">\n {\n \"imports\": {\n \"three\": \"/node_modules/three/build/three.webgpu.js\",\n \"three/webgpu\": \"/node_modules/three/build/three.webgpu.js\",\n \"three/addons/\": \"/node_modules/three/examples/jsm/\",\n \"camera-controls\": \"/node_modules/camera-controls/dist/camera-controls.module.js\",\n \"u-space\": \"path/to/u-space/dist/index.js\"\n }\n }\n</script>\n```\n\n### 3. 初始化 Viewer 并添加对象\n\n初始化 `Viewer` 并向场景中添加一个基础 Three.js 对象。\n\n```html\n<script type=\"module\">\n import { Color, GridHelper, Mesh, BoxGeometry, MeshBasicMaterial } from 'three/webgpu';\n import { Viewer } from 'u-space';\n\n const app = document.getElementById('app');\n\n // 初始化查看器\n const viewer = new Viewer({\n el: app,\n rendererOptions: { forceWebGL: false }, // 优先使用 WebGPU\n });\n await viewer.init();\n\n // 设置背景颜色\n viewer.scene.background = new Color(0x666666);\n\n // 添加网格辅助线\n const gridHelper = new GridHelper(10, 10);\n viewer.scene.add(gridHelper);\n\n // 添加一个立方体\n const geometry = new BoxGeometry(1, 1, 1);\n const material = new MeshBasicMaterial({ color: 0xff0000 });\n const box = new Mesh(geometry, material);\n box.position.set(0, 0.5, 0);\n viewer.scene.add(box);\n\n // 渲染场景\n viewer.render();\n</script>\n```\n\n### 4. 启用交互\n\n`u-space` 内置了 `InteractionManager`,可以轻松为 3D 对象添加事件监听。\n\n```javascript\n// 启用指针移动事件(pointerenter / pointerleave 需要此开关)\nviewer.interactionManager.pointerMoveEventsEnabled = true;\n\n// 添加点击事件\nbox.addEventListener('click', (e) => {\n console.log('点击位置:', e.event.intersect.point);\n material.color.set(Math.random() * 0xffffff);\n viewer.render(); // 请求新一帧渲染\n});\n\n// 添加悬停事件\nbox.addEventListener('pointerenter', (e) => {\n document.body.style.cursor = 'pointer';\n material.color.set(0x00ff00);\n viewer.render();\n});\n\nbox.addEventListener('pointerleave', (e) => {\n document.body.style.cursor = 'default';\n material.color.set(0xff0000);\n viewer.render();\n});\n```\n\n这样就完成了!你现在拥有一个基础的 `u-space` 应用。\n\n## 版本信息\n\n`u-space` 导出了 `version` 常量,同时在 `window.__USPACE__` 上挂载了版本信息,方便调试和版本检查。\n\n```typescript\nimport { version } from 'u-space';\nconsole.log(version); // e.g. '0.0.
|
|
1880
|
+
"content": "# 快速上手 `u-space`\n\n`u-space` 是一个基于 Three.js 的 WebGPU 3D 引擎。本指南将向你展示如何搭建一个基础的 `u-space` 应用。\n\n## 安装\n\n确保已将 `@types/three` 和 `three` 安装为对等依赖。如果你计划使用交互功能,还需要安装 `camera-controls`。\n\n```bash\npnpm install three camera-controls\n```\n\n## 基础配置\n\n以下是一个最小示例,展示如何初始化 `u-space` 的 `Viewer` 并向场景中添加一个可交互对象。\n\n### 1. HTML 结构\n\n创建一个容纳 3D 查看器的元素。\n\n```html\n<!DOCTYPE html>\n<html lang=\"zh\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>u-space App</title>\n </head>\n <body style=\"margin: 0\">\n <div id=\"app\" style=\"width: 100vw; height: 100vh\"></div>\n <!-- 脚本放这里 -->\n </body>\n</html>\n```\n\n### 2. Import Maps\n\n由于 `u-space` 依赖 Three.js 的 WebGPU 特性,建议配置 Import Map 以解析 Three.js 的 WebGPU 构建版本。\n\n```html\n<script type=\"importmap\">\n {\n \"imports\": {\n \"three\": \"/node_modules/three/build/three.webgpu.js\",\n \"three/webgpu\": \"/node_modules/three/build/three.webgpu.js\",\n \"three/addons/\": \"/node_modules/three/examples/jsm/\",\n \"camera-controls\": \"/node_modules/camera-controls/dist/camera-controls.module.js\",\n \"u-space\": \"path/to/u-space/dist/index.js\"\n }\n }\n</script>\n```\n\n### 3. 初始化 Viewer 并添加对象\n\n初始化 `Viewer` 并向场景中添加一个基础 Three.js 对象。\n\n```html\n<script type=\"module\">\n import { Color, GridHelper, Mesh, BoxGeometry, MeshBasicMaterial } from 'three/webgpu';\n import { Viewer } from 'u-space';\n\n const app = document.getElementById('app');\n\n // 初始化查看器\n const viewer = new Viewer({\n el: app,\n rendererOptions: { forceWebGL: false }, // 优先使用 WebGPU\n });\n await viewer.init();\n\n // 设置背景颜色\n viewer.scene.background = new Color(0x666666);\n\n // 添加网格辅助线\n const gridHelper = new GridHelper(10, 10);\n viewer.scene.add(gridHelper);\n\n // 添加一个立方体\n const geometry = new BoxGeometry(1, 1, 1);\n const material = new MeshBasicMaterial({ color: 0xff0000 });\n const box = new Mesh(geometry, material);\n box.position.set(0, 0.5, 0);\n viewer.scene.add(box);\n\n // 渲染场景\n viewer.render();\n</script>\n```\n\n### 4. 启用交互\n\n`u-space` 内置了 `InteractionManager`,可以轻松为 3D 对象添加事件监听。\n\n```javascript\n// 启用指针移动事件(pointerenter / pointerleave 需要此开关)\nviewer.interactionManager.pointerMoveEventsEnabled = true;\n\n// 添加点击事件\nbox.addEventListener('click', (e) => {\n console.log('点击位置:', e.event.intersect.point);\n material.color.set(Math.random() * 0xffffff);\n viewer.render(); // 请求新一帧渲染\n});\n\n// 添加悬停事件\nbox.addEventListener('pointerenter', (e) => {\n document.body.style.cursor = 'pointer';\n material.color.set(0x00ff00);\n viewer.render();\n});\n\nbox.addEventListener('pointerleave', (e) => {\n document.body.style.cursor = 'default';\n material.color.set(0xff0000);\n viewer.render();\n});\n```\n\n这样就完成了!你现在拥有一个基础的 `u-space` 应用。\n\n## 版本信息\n\n`u-space` 导出了 `version` 常量,同时在 `window.__USPACE__` 上挂载了版本信息,方便调试和版本检查。\n\n```typescript\nimport { version } from 'u-space';\nconsole.log(version); // e.g. '0.0.30'\n\n// 也可以通过全局变量访问\nconsole.log(window.__USPACE__.version);\n```\n\n## 下一步\n\n- 深入了解 [Viewer API](./api-viewer.md)\n- 探索如何 [加载模型与使用对象](./api-objects.md)\n- 理解 [交互系统](./api-interactions.md)\n- 使用 [Managers API](./api-managers.md) 注册和检索对象\n- 通过插件添加 [瓦片地图](./api-plugin-tiles.md)、[小地图](./api-plugin-minimap.md)、[键盘控制](./api-plugin-keyboard-controls.md) 等功能\n- 使用 [Animations API](./api-animations.md) 为属性添加动画\n- 使用 [Effects API](./api-effects.md) 应用视觉特效\n- 浏览 [示例指南](./examples-guide.md)\n"
|
|
1640
1881
|
},
|
|
1641
1882
|
{
|
|
1642
1883
|
"id": "index",
|
|
@@ -1659,15 +1900,19 @@ export const docsIndex = [
|
|
|
1659
1900
|
{
|
|
1660
1901
|
"level": 3,
|
|
1661
1902
|
"text": "MCP"
|
|
1903
|
+
},
|
|
1904
|
+
{
|
|
1905
|
+
"level": 3,
|
|
1906
|
+
"text": "维护"
|
|
1662
1907
|
}
|
|
1663
1908
|
],
|
|
1664
|
-
"content": "---\nlayout: home\n\nhero:\n name: u-space\n text: WebGPU-ready 3D Engine\n tagline: 基于 Three.js 的插件化 3D 可视化库,支持 GIS 瓦片、动画、场景管理与交互系统。\n actions:\n - theme: brand\n text: 快速上手\n link: /getting-started\n - theme: alt\n text: API 文档\n link: /api-viewer\n - theme: alt\n text: 在线示例\n link: /examples/\n target: _self\n\nfeatures:\n - title: WebGPU 优先\n details: 基于 Three.js WebGPU 渲染器,支持 TSL(Three Shading Language)着色器节点,同时保持 WebGL 兼容回退。\n - title: 插件化架构\n details: 核心轻量,通过独立插件按需扩展:GIS 瓦片、小地图、键盘控制、场景管理、曲线动画等。\n - title: 交互系统\n details: 内置射线检测与事件冒泡,像操作 DOM 元素一样为 3D 对象添加 click、pointerenter 等事件。\n---\n\n## 文档目录\n\n### 核心 API\n\n| 文档 | 说明 |\n| :--- | :--- |\n| [Viewer](./api-viewer) |
|
|
1909
|
+
"content": "---\nlayout: home\n\nhero:\n name: u-space\n text: WebGPU-ready 3D Engine\n tagline: 基于 Three.js 的插件化 3D 可视化库,支持 GIS 瓦片、动画、场景管理与交互系统。\n actions:\n - theme: brand\n text: 快速上手\n link: /getting-started\n - theme: alt\n text: API 文档\n link: /api-viewer\n - theme: alt\n text: 在线示例\n link: /examples/\n target: _self\n\nfeatures:\n - title: WebGPU 优先\n details: 基于 Three.js WebGPU 渲染器,支持 TSL(Three Shading Language)着色器节点,同时保持 WebGL 兼容回退。\n - title: 插件化架构\n details: 核心轻量,通过独立插件按需扩展:GIS 瓦片、小地图、键盘控制、场景管理、曲线动画等。\n - title: 交互系统\n details: 内置射线检测与事件冒泡,像操作 DOM 元素一样为 3D 对象添加 click、pointerenter 等事件。\n---\n\n## 文档目录\n\n### 核心 API\n\n| 文档 | 说明 |\n| :--- | :--- |\n| [Viewer](./api-viewer) | 核心类:渲染器、场景、相机、控制器、事件,以及实验性 OffscreenCanvas Worker runtime |\n| [CameraControls](./api-camera-controls) | 相机控制:飞行、视角切换、视点过渡 |\n| [CSSRenderer](./api-css-renderer) | CSS 渲染器:CSS2D / CSS2.5D / CSS3D 叠加 HTML 元素 |\n| [RenderPipeline](./api-render-pipeline) | 后处理管线:Bloom、SSGI、SSR 时空降噪、TRAA、自定义后处理 |\n| [Objects](./api-objects) | 3D 对象:统一 `InstanceObject` API、模型加载、基础网格、Poi、Topology |\n| [Batches](./api-batches) | 公共批处理:`ModelInstancedLayer`、`EditableGeometryBatchLayer`、fallback、materialize 与 raycast remap |\n| [Interactions](./api-interactions) | 交互管理:射线检测、鼠标/触摸事件、框选 |\n| [Managers](./api-managers) | 对象管理、场景管理、灯光管理 |\n| [Animations](./api-animations) | 补间动画:`tweenAnimation`、`Tween`、缓动模式 |\n| [Effects](./api-effects) | 视觉特效:高亮、呼吸、线框、淡入淡出、流动、流体(TSL) |\n| [Tools](./api-tools) | 工具:测量(距离/面积/角度)、剖切(面/盒)、标注管理 |\n\n### 插件\n\n所有插件从 `u-space/plugins/<name>` 导入。\n\n| 插件 | 说明 |\n| :--- | :--- |\n| [keyboard-controls](./api-plugin-keyboard-controls) | WASD / 方向键控制相机移动与旋转 |\n| [minimap](./api-plugin-minimap) | 2D 小地图叠加层 |\n| [tiles](./api-plugin-tiles) | ArcGIS 3D 瓦片 / GIS 地球渲染 |\n| [u-manager](./api-plugin-u-manager) | 场景加载、可编辑静态 Geometry batching、模型 instancing、语义楼层/井道/设备、拓扑、动画、属性与视点管理 |\n| [curve-movement](./api-plugin-curve-movement) | 沿样条曲线移动相机或对象 |\n| [tracking-controls](./api-plugin-tracking-controls) | 相机跟随移动目标 |\n| [atmosphere](./api-plugin-atmosphere) | 天空背景、天空环境、太阳/月亮光照与阴影 |\n| [fire](./api-plugin-fire) | WebGPU 体积火焰、烟雾、湍流、动态光照与阴影 |\n| [object-controls](./api-plugin-object-controls) | 交互式移动、旋转、缩放 3D 对象 |\n| [topology-drawer](./api-plugin-topology-drawer) | 在场景任意表面交互式绘制拓扑路径图 |\n\n### MCP\n\n| 文档 | 说明 |\n| :--- | :--- |\n| [u-space MCP](./mcp) | 面向 Mastra、Claude Desktop、Cursor 等 MCP 客户端的只读文档检索服务器 |\n\n### 维护\n\n| 文档 | 说明 |\n| :--- | :--- |\n| [一键发布](./release) | 通过 GitHub Actions、npm Trusted Publishing 和 Vercel 发布 `u-space`、`u-space-mcp`、docs 与 examples |\n"
|
|
1665
1910
|
},
|
|
1666
1911
|
{
|
|
1667
1912
|
"id": "mcp",
|
|
1668
1913
|
"path": "mcp.md",
|
|
1669
1914
|
"title": "u-space MCP",
|
|
1670
|
-
"summary": "`u-space-mcp` 是面向 `u-space` 文档的 Model Context Protocol(MCP)服务器。它把当前文档打包成只读 MCP 工具,方便 Mastra、Claude Desktop、Cursor 等 MCP 客户端在回答 `u-space` API
|
|
1915
|
+
"summary": "`u-space-mcp` 是面向 `u-space` 文档的 Model Context Protocol(MCP)服务器。它把当前文档打包成只读 MCP 工具,方便 Mastra、Claude Desktop、Cursor 等 MCP 客户端在回答 `u-space` API、插件和示例问题时直接检索官方文档,包括实验性 `u-space/worker` OffscreenCanvas runtime、`OffscreenViewerHost`、top-level await `createWorkerViewer()`、Worker command/事件桥接、Three.js 原生静态场景矩阵策略及其与 `setEditableBatching()` 的组合;核心 `src/batches` 导出的 `ModelInstancedLayer` 与 `EditableGeometryB",
|
|
1671
1916
|
"headings": [
|
|
1672
1917
|
{
|
|
1673
1918
|
"level": 1,
|
|
@@ -1691,7 +1936,11 @@ export const docsIndex = [
|
|
|
1691
1936
|
},
|
|
1692
1937
|
{
|
|
1693
1938
|
"level": 2,
|
|
1694
|
-
"text": "
|
|
1939
|
+
"text": "OffscreenCanvas Worker 检索范围"
|
|
1940
|
+
},
|
|
1941
|
+
{
|
|
1942
|
+
"level": 2,
|
|
1943
|
+
"text": "InstanceObject 与 u-manager 检索范围"
|
|
1695
1944
|
},
|
|
1696
1945
|
{
|
|
1697
1946
|
"level": 2,
|
|
@@ -1702,7 +1951,48 @@ export const docsIndex = [
|
|
|
1702
1951
|
"text": "包结构"
|
|
1703
1952
|
}
|
|
1704
1953
|
],
|
|
1705
|
-
"content": "# u-space MCP\n\n`u-space-mcp` 是面向 `u-space` 文档的 Model Context Protocol(MCP)服务器。它把当前文档打包成只读 MCP 工具,方便 Mastra、Claude Desktop、Cursor 等 MCP 客户端在回答 `u-space` API
|
|
1954
|
+
"content": "# u-space MCP\n\n`u-space-mcp` 是面向 `u-space` 文档的 Model Context Protocol(MCP)服务器。它把当前文档打包成只读 MCP 工具,方便 Mastra、Claude Desktop、Cursor 等 MCP 客户端在回答 `u-space` API、插件和示例问题时直接检索官方文档,包括实验性 `u-space/worker` OffscreenCanvas runtime、`OffscreenViewerHost`、top-level await `createWorkerViewer()`、Worker command/事件桥接、Three.js 原生静态场景矩阵策略及其与 `setEditableBatching()` 的组合;核心 `src/batches` 导出的 `ModelInstancedLayer` 与 `EditableGeometryBatchLayer`;`u-manager` 的 `UManagerLoader` 一体化加载入口、`SceneLoader` 语义去重、path-based model instancing 和 scene-specific editable batching adapter、Semantic/Facilities API,以及 atmosphere 和 fire 插件的 WebGPU 效果。\n\n## 安装与启动\n\n推荐通过 `npx` 使用已发布的 npm 包:\n\n```json\n{\n \"mcpServers\": {\n \"uSpace\": {\n \"command\": \"npx\",\n \"args\": [\"-y\", \"u-space-mcp@latest\"]\n }\n }\n}\n```\n\n本地开发文档时,可以通过 `--docs` 指向仓库里的 docs 目录:\n\n```json\n{\n \"mcpServers\": {\n \"uSpace\": {\n \"command\": \"npx\",\n \"args\": [\n \"-y\",\n \"u-space-mcp@latest\",\n \"--docs\",\n \"/Users/jiangqi/Desktop/xunwei/SoonSpace/u-space/docs\"\n ]\n }\n }\n}\n```\n\n也可以使用环境变量:\n\n```bash\nU_SPACE_DOCS_PATH=/path/to/u-space/docs npx -y u-space-mcp@latest\n```\n\n## Mastra 接入\n\n在 Mastra 中通过 `MCPClient` 使用 stdio 形式接入:\n\n```typescript\nimport { MCPClient } from '@mastra/mcp';\n\nexport const uSpaceMcpClient = new MCPClient({\n id: 'u-space-mcp-client',\n servers: {\n uSpace: {\n command: 'npx',\n args: ['-y', 'u-space-mcp@latest'],\n stderr: 'pipe',\n },\n },\n});\n```\n\n然后把工具挂到 Agent 上:\n\n```typescript\nexport const codingAgent = new Agent({\n // ...\n tools: {\n ...(await uSpaceMcpClient.listTools()),\n },\n});\n```\n\n## 工具列表\n\n`u-space-mcp` 只提供只读工具:\n\n| 工具 | 说明 |\n| :--- | :--- |\n| `list_docs` | 列出可用文档页 |\n| `search_docs` | 按关键词、API 名、插件名或概念检索文档 |\n| `get_doc` | 按 path 或 id 读取单篇文档 |\n| `get_api_reference` | 面向 API / 类 / 方法 / 插件的检索 |\n| `find_examples` | 查找示例和用法相关文档 |\n| `get_changelog` | 读取更新日志 |\n\n## 示例检索\n\nMCP 文档索引包含 `examples/test_umanager_loader.html`、`examples/test_umanager_dynamic_instances.html`、`examples/test_umanager2.html` 和 `examples/offscreen/test_umanager2_offscreen.html` 的说明。检索 `UManagerLoader example`、`test_umanager_loader` 或 `UManagerLoader 用法` 可以找到一体化加载示例;检索 `dynamic instances`、`getInstanceById` 或 `test_umanager_dynamic_instances` 可以找到运行时实例编辑示例;检索 `setEditableBatching`、`SceneEditableBatchLayer`、`SceneEditableBatchFallback`、`editable batching`、`materialize` 或 `test_umanager2` 可以找到大型静态场景合批及其与 `SceneInstancedLayer` fallback 的关系;检索 `OffscreenCanvas`、`OffscreenViewerHost`、`host.init`、`initialized`、`ready`、`createWorkerViewer`、`top-level await`、`u-space/worker`、`Worker WebGPU` 或 `test_umanager2_offscreen` 可以找到 Worker 渲染、生命周期、事件/command 桥接、editable batching、pipeline 预热和主线程/GPU 性能边界。\n\n## OffscreenCanvas Worker 检索范围\n\n| 关键词 / API | 可检索内容 |\n| :----------- | :--------- |\n| `OffscreenViewerHost` | 主线程 canvas 所有权、完整构造选项/方法/事件、`transferControlToOffscreen()`、pointer/wheel/resize 转发、renderer 滚动统计和 `request()` command 调用;`host.init()` / `initialized` 只等待 `viewer.init()`,业务资源完成由一次性 `ready` 表示。还包括 init canvas transfer/post 失败或初始化期间 dispose 的 Promise rejection/资源清理,以及 dispose 后忽略迟到业务消息的语义。 |\n| `createWorkerViewer` | module Worker 顶层可直接 await 的命令式入口;同步安装 host 消息监听,完成 OffscreenCanvas/`viewer.init()` 后返回 `WorkerViewerRuntime`,提供状态、事件、command、逆序 dispose 和一次性业务 ready 生命周期、ready 前 fatal cleanup、去重 ArrayBuffer transfer,以及内置 `getStats` / `getViewpoint` / `setViewpoint` / `render` 命令。 |\n| `test_umanager2_offscreen` | UManager Worker 示例启动方式、HDR/语义/场景加载、`setEditableBatching()`、`compileAsync()` 首帧预热、滚动帧率 HUD、标准透明合成、静态场景矩阵冻结/显式提交,以及默认隐藏半透明语义分析叠加层以保持楼壳原始材质的策略。 |\n| `matrixWorldAutoUpdate` / `updateMatrixWorld` / `updateWorldMatrix` | 使用 Three.js 原生 API 冻结静态 Scene,按需提交单个对象或新增子树,并通过 `viewer.invalidate()` 请求新帧;单个 `InstanceObject.updateWorldMatrix(true, false)` 或父 Group 的 `updateWorldMatrix(true, true)` 仍会触发 dirty callback、instanced buffer 同步和 editable-batch materialize,相机矩阵继续独立更新。 |\n| `OffscreenCanvas` + `setEditableBatching` | Offscreen 移走主线程解析/合并/渲染提交,editable batching 减少 draw submission;Three.js 对象必须留在 Worker,业务对象操作通过 structured-clone command 调用。 |\n\n## InstanceObject 与 u-manager 检索范围\n\nMCP 文档索引会同步 `docs/api-objects.md` 中的核心 `InstanceObject` API,以及 `docs/api-plugin-u-manager.md` 中的 u-manager 子类、加载器和合批层 API。客户端可以直接检索以下关键词:\n\n| 关键词 / API | 可检索内容 |\n| :----------- | :--------- |\n| `UManagerLoader` | 同时加载 `SemanticLoader` 和 `SceneLoader`、自动复用 semantic id 去重、返回带 `semanticGroup` / `sceneGroup` 直接属性的 `UManagerSceneGroup`;可通过 `semanticGroup.getDefaultFacilityLayer()` 和 `sceneGroup.getDefaultSceneLayer()` 直接访问默认合批层。 |\n| `InstanceObject` | 核心 `u-space` 导出 `InstanceObject`、`InstanceStyle`、`InstanceObjectOptions`、`InstanceIdentity`、`InstanceMaterializer`、`INSTANCE_COLOR_MODE_*` 和 `isInstanceObject()` 类型守卫;包括身份、样式、bounds、dirty、fallback render object,以及公共 `setInstanceMaterializer()` / ownership-aware `clearInstanceMaterializer()` / `materialize()` 协议;默认自动矩阵遍历和冻结根 Scene 后显式 `updateWorldMatrix()` 都会同步 batching 状态。实例可直接用于 `controls.flyToObject()`。 |\n| `ModelInstancedLayer` | `SceneInstancedLayer` 与 `FacilityInstancedLayer` 的共享实现:模板 mesh 保持独立以保留透明排序和局部包围体,复杂静态几何在射线通过实例包围体后按需建立并复用 `three-mesh-bvh`;普通静态模型可对明确的热点 root 显式调用 `enableMeshBVHRaycast(root)`,不对整个大场景自动建树;同时提供合法 material groups、多材质 instancing、`getInstances()`、`getInstanceById()`、运行时实例新增/删除、capacity 预分配与扩容、重复 `instanceId` 拒绝加入、身份字段变化后按需刷新 id 索引、dirty-driven buffer sync、transform 变化后渲染前同步 instance matrix、raycast hit remap 和可选相机实例裁剪;内部索引使用实例独立字段,不依赖 `userData`。 |\n| `EditableGeometryBatchLayer` | 核心 `src/batches` one-shot 公共类,接收 `Model` 模板和不重复的 `InstanceObject[]` source,按材质/Geometry 布局/阴影/renderOrder 烘焙合并静态 Geometry;每个 layer 独立维护矩阵状态,使用 `batchObjectIndex` + Float `DataTexture` + TSL 立即同步 dirty CPU 状态,返回带 `reasons` 的 unsupported source,支持同帧世界矩阵提交、跨 layer 旧副本隐藏、独立材质 materialize、typed event、materialized BVH hit 过滤、按实例 negative-scale fallback、安全 ownership dispose 和统计。 |\n| `SceneLoader` | 场景树加载、语义 `ID` 去重、返回 `SceneGroup`、默认重复 3D 模型 path instancing,以及 opt-in `setEditableBatching({ maxVerticesPerBatch, maxIndicesPerBatch, freezeAnimations })` 静态 Geometry 合批;unsupported template/子 Mesh 安全 fallback,负缩放实例绕过 `InstancedMesh`,build 期间已 materialize 的实例不会重复进入透明 fallback。 |\n| `SceneGroup` | `SceneLoader.loadAsync()` 返回根组,保留原始场景树层级,并通过 `sceneLayer` / `getDefaultSceneLayer()` 暴露主渲染层:默认是 `SceneInstancedLayer`,opt-in editable batching 且存在 merged Mesh 时是与其没有继承关系的 `SceneEditableBatchLayer`;只有 fallback 而无 merged Mesh 时返回 `null`。editable batch 统计位于 `userData.editableBatch`。 |\n| `SceneInstanceObject` | `SceneLoader` 的单模型逻辑引用、`id` / `sid` 检索、`instanceKind = 'SceneInstances'`、统一显隐/颜色/透明度/高亮/bounds/dirty API,以及 editable batch 的 `materialize()`;世界 transform 或半透明变化时可只恢复当前普通 `Model`。 |\n| `SceneInstancedLayer` | `SceneLoader` 内部批量渲染层、共享 `ModelInstancedLayer`、dirty-driven instance buffer 同步、raycast hit remap。 |\n| `SceneEditableBatchLayer` | `EditableGeometryBatchLayer<SceneInstanceObject>` 的 u-manager 薄适配层,作为 opt-in 静态 Geometry 合并主层;通用 batching、materialize、内部 `BaseMesh`、不可见 raycast 短路和 lazy BVH 由核心层实现。`SceneLoader` 负责 URL source 与同级 `SceneEditableBatchFallback`(`SceneInstancedLayer`)/普通 `Model` fallback,并监听 typed `materialize` 事件移除对应 fallback instance。 |\n| `FacilityInstanceObject` | 楼层下的设备引用、`objectManager.getById()` 全局检索、对象级包围盒缓存 + `controls.flyToObject()` 飞向单设备、统一 `setInstance*` 控制、fallback `Model` wrapper 和事件目标。 |\n| `FacilityInstancedLayer` | `SemanticGroup.facilityLayer` / `SemanticGroup.getDefaultFacilityLayer()`、`getInstances()`、`getInstanceById()`、`reserveBatch()`、`addInstance()` / `addInstances()`、`removeInstance()` / `removeInstances()`(支持单个 `instanceId` 字符串)、`removeBatch()`、`clearBatches()`、batch 创建条件、`setInstanceCulling()`、动态 Facilities batch、可选按相机视锥压缩 active instances、可选 `minScreenRadius` 屏幕尺寸裁剪、dirty-driven instance buffer 同步和 raycast hit remap。 |\n| `Facilities` | `SemanticLoader` 解析、`FloorMesh.getFacilityById()`、`FacilityInstanceObject.setInstanceOpacity()`、普通 `Model` fallback wrapper 与 scene-level instancing 的一致 API;`SemanticGroup` / `BuildingGroup` / `FloorMesh` 查询使用对象语义 ID、实例 `instanceId` 或显式别名,不扫描 `userData` ID。 |\n\n## fire 检索范围\n\nMCP 文档索引会同步 `docs/api-plugin-fire.md` 中的 FireEffect API、参数和示例。客户端可以直接检索以下关键词:\n\n| 关键词 / API | 可检索内容 |\n| :----------- | :--------- |\n| `FireEffect` | 插件创建、`enable()`、`update()`、`setEmitter()`、`reset()`、`disable()` 和 `dispose()`。 |\n| `fire` / `volumetric fire` | WebGPU 体积火焰、烟雾散射、curl noise、buoyancy、Jacobi pressure projection 和 raymarch 输出。 |\n| `emitterGeometry` / `emitterRadius` | 默认 `TeapotGeometry(0.8, 28)`、自定义 emitter 几何、业务 `Object3D` matrix 驱动和移动扰动半径。 |\n| `size` / `gridSize` | 体积盒世界范围、烟雾扩散边界和 3D 模拟网格尺寸。 |\n| `addOutputEffect` | `FireEffect` 通过 RenderPipeline output effect 叠加体积 pass,不占用业务 `setOutputComposer()`。 |\n| `keyLight` / `pointLight` | 体积阴影 spot light、跟随火焰核心的动态点光源和可关闭的内置光源。 |\n\n## 包结构\n\n源码位于 `packages/u-space-mcp`。本地兼容发布入口会先递增 `u-space-mcp` 的 patch 版本,再从 `docs/*.md` 生成内置文档索引、编译并通过当前 npm 登录态发布,因此启用 npm 2FA 时仍会请求 OTP:\n\n```bash\npnpm publish:mcp\n```\n\n维护者发布完整版本时应使用 [`pnpm release:all`](./release)。该命令触发 GitHub Actions,通过 npm Trusted Publishing/OIDC 同时发布 `u-space` 与 `u-space-mcp`,不需要 `NPM_TOKEN` 或每次输入 OTP,并继续部署 docs 与 examples。\n\n发布后的包不依赖本机绝对路径;只有在传入 `--docs` 或设置 `U_SPACE_DOCS_PATH` 时,才会读取本地文档目录。\n"
|
|
1955
|
+
},
|
|
1956
|
+
{
|
|
1957
|
+
"id": "release",
|
|
1958
|
+
"path": "release.md",
|
|
1959
|
+
"title": "一键发布",
|
|
1960
|
+
"summary": "仓库提供一个手动 GitHub Actions 工作流,一次发布四个交付面: 1. `u-space` npm 包 2. `u-space-mcp` npm 包",
|
|
1961
|
+
"headings": [
|
|
1962
|
+
{
|
|
1963
|
+
"level": 1,
|
|
1964
|
+
"text": "一键发布"
|
|
1965
|
+
},
|
|
1966
|
+
{
|
|
1967
|
+
"level": 2,
|
|
1968
|
+
"text": "一次性配置"
|
|
1969
|
+
},
|
|
1970
|
+
{
|
|
1971
|
+
"level": 3,
|
|
1972
|
+
"text": "1. 配置两个 npm Trusted Publisher"
|
|
1973
|
+
},
|
|
1974
|
+
{
|
|
1975
|
+
"level": 3,
|
|
1976
|
+
"text": "2. 配置 Vercel Token"
|
|
1977
|
+
},
|
|
1978
|
+
{
|
|
1979
|
+
"level": 2,
|
|
1980
|
+
"text": "发布"
|
|
1981
|
+
},
|
|
1982
|
+
{
|
|
1983
|
+
"level": 2,
|
|
1984
|
+
"text": "发布顺序"
|
|
1985
|
+
},
|
|
1986
|
+
{
|
|
1987
|
+
"level": 2,
|
|
1988
|
+
"text": "失败重试"
|
|
1989
|
+
},
|
|
1990
|
+
{
|
|
1991
|
+
"level": 2,
|
|
1992
|
+
"text": "参考"
|
|
1993
|
+
}
|
|
1994
|
+
],
|
|
1995
|
+
"content": "# 一键发布\n\n仓库提供一个手动 GitHub Actions 工作流,一次发布四个交付面:\n\n1. `u-space` npm 包\n2. `u-space-mcp` npm 包\n3. VitePress 文档\n4. `/examples/` 在线示例\n\nnpm 发布使用 Trusted Publishing/OIDC,不保存 `NPM_TOKEN`,日常发布不再要求输入 OTP。docs 和 examples 使用同一个 Vercel production deployment;`scripts/build-vercel-docs.mjs` 会在文档构建完成后复制 `examples/`,并将发布中的 `u-space` 版本写入部署产物的 import map。\n\n## 一次性配置\n\n### 1. 配置两个 npm Trusted Publisher\n\n分别打开 npmjs.com 上 `u-space` 和 `u-space-mcp` 的 package settings,在 **Trusted Publisher** 中添加相同的 GitHub Actions 配置:\n\n| 字段 | 值 |\n| :--- | :--- |\n| Organization or user | `spatial-claw` |\n| Repository | `u-space` |\n| Workflow filename | `release.yml` |\n| Environment | 留空 |\n| Allowed actions | `npm publish` |\n\n两个包都必须单独配置一次。工作流运行在 GitHub-hosted runner,授予 `id-token: write`,使用 Node.js 24 和满足 Trusted Publishing 要求的 npm 11;package metadata 中的 repository URL 已统一为 `https://github.com/spatial-claw/u-space`。\n\n配置成功后不需要创建 `NPM_TOKEN`。npm 官方也建议在验证 Trusted Publisher 可用后,将传统 publishing access 调整为 “Require two-factor authentication and disallow tokens”。\n\n### 2. 配置 Vercel Token\n\n仓库已经提交 `.vercel/project.json`,因此 GitHub Actions 只需要一个 repository secret:\n\n```bash\ngh secret set VERCEL_TOKEN\n```\n\n输入有权部署 `u-space` Vercel 项目的 token。工作流会依次运行 `vercel pull`、`vercel build --prod` 和 `vercel deploy --prebuilt --prod`。\n\n工作流已经显式声明 `contents: write` 和 `id-token: write`。因此组织或仓库可以继续保持默认的只读 `GITHUB_TOKEN` 权限;GitHub 会按该 workflow 的最小权限声明授权。工作流需要把版本提交和 release tags 推回 `main`,如果 `main` 启用了 branch protection,还需要允许该 release workflow 写入,或为 `github-actions[bot]` 配置对应 bypass。\n\n## 发布\n\n默认递增两个 npm 包的 patch 版本,并等待 GitHub Actions 完成:\n\n```bash\npnpm release:all\n```\n\n也可以显式选择版本级别:\n\n```bash\npnpm release:all patch\npnpm release:all minor\npnpm release:all major\n```\n\n不想在本地等待时使用:\n\n```bash\npnpm release:all patch --no-wait\n```\n\n也可以打开 GitHub 仓库的 **Actions → Release all → Run workflow**,选择 `patch`、`minor` 或 `major` 后运行。\n\n## 发布顺序\n\n工作流按以下顺序执行:\n\n1. 递增 `u-space` 和 `u-space-mcp` 版本,同步 examples/docs 中的当前版本引用。\n2. 执行完整测试、库构建、MCP 文档索引构建和 VitePress 构建。\n3. 将版本元数据提交并推送到 `main`。\n4. 通过 npm OIDC 发布两个包;精确版本已经存在时自动跳过。\n5. 构建并部署 docs 和 examples 到 Vercel production。\n6. 推送 `vX.Y.Z` 和 `u-space-mcp-vX.Y.Z` 标签。\n\n`release-all` concurrency group 会阻止两次发布并发运行。\n\n## 失败重试\n\n版本提交已经推送,但 npm 或 Vercel 后续步骤失败时,不要再次选择 `patch`,否则会再递增一次版本。使用 `current` 重试当前提交版本:\n\n```bash\npnpm release:all current\n```\n\n工作流会跳过 registry 中已经存在的精确版本,只重试尚未完成的 npm 包、Vercel 部署和标签。\n\n本地旧命令 `pnpm release`、`pnpm publish:mcp` 和 `pnpm docs:deploy` 继续保留,但它们不使用 GitHub OIDC,可能请求 npm OTP 或本机 Vercel 登录。\n\n## 参考\n\n- [npm Trusted Publishing](https://docs.npmjs.com/trusted-publishers/)\n- [GitHub Actions OIDC](https://docs.github.com/en/actions/reference/security/oidc)\n- [Vercel GitHub Actions deployment](https://vercel.com/docs/git/vercel-for-github)\n"
|
|
1706
1996
|
}
|
|
1707
1997
|
];
|
|
1708
1998
|
//# sourceMappingURL=docs-index.js.map
|