u-space 0.0.27 → 0.0.29

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 (29) hide show
  1. package/dist/index.cjs +3 -3
  2. package/dist/index.js +363 -368
  3. package/dist/plugins/u-manager/index.cjs +55 -55
  4. package/dist/plugins/u-manager/index.d.ts +2 -0
  5. package/dist/plugins/u-manager/index.js +9429 -9292
  6. package/dist/plugins/u-manager/instances/SemanticInstanceObject.d.ts +51 -0
  7. package/dist/plugins/u-manager/instances/SemanticModelInstancedLayer.d.ts +42 -0
  8. package/dist/plugins/u-manager/instances/index.d.ts +2 -0
  9. package/dist/plugins/u-manager/loaders/SceneInstancedLayer.d.ts +6 -22
  10. package/dist/plugins/u-manager/loaders/SceneLoader.d.ts +12 -3
  11. package/dist/plugins/u-manager/loaders/UManagerLoader.d.ts +24 -0
  12. package/dist/plugins/u-manager/semantics/SemanticLoader.d.ts +2 -2
  13. package/dist/plugins/u-manager/semantics/SemanticParser.d.ts +3 -3
  14. package/dist/plugins/u-manager/semantics/index.d.ts +1 -1
  15. package/dist/plugins/u-manager/semantics/objects/BuildingGroup.d.ts +12 -10
  16. package/dist/plugins/u-manager/semantics/objects/FacilityInstancedLayer.d.ts +5 -39
  17. package/dist/plugins/u-manager/semantics/objects/FloorMesh.d.ts +38 -15
  18. package/dist/plugins/u-manager/semantics/objects/SemanticGroup.d.ts +27 -0
  19. package/dist/plugins/u-manager/semantics/types.d.ts +1 -0
  20. package/dist/src/effects/MaterialEffects.d.ts +1 -0
  21. package/docs/api-effects.md +3 -2
  22. package/docs/api-managers.md +1 -1
  23. package/docs/api-plugin-u-manager.md +217 -104
  24. package/docs/changelog.md +43 -9
  25. package/docs/examples-guide.md +44 -14
  26. package/docs/getting-started.md +1 -1
  27. package/docs/mcp.md +16 -8
  28. package/package.json +1 -1
  29. package/dist/plugins/u-manager/semantics/objects/SemanticSceneGroup.d.ts +0 -31
@@ -4,9 +4,31 @@
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
+ const facilityLayer = semanticGroup.getDefaultFacilityLayer();
24
+ const sceneLayer = sceneGroup.getDefaultSceneLayer();
25
+ ```
26
+
27
+ 完整可运行示例见 [`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()`、`setSemanticHighlight()`、`setSemanticVisible()` 和 `setSemanticOpacity()`。运行时动态新增/删除 batch 实例的示例见 [`examples/test_umanager_dynamic_instances.html`](https://u-space-phi.vercel.app/examples/test_umanager_dynamic_instances.html)。
28
+
7
29
  ## `SemanticLoader`
8
30
 
9
- 加载 `db/semantic_model.json` 楼层语义数据。`loadAsync()` 返回顶层 `SemanticSceneGroup`,用于组织整次语义解析产生的建筑、楼层以及 scene-level facility layer。每个建筑包含多个 `FloorMesh`,每个楼层把墙、柱、空间、楼梯等语义对象合并为一个 TSL 材质驱动的网格;建筑级电梯井和通风井会使用 `ExtrudeMesh` 独立渲染。语义模型包含 `Facilities` 时,`SemanticLoader` 会额外读取 `SceneMetadata.json` 指向的 `tree_models`,按场景授权配置解密,再把对应设备模型挂载到所属楼层。
31
+ 加载 `db/semantic_model.json` 楼层语义数据。`loadAsync()` 返回顶层 `SemanticGroup`,用于组织整次语义解析产生的建筑、楼层以及 scene-level facility layer。每个建筑包含多个 `FloorMesh`,每个楼层把墙、柱、空间、楼梯等语义对象合并为一个 TSL 材质驱动的网格;建筑级电梯井和通风井会使用 `ExtrudeMesh` 独立渲染。语义模型包含 `Facilities` 时,`SemanticLoader` 会额外读取 `SceneMetadata.json` 指向的 `tree_models`,按场景授权配置解密,再把对应设备模型挂载到所属楼层。
10
32
 
11
33
  ```typescript
12
34
  import { Vector3, Vector4 } from 'three';
@@ -16,30 +38,36 @@ const loader = new SemanticLoader(viewer);
16
38
  loader.setPath('./scenes/my-scene');
17
39
  loader.setKey('YOUR_LICENSE_KEY'); // 官方授权场景解析 tree_models 时必需
18
40
 
19
- const semanticScene = await loader.loadAsync(); // SemanticSceneGroup
20
- viewer.scene.add(semanticScene);
41
+ const semanticGroup = await loader.loadAsync(); // SemanticGroup
42
+ viewer.scene.add(semanticGroup);
21
43
 
22
- const building = semanticScene.getBuildingAt(0);
44
+ const building = semanticGroup.getBuildingAt(0);
23
45
  const floor = building.getFloorAt(0);
24
46
 
25
47
  building.isolateFloor(floor);
26
48
  await viewer.controls.flyToObject(floor);
27
49
 
28
50
  floor.addEventListener('click', ({ event }) => {
29
- const { target, intersect } = event;
51
+ const { currentTarget, intersect } = event;
52
+ if (!intersect) return;
53
+
30
54
  const index = intersect.semanticIndex;
31
55
 
32
- target.setColorAt(index, new Vector4(1, 0, 0, 0.5));
33
- target.setScaleAt(index, new Vector3(1, 0.1, 1));
56
+ currentTarget.setColorAt(index, new Vector4(1, 0, 0, 0.5));
57
+ currentTarget.setScaleAt(index, new Vector3(1, 0.1, 1));
34
58
  viewer.render();
35
59
  });
36
60
  ```
37
61
 
38
- ### `SemanticSceneGroup`
62
+ 所有可按 ID 操作的实例都会尽量暴露统一的 `SemanticInstanceObject` API。楼层多边形语义使用轻量 `FloorSemanticInstanceObject` 代理合并几何中的一个实例;Facilities 和 SceneLoader path instancing 使用同一套模型实例化底层。业务代码优先使用 `setSemanticVisible()`、`setSemanticColor()`、`setSemanticOpacity()` 和 `viewer.controls.flyToObject(instance)`,而不是关心底层是合并几何、`InstancedMesh` 还是 fallback `Model`。
63
+
64
+ `SemanticInstanceObject` 也按 Three.js 对象级包围盒约定提供 `boundingBox`、`boundingSphere`、`computeBoundingBox()` 和 `computeBoundingSphere()`。它们缓存的是实例本地坐标包围体,`Box3.setFromObject()` 会自动调用 `computeBoundingBox()` 并应用 `matrixWorld`,因此 `viewer.controls.flyToObject()` 可以直接飞向 `SceneInstanceObject`、`FacilityInstanceObject` 和楼层语义实例。需要手动处理世界包围盒时,仍可使用 `getSemanticBoundingBox(target, true)`。
39
65
 
40
- `SemanticSceneGroup` 继承自 `BaseGroup`,是单次 `SemanticParser` 解析结果的顶层容器。它管理所有 `BuildingGroup`,并把跨楼层共享的 Facilities `InstancedMesh` 渲染层作为建筑的同级对象挂在 `facilityLayer`,而不是挂到某个 `BuildingGroup` 内。
66
+ ### `SemanticGroup`
41
67
 
42
- 建筑 ID 和设备 ID 查询使用内部 `Map` 索引维护;`getBuildingById()`、`getFacilityById()` 以及单设备控制方法不会通过数组扫描查找语义对象。
68
+ `SemanticGroup` 继承自 `BaseGroup`,是单次 `SemanticParser` 解析结果的顶层容器。它管理所有 `BuildingGroup`,并把跨楼层共享的 Facilities `InstancedMesh` 渲染层作为建筑的同级对象挂在 `facilityLayer`,而不是挂到某个 `BuildingGroup` 内。
69
+
70
+ 建筑 ID 和设备 ID 查询使用内部 `Map` 索引维护;`getBuildingById()` 和 `getFacilityById()` 不会通过数组扫描查找语义对象。
43
71
 
44
72
  | 属性 / 方法 | 说明 |
45
73
  | :---------- | :--- |
@@ -49,19 +77,15 @@ floor.addEventListener('click', ({ event }) => {
49
77
  | `getBuildingAt(index)` / `getBuildingById(id)` / `getBuildings()` | 按下标、`Building.ID` 或整体列表获取建筑。 |
50
78
  | `floorMeshes` | 聚合返回所有建筑内的 `FloorMesh[]`。 |
51
79
  | `setFacilityLayer(layer)` | 设置跨楼层设备渲染层;传入 `null` 会移除已有 layer。 |
80
+ | `getDefaultFacilityLayer()` | 返回当前默认 `FacilityInstancedLayer`;没有可实例化 Facilities 时返回 `null`。 |
52
81
  | `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
82
  | `showAllFacilities()` / `hideAllFacilities()` | 显示或隐藏整次语义解析结果中的全部设备。 |
59
83
  | `showAllFloors()` | 显示所有建筑的所有楼层。 |
60
84
  | `planishFloors(kind?)` / `unplanishFloors(kind?)` | 压扁或恢复所有楼层;默认覆盖所有合并语义实例和 Facilities,并按包围盒贴到楼层平面附近,同时添加稳定的轻微 Y 偏移以减少共面闪烁;传入 `kind` 时只作用于该语义类型,`kind` 可包含 `Facilities`。 |
61
85
 
62
86
  ### `BuildingGroup`
63
87
 
64
- `BuildingGroup` 继承自 `BaseGroup`,用于组织一个建筑下的所有楼层、电梯井和通风井。设备语义对象不会直接挂到 `BuildingGroup`;能 instancing 的设备会渲染到 `SemanticSceneGroup.facilityLayer`,同时在对应 `FloorMesh` 下保留可检索、可控制的轻量引用。
88
+ `BuildingGroup` 继承自 `BaseGroup`,用于组织一个建筑下的所有楼层、电梯井和通风井。设备语义对象不会直接挂到 `BuildingGroup`;能 instancing 的设备会渲染到 `SemanticGroup.facilityLayer`,同时在对应 `FloorMesh` 下保留可检索、可控制的轻量引用。
65
89
 
66
90
  建筑内设备 ID 到楼层的关系由内部 `Map` 索引维护,并通过楼层设备索引版本自动同步。
67
91
 
@@ -83,12 +107,7 @@ floor.addEventListener('click', ({ event }) => {
83
107
  | `isolateFloor(floor \| index)` | 只显示指定楼层,隐藏其他楼层;也兼容传入楼层数组。 |
84
108
  | `isolateFloors(floors \| indexes)` | 只显示指定多个楼层,隐藏其他楼层。 |
85
109
  | `showAllFloors()` | 显示全部楼层。 |
86
- | `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)` | 设置当前建筑内单个设备材质透明度。 |
110
+ | `getFacilityById(id)` | 在当前建筑的楼层中按 `Facility.ID` 或 `FloorMesh.addFacility()` 显式传入的别名查找设备。 |
92
111
  | `showAllFacilities()` | 显示整栋建筑内全部楼层设备。 |
93
112
  | `hideAllFacilities()` | 隐藏整栋建筑内全部楼层设备。 |
94
113
  | `planishFloors(kind?)` | 压扁所有楼层;默认覆盖所有合并语义实例和 Facilities,并按包围盒贴到楼层平面附近,同时添加稳定的轻微 Y 偏移以减少共面闪烁;传入 `kind` 时只压扁该语义类型,`kind` 可包含 `Facilities`。 |
@@ -121,30 +140,22 @@ if (vent) {
121
140
 
122
141
  `tree_models.matrix` 是相对父节点的本地矩阵;`SemanticLoader` 会沿树父级累乘得到设备世界矩阵,再转换为对应 `FloorMesh` 的局部矩阵。因此楼层移动、隔离或显示隐藏时设备会跟随楼层一起工作。即使某个 story 只有 `Facilities`、没有墙柱空间等合并几何,也会创建一个空的 `FloorMesh` 作为设备容器。
123
142
 
124
- Facilities 会在单次 `SemanticParser` 解析范围内按模型 URL 自动分组。静态模型会被转换为 scene-level `InstancedMesh` batch 并挂到 `SemanticSceneGroup.facilityLayer`;每个设备仍会在所属 `FloorMesh` 下保留一个轻量引用,用于 `getFacilityById()`、显隐、颜色、透明度和 `viewer.objectManager` 检索。设备 batch 使用普通 `Group` 包裹,以保证每帧渲染前都能执行相机实例裁剪和 hide/show 后的实例同步;带动画、骨骼、morph target 或缺少合法 `geometry.groups` 的多材质 Mesh 会自动回退为普通 `Model`,继续作为实际 `Object3D` 挂到所属 `FloorMesh`。
143
+ 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
144
 
126
- `facilityLayer` 使用 `FacilityInstancedLayer`,并继承自 `BaseGroup`。当 `facilityLayer.visible = false` 时,layer 会阻止内部 `InstancedMesh` 继续参与射线检测,避免隐藏的批处理设备仍被点击命中;单个设备的显隐仍应通过楼层下的设备引用或 `showFacility()` / `hideFacility()` 控制。
145
+ `facilityLayer` 使用 `FacilityInstancedLayer`,并继承自 `BaseGroup`。当 `facilityLayer.visible = false` 时,layer 会阻止内部 `InstancedMesh` 继续参与射线检测,避免隐藏的批处理设备仍被点击命中;单个设备的显隐应先通过 `getFacilityById()` 或 `viewer.objectManager.getById()` 取回实例,再调用 `setSemanticVisible()` 控制。
127
146
 
128
- Instanced 设备通过 per-instance opacity attribute 控制单个设备透明度。batch 材质默认沿用源材质的透明状态;只有源材质本身透明,或当前参与渲染的设备存在 `opacity < 1` 时,材质才会切到 `transparent: true`,`setFacilityOpacity(id, 1)` 可在没有其他半透明实例时回到不透明渲染队列。`resetFacilityColor()` 只清除颜色 override,不影响透明度 override。`FacilityInstancedLayer` 会在设备显隐、颜色、透明度、floor planish/unplanish 改变设备 transform,或相机矩阵、投影矩阵、viewport 高度变化时,在下一次渲染前同步实例矩阵、颜色、透明度和当前 active instance count。普通 fallback `Model` 的颜色和透明度控制依赖 `MaterialEffects`,单材质和多材质 Mesh 都会应用 override。
147
+ 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
148
 
130
- 加载成功后,设备引用会写入 `userData.facilityId`、`semanticId`、`semanticKind: 'Facilities'`、`semanticName`、`spaces`、`twinsIdentifier`、`storyId` 和 `floorIndex`,并按 `Facility.ID` 注册到 `viewer.objectManager`。自动 instancing 的引用还会带有 `userData.instanced: true` 和 `userData.modelUrl`。
149
+ 加载成功后,`BuildingGroup`、`FloorMesh` 和设备引用都会在对象字段上写入独立语义身份,例如 `semanticId`、`semanticKind` 和 `semanticName`。`SemanticGroup.getBuildingById()`、`BuildingGroup.getFloorById()`、`BuildingGroup.getFacilityById()`、`FloorMesh.getFacilityById()` 以及 `SemanticModelInstancedLayer` 的查询、删除和 raycast remap 都使用这些对象字段或显式传入的别名,不再从 `userData.id`、`userData.semanticId`、`userData.facilityId` 里扫描 ID。`userData` 只保留原始业务元数据,例如 `facilityId`、`spaces`、`twinsIdentifier`、`storyId`、`floorIndex`、`instanced` 和 `modelUrl`。
131
150
 
132
151
  ```typescript
133
- import { Box3 } from 'three';
134
-
135
152
  const floor = building.getFloorAt(0);
136
- const box = new Box3();
137
153
 
138
154
  const facility = floor.getFacilityById('FACILITY_001') ?? viewer.objectManager.getById('FACILITY_001');
139
155
  const facilityEntity = floor.getSemanticById('FACILITY_001');
140
156
 
141
157
  if (facility && facilityEntity?.type === 'object') {
142
- const facilityBox =
143
- typeof facility.getFacilityBoundingBox === 'function'
144
- ? facility.getFacilityBoundingBox(box, true)
145
- : box.setFromObject(facility);
146
-
147
- await viewer.controls.flyToBox(facilityBox, {
158
+ await viewer.controls.flyToObject(facility, {
148
159
  viewpoint: 'rightFrontTop',
149
160
  padding: 0.2,
150
161
  });
@@ -152,42 +163,33 @@ 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);
161
171
  ```
162
172
 
163
- 对 Facilities 推荐使用 `controls.flyToBox()` 飞向设备包围盒。instanced 设备引用是楼层下的轻量 `FacilityInstanceObject`,可通过 `getFacilityBoundingBox(box, true)` 取得完整世界包围盒;普通 fallback `Model` 也可以直接使用 `viewer.controls.flyToObject(facility)`。
173
+ 对 Facilities 推荐直接使用 `controls.flyToObject()` 飞向设备。无论设备实际渲染来自 `InstancedMesh` 还是 fallback `Model`,`FacilityInstanceObject` 都会暴露本地 `boundingBox` / `boundingSphere`,让 Three.js 的 `Box3.setFromObject()` 能计算完整世界包围盒。
164
174
 
165
175
  ### `FacilityInstanceObject`
166
176
 
167
- `FacilityInstanceObject` 继承自 Three.js `Group`,表示一个被 scene-level `FacilityInstancedLayer` 批量渲染的设备引用。它会挂在所属 `FloorMesh` 下,用于 ID 检索、事件派发、显隐、颜色、透明度和包围盒查询;真正的设备几何不会作为它的子网格重复挂载,而是由 `SemanticSceneGroup.facilityLayer` 里的 `InstancedMesh` 统一渲染。
177
+ `FacilityInstanceObject` 继承自统一的 `SemanticInstanceObject`,表示一个设备语义实例。它会挂在所属 `FloorMesh` 下,用于 ID 检索、事件派发、显隐、颜色、透明度和包围盒查询;如果设备可以 instancing,真正几何由 `SemanticGroup.facilityLayer` 里的 `InstancedMesh` 统一渲染;如果不能 instancing,fallback `Model` 会作为它的子对象挂载。
168
178
 
169
- 通常不需要手动创建 `FacilityInstanceObject`。`SemanticParser` 在解析 `Facilities` 时会自动创建它,并写入 `userData.facilityId`、`semanticId`、`semanticKind: 'Facilities'`、`semanticName`、`spaces`、`twinsIdentifier`、`storyId`、`floorIndex`、`instanced: true` 和 `modelUrl`。如果设备模型无法 instancing,则会回退为普通 `Model`,不会具备这些实例引用专用方法。
179
+ 通常不需要手动创建 `FacilityInstanceObject`。`SemanticParser` 在解析 `Facilities` 时会自动创建它,并通过 `setSemanticIdentity()` 写入 `semanticId`、`semanticKind` 和 `semanticName`。`userData.instanced` 表示当前设备是否由 `InstancedMesh` 渲染;fallback 时仍然保留同一套语义实例 API。
170
180
 
171
- 加载完成后,instanced 设备也可以按 `Facility.ID` 从全局 `objectManager` 取回。推荐使用 `getFacilityBoundingBox(box, true)` 取得完整世界包围盒,再调用 `viewer.controls.flyToBox()`;如果取到的是普通 fallback `Model`,则继续用 `viewer.controls.flyToObject(facility)`。
181
+ 加载完成后,设备可以按 `Facility.ID` 从全局 `objectManager` 取回,并直接传给 `viewer.controls.flyToObject()`。如果需要自己合并多个对象或调整包围盒,也可以继续调用 `getSemanticBoundingBox(box, true)`。
172
182
 
173
183
  ```typescript
174
- import { Box3 } from 'three';
175
184
  import { FacilityInstanceObject } from 'u-space/plugins/u-manager';
176
185
 
177
186
  const facility = viewer.objectManager.getById<FacilityInstanceObject>('FACILITY_001');
178
187
 
179
188
  if (facility?.isFacilityInstanceObject) {
180
- const box = facility.getFacilityBoundingBox(new Box3(), true);
181
-
182
- await viewer.controls.flyToBox(box, {
183
- viewpoint: 'rightFrontTop',
184
- padding: 0.2,
185
- enableTransition: true,
186
- });
187
- } else if (facility) {
188
189
  await viewer.controls.flyToObject(facility, {
189
190
  viewpoint: 'rightFrontTop',
190
191
  padding: 0.2,
192
+ enableTransition: true,
191
193
  });
192
194
  }
193
195
  ```
@@ -196,30 +198,35 @@ if (facility?.isFacilityInstanceObject) {
196
198
  | :---------- | :--- |
197
199
  | `isFacilityInstanceObject` | 类型标记,值为 `true`。 |
198
200
  | `type` | Three.js 对象类型名,值为 `'FacilityInstanceObject'`。 |
199
- | `setLocalBounds(bounds)` | 设置设备模板在引用本地空间内的包围盒,并返回自身;主要由 `FacilityInstancedLayer.addFacilityBatch()` 在建 batch 时写入。 |
200
- | `getFacilityBoundingBox(target?, world?)` | 获取设备完整包围盒。`world = true` 时会应用 `matrixWorld`,适合传给 `viewer.controls.flyToBox()`;没有 bounds 时返回 empty `Box3`。 |
201
- | `setFacilityVisible(visible)` | 设置单个设备引用的 `visible`,并返回自身;渲染和射线检测都会跳过不可见引用。 |
202
- | `setFacilityColor(color)` | 设置单个设备的颜色 override,并返回自身。`color` 支持 Three.js `ColorRepresentation`。 |
203
- | `resetFacilityColor()` | 清除颜色 override,恢复使用源材质颜色,并返回自身;不会清除透明度 override。 |
204
- | `setFacilityOpacity(opacity)` | 设置单个设备透明度,并返回自身;值会被限制在 `0..1`。 |
205
- | `getFacilityColor(target?)` | 读取当前颜色 override;没有 override 时返回白色 `(1, 1, 1)`。 |
206
- | `hasFacilityColor()` | 判断当前设备是否存在颜色 override。 |
207
- | `getFacilityOpacity()` | 读取当前透明度 override。 |
201
+ | `semanticId` | 实例独立 ID,设备默认等于 `Facility.ID`;layer 的按 ID 查询、删除和 raycast remap 都使用该字段。 |
202
+ | `semanticKind` | 实例语义类型,设备为 `'Facilities'`。 |
203
+ | `semanticName` | 实例显示名称,设备默认使用 `Facility.Name`。 |
204
+ | `setSemanticIdentity({ id, kind, name })` | 设置实例独立身份字段,并返回自身;`semanticId` 在同一个 `SemanticModelInstancedLayer` 内必须唯一,加入 layer 后修改 id 时,下一次按 id 查询会刷新索引。 |
205
+ | `boundingBox` | 实例本地包围盒缓存,初始为 `null`;`Box3.setFromObject()` 或手动调用 `computeBoundingBox()` 时会计算。 |
206
+ | `boundingSphere` | 实例本地包围球缓存,初始为 `null`;手动调用 `computeBoundingSphere()` 时会由当前本地包围盒派生。 |
207
+ | `setSemanticBounds(bounds)` | 设置模板在引用本地空间内的包围盒,并返回自身;主要由 `SemanticModelInstancedLayer.addSemanticInstances()` / `reserveSemanticBatch()` 在建 batch 时写入。 |
208
+ | `computeBoundingBox()` / `computeBoundingSphere()` | 按 Three.js 对象级包围盒约定更新 `boundingBox` / `boundingSphere`,使 `viewer.controls.flyToObject()` 能直接飞向实例。 |
209
+ | `getSemanticBoundingBox(target?, world?)` | 获取完整包围盒。`world = true` 时会应用 `matrixWorld`,适合需要手动传给 `viewer.controls.flyToBox()` 的场景;没有 bounds 时返回 empty `Box3`。 |
210
+ | `setSemanticVisible(visible)` | 设置单个语义实例的 `visible`,并返回自身;渲染和射线检测都会跳过不可见引用。 |
211
+ | `setSemanticColor(color)` | 设置单个语义实例的颜色 override,并返回自身。`color` 支持 Three.js `ColorRepresentation`。 |
212
+ | `resetSemanticColor()` | 清除颜色 override,恢复使用源材质颜色,并返回自身;不会清除透明度 override。 |
213
+ | `setSemanticOpacity(opacity)` | 设置单个语义实例透明度,并返回自身;值会被限制在 `0..1`。 |
214
+ | `getSemanticColor(target?)` | 读取当前颜色 override;没有 override 时返回白色 `(1, 1, 1)`。 |
215
+ | `hasSemanticColor()` | 判断当前实例是否存在颜色 override。 |
216
+ | `getSemanticOpacity()` | 读取当前透明度 override。 |
208
217
 
209
218
  ```typescript
210
- import { Box3 } from 'three';
211
219
  import { FacilityInstanceObject } from 'u-space/plugins/u-manager';
212
220
 
213
- const box = new Box3();
214
221
  const facility = floor.getFacilityById('FACILITY_001');
215
222
 
216
223
  if (facility instanceof FacilityInstanceObject) {
217
224
  facility
218
- .setFacilityColor('#29ccff')
219
- .setFacilityOpacity(0.6)
220
- .setFacilityVisible(true);
225
+ .setSemanticColor('#29ccff')
226
+ .setSemanticOpacity(0.6)
227
+ .setSemanticVisible(true);
221
228
 
222
- await viewer.controls.flyToBox(facility.getFacilityBoundingBox(box, true), {
229
+ await viewer.controls.flyToObject(facility, {
223
230
  viewpoint: 'rightFrontTop',
224
231
  padding: 0.2,
225
232
  });
@@ -228,9 +235,9 @@ if (facility instanceof FacilityInstanceObject) {
228
235
 
229
236
  ### `FacilityInstancedLayer`
230
237
 
231
- `FacilityInstancedLayer` 继承自 `BaseGroup`,是 scene-level Facilities 的批量渲染层。`SemanticSceneGroup.facilityLayer` 默认就是该类型;它会按模型 URL 把同一模板的静态设备合并为一个或多个 `InstancedMesh`,并用普通 `Group` 包裹每个模型 URL batch,保证设备显隐、颜色、透明度和相机实例裁剪能在每帧渲染前同步,同时保留每个楼层下的 `FacilityInstanceObject` 引用用于业务 API 和事件冒泡。
238
+ `FacilityInstancedLayer` 继承自 `SemanticModelInstancedLayer`,是 scene-level Facilities 的批量渲染层。`SemanticGroup.facilityLayer` 默认就是该类型;它会按模型 URL 把同一模板的静态设备合并为一个或多个 `InstancedMesh`,并用普通 `Group` 包裹每个模型 URL batch,保证设备显隐、颜色、透明度和相机实例裁剪能在每帧渲染前同步,同时保留每个楼层下的 `FacilityInstanceObject` 引用用于业务 API 和事件冒泡。
232
239
 
233
- 使用 `SemanticLoader` 时通常不需要直接调用 `addFacilityBatch()`;只有自定义语义解析或自建设备 batch 时才需要手动创建 layer,然后通过 `semanticScene.setFacilityLayer(layer)` 挂回语义场景。
240
+ 使用 `SemanticLoader` 时通常不需要直接调用 `addSemanticInstances()`;只有自定义语义解析、运行时新增设备或自建设备 batch 时才需要手动创建 layer,然后通过 `semanticGroup.setFacilityLayer(layer)` 挂回语义场景。
234
241
 
235
242
  | 属性 / 方法 | 说明 |
236
243
  | :---------- | :--- |
@@ -239,11 +246,41 @@ if (facility instanceof FacilityInstanceObject) {
239
246
  | `name` | 构造函数默认设为 `'Facilities'`。 |
240
247
  | `getInstanceCulling()` | 返回当前实例裁剪配置:`enabled`、`frustum`、`minScreenRadius`。 |
241
248
  | `setInstanceCulling(options)` | 设置实例裁剪配置,并返回自身。默认 `enabled: false`、`frustum: true`、`minScreenRadius: 0`;默认不按相机裁剪设备,避免相机距离影响设备显隐。需要性能压缩时可设置 `enabled: true` 开启视锥裁剪,或再把 `minScreenRadius` 设为大于 0 的值开启屏幕尺寸裁剪。 |
242
- | `addFacilityBatch(url, template, facilities)` | 尝试把同一模型 URL 的设备引用合并为 instanced batch。成功返回 `true`,失败返回 `false`,失败时调用方应回退为普通模型渲染。 |
249
+ | `getSemanticInstances()` | 返回当前 layer 内全部语义实例对象数组。 |
250
+ | `getSemanticInstanceById(id)` | 根据实例对象的 `semanticId` 快速获取语义实例;找不到时返回 `undefined`。 |
251
+ | `hasSemanticInstance(id)` | 判断当前 layer 是否包含对应 `semanticId` 的实例。 |
252
+ | `reserveSemanticBatch(url, template, capacity)` | 为某个模型 URL 预分配 batch 容量。适合接下来会多次运行时新增实例的场景,成功返回 `true`,模板不支持 instancing 时返回 `false`。 |
253
+ | `addSemanticInstance(url, template, instance)` | 向某个模型 URL 的 batch 新增一个语义实例。成功返回 `true`,失败时调用方应回退为普通模型渲染。 |
254
+ | `addSemanticInstances(url, template, instances)` | 向某个模型 URL 的 batch 批量新增语义实例。推荐运行时新增多个实例时优先使用,避免连续单个新增造成重复扩容检查。 |
255
+ | `removeSemanticInstance(instanceOrId)` | 删除一个语义实例,可传实例对象或完整 `semanticId`。删除成功返回 `true`。 |
256
+ | `removeSemanticInstances(instancesOrIds)` | 删除一个或多个语义实例。可传单个实例对象、单个字符串 `semanticId`,或混合实例对象和 ID 的 iterable;返回实际删除数量。 |
257
+ | `removeSemanticBatch(url)` | 删除某个模型 URL 的完整 batch,释放对应内部 `InstancedMesh` 和 cloned materials。 |
258
+ | `clearSemanticBatches()` | 清空当前 layer 的所有模型 batch,并返回自身。 |
259
+
260
+ `addSemanticInstances(url, template, instances)` 会按模型 URL 复用或创建 batch,从 `template` 收集可 instancing 的可见 `Mesh`,克隆源材质,并为每个 batch 建立 instance matrix、颜色和透明度 attribute。没有动画、没有骨骼、没有 morph target,且每个多材质 `Mesh` 都有合法 `geometry.groups` 的模板会进入 instancing;不满足条件、模板没有可用 Mesh、`instances` 为空,或待加入实例的 `semanticId` 与当前 layer 内已有实例重复时会返回 `false`。
243
261
 
244
- `addFacilityBatch(url, template, facilities)` 会从 `template` 收集可 instancing 的可见 `Mesh`,克隆源材质,并为每个 batch 建立 instance matrix、颜色和透明度 attribute。没有动画、没有骨骼、没有 morph target,且每个多材质 `Mesh` 都有合法 `geometry.groups` 的模板会进入 instancing;不满足条件、模板没有可用 Mesh 或 `facilities` 为空时会返回 `false`。
262
+ 动态新增时,layer 会维护每个 URL batch 的 `capacity`。容量足够时,新增实例只会写入逻辑数组、注册 dirty callback 并标记下一帧同步;容量不足时才按 2 倍增长策略重建该 URL 下的内部 `InstancedMesh` 和 attribute buffer。运行时需要连续新增大量实例时,推荐先调用 `reserveSemanticBatch(url, template, expectedCount)`,再调用 `addSemanticInstances()`。删除实例不会立即收缩 GPU buffer;只有 batch 为空、调用 `removeSemanticBatch()` 或 `clearSemanticBatches()` 时才释放内部渲染资源。
245
263
 
246
- 渲染前,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`,并附加 `facility`、`facilityId`、`semanticId`、`semanticKind: 'Facilities'`、`semanticName` 和 `semanticIndex`,事件随后会按楼层引用对象继续冒泡。
264
+ 外部代码不需要直接写内部 `InstancedMesh.instanceMatrix`。要移动、旋转或缩放实例时,直接修改对应 `SemanticInstanceObject` 的 transform,然后让它更新世界矩阵或等待下一帧渲染即可;`SemanticInstanceObject.updateMatrixWorld()` 会标记 batch dirty,layer 会在渲染前把世界矩阵同步到内部 instance matrix 并设置 `instanceMatrix.needsUpdate = true`。因此即使底层使用 WebGPU storage instanced buffer,公开更新方式仍然是面向语义实例对象:
265
+
266
+ ```typescript
267
+ const instance = layer.getSemanticInstanceById('FACILITY_001');
268
+
269
+ if (instance) {
270
+ instance.position.set(10, 0, 5);
271
+ instance.updateMatrixWorld(true);
272
+ viewer.render();
273
+ }
274
+ ```
275
+
276
+ 删除 API 支持单个字符串 ID,不需要为了删除一个实例额外包一层数组。下面两个调用等价,都会按完整 ID 删除实例:
277
+
278
+ ```typescript
279
+ layer.removeSemanticInstance('FACILITY_001');
280
+ layer.removeSemanticInstances('FACILITY_001');
281
+ ```
282
+
283
+ 渲染前,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`、`semanticName` 和 `semanticIndex`,事件随后会按楼层引用对象继续冒泡。
247
284
 
248
285
  ```typescript
249
286
  import { FacilityInstancedLayer, FacilityInstanceObject } from 'u-space/plugins/u-manager';
@@ -251,16 +288,25 @@ import { FacilityInstancedLayer, FacilityInstanceObject } from 'u-space/plugins/
251
288
  const layer = new FacilityInstancedLayer();
252
289
  const refs = facilityItems.map(() => new FacilityInstanceObject());
253
290
 
254
- if (layer.addFacilityBatch('/models/camera.glb', templateObject, refs)) {
255
- semanticScene.setFacilityLayer(layer);
291
+ layer.reserveSemanticBatch('/models/camera.glb', templateObject, refs.length + 100);
292
+
293
+ if (layer.addSemanticInstances('/models/camera.glb', templateObject, refs)) {
294
+ semanticGroup.setFacilityLayer(layer);
256
295
  }
296
+
297
+ // 运行时按完整字符串 ID 删除一个实例;返回实际删除数量。
298
+ layer.removeSemanticInstances('FACILITY_001');
257
299
  ```
258
300
 
259
- 如果只想整体隐藏或禁用 scene-level batch,可以设置 `semanticScene.facilityLayer.visible = false`;如果只想控制单个设备,继续使用 `floor.showFacility(id)`、`semanticScene.setFacilityOpacity(id, opacity)` 或设备引用自身的 `setFacilityVisible()` / `setFacilityOpacity()`。
301
+ 如果只想整体隐藏或禁用 scene-level batch,可以设置 `semanticGroup.facilityLayer.visible = false`;如果只想控制单个设备,推荐从 `floor.getFacilityById(id)` 或 `viewer.objectManager.getById(id)` 取回 `FacilityInstanceObject` 后调用 `setSemanticVisible()` / `setSemanticOpacity()`。
302
+
303
+ ### `SemanticModelInstancedLayer`
304
+
305
+ `SemanticModelInstancedLayer` 是 `SceneInstancedLayer` 和 `FacilityInstancedLayer` 的共享实现。它负责模板 mesh 收集、材质克隆、`InstancedMesh` 创建、运行时实例新增/删除、capacity 扩容、instance matrix/color/opacity buffer 同步、raycast hit remap、fallback 判定和可选相机实例裁剪。layer 内部只依赖 `SemanticInstanceObject` 的 `semanticId`、`semanticKind`、`semanticName` 独立字段,不再扫描 `instance.userData` 的 `facilityId`、`id` 或 `sid`。`getSemanticInstanceById()` / `removeSemanticInstances(id)` 会按需刷新 id 索引;新增实例时如果发现重复 `semanticId` 会拒绝加入,避免同一个 layer 内出现不确定查询结果。通常业务代码不需要直接使用它,除非要为新的语义实例类型复用同一套模型 instancing 能力。
260
306
 
261
307
  ### `FloorMesh`
262
308
 
263
- `FloorMesh` 继承自 `BaseMesh`,表示一个楼层。它不是 `BatchedMesh`,而是把同一楼层内的语义对象合并为一份几何,并通过 TSL + `DataTexture` 在 GPU 侧按 `semanticIndex` 控制颜色、透明度、显示隐藏和实例矩阵。楼层语义网格会按实例透明度拆分为不透明和透明两个材质通道;墙、柱、门等默认不透明实例会进入 opaque pipeline,`Spaces`、`Windows` 或通过 `setOpacityAt()` 改成半透明的实例会进入 transparent pipeline。Facilities 通过楼层下的引用对象进行检索和控制;实际渲染可能来自 `SemanticSceneGroup.facilityLayer` 的 `InstancedMesh` batch,也可能是 fallback 的普通 `Model`。
309
+ `FloorMesh` 继承自 `BaseMesh`,表示一个楼层。它不是 `BatchedMesh`,而是把同一楼层内的语义对象合并为一份几何,并通过 TSL + `DataTexture` 在 GPU 侧按 `semanticIndex` 控制颜色、透明度、显示隐藏和实例矩阵。楼层语义网格会按实例透明度拆分为不透明和透明两个材质通道;墙、柱、门等默认不透明实例会进入 opaque pipeline,`Spaces`、`Windows` 或通过 `setOpacityAt()` 改成半透明的实例会进入 transparent pipeline。Facilities 通过楼层下的引用对象进行检索和控制;实际渲染可能来自 `SemanticGroup.facilityLayer` 的 `InstancedMesh` batch,也可能是 fallback 的普通 `Model`。
264
310
 
265
311
  这种结构的目标是让每个楼层通常只占一个主渲染 draw call,同时仍保留实例级控制能力。`xxxAt` 方法全部作用于单个语义实例;没有 `At` 后缀的方法作用于整个楼层 mesh。
266
312
 
@@ -270,7 +316,7 @@ if (layer.addFacilityBatch('/models/camera.glb', templateObject, refs)) {
270
316
  | `semanticInstances` | `Map<string, number>`,从语义对象 ID 映射到实例下标。 |
271
317
  | `wallsInstances` / `columnsInstances` / `spacesInstances` / `doorsInstances` / `windowsInstances` / `staircasesInstances` | 按语义类型维护的 ID 到实例下标映射。 |
272
318
  | `semanticEntities` | `Map<string, FloorSemanticEntity>`,统一索引楼层内合并几何语义和 `Facilities` 对象语义。 |
273
- | `facilityObjectsById` | `Map<string, Object3D>`,按 `Facility.ID` 或显式别名查找设备引用。 |
319
+ | `facilityObjectsById` | `Map<string, FacilityInstanceObject>`,按 `Facility.ID` 或显式别名查找设备引用。 |
274
320
  | `facilityObjects` | 当前楼层挂载的设备引用数组。 |
275
321
  | `getSemanticIndexById(id)` | 根据语义 ID 获取实例下标。 |
276
322
  | `getSemanticById(id)` | 根据语义 ID 或设备别名获取统一语义实体。 |
@@ -279,17 +325,13 @@ if (layer.addFacilityBatch('/models/camera.glb', templateObject, refs)) {
279
325
  | `getSemanticIdAt(index)` | 获取实例语义 ID。 |
280
326
  | `getSemanticKindAt(index)` | 获取合并几何实例类型:`'Walls' \| 'Columns' \| 'Spaces' \| 'Doors' \| 'Windows' \| 'Staircases'`。 |
281
327
  | `getSemanticNameAt(index)` | 获取实例名称;当前 `Spaces` 会从语义数据的 `Name` 写入,其他类型可能为 `undefined`。 |
328
+ | `getSemanticObjectAt(index)` | 获取合并几何实例对应的 `FloorSemanticInstanceObject` 轻量对象。 |
282
329
  | `showAllFacilities()` | 显示当前楼层的全部设备对象。 |
283
330
  | `hideAllFacilities()` | 隐藏当前楼层的全部设备对象。 |
284
- | `addFacility(object, ids?)` | 把设备对象挂到楼层,并按显式 ID、`facilityId`、`semanticId`、`id` 建立检索别名。 |
331
+ | `addFacility(object, ids?)` | 把设备对象挂到楼层,并按对象 `semanticId` 和显式 `ids` 建立检索别名。不会从 `object.userData` 自动扫描 ID。 |
285
332
  | `getFacilityById(id)` | 根据 `Facility.ID` 或别名获取设备对象。 |
286
333
  | `getFacilityAt(index)` | 按楼层设备数组下标获取设备对象。 |
287
334
  | `getFacilities()` | 获取当前楼层全部设备对象。 |
288
- | `showFacility(id \| object)` / `hideFacility(id \| object)` | 显示或隐藏当前楼层内的单个设备。 |
289
- | `setFacilityVisible(id \| object, visible)` | 设置当前楼层内单个设备显隐状态。 |
290
- | `setFacilityColor(id \| object, color)` | 设置当前楼层内单个设备材质颜色。 |
291
- | `resetFacilityColor(id \| object)` | 清除当前楼层内单个设备颜色 override。 |
292
- | `setFacilityOpacity(id \| object, opacity)` | 设置当前楼层内单个设备材质透明度。 |
293
335
  | `removeFacility(facility)` | 按设备对象或 ID 从楼层移除设备,并同步清理检索索引。 |
294
336
  | `setColorAt(index, color)` / `getColorAt(index, target?)` | 修改或读取实例颜色,支持 `Color`、`Vector3`、`Vector4`。 |
295
337
  | `setOpacityAt(index, opacity)` / `getOpacityAt(index)` | 修改或读取实例透明度。 |
@@ -305,7 +347,7 @@ if (layer.addFacilityBatch('/models/camera.glb', templateObject, refs)) {
305
347
 
306
348
  ### 飞向语义实例
307
349
 
308
- 点击、射线检测命中 `FloorMesh` 里的语义几何或设备时,`intersect` 上会附加语义字段:`semanticIndex`、`semanticId`、`semanticKind`。普通楼层语义几何的事件目标是 `FloorMesh`;instanced Facilities 的事件目标是楼层下的 `FacilityInstanceObject` 引用,事件会继续冒泡到所属 `FloorMesh`、`BuildingGroup` 和 `SemanticSceneGroup`。因此楼层监听器中应使用 `event.currentTarget` 操作楼层,用 `intersect.facility` 或 `event.target` 取得设备对象。
350
+ 点击、射线检测命中 `FloorMesh` 里的语义几何或设备时,`intersect` 上会附加语义字段:`semanticIndex`、`semanticId`、`semanticKind` 和 `semanticObject`。普通楼层语义几何的事件目标是 `FloorSemanticInstanceObject`;instanced Facilities 的事件目标是楼层下的 `FacilityInstanceObject` 引用,事件会继续冒泡到所属 `FloorMesh`、`BuildingGroup` 和 `SemanticGroup`。因此楼层监听器中应使用 `event.currentTarget` 操作楼层,用 `intersect.semanticObject` 或 `event.target` 取得具体语义实例。
309
351
 
310
352
  ```typescript
311
353
  import { Box3 } from 'three';
@@ -317,13 +359,17 @@ floor.addEventListener('click', async ({ event }) => {
317
359
  if (!intersect) return;
318
360
 
319
361
  if (intersect.semanticKind === 'Facilities') {
320
- const facility = intersect.facility ?? event.target;
321
- const facilityBox =
322
- typeof facility.getFacilityBoundingBox === 'function'
323
- ? facility.getFacilityBoundingBox(box, true)
324
- : box.setFromObject(facility);
362
+ const facility = intersect.semanticObject ?? event.target;
325
363
 
326
- await viewer.controls.flyToBox(facilityBox, {
364
+ await viewer.controls.flyToObject(facility, {
365
+ viewpoint: 'rightFrontTop',
366
+ padding: 0.2,
367
+ });
368
+ return;
369
+ }
370
+
371
+ if (intersect.semanticObject) {
372
+ await viewer.controls.flyToObject(intersect.semanticObject, {
327
373
  viewpoint: 'rightFrontTop',
328
374
  padding: 0.2,
329
375
  });
@@ -354,17 +400,17 @@ if (index !== undefined) {
354
400
 
355
401
  ### 楼层压扁
356
402
 
357
- `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`。
403
+ `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`。
358
404
 
359
405
  如果只想调整单个合并几何实例,可以使用 `setScaleAt()` 或 `setScaleYAt()`,它们会保留该语义对象自己的基点矩阵,因此压扁墙、柱、房间时不会被压到世界原点。
360
406
 
361
407
  ## `SceneLoader`
362
408
 
363
- 从服务端场景包加载完整场景树(模型、组、形状、拉伸区域),并自动将所有已加载对象按 `id` 和 `sid` 注册到 `viewer.objectManager`。
409
+ 从服务端场景包加载完整场景树(模型、组、基础形状),并自动将所有已加载对象按 `id` 和 `sid` 注册到 `viewer.objectManager`。
364
410
  默认会尝试读取同一路径下的 `db/semantic_model.json`,如果场景树节点的 `id` 已存在于语义文件中,则跳过该节点,方便和 `SemanticLoader` 同时加载而不重复创建建筑或设备。
365
- 对语义去重后仍保留的 3D 节点,`SceneLoader` 默认会按模型 `path` 自动合并重复静态模型:同一 `path` 的多个实例只加载一次模板,并由 `SceneInstancedLayer` 通过 `InstancedMesh` + `BundleGroup` 批量渲染。原场景树中仍会保留每个 `id` / `sid` 对应的 `SceneInstanceObject` 逻辑对象,用于业务检索、显隐、颜色、透明度、包围盒和事件派发。
411
+ 对语义去重后仍保留的 3D 节点,`SceneLoader` 默认会按模型 `path` 自动合并重复静态模型:同一 `path` 的多个实例只加载一次模板,并由 `SceneInstancedLayer` 通过共享的 `SemanticModelInstancedLayer` 批量渲染。原场景树中仍会保留每个 `id` / `sid` 对应的 `SceneInstanceObject` 逻辑对象,用于业务检索、显隐、颜色、透明度、包围盒和事件派发。
366
412
 
367
- 模板包含动画、骨骼、morph target、材质数组或没有可实例化网格时,会自动回退为普通 `Model` 渲染;调用侧不需要额外配置。
413
+ 模板包含动画、骨骼、morph target、非法多材质 groups 或没有可实例化网格时,会自动回退为普通 `Model` 渲染;fallback `Model` 会挂在同一个 `SceneInstanceObject` 下,因此调用侧不需要额外配置。
368
414
 
369
415
  ```typescript
370
416
  import { SceneLoader } from 'u-space/plugins/u-manager';
@@ -372,8 +418,10 @@ import { SceneLoader } from 'u-space/plugins/u-manager';
372
418
  const sceneLoader = new SceneLoader(viewer);
373
419
  sceneLoader.setPath('./scenes/my-scene');
374
420
  sceneLoader.setKey('YOUR_LICENSE_KEY'); // 官方授权场景必需
375
- const group = await sceneLoader.loadAsync();
421
+ const group = await sceneLoader.loadAsync(); // SceneGroup
376
422
  viewer.scene.add(group);
423
+
424
+ const sceneLayer = group.getDefaultSceneLayer();
377
425
  ```
378
426
 
379
427
  **方法:**
@@ -381,17 +429,69 @@ viewer.scene.add(group);
381
429
  | 方法 | 说明 |
382
430
  | :------------------ | :------------------------------------------------------------------- |
383
431
  | `setKey(key)` | 设置授权场景的 RSA 解密密钥。 |
384
- | `loadAsync()` | 加载并解析场景,返回去除语义重复节点后的 `Group`。 |
432
+ | `loadAsync()` | 加载并解析场景,返回去除语义重复节点后的 `SceneGroup`。 |
385
433
  | `clearCache()` | 从 `objectManager` 中移除此加载器注册的所有 ID。 |
386
434
  | `dispose()` | 清除缓存并释放水印叠加层。 |
387
435
 
436
+ ### `SceneGroup`
437
+
438
+ `SceneGroup` 继承自 `BaseGroup`,是 `SceneLoader.loadAsync()` 的返回根组。它保留 `tree_models.json` 解析后的原始父子层级,并把重复静态模型的默认合批渲染层挂在 `sceneLayer`,方便外部直接访问,而不需要遍历 `children`。
439
+
440
+ | 属性 / 方法 | 说明 |
441
+ | :---------- | :--- |
442
+ | `isSceneGroup` | 固定为 `true`,用于判断对象是否为 `SceneLoader` 根组。 |
443
+ | `sceneLayer` | 当前默认 `SceneInstancedLayer`;没有可合批的重复 3D 模型时为 `null`。 |
444
+ | `setSceneLayer(layer)` | 设置默认 `SceneInstancedLayer`;传入 `null` 会移除已有 layer。 |
445
+ | `getDefaultSceneLayer()` | 返回当前默认 `SceneInstancedLayer`;没有可合批模型时返回 `null`。 |
446
+
388
447
  ### `SceneInstanceObject`
389
448
 
390
- `SceneInstanceObject` 继承自 Three.js `Group`,表示一个由 `SceneInstancedLayer` 批量渲染的场景模型引用。它会保留在原场景树层级中,并注册到 `viewer.objectManager`,因此 `getById()`、`show()` / `hide()`、`isolate()`、`showAll()`、`setOpacity()` 和 `getBoundingBox()` 可以继续按单个模型 ID 使用。
449
+ `SceneInstanceObject` 继承自统一的 `SemanticInstanceObject`,表示一个由 `SceneInstancedLayer` 批量渲染或 fallback 普通渲染的场景模型引用。它会保留在原场景树层级中,并注册到 `viewer.objectManager`,因此 `getById()`、`show()` / `hide()`、`isolate()`、`showAll()`、`setOpacity()` 和 `getBoundingBox()` 可以继续按单个模型 ID 使用。
391
450
 
392
- 可用方法包括 `setSceneInstanceVisible(visible)`、`setSceneInstanceColor(color)`、`resetSceneInstanceColor()`、`setSceneInstanceOpacity(opacity)`、`getSceneInstanceOpacity()`、`getSceneInstanceBoundingBox(target?, world?)` 和 `markSceneInstanceRenderDirty()`。`MaterialEffects.highlightColor()` / `removeHighlightColor()` 也会识别该逻辑对象并写入 per-instance color / opacity。
451
+ `SceneInstanceObject` 本身只增加场景模型语义:`isSceneInstanceObject = true`、`type = 'SceneInstanceObject'`、`semanticKind = 'SceneInstances'`。加载器会把场景树节点的 `id` 和 `sid` 都注册到 `viewer.objectManager`,因此同一个对象可以通过任一 ID 取回。对重复模型的 instanced 路径,`semanticId` 默认等于场景树节点 `id`,`semanticName` 默认等于节点名称;`userData.instanced = true`,并保留 `modelPath` 和 `modelUrl` 等原始加载元数据。如果模板不支持 instancing,会退回普通 `Model`,但仍挂在同一个 `SceneInstanceObject` 下,对外 API 不变。
393
452
 
394
- 加载完成后可以按场景树节点 `id` 从 `objectManager` 取回 `SceneInstanceObject` 并直接传给 `viewer.controls.flyToObject()`。`SceneInstanceObject` 内部带有不可见的本地 bounds,因此不需要加载真实 Mesh 副本也能计算飞行包围盒。
453
+ **识别字段:**
454
+
455
+ | 字段 | 说明 |
456
+ | :--- | :--- |
457
+ | `isSceneInstanceObject` | 固定为 `true`,用于判断对象是否来自 `SceneLoader` 的场景实例。 |
458
+ | `isSemanticInstanceObject` | 固定为 `true`,表示它支持统一语义实例 API。 |
459
+ | `type` | 固定为 `SceneInstanceObject`。 |
460
+ | `semanticId` | 实例独立 ID,默认等于场景树节点 `id`;layer 的按 ID 查询、删除和 raycast remap 都使用该字段。 |
461
+ | `semanticKind` | 实例语义类型,场景实例为 `'SceneInstances'`。 |
462
+ | `semanticName` | 实例显示名称,默认等于场景树节点名称。 |
463
+ | `setSemanticIdentity({ id, kind, name })` | 设置实例独立身份字段,并返回自身;运行时手动创建实例时建议先设置再加入 layer,加入后修改 id 会在下一次按 id 查询时刷新索引。 |
464
+ | `boundingBox` | 实例本地包围盒缓存,初始为 `null`;`Box3.setFromObject()` 或手动调用 `computeBoundingBox()` 时会计算。 |
465
+ | `boundingSphere` | 实例本地包围球缓存,初始为 `null`;手动调用 `computeBoundingSphere()` 时会由当前本地包围盒派生。 |
466
+ | `userData.id` / `userData.sid` | 原始场景树节点 ID;两个 ID 都会注册到 `viewer.objectManager`。 |
467
+ | `userData.instanced` | `true` 表示实际渲染来自 `SceneInstancedLayer`;`false` 表示 fallback `Model` 挂在当前对象下。 |
468
+ | `userData.modelPath` / `userData.modelUrl` | instanced 场景实例对应的模型相对路径和完整 URL。 |
469
+
470
+ **方法:**
471
+
472
+ | 方法 | 说明 |
473
+ | :--- | :--- |
474
+ | `setSemanticBounds(bounds)` | 设置本地包围盒。`SceneLoader` 会在创建 instanced batch 时根据模板模型自动设置;业务一般不需要手动调用。 |
475
+ | `setSemanticRenderObject(object)` | 设置 fallback 渲染对象并作为子对象挂载。模板无法 instancing 时由 `SceneLoader` 自动调用;自定义语义实例渲染时可手动使用。 |
476
+ | `getSemanticRenderObject()` | 返回 fallback 渲染对象;instanced 渲染路径通常返回 `null`,因为真实 Mesh 在 `SceneInstancedLayer` 中。 |
477
+ | `computeBoundingBox()` / `computeBoundingSphere()` | 按 Three.js 对象级包围盒约定更新 `boundingBox` / `boundingSphere`,使 `viewer.controls.flyToObject()` 能直接飞向实例。 |
478
+ | `getSemanticBoundingBox(target?, world?)` | 返回实例包围盒。`world = true` 时会应用 `matrixWorld`,适合直接传给 `viewer.controls.flyToBox()`;无 bounds 和 render object 时返回 empty `Box3`。 |
479
+ | `setSemanticVisible(visible)` | 设置单个实例显隐并标记 batch dirty;隐藏后该实例不会继续渲染,也不会被 instanced raycast 命中。 |
480
+ | `setSemanticColor(color)` | 设置单个实例颜色 override。instanced 路径写入 per-instance color;fallback `Model` 路径会委托到材质高亮逻辑。 |
481
+ | `resetSemanticColor()` | 清除颜色 override,恢复使用源模型颜色;不会清除透明度 override。 |
482
+ | `setSemanticOpacity(opacity)` | 设置单个实例透明度,值会限制在 `0..1`。instanced 路径写入 per-instance opacity;fallback 路径会按原始材质透明度相乘。 |
483
+ | `getSemanticColor(target?)` | 获取当前生效颜色。存在颜色或高亮 override 时返回该颜色,否则返回白色。传入 `Color` 可复用对象。 |
484
+ | `hasSemanticColor()` | 返回当前是否存在颜色或高亮颜色 override。 |
485
+ | `getSemanticOpacity()` | 获取当前生效透明度;高亮存在时返回高亮透明度,否则返回基础透明度。 |
486
+ | `setSemanticHighlight(color, opacity, overwrite?)` | 设置临时高亮样式。`overwrite = false` 时按 tint 模式与原色混合,`true` 时直接替换颜色;高亮会覆盖基础颜色/透明度直到调用 `clearSemanticHighlight()`。 |
487
+ | `clearSemanticHighlight()` | 清除临时高亮,并恢复到基础颜色/透明度状态。 |
488
+ | `getSemanticColorMode()` | 返回当前颜色模式:`SEMANTIC_COLOR_MODE_NONE`、`SEMANTIC_COLOR_MODE_OVERRIDE` 或 `SEMANTIC_COLOR_MODE_TINT`。 |
489
+ | `onSemanticRenderDirty(callback)` | 监听实例渲染状态变化,返回取消监听函数。实例显隐、颜色、透明度或世界矩阵变化时会触发。 |
490
+ | `markSemanticRenderDirty()` | 手动标记实例渲染状态已变化,通知所属 batch 在下一帧同步 instance buffer。 |
491
+
492
+ `MaterialEffects.highlightColor()` / `removeHighlightColor()` 会识别 `SceneInstanceObject` 并调用 `setSemanticHighlight()` / `clearSemanticHighlight()`,不会直接修改共享 batch 材质。`ObjectManager.setOpacity()`、`ObjectManager.hide()`、`ObjectManager.show()` 也会通过统一 API 作用到单个实例。
493
+
494
+ 加载完成后可以按场景树节点 `id` 从 `objectManager` 取回 `SceneInstanceObject`,再直接传给 `viewer.controls.flyToObject()`。`SceneInstanceObject` 内部带有本地 `boundingBox` 缓存能力,因此不需要加载真实 Mesh 副本也能计算飞行包围盒。
395
495
 
396
496
  ```typescript
397
497
  import { SceneInstanceObject } from 'u-space/plugins/u-manager';
@@ -407,20 +507,33 @@ if (object?.isSceneInstanceObject) {
407
507
  }
408
508
  ```
409
509
 
510
+ 颜色、透明度和显隐可以直接链式调用:
511
+
512
+ ```typescript
513
+ object
514
+ ?.setSemanticColor('#29ccff')
515
+ .setSemanticOpacity(0.55)
516
+ .setSemanticVisible(true);
517
+
518
+ object?.setSemanticHighlight('#ffb703', 0.75, true);
519
+ object?.clearSemanticHighlight();
520
+ object?.resetSemanticColor();
521
+ ```
522
+
410
523
  如果需要自己控制包围盒,也可以取世界包围盒后调用 `flyToBox()`:
411
524
 
412
525
  ```typescript
413
526
  import { Box3 } from 'three/webgpu';
414
527
 
415
- const box = object.getSceneInstanceBoundingBox(new Box3(), true);
528
+ const box = object.getSemanticBoundingBox(new Box3(), true);
416
529
  await viewer.controls.flyToBox(box, { viewpoint: 'frontTop', padding: 0.2 });
417
530
  ```
418
531
 
419
- 射线检测命中批处理实例时,`intersect.object` 会被改写为对应的 `SceneInstanceObject`,并附加 `sceneInstance`、`sceneInstanceId` 和 `sceneInstanceIndex`,事件会按逻辑对象所在的原场景树继续冒泡。
532
+ 射线检测命中批处理实例时,`intersect.object` 会被改写为对应的 `SceneInstanceObject`,并附加 `semanticObject`、`semanticId`、`semanticKind: 'SceneInstances'` 和 `semanticIndex`,事件会按逻辑对象所在的原场景树继续冒泡。
420
533
 
421
534
  ### `SceneInstancedLayer`
422
535
 
423
- `SceneInstancedLayer` 是 `SceneLoader` 内部使用的批量渲染层,默认 `name = 'SceneInstances'`。每个重复模型 URL 会生成一个 `BundleGroup` batch,batch 内按模板的可实例化 Mesh 创建 `InstancedMesh`。渲染前只在实例矩阵、显隐、颜色或透明度变化时同步 instance buffer;普通相机移动不会重新上传所有实例数据。
536
+ `SceneInstancedLayer` 是 `SceneLoader` 内部使用的批量渲染层,默认 `name = 'SceneInstances'`。它继承自 `SemanticModelInstancedLayer`,每个重复模型 URL 会生成一个 batch,batch 内按模板的可实例化 Mesh 创建 `InstancedMesh`。渲染前只在实例矩阵、显隐、颜色或透明度变化时同步 instance buffer;普通相机移动不会重新上传所有实例数据。
424
537
 
425
538
  ## `TopologiesLoader` / `TopologyParser`
426
539