u-space 0.0.26 → 0.0.28

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.
Files changed (42) hide show
  1. package/dist/index.cjs +3 -3
  2. package/dist/index.js +411 -373
  3. package/dist/plugins/atmosphere/index.cjs +1 -1
  4. package/dist/plugins/atmosphere/index.js +3255 -129
  5. package/dist/plugins/atmosphere/threeR185Compat.d.ts +2 -0
  6. package/dist/plugins/fire/FireEffect.d.ts +90 -0
  7. package/dist/plugins/fire/index.cjs +1 -0
  8. package/dist/plugins/fire/index.d.ts +1 -0
  9. package/dist/plugins/fire/index.js +433 -0
  10. package/dist/plugins/u-manager/index.cjs +60 -60
  11. package/dist/plugins/u-manager/index.d.ts +3 -0
  12. package/dist/plugins/u-manager/index.js +10070 -9660
  13. package/dist/plugins/u-manager/instances/SemanticInstanceObject.d.ts +37 -0
  14. package/dist/plugins/u-manager/instances/SemanticModelInstancedLayer.d.ts +33 -0
  15. package/dist/plugins/u-manager/instances/index.d.ts +2 -0
  16. package/dist/plugins/u-manager/loaders/SceneInstancedLayer.d.ts +13 -0
  17. package/dist/plugins/u-manager/loaders/SceneLoader.d.ts +5 -5
  18. package/dist/plugins/u-manager/loaders/UManagerLoader.d.ts +24 -0
  19. package/dist/plugins/u-manager/semantics/SemanticLoader.d.ts +2 -2
  20. package/dist/plugins/u-manager/semantics/SemanticParser.d.ts +3 -3
  21. package/dist/plugins/u-manager/semantics/index.d.ts +1 -1
  22. package/dist/plugins/u-manager/semantics/objects/BuildingGroup.d.ts +4 -10
  23. package/dist/plugins/u-manager/semantics/objects/FacilityInstancedLayer.d.ts +5 -24
  24. package/dist/plugins/u-manager/semantics/objects/FloorMesh.d.ts +30 -15
  25. package/dist/plugins/u-manager/semantics/objects/SemanticGroup.d.ts +26 -0
  26. package/dist/plugins/u-manager/semantics/types.d.ts +1 -0
  27. package/dist/src/effects/MaterialEffects.d.ts +1 -0
  28. package/dist/src/tools/AnnotationManager.d.ts +4 -1
  29. package/dist/src/viewers/RenderPipeline.d.ts +6 -5
  30. package/docs/api-effects.md +3 -0
  31. package/docs/api-interactions.md +1 -0
  32. package/docs/api-managers.md +1 -1
  33. package/docs/api-plugin-fire.md +174 -0
  34. package/docs/api-plugin-u-manager.md +249 -57
  35. package/docs/api-tools.md +1 -1
  36. package/docs/changelog.md +41 -8
  37. package/docs/examples-guide.md +39 -13
  38. package/docs/getting-started.md +1 -1
  39. package/docs/index.md +1 -0
  40. package/docs/mcp.md +34 -1
  41. package/package.json +4 -4
  42. package/dist/plugins/u-manager/semantics/objects/SemanticSceneGroup.d.ts +0 -31
@@ -0,0 +1,174 @@
1
+ # fire
2
+
3
+ 体积火焰插件,基于 Three.js 官方 `webgpu_volume_fire` 示例封装。插件在 GPU 上运行 3D 流体模拟,并通过独立的 volumetric pass 叠加到 `Viewer.renderPipeline` output effect 链中,提供火焰、烟雾、扰动、阴影和动态点光源效果。
4
+
5
+ ```typescript
6
+ import { Vector3 } from 'three/webgpu';
7
+ import { FireEffect } from 'u-space/plugins/fire';
8
+
9
+ const fire = new FireEffect(viewer, {
10
+ position: new Vector3(0, 0, 0),
11
+ size: new Vector3(12, 12, 24),
12
+ fireIntensity: 40,
13
+ smokeLifespan: 3.5,
14
+ });
15
+
16
+ fire.enable();
17
+ ```
18
+
19
+ ## 能力
20
+
21
+ - 按官方示例的 semi-Lagrangian advection、curl noise、buoyancy、Jacobi pressure projection 流程模拟体积火焰和烟雾。
22
+ - 使用 3D storage texture 保存速度、密度、温度、压力和 curl noise 数据。
23
+ - 默认使用 `TeapotGeometry(0.8, 28)` 作为 emitter 采样几何;也可以传入业务对象和自定义 `BufferGeometry`。
24
+ - 通过 `RenderPipeline.addOutputEffect()` 叠加体积结果,不占用业务侧的 `setOutputComposer()`。
25
+ - 内置 spot light 和 point light:spot light 负责体积阴影方向,point light 跟随火焰核心投射暖色动态光照。
26
+ - 监听 `cameraChange`,相机对象切换后会重建 fire 自己的 volumetric pass。
27
+
28
+ ## API
29
+
30
+ ### `new FireEffect(viewer, options?)`
31
+
32
+ 创建火焰效果实例。构造函数不会立即往场景写入模拟资源;调用 `enable()` 后才会创建体积盒、storage texture、compute pass、灯光和 output effect。
33
+
34
+ ### `enable(options?)`
35
+
36
+ 开启火焰效果。重复调用时等同于 `update(options)`。启用期间会把 `viewer.frameloop` 临时切到 `always`,关闭时恢复启用前的值。
37
+
38
+ ```typescript
39
+ fire.enable({
40
+ position: new Vector3(0, 0, 0),
41
+ size: new Vector3(12, 12, 24),
42
+ renderResolution: 0.5,
43
+ });
44
+ ```
45
+
46
+ ### `update(options?)`
47
+
48
+ 更新火焰参数。大多数视觉和模拟强度参数会通过 uniform 直接生效。`gridSize`、`emitterGeometry`、`volumeLayer` 或 `raymarchSteps` 改变时会重建相关资源。
49
+
50
+ ```typescript
51
+ fire.update({
52
+ emitTemperature: 6,
53
+ emitDensity: 8,
54
+ emitterRadius: 1.4,
55
+ smokeLifespan: 6,
56
+ });
57
+ ```
58
+
59
+ ### `setPosition(position)`
60
+
61
+ 更新体积盒底部中心位置。等同于 `update({ position })`。
62
+
63
+ ### `setEmitter(emitter, geometry?)`
64
+
65
+ 设置驱动 emitter 的对象和采样几何。`emitter` 的 `matrixWorld` 会在每帧写入模拟;如果只想使用默认内部 emitter,可以传入 `null`。
66
+
67
+ ### `reset()`
68
+
69
+ 清空当前模拟状态并重新创建模拟资源。
70
+
71
+ ### `disable()`
72
+
73
+ 关闭火焰效果,移除 `beforeRender` / `cameraChange` 监听、output effect、体积盒、灯光、storage texture 和 compute pass,并恢复启用前的 `viewer.frameloop`。
74
+
75
+ ### `dispose()`
76
+
77
+ 关闭效果并释放插件持有的默认 emitter geometry。
78
+
79
+ ## Options
80
+
81
+ | 参数 | 类型 | 默认值 | 说明 |
82
+ | --- | --- | --- | --- |
83
+ | `position` | `Vector3` | `(0, 0, 0)` | 体积盒底部中心世界坐标 |
84
+ | `size` | `Vector3` | `(12, 12, 24)` | 体积盒世界尺寸,也是烟雾扩散的约束范围 |
85
+ | `gridSize` | `{ x?: number; y?: number; z?: number }` | `{ x: 100, y: 100, z: 200 }` | 3D 模拟网格尺寸,越大越细腻也越耗显存 |
86
+ | `volumeLayer` | `number` | `10` | 体积盒所在 layer,用于隔离 volumetric pass |
87
+ | `emitterGeometry` | `BufferGeometry \| null` | `TeapotGeometry(0.8, 28)` | emit pass 采样的几何顶点 |
88
+ | `emitter` | `Object3D \| null` | 内部 `Object3D` | 驱动 emitter matrix 和扰动速度的对象 |
89
+ | `emitterRadius` | `number` | `1` | emitter 移动时影响流场的半径,作为 uniform 更新 |
90
+ | `simulate` | `boolean` | `true` | 是否推进流体模拟 |
91
+ | `simSpeed` | `number` | `1.2` | 模拟速度倍率 |
92
+ | `pressureIterations` | `number` | `2` | Jacobi pressure projection 次数,会自动向上限制为正偶数 |
93
+ | `raymarchSteps` | `number` | `16` | 体积 raymarch 步数,改变后会重建体积材质 |
94
+ | `renderResolution` | `number` | `0.5` | volumetric pass 分辨率倍率,范围 `0.1..1` |
95
+ | `denoise` | `boolean` | `true` | 是否对体积 pass 做 Gaussian blur |
96
+ | `denoiseStrength` | `number` | `0.5` | 降噪强度 |
97
+ | `bloom` | `boolean` | `true` | 是否对火焰输出追加 bloom |
98
+ | `bloomStrength` | `number` | `0.1` | bloom 强度 |
99
+ | `bloomRadius` | `number` | `1` | bloom 半径 |
100
+ | `bloomThreshold` | `number` | `0.5` | bloom 阈值 |
101
+ | `smokeLifespan` | `number` | `3.5` | 烟雾寿命,越大烟雾保留越久 |
102
+ | `smokeIntensity` | `number` | `1` | 烟雾散射可见度倍率 |
103
+ | `smokeColor` | `ColorRepresentation` | `0xffffff` | 烟雾散射颜色 |
104
+ | `fireLifespan` | `number` | `1.3` | 火焰温度衰减时间 |
105
+ | `turbulence` | `number` | `3.2` | 火焰和烟雾湍流强度 |
106
+ | `turbulenceDecay` | `number` | `0.1` | 湍流随年龄衰减速度 |
107
+ | `turbulenceFrequency` | `number` | `10` | curl noise 频率 |
108
+ | `buoyancy` | `number` | `3` | 热浮力 |
109
+ | `velocityDamping` | `number` | `0.25` | 速度阻尼 |
110
+ | `emitDensity` | `number` | `7` | emitter 注入密度 |
111
+ | `emitTemperature` | `number` | `5.5` | emitter 注入温度 |
112
+ | `motionBoost` | `number` | `0.25` | emitter 移动带来的额外发射量 |
113
+ | `windStrength` | `number` | `6.5` | emitter 移动对流场的扰动强度 |
114
+ | `fireIntensity` | `number` | `40` | 火焰发光强度 |
115
+ | `glowSpread` | `number` | `5` | 火焰亮度扩散范围 |
116
+ | `fireHue` | `number` | `0` | 火焰色相偏移,单位为度 |
117
+ | `saturation` | `number` | `1.1` | 输出饱和度 |
118
+ | `fireStartColor` | `ColorRepresentation` | `0xffe68c` | 高温火焰颜色 |
119
+ | `fireMidColor` | `ColorRepresentation` | `0xff7305` | 中段火焰颜色 |
120
+ | `fireEndColor` | `ColorRepresentation` | `0xff0000` | 低温火焰颜色 |
121
+ | `phaseAsymmetry` | `number` | `0` | Henyey-Greenstein 相函数 g 值 |
122
+ | `powderStrength` | `number` | `0.59` | powder effect 强度 |
123
+ | `multiScattering` | `number` | `1` | 多次散射近似强度 |
124
+ | `shadowAbsorption` | `number` | `2` | 体积阴影吸收系数 |
125
+ | `shadowAmbient` | `number` | `0.5` | 阴影环境补光 |
126
+ | `keyLight` | `boolean` | `true` | 是否创建体积阴影 spot light |
127
+ | `pointLight` | `boolean` | `true` | 是否创建火焰动态点光源 |
128
+ | `pointLightIntensity` | `number` | `1` | 动态点光源强度 |
129
+ | `pointLightDistance` | `number` | `40` | 动态点光源距离 |
130
+
131
+ ## Emitter
132
+
133
+ 如果不设置 `emitterGeometry`,插件会创建一个内部 `TeapotGeometry(0.8, 28)`,这和官方示例保持一致。emit pass 会遍历该几何的 position 顶点,把密度和温度写入体积纹理。传入自定义 `emitterGeometry` 时,该几何必须包含 `position` attribute。
134
+
135
+ 如果不设置 `emitter`,插件会使用内部 `Object3D`,并把它放在 `position` 位置。传入业务对象后,插件每帧读取该对象的 `matrixWorld` 和世界位置变化,移动速度会影响流场扰动和额外发射量。
136
+
137
+ ```typescript
138
+ const fire = new FireEffect(viewer, {
139
+ emitter: torchMesh,
140
+ emitterGeometry: torchFlameGeometry,
141
+ emitterRadius: 0.8,
142
+ });
143
+ ```
144
+
145
+ ## 光源与体积范围
146
+
147
+ 火焰本体不是场景里的普通透明 mesh 直接混合,而是先放在 `volumeLayer` 的体积盒里单独渲染,再通过 output effect 叠加到主场景。烟雾和火焰的可见范围由 `position + size` 定义的体积盒限制,超出盒子的密度会在边界处淡出。要让烟雾扩散范围更大,优先增大 `size`;要提升细节,再同步增大 `gridSize`。
148
+
149
+ 插件会按需创建两个光源:
150
+
151
+ - `FireEffect.keyLight`:`SpotLight`,用于体积阴影方向和烟雾散射计算。
152
+ - `FireEffect.pointLight`:`PointLight`,跟随 emitter / 火焰核心移动,并通过 TSL `colorNode` 产生随温度、密度和噪声变化的暖色照明。
153
+
154
+ 如果业务场景已经有自己的灯光,可以通过 `keyLight: false` 或 `pointLight: false` 关闭插件内置光源。
155
+
156
+ ## 与 RenderPipeline 的关系
157
+
158
+ `FireEffect` 使用 `viewer.renderPipeline.addOutputEffect()` 和 `removeOutputEffect()` 接入输出链。它不会调用 `setOutputComposer()`,因此不会覆盖业务或其他插件的 output effects。注意如果业务主动设置自定义 `setOutputComposer()`,RenderPipeline 会按自定义 composer 的语义跳过 output effects,火焰叠加也会被跳过。
159
+
160
+ `renderResolution` 只影响独立 volumetric pass 的分辨率;主场景分辨率仍由 `Viewer` 和 renderer 控制。启用 `denoise` 和 `bloom` 时,插件会在 output effect 内创建并释放对应节点。
161
+
162
+ ## 示例
163
+
164
+ 示例页见 `examples/test_fire.html`。本地运行时先构建插件:
165
+
166
+ ```bash
167
+ pnpm build:plugins -- plugins/fire
168
+ ```
169
+
170
+ 然后通过本地静态服务访问:
171
+
172
+ ```text
173
+ http://127.0.0.1:5174/examples/test_fire.html
174
+ ```
@@ -4,9 +4,29 @@
4
4
 
5
5
  所有加载器均继承自 Three.js 的 `Loader`,并提供 `setPath(path)` 方法,在调用 `loadAsync()` 前配置基础数据目录。
6
6
 
7
+ ## `UManagerLoader`
8
+
9
+ `UManagerLoader` 是推荐的一体化入口。它会在同一路径下同时加载 `SemanticLoader` 和 `SceneLoader`:语义文件中已经存在的 `id` 会由 `SceneLoader` 自动跳过,避免建筑、楼层、设备被重复创建;非语义场景树节点仍会正常加载。返回值是一个 `UManagerSceneGroup`,可直接通过 `semanticGroup` 访问 `SemanticGroup`,通过 `sceneGroup` 访问非语义场景组。
10
+
11
+ ```typescript
12
+ import { UManagerLoader } from 'u-space/plugins/u-manager';
13
+
14
+ const loader = new UManagerLoader(viewer);
15
+ loader.setPath('./scenes/my-scene');
16
+ loader.setKey('YOUR_LICENSE_KEY');
17
+
18
+ const group = await loader.loadAsync();
19
+ viewer.scene.add(group);
20
+
21
+ const semanticGroup = group.semanticGroup;
22
+ const sceneGroup = group.sceneGroup;
23
+ ```
24
+
25
+ 完整可运行示例见 [`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()`。
26
+
7
27
  ## `SemanticLoader`
8
28
 
9
- 加载 `db/semantic_model.json` 楼层语义数据。`loadAsync()` 返回顶层 `SemanticSceneGroup`,用于组织整次语义解析产生的建筑、楼层以及 scene-level facility layer。每个建筑包含多个 `FloorMesh`,每个楼层把墙、柱、空间、楼梯等语义对象合并为一个 TSL 材质驱动的网格;建筑级电梯井和通风井会使用 `ExtrudeMesh` 独立渲染。语义模型包含 `Facilities` 时,`SemanticLoader` 会额外读取 `SceneMetadata.json` 指向的 `tree_models`,按场景授权配置解密,再把对应设备模型挂载到所属楼层。
29
+ 加载 `db/semantic_model.json` 楼层语义数据。`loadAsync()` 返回顶层 `SemanticGroup`,用于组织整次语义解析产生的建筑、楼层以及 scene-level facility layer。每个建筑包含多个 `FloorMesh`,每个楼层把墙、柱、空间、楼梯等语义对象合并为一个 TSL 材质驱动的网格;建筑级电梯井和通风井会使用 `ExtrudeMesh` 独立渲染。语义模型包含 `Facilities` 时,`SemanticLoader` 会额外读取 `SceneMetadata.json` 指向的 `tree_models`,按场景授权配置解密,再把对应设备模型挂载到所属楼层。
10
30
 
11
31
  ```typescript
12
32
  import { Vector3, Vector4 } from 'three';
@@ -16,30 +36,34 @@ const loader = new SemanticLoader(viewer);
16
36
  loader.setPath('./scenes/my-scene');
17
37
  loader.setKey('YOUR_LICENSE_KEY'); // 官方授权场景解析 tree_models 时必需
18
38
 
19
- const semanticScene = await loader.loadAsync(); // SemanticSceneGroup
20
- viewer.scene.add(semanticScene);
39
+ const semanticGroup = await loader.loadAsync(); // SemanticGroup
40
+ viewer.scene.add(semanticGroup);
21
41
 
22
- const building = semanticScene.getBuildingAt(0);
42
+ const building = semanticGroup.getBuildingAt(0);
23
43
  const floor = building.getFloorAt(0);
24
44
 
25
45
  building.isolateFloor(floor);
26
46
  await viewer.controls.flyToObject(floor);
27
47
 
28
48
  floor.addEventListener('click', ({ event }) => {
29
- const { target, intersect } = event;
49
+ const { currentTarget, intersect } = event;
50
+ if (!intersect) return;
51
+
30
52
  const index = intersect.semanticIndex;
31
53
 
32
- target.setColorAt(index, new Vector4(1, 0, 0, 0.5));
33
- target.setScaleAt(index, new Vector3(1, 0.1, 1));
54
+ currentTarget.setColorAt(index, new Vector4(1, 0, 0, 0.5));
55
+ currentTarget.setScaleAt(index, new Vector3(1, 0.1, 1));
34
56
  viewer.render();
35
57
  });
36
58
  ```
37
59
 
38
- ### `SemanticSceneGroup`
60
+ 所有可按 ID 操作的实例都会尽量暴露统一的 `SemanticInstanceObject` API。楼层多边形语义使用轻量 `FloorSemanticInstanceObject` 代理合并几何中的一个实例;Facilities 和 SceneLoader path instancing 使用同一套模型实例化底层。业务代码优先使用 `setSemanticVisible()`、`setSemanticColor()`、`setSemanticOpacity()` 和 `getSemanticBoundingBox()`,而不是关心底层是合并几何、`InstancedMesh` 还是 fallback `Model`。
61
+
62
+ ### `SemanticGroup`
39
63
 
40
- `SemanticSceneGroup` 继承自 `BaseGroup`,是单次 `SemanticParser` 解析结果的顶层容器。它管理所有 `BuildingGroup`,并把跨楼层共享的 Facilities `InstancedMesh` 渲染层作为建筑的同级对象挂在 `facilityLayer`,而不是挂到某个 `BuildingGroup` 内。
64
+ `SemanticGroup` 继承自 `BaseGroup`,是单次 `SemanticParser` 解析结果的顶层容器。它管理所有 `BuildingGroup`,并把跨楼层共享的 Facilities `InstancedMesh` 渲染层作为建筑的同级对象挂在 `facilityLayer`,而不是挂到某个 `BuildingGroup` 内。
41
65
 
42
- 建筑 ID 和设备 ID 查询使用内部 `Map` 索引维护;`getBuildingById()`、`getFacilityById()` 以及单设备控制方法不会通过数组扫描查找语义对象。
66
+ 建筑 ID 和设备 ID 查询使用内部 `Map` 索引维护;`getBuildingById()` 和 `getFacilityById()` 不会通过数组扫描查找语义对象。
43
67
 
44
68
  | 属性 / 方法 | 说明 |
45
69
  | :---------- | :--- |
@@ -50,18 +74,13 @@ floor.addEventListener('click', ({ event }) => {
50
74
  | `floorMeshes` | 聚合返回所有建筑内的 `FloorMesh[]`。 |
51
75
  | `setFacilityLayer(layer)` | 设置跨楼层设备渲染层;传入 `null` 会移除已有 layer。 |
52
76
  | `getFacilityById(id)` | 在整次语义解析结果中按 `Facility.ID` 或显式别名查找设备。 |
53
- | `showFacility(id \| object)` / `hideFacility(id \| object)` | 显示或隐藏单个设备。 |
54
- | `setFacilityVisible(id \| object, visible)` | 设置单个设备显隐状态。 |
55
- | `setFacilityColor(id \| object, color)` | 设置单个设备材质颜色。 |
56
- | `resetFacilityColor(id \| object)` | 清除单个设备颜色 override,恢复模型原始颜色;透明度 override 不受影响。 |
57
- | `setFacilityOpacity(id \| object, opacity)` | 设置单个设备材质透明度,`opacity` 会被限制在 `0..1`。 |
58
77
  | `showAllFacilities()` / `hideAllFacilities()` | 显示或隐藏整次语义解析结果中的全部设备。 |
59
78
  | `showAllFloors()` | 显示所有建筑的所有楼层。 |
60
79
  | `planishFloors(kind?)` / `unplanishFloors(kind?)` | 压扁或恢复所有楼层;默认覆盖所有合并语义实例和 Facilities,并按包围盒贴到楼层平面附近,同时添加稳定的轻微 Y 偏移以减少共面闪烁;传入 `kind` 时只作用于该语义类型,`kind` 可包含 `Facilities`。 |
61
80
 
62
81
  ### `BuildingGroup`
63
82
 
64
- `BuildingGroup` 继承自 `BaseGroup`,用于组织一个建筑下的所有楼层、电梯井和通风井。设备语义对象不会直接挂到 `BuildingGroup`;能 instancing 的设备会渲染到 `SemanticSceneGroup.facilityLayer`,同时在对应 `FloorMesh` 下保留可检索、可控制的轻量引用。
83
+ `BuildingGroup` 继承自 `BaseGroup`,用于组织一个建筑下的所有楼层、电梯井和通风井。设备语义对象不会直接挂到 `BuildingGroup`;能 instancing 的设备会渲染到 `SemanticGroup.facilityLayer`,同时在对应 `FloorMesh` 下保留可检索、可控制的轻量引用。
65
84
 
66
85
  建筑内设备 ID 到楼层的关系由内部 `Map` 索引维护,并通过楼层设备索引版本自动同步。
67
86
 
@@ -84,11 +103,6 @@ floor.addEventListener('click', ({ event }) => {
84
103
  | `isolateFloors(floors \| indexes)` | 只显示指定多个楼层,隐藏其他楼层。 |
85
104
  | `showAllFloors()` | 显示全部楼层。 |
86
105
  | `getFacilityById(id)` | 在当前建筑的楼层中按 `Facility.ID` 或显式别名查找设备。 |
87
- | `showFacility(id \| object)` / `hideFacility(id \| object)` | 显示或隐藏当前建筑内的单个设备。 |
88
- | `setFacilityVisible(id \| object, visible)` | 设置当前建筑内单个设备显隐状态。 |
89
- | `setFacilityColor(id \| object, color)` | 设置当前建筑内单个设备材质颜色。 |
90
- | `resetFacilityColor(id \| object)` | 清除当前建筑内单个设备颜色 override。 |
91
- | `setFacilityOpacity(id \| object, opacity)` | 设置当前建筑内单个设备材质透明度。 |
92
106
  | `showAllFacilities()` | 显示整栋建筑内全部楼层设备。 |
93
107
  | `hideAllFacilities()` | 隐藏整栋建筑内全部楼层设备。 |
94
108
  | `planishFloors(kind?)` | 压扁所有楼层;默认覆盖所有合并语义实例和 Facilities,并按包围盒贴到楼层平面附近,同时添加稳定的轻微 Y 偏移以减少共面闪烁;传入 `kind` 时只压扁该语义类型,`kind` 可包含 `Facilities`。 |
@@ -121,11 +135,11 @@ if (vent) {
121
135
 
122
136
  `tree_models.matrix` 是相对父节点的本地矩阵;`SemanticLoader` 会沿树父级累乘得到设备世界矩阵,再转换为对应 `FloorMesh` 的局部矩阵。因此楼层移动、隔离或显示隐藏时设备会跟随楼层一起工作。即使某个 story 只有 `Facilities`、没有墙柱空间等合并几何,也会创建一个空的 `FloorMesh` 作为设备容器。
123
137
 
124
- Facilities 会在单次 `SemanticParser` 解析范围内按模型 URL 自动分组。静态模型会被转换为 scene-level `InstancedMesh` batch,挂到 `SemanticSceneGroup.facilityLayer`;每个设备仍会在所属 `FloorMesh` 下保留一个轻量引用,用于 `getFacilityById()`、显隐、颜色、透明度和 `viewer.objectManager` 检索。带动画、骨骼、morph target 或多材质 Mesh 的模型会自动回退为普通 `Model`,继续作为实际 `Object3D` 挂到所属 `FloorMesh`。
138
+ Facilities 会在单次 `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 不变。
125
139
 
126
- `facilityLayer` 使用 `FacilityInstancedLayer`,并继承自 `BaseGroup`。当 `facilityLayer.visible = false` 时,layer 会阻止内部 `InstancedMesh` 继续参与射线检测,避免隐藏的批处理设备仍被点击命中;单个设备的显隐仍应通过楼层下的设备引用或 `showFacility()` / `hideFacility()` 控制。
140
+ `facilityLayer` 使用 `FacilityInstancedLayer`,并继承自 `BaseGroup`。当 `facilityLayer.visible = false` 时,layer 会阻止内部 `InstancedMesh` 继续参与射线检测,避免隐藏的批处理设备仍被点击命中;单个设备的显隐应先通过 `getFacilityById()` 或 `viewer.objectManager.getById()` 取回实例,再调用 `setSemanticVisible()` 控制。
127
141
 
128
- Instanced 设备通过 per-instance opacity attribute 控制单个设备透明度。batch 材质默认沿用源材质的透明状态;只有源材质本身透明,或当前参与渲染的设备存在 `opacity < 1` 时,材质才会切到 `transparent: true`,`setFacilityOpacity(id, 1)` 可在没有其他半透明实例时回到不透明渲染队列。`resetFacilityColor()` 只清除颜色 override,不影响透明度 override。`FacilityInstancedLayer` 会在渲染前同步实例矩阵、颜色和透明度,但只有检测到数据变化时才上传对应 buffer,静态设备不会每帧重复提交全部 instance 数据。layer 会把每个 Facility 的完整模板包围盒转成世界包围盒,并用当前相机视锥压缩本帧 active instance count;裁剪不再使用相机距离阈值,因此侧视、立面和俯视视角都只由包围盒是否进入视锥决定。`cameraDistanceCulling`、`cameraCullDistance`、`cameraCullOverviewFactor` 和 `cameraCullPlanViewThreshold` 旧字段仅保留兼容,不再影响裁剪结果。普通 fallback `Model` 的颜色和透明度控制依赖 `MaterialEffects`,单材质和多材质 Mesh 都会应用 override。
142
+ Instanced 设备通过 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。
129
143
 
130
144
  加载成功后,设备引用会写入 `userData.facilityId`、`semanticId`、`semanticKind: 'Facilities'`、`semanticName`、`spaces`、`twinsIdentifier`、`storyId` 和 `floorIndex`,并按 `Facility.ID` 注册到 `viewer.objectManager`。自动 instancing 的引用还会带有 `userData.instanced: true` 和 `userData.modelUrl`。
131
145
 
@@ -139,10 +153,7 @@ const facility = floor.getFacilityById('FACILITY_001') ?? viewer.objectManager.g
139
153
  const facilityEntity = floor.getSemanticById('FACILITY_001');
140
154
 
141
155
  if (facility && facilityEntity?.type === 'object') {
142
- const facilityBox =
143
- typeof facility.getFacilityBoundingBox === 'function'
144
- ? facility.getFacilityBoundingBox(box, true)
145
- : box.setFromObject(facility);
156
+ const facilityBox = facility.getSemanticBoundingBox(box, true);
146
157
 
147
158
  await viewer.controls.flyToBox(facilityBox, {
148
159
  viewpoint: 'rightFrontTop',
@@ -152,19 +163,113 @@ if (facility && facilityEntity?.type === 'object') {
152
163
 
153
164
  const facilities = floor.getSemanticsByKind('Facilities');
154
165
 
155
- semanticScene.hideFacility('FACILITY_001');
156
- semanticScene
157
- .setFacilityColor('FACILITY_001', '#ff5533')
158
- .setFacilityOpacity('FACILITY_001', 0.45)
159
- .resetFacilityColor('FACILITY_001')
160
- .showFacility('FACILITY_001');
166
+ facility
167
+ .setSemanticColor('#ff5533')
168
+ .setSemanticOpacity(0.45)
169
+ .resetSemanticColor()
170
+ .setSemanticVisible(true);
171
+ ```
172
+
173
+ 对 Facilities 推荐使用 `controls.flyToBox()` 飞向设备包围盒。无论设备实际渲染来自 `InstancedMesh` 还是 fallback `Model`,都可以通过 `getSemanticBoundingBox(box, true)` 取得完整世界包围盒。
174
+
175
+ ### `FacilityInstanceObject`
176
+
177
+ `FacilityInstanceObject` 继承自统一的 `SemanticInstanceObject`,表示一个设备语义实例。它会挂在所属 `FloorMesh` 下,用于 ID 检索、事件派发、显隐、颜色、透明度和包围盒查询;如果设备可以 instancing,真正几何由 `SemanticGroup.facilityLayer` 里的 `InstancedMesh` 统一渲染;如果不能 instancing,fallback `Model` 会作为它的子对象挂载。
178
+
179
+ 通常不需要手动创建 `FacilityInstanceObject`。`SemanticParser` 在解析 `Facilities` 时会自动创建它,并写入 `userData.facilityId`、`semanticId`、`semanticKind: 'Facilities'`、`semanticName`、`spaces`、`twinsIdentifier`、`storyId`、`floorIndex`、`instanced` 和 `modelUrl`。`userData.instanced` 表示当前设备是否由 `InstancedMesh` 渲染;fallback 时仍然保留同一套语义实例 API。
180
+
181
+ 加载完成后,设备可以按 `Facility.ID` 从全局 `objectManager` 取回。推荐使用 `getSemanticBoundingBox(box, true)` 取得完整世界包围盒,再调用 `viewer.controls.flyToBox()`。
182
+
183
+ ```typescript
184
+ import { Box3 } from 'three';
185
+ import { FacilityInstanceObject } from 'u-space/plugins/u-manager';
186
+
187
+ const facility = viewer.objectManager.getById<FacilityInstanceObject>('FACILITY_001');
188
+
189
+ if (facility?.isFacilityInstanceObject) {
190
+ const box = facility.getSemanticBoundingBox(new Box3(), true);
191
+
192
+ await viewer.controls.flyToBox(box, {
193
+ viewpoint: 'rightFrontTop',
194
+ padding: 0.2,
195
+ enableTransition: true,
196
+ });
197
+ }
198
+ ```
199
+
200
+ | 属性 / 方法 | 说明 |
201
+ | :---------- | :--- |
202
+ | `isFacilityInstanceObject` | 类型标记,值为 `true`。 |
203
+ | `type` | Three.js 对象类型名,值为 `'FacilityInstanceObject'`。 |
204
+ | `setSemanticBounds(bounds)` | 设置模板在引用本地空间内的包围盒,并返回自身;主要由 `SemanticModelInstancedLayer.addSemanticBatch()` 在建 batch 时写入。 |
205
+ | `getSemanticBoundingBox(target?, world?)` | 获取完整包围盒。`world = true` 时会应用 `matrixWorld`,适合传给 `viewer.controls.flyToBox()`;没有 bounds 时返回 empty `Box3`。 |
206
+ | `setSemanticVisible(visible)` | 设置单个语义实例的 `visible`,并返回自身;渲染和射线检测都会跳过不可见引用。 |
207
+ | `setSemanticColor(color)` | 设置单个语义实例的颜色 override,并返回自身。`color` 支持 Three.js `ColorRepresentation`。 |
208
+ | `resetSemanticColor()` | 清除颜色 override,恢复使用源材质颜色,并返回自身;不会清除透明度 override。 |
209
+ | `setSemanticOpacity(opacity)` | 设置单个语义实例透明度,并返回自身;值会被限制在 `0..1`。 |
210
+ | `getSemanticColor(target?)` | 读取当前颜色 override;没有 override 时返回白色 `(1, 1, 1)`。 |
211
+ | `hasSemanticColor()` | 判断当前实例是否存在颜色 override。 |
212
+ | `getSemanticOpacity()` | 读取当前透明度 override。 |
213
+
214
+ ```typescript
215
+ import { Box3 } from 'three';
216
+ import { FacilityInstanceObject } from 'u-space/plugins/u-manager';
217
+
218
+ const box = new Box3();
219
+ const facility = floor.getFacilityById('FACILITY_001');
220
+
221
+ if (facility instanceof FacilityInstanceObject) {
222
+ facility
223
+ .setSemanticColor('#29ccff')
224
+ .setSemanticOpacity(0.6)
225
+ .setSemanticVisible(true);
226
+
227
+ await viewer.controls.flyToBox(facility.getSemanticBoundingBox(box, true), {
228
+ viewpoint: 'rightFrontTop',
229
+ padding: 0.2,
230
+ });
231
+ }
161
232
  ```
162
233
 
163
- 对 Facilities 推荐使用 `controls.flyToBox()` 飞向设备包围盒。instanced 设备引用是楼层下的轻量 `FacilityInstanceObject`,可通过 `getFacilityBoundingBox(box, true)` 取得完整世界包围盒;普通 fallback `Model` 也可以直接使用 `viewer.controls.flyToObject(facility)`。
234
+ ### `FacilityInstancedLayer`
235
+
236
+ `FacilityInstancedLayer` 继承自 `SemanticModelInstancedLayer`,是 scene-level Facilities 的批量渲染层。`SemanticGroup.facilityLayer` 默认就是该类型;它会按模型 URL 把同一模板的静态设备合并为一个或多个 `InstancedMesh`,并用普通 `Group` 包裹每个模型 URL batch,保证设备显隐、颜色、透明度和相机实例裁剪能在每帧渲染前同步,同时保留每个楼层下的 `FacilityInstanceObject` 引用用于业务 API 和事件冒泡。
237
+
238
+ 使用 `SemanticLoader` 时通常不需要直接调用 `addSemanticBatch()`;只有自定义语义解析或自建设备 batch 时才需要手动创建 layer,然后通过 `semanticGroup.setFacilityLayer(layer)` 挂回语义场景。
239
+
240
+ | 属性 / 方法 | 说明 |
241
+ | :---------- | :--- |
242
+ | `isFacilityInstancedLayer` | 类型标记,值为 `true`。 |
243
+ | `type` | Three.js 对象类型名,值为 `'FacilityInstancedLayer'`。 |
244
+ | `name` | 构造函数默认设为 `'Facilities'`。 |
245
+ | `getInstanceCulling()` | 返回当前实例裁剪配置:`enabled`、`frustum`、`minScreenRadius`。 |
246
+ | `setInstanceCulling(options)` | 设置实例裁剪配置,并返回自身。默认 `enabled: false`、`frustum: true`、`minScreenRadius: 0`;默认不按相机裁剪设备,避免相机距离影响设备显隐。需要性能压缩时可设置 `enabled: true` 开启视锥裁剪,或再把 `minScreenRadius` 设为大于 0 的值开启屏幕尺寸裁剪。 |
247
+ | `addSemanticBatch(url, template, instances)` | 尝试把同一模型 URL 的语义实例引用合并为 instanced batch。成功返回 `true`,失败返回 `false`,失败时调用方应回退为普通模型渲染。 |
248
+
249
+ `addSemanticBatch(url, template, instances)` 会从 `template` 收集可 instancing 的可见 `Mesh`,克隆源材质,并为每个 batch 建立 instance matrix、颜色和透明度 attribute。没有动画、没有骨骼、没有 morph target,且每个多材质 `Mesh` 都有合法 `geometry.groups` 的模板会进入 instancing;不满足条件、模板没有可用 Mesh 或 `instances` 为空时会返回 `false`。
250
+
251
+ 渲染前,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`,事件随后会按楼层引用对象继续冒泡。
252
+
253
+ ```typescript
254
+ import { FacilityInstancedLayer, FacilityInstanceObject } from 'u-space/plugins/u-manager';
255
+
256
+ const layer = new FacilityInstancedLayer();
257
+ const refs = facilityItems.map(() => new FacilityInstanceObject());
258
+
259
+ if (layer.addSemanticBatch('/models/camera.glb', templateObject, refs)) {
260
+ semanticGroup.setFacilityLayer(layer);
261
+ }
262
+ ```
263
+
264
+ 如果只想整体隐藏或禁用 scene-level batch,可以设置 `semanticGroup.facilityLayer.visible = false`;如果只想控制单个设备,推荐从 `floor.getFacilityById(id)` 或 `viewer.objectManager.getById(id)` 取回 `FacilityInstanceObject` 后调用 `setSemanticVisible()` / `setSemanticOpacity()`。
265
+
266
+ ### `SemanticModelInstancedLayer`
267
+
268
+ `SemanticModelInstancedLayer` 是 `SceneInstancedLayer` 和 `FacilityInstancedLayer` 的共享实现。它负责模板 mesh 收集、材质克隆、`InstancedMesh` 创建、instance matrix/color/opacity buffer 同步、raycast hit remap、fallback 判定和可选相机实例裁剪。通常业务代码不需要直接使用它,除非要为新的语义实例类型复用同一套模型 instancing 能力。
164
269
 
165
270
  ### `FloorMesh`
166
271
 
167
- `FloorMesh` 继承自 `BaseMesh`,表示一个楼层。它不是 `BatchedMesh`,而是把同一楼层内的语义对象合并为一份几何,并通过 TSL + `DataTexture` 在 GPU 侧按 `semanticIndex` 控制颜色、透明度、显示隐藏和实例矩阵。楼层语义网格会按实例透明度拆分为不透明和透明两个材质通道;墙、柱、门等默认不透明实例会进入 opaque pipeline,`Spaces`、`Windows` 或通过 `setOpacityAt()` 改成半透明的实例会进入 transparent pipeline。Facilities 通过楼层下的引用对象进行检索和控制;实际渲染可能来自 `SemanticSceneGroup.facilityLayer` 的 `InstancedMesh` batch,也可能是 fallback 的普通 `Model`。
272
+ `FloorMesh` 继承自 `BaseMesh`,表示一个楼层。它不是 `BatchedMesh`,而是把同一楼层内的语义对象合并为一份几何,并通过 TSL + `DataTexture` 在 GPU 侧按 `semanticIndex` 控制颜色、透明度、显示隐藏和实例矩阵。楼层语义网格会按实例透明度拆分为不透明和透明两个材质通道;墙、柱、门等默认不透明实例会进入 opaque pipeline,`Spaces`、`Windows` 或通过 `setOpacityAt()` 改成半透明的实例会进入 transparent pipeline。Facilities 通过楼层下的引用对象进行检索和控制;实际渲染可能来自 `SemanticGroup.facilityLayer` 的 `InstancedMesh` batch,也可能是 fallback 的普通 `Model`。
168
273
 
169
274
  这种结构的目标是让每个楼层通常只占一个主渲染 draw call,同时仍保留实例级控制能力。`xxxAt` 方法全部作用于单个语义实例;没有 `At` 后缀的方法作用于整个楼层 mesh。
170
275
 
@@ -174,7 +279,7 @@ semanticScene
174
279
  | `semanticInstances` | `Map<string, number>`,从语义对象 ID 映射到实例下标。 |
175
280
  | `wallsInstances` / `columnsInstances` / `spacesInstances` / `doorsInstances` / `windowsInstances` / `staircasesInstances` | 按语义类型维护的 ID 到实例下标映射。 |
176
281
  | `semanticEntities` | `Map<string, FloorSemanticEntity>`,统一索引楼层内合并几何语义和 `Facilities` 对象语义。 |
177
- | `facilityObjectsById` | `Map<string, Object3D>`,按 `Facility.ID` 或显式别名查找设备引用。 |
282
+ | `facilityObjectsById` | `Map<string, FacilityInstanceObject>`,按 `Facility.ID` 或显式别名查找设备引用。 |
178
283
  | `facilityObjects` | 当前楼层挂载的设备引用数组。 |
179
284
  | `getSemanticIndexById(id)` | 根据语义 ID 获取实例下标。 |
180
285
  | `getSemanticById(id)` | 根据语义 ID 或设备别名获取统一语义实体。 |
@@ -183,17 +288,13 @@ semanticScene
183
288
  | `getSemanticIdAt(index)` | 获取实例语义 ID。 |
184
289
  | `getSemanticKindAt(index)` | 获取合并几何实例类型:`'Walls' \| 'Columns' \| 'Spaces' \| 'Doors' \| 'Windows' \| 'Staircases'`。 |
185
290
  | `getSemanticNameAt(index)` | 获取实例名称;当前 `Spaces` 会从语义数据的 `Name` 写入,其他类型可能为 `undefined`。 |
291
+ | `getSemanticObjectAt(index)` | 获取合并几何实例对应的 `FloorSemanticInstanceObject` 轻量对象。 |
186
292
  | `showAllFacilities()` | 显示当前楼层的全部设备对象。 |
187
293
  | `hideAllFacilities()` | 隐藏当前楼层的全部设备对象。 |
188
294
  | `addFacility(object, ids?)` | 把设备对象挂到楼层,并按显式 ID、`facilityId`、`semanticId`、`id` 建立检索别名。 |
189
295
  | `getFacilityById(id)` | 根据 `Facility.ID` 或别名获取设备对象。 |
190
296
  | `getFacilityAt(index)` | 按楼层设备数组下标获取设备对象。 |
191
297
  | `getFacilities()` | 获取当前楼层全部设备对象。 |
192
- | `showFacility(id \| object)` / `hideFacility(id \| object)` | 显示或隐藏当前楼层内的单个设备。 |
193
- | `setFacilityVisible(id \| object, visible)` | 设置当前楼层内单个设备显隐状态。 |
194
- | `setFacilityColor(id \| object, color)` | 设置当前楼层内单个设备材质颜色。 |
195
- | `resetFacilityColor(id \| object)` | 清除当前楼层内单个设备颜色 override。 |
196
- | `setFacilityOpacity(id \| object, opacity)` | 设置当前楼层内单个设备材质透明度。 |
197
298
  | `removeFacility(facility)` | 按设备对象或 ID 从楼层移除设备,并同步清理检索索引。 |
198
299
  | `setColorAt(index, color)` / `getColorAt(index, target?)` | 修改或读取实例颜色,支持 `Color`、`Vector3`、`Vector4`。 |
199
300
  | `setOpacityAt(index, opacity)` / `getOpacityAt(index)` | 修改或读取实例透明度。 |
@@ -209,7 +310,7 @@ semanticScene
209
310
 
210
311
  ### 飞向语义实例
211
312
 
212
- 点击、射线检测命中 `FloorMesh` 里的语义几何或设备时,`intersect` 上会附加语义字段:`semanticIndex`、`semanticId`、`semanticKind`。普通楼层语义几何的事件目标是 `FloorMesh`;instanced Facilities 的事件目标是楼层下的 `FacilityInstanceObject` 引用,事件会继续冒泡到所属 `FloorMesh`、`BuildingGroup` 和 `SemanticSceneGroup`。因此楼层监听器中应使用 `event.currentTarget` 操作楼层,用 `intersect.facility` 或 `event.target` 取得设备对象。
313
+ 点击、射线检测命中 `FloorMesh` 里的语义几何或设备时,`intersect` 上会附加语义字段:`semanticIndex`、`semanticId`、`semanticKind` 和 `semanticObject`。普通楼层语义几何的事件目标是 `FloorSemanticInstanceObject`;instanced Facilities 的事件目标是楼层下的 `FacilityInstanceObject` 引用,事件会继续冒泡到所属 `FloorMesh`、`BuildingGroup` 和 `SemanticGroup`。因此楼层监听器中应使用 `event.currentTarget` 操作楼层,用 `intersect.semanticObject` 或 `event.target` 取得具体语义实例。
213
314
 
214
315
  ```typescript
215
316
  import { Box3 } from 'three';
@@ -221,11 +322,8 @@ floor.addEventListener('click', async ({ event }) => {
221
322
  if (!intersect) return;
222
323
 
223
324
  if (intersect.semanticKind === 'Facilities') {
224
- const facility = intersect.facility ?? event.target;
225
- const facilityBox =
226
- typeof facility.getFacilityBoundingBox === 'function'
227
- ? facility.getFacilityBoundingBox(box, true)
228
- : box.setFromObject(facility);
325
+ const facility = intersect.semanticObject ?? event.target;
326
+ const facilityBox = facility.getSemanticBoundingBox(box, true);
229
327
 
230
328
  await viewer.controls.flyToBox(facilityBox, {
231
329
  viewpoint: 'rightFrontTop',
@@ -234,7 +332,8 @@ floor.addEventListener('click', async ({ event }) => {
234
332
  return;
235
333
  }
236
334
 
237
- const semanticBox = currentTarget.getBoundingBoxAt(intersect.semanticIndex, box, true);
335
+ const semanticBox = intersect.semanticObject?.getSemanticBoundingBox(box, true)
336
+ ?? currentTarget.getBoundingBoxAt(intersect.semanticIndex, box, true);
238
337
 
239
338
  if (semanticBox) {
240
339
  await viewer.controls.flyToBox(semanticBox, {
@@ -258,13 +357,17 @@ if (index !== undefined) {
258
357
 
259
358
  ### 楼层压扁
260
359
 
261
- `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 同步到 `SemanticSceneGroup.facilityLayer`。
360
+ `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`。
262
361
 
263
362
  如果只想调整单个合并几何实例,可以使用 `setScaleAt()` 或 `setScaleYAt()`,它们会保留该语义对象自己的基点矩阵,因此压扁墙、柱、房间时不会被压到世界原点。
264
363
 
265
364
  ## `SceneLoader`
266
365
 
267
- 从服务端场景包加载完整场景树(模型、组、形状、拉伸区域),并自动将所有已加载对象按 `id` 和 `sid` 注册到 `viewer.objectManager`。
366
+ 从服务端场景包加载完整场景树(模型、组、基础形状),并自动将所有已加载对象按 `id` 和 `sid` 注册到 `viewer.objectManager`。
367
+ 默认会尝试读取同一路径下的 `db/semantic_model.json`,如果场景树节点的 `id` 已存在于语义文件中,则跳过该节点,方便和 `SemanticLoader` 同时加载而不重复创建建筑或设备。
368
+ 对语义去重后仍保留的 3D 节点,`SceneLoader` 默认会按模型 `path` 自动合并重复静态模型:同一 `path` 的多个实例只加载一次模板,并由 `SceneInstancedLayer` 通过共享的 `SemanticModelInstancedLayer` 批量渲染。原场景树中仍会保留每个 `id` / `sid` 对应的 `SceneInstanceObject` 逻辑对象,用于业务检索、显隐、颜色、透明度、包围盒和事件派发。
369
+
370
+ 模板包含动画、骨骼、morph target、非法多材质 groups 或没有可实例化网格时,会自动回退为普通 `Model` 渲染;fallback `Model` 会挂在同一个 `SceneInstanceObject` 下,因此调用侧不需要额外配置。
268
371
 
269
372
  ```typescript
270
373
  import { SceneLoader } from 'u-space/plugins/u-manager';
@@ -278,12 +381,101 @@ viewer.scene.add(group);
278
381
 
279
382
  **方法:**
280
383
 
281
- | 方法 | 说明 |
282
- | :--------------- | :------------------------------------------------------------ |
283
- | `setKey(key)` | 设置授权场景的 RSA 解密密钥。 |
284
- | `loadAsync()` | 加载并解析场景,返回包含完整层级的 `Group`。 |
285
- | `clearCache()` | 从 `objectManager` 中移除此加载器注册的所有 ID。 |
286
- | `dispose()` | 清除缓存并释放水印叠加层。 |
384
+ | 方法 | 说明 |
385
+ | :------------------ | :------------------------------------------------------------------- |
386
+ | `setKey(key)` | 设置授权场景的 RSA 解密密钥。 |
387
+ | `loadAsync()` | 加载并解析场景,返回去除语义重复节点后的 `Group`。 |
388
+ | `clearCache()` | 从 `objectManager` 中移除此加载器注册的所有 ID。 |
389
+ | `dispose()` | 清除缓存并释放水印叠加层。 |
390
+
391
+ ### `SceneInstanceObject`
392
+
393
+ `SceneInstanceObject` 继承自统一的 `SemanticInstanceObject`,表示一个由 `SceneInstancedLayer` 批量渲染或 fallback 普通渲染的场景模型引用。它会保留在原场景树层级中,并注册到 `viewer.objectManager`,因此 `getById()`、`show()` / `hide()`、`isolate()`、`showAll()`、`setOpacity()` 和 `getBoundingBox()` 可以继续按单个模型 ID 使用。
394
+
395
+ `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 不变。
396
+
397
+ **识别字段:**
398
+
399
+ | 字段 | 说明 |
400
+ | :--- | :--- |
401
+ | `isSceneInstanceObject` | 固定为 `true`,用于判断对象是否来自 `SceneLoader` 的场景实例。 |
402
+ | `isSemanticInstanceObject` | 固定为 `true`,表示它支持统一语义实例 API。 |
403
+ | `type` | 固定为 `SceneInstanceObject`。 |
404
+ | `userData.id` / `userData.sid` | 原始场景树节点 ID;两个 ID 都会注册到 `viewer.objectManager`。 |
405
+ | `userData.semanticId` | 语义事件和 raycast remap 使用的实例 ID,默认等于场景树节点 `id`。 |
406
+ | `userData.semanticKind` | 固定为 `SceneInstances`。 |
407
+ | `userData.semanticName` | 原始场景树节点名称。 |
408
+ | `userData.instanced` | `true` 表示实际渲染来自 `SceneInstancedLayer`;`false` 表示 fallback `Model` 挂在当前对象下。 |
409
+ | `userData.modelPath` / `userData.modelUrl` | instanced 场景实例对应的模型相对路径和完整 URL。 |
410
+
411
+ **方法:**
412
+
413
+ | 方法 | 说明 |
414
+ | :--- | :--- |
415
+ | `setSemanticBounds(bounds)` | 设置本地包围盒。`SceneLoader` 会在创建 instanced batch 时根据模板模型自动设置;业务一般不需要手动调用。 |
416
+ | `setSemanticRenderObject(object)` | 设置 fallback 渲染对象并作为子对象挂载。模板无法 instancing 时由 `SceneLoader` 自动调用;自定义语义实例渲染时可手动使用。 |
417
+ | `getSemanticRenderObject()` | 返回 fallback 渲染对象;instanced 渲染路径通常返回 `null`,因为真实 Mesh 在 `SceneInstancedLayer` 中。 |
418
+ | `getSemanticBoundingBox(target?, world?)` | 返回实例包围盒。`world = true` 时会应用 `matrixWorld`,适合直接传给 `viewer.controls.flyToBox()`;无 bounds 和 render object 时返回 empty `Box3`。 |
419
+ | `setSemanticVisible(visible)` | 设置单个实例显隐并标记 batch dirty;隐藏后该实例不会继续渲染,也不会被 instanced raycast 命中。 |
420
+ | `setSemanticColor(color)` | 设置单个实例颜色 override。instanced 路径写入 per-instance color;fallback `Model` 路径会委托到材质高亮逻辑。 |
421
+ | `resetSemanticColor()` | 清除颜色 override,恢复使用源模型颜色;不会清除透明度 override。 |
422
+ | `setSemanticOpacity(opacity)` | 设置单个实例透明度,值会限制在 `0..1`。instanced 路径写入 per-instance opacity;fallback 路径会按原始材质透明度相乘。 |
423
+ | `getSemanticColor(target?)` | 获取当前生效颜色。存在颜色或高亮 override 时返回该颜色,否则返回白色。传入 `Color` 可复用对象。 |
424
+ | `hasSemanticColor()` | 返回当前是否存在颜色或高亮颜色 override。 |
425
+ | `getSemanticOpacity()` | 获取当前生效透明度;高亮存在时返回高亮透明度,否则返回基础透明度。 |
426
+ | `setSemanticHighlight(color, opacity, overwrite?)` | 设置临时高亮样式。`overwrite = false` 时按 tint 模式与原色混合,`true` 时直接替换颜色;高亮会覆盖基础颜色/透明度直到调用 `clearSemanticHighlight()`。 |
427
+ | `clearSemanticHighlight()` | 清除临时高亮,并恢复到基础颜色/透明度状态。 |
428
+ | `getSemanticColorMode()` | 返回当前颜色模式:`SEMANTIC_COLOR_MODE_NONE`、`SEMANTIC_COLOR_MODE_OVERRIDE` 或 `SEMANTIC_COLOR_MODE_TINT`。 |
429
+ | `onSemanticRenderDirty(callback)` | 监听实例渲染状态变化,返回取消监听函数。实例显隐、颜色、透明度或世界矩阵变化时会触发。 |
430
+ | `markSemanticRenderDirty()` | 手动标记实例渲染状态已变化,通知所属 batch 在下一帧同步 instance buffer。 |
431
+
432
+ `MaterialEffects.highlightColor()` / `removeHighlightColor()` 会识别 `SceneInstanceObject` 并调用 `setSemanticHighlight()` / `clearSemanticHighlight()`,不会直接修改共享 batch 材质。`ObjectManager.setOpacity()`、`ObjectManager.hide()`、`ObjectManager.show()` 也会通过统一 API 作用到单个实例。
433
+
434
+ 加载完成后可以按场景树节点 `id` 从 `objectManager` 取回 `SceneInstanceObject`,再用 `getSemanticBoundingBox(box, true)` 取得世界包围盒并传给 `viewer.controls.flyToBox()`。`SceneInstanceObject` 内部带有不可见的本地 bounds,因此不需要加载真实 Mesh 副本也能计算飞行包围盒。
435
+
436
+ ```typescript
437
+ import { Box3 } from 'three/webgpu';
438
+ import { SceneInstanceObject } from 'u-space/plugins/u-manager';
439
+
440
+ const object = viewer.objectManager.getById<SceneInstanceObject>('SCENE_NODE_ID');
441
+
442
+ if (object?.isSceneInstanceObject) {
443
+ const box = object.getSemanticBoundingBox(new Box3(), true);
444
+ await viewer.controls.flyToBox(box, {
445
+ viewpoint: 'current',
446
+ padding: 0.2,
447
+ enableTransition: true,
448
+ });
449
+ }
450
+ ```
451
+
452
+ 颜色、透明度和显隐可以直接链式调用:
453
+
454
+ ```typescript
455
+ object
456
+ ?.setSemanticColor('#29ccff')
457
+ .setSemanticOpacity(0.55)
458
+ .setSemanticVisible(true);
459
+
460
+ object?.setSemanticHighlight('#ffb703', 0.75, true);
461
+ object?.clearSemanticHighlight();
462
+ object?.resetSemanticColor();
463
+ ```
464
+
465
+ 如果需要自己控制包围盒,也可以取世界包围盒后调用 `flyToBox()`:
466
+
467
+ ```typescript
468
+ import { Box3 } from 'three/webgpu';
469
+
470
+ const box = object.getSemanticBoundingBox(new Box3(), true);
471
+ await viewer.controls.flyToBox(box, { viewpoint: 'frontTop', padding: 0.2 });
472
+ ```
473
+
474
+ 射线检测命中批处理实例时,`intersect.object` 会被改写为对应的 `SceneInstanceObject`,并附加 `semanticObject`、`semanticId`、`semanticKind: 'SceneInstances'` 和 `semanticIndex`,事件会按逻辑对象所在的原场景树继续冒泡。
475
+
476
+ ### `SceneInstancedLayer`
477
+
478
+ `SceneInstancedLayer` 是 `SceneLoader` 内部使用的批量渲染层,默认 `name = 'SceneInstances'`。它继承自 `SemanticModelInstancedLayer`,每个重复模型 URL 会生成一个 batch,batch 内按模板的可实例化 Mesh 创建 `InstancedMesh`。渲染前只在实例矩阵、显隐、颜色或透明度变化时同步 instance buffer;普通相机移动不会重新上传所有实例数据。
287
479
 
288
480
  ## `TopologiesLoader` / `TopologyParser`
289
481