u-space 0.0.25 → 0.0.26

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.
@@ -16,12 +16,15 @@ export declare class BuildingGroup extends BaseGroup {
16
16
  addFloor(mesh: FloorMesh): this;
17
17
  hasFloor(mesh: FloorMesh): boolean;
18
18
  getFloorAt(index: number): FloorMesh;
19
+ getFloorById(id: string): FloorMesh | undefined;
19
20
  removeFloor(mesh: FloorMesh): this;
20
21
  addElevator(mesh: ElevatorMesh): this;
21
22
  getElevatorAt(index: number): ElevatorMesh;
23
+ getElevatorById(id: string): ElevatorMesh | undefined;
22
24
  removeElevator(mesh: ElevatorMesh): this;
23
25
  addVent(mesh: VentMesh): this;
24
26
  getVentAt(index: number): VentMesh;
27
+ getVentById(id: string): VentMesh | undefined;
25
28
  removeVent(mesh: VentMesh): this;
26
29
  isolateFloor(floor: BuildingFloorReference | BuildingFloorReference[]): this;
27
30
  isolateFloors(floors: BuildingFloorReference[]): this;
@@ -5,6 +5,7 @@ export declare class FacilityInstanceObject extends Group {
5
5
  isFacilityInstanceObject: boolean;
6
6
  type: string;
7
7
  setLocalBounds(bounds: Box3): this;
8
+ getFacilityBoundingBox(target?: Box3, world?: boolean): Box3;
8
9
  setFacilityVisible(visible: boolean): this;
9
10
  setFacilityColor(color: ColorRepresentation): this;
10
11
  resetFacilityColor(): this;
@@ -17,6 +18,14 @@ export declare class FacilityInstancedLayer extends BaseGroup {
17
18
  #private;
18
19
  isFacilityInstancedLayer: boolean;
19
20
  type: string;
21
+ /** @deprecated No effect; facility culling uses per-instance bounding-box frustum tests only. */
22
+ cameraDistanceCulling: boolean;
23
+ /** @deprecated No effect; facility culling uses per-instance bounding-box frustum tests only. */
24
+ cameraCullDistance: number;
25
+ /** @deprecated No effect; facility culling uses per-instance bounding-box frustum tests only. */
26
+ cameraCullOverviewFactor: number;
27
+ /** @deprecated No effect; facility culling uses per-instance bounding-box frustum tests only. */
28
+ cameraCullPlanViewThreshold: number;
20
29
  constructor();
21
30
  addFacilityBatch(url: string, template: Object3D, facilities: FacilityInstanceObject[]): boolean;
22
31
  }
@@ -3,6 +3,7 @@ import { BaseMesh, type InteractionEvent, type InteractionEventMap } from 'u-spa
3
3
  import type { SemanticColor, SemanticGeometryEntry, SemanticKind } from '../types';
4
4
  type FacilityReference = Object3D | string;
5
5
  type FacilitySemanticKind = 'Facilities';
6
+ type FloorMeshMaterial = MeshStandardNodeMaterial | MeshStandardNodeMaterial[];
6
7
  export type FloorSemanticKind = SemanticKind | FacilitySemanticKind;
7
8
  export interface FloorGeometrySemanticEntity {
8
9
  type: 'geometry';
@@ -43,7 +44,7 @@ export type FloorMeshInteractionEvent = Omit<InteractionEvent, 'target' | 'curre
43
44
  export type FloorMeshEventMap = {
44
45
  [K in keyof InteractionEventMap]: FloorMeshEventPayload<InteractionEventMap[K]>;
45
46
  };
46
- export declare class FloorMesh extends BaseMesh<BufferGeometry, MeshStandardNodeMaterial, FloorMeshEventMap> {
47
+ export declare class FloorMesh extends BaseMesh<BufferGeometry, FloorMeshMaterial, FloorMeshEventMap> {
47
48
  #private;
48
49
  isFloorMergedSemanticMesh: boolean;
49
50
  type: string;
@@ -13,6 +13,8 @@ export interface SemanticObjectState {
13
13
  kind: SemanticKind;
14
14
  name?: string;
15
15
  index: number;
16
+ indexStart: number;
17
+ indexCount: number;
16
18
  baseMatrix: Matrix4;
17
19
  matrix: Matrix4;
18
20
  finalMatrix: Matrix4;
@@ -57,7 +57,7 @@ floor.addEventListener('click', ({ event }) => {
57
57
  | `setFacilityOpacity(id \| object, opacity)` | 设置单个设备材质透明度,`opacity` 会被限制在 `0..1`。 |
58
58
  | `showAllFacilities()` / `hideAllFacilities()` | 显示或隐藏整次语义解析结果中的全部设备。 |
59
59
  | `showAllFloors()` | 显示所有建筑的所有楼层。 |
60
- | `planishFloors(kind?)` / `unplanishFloors(kind?)` | 压扁或恢复所有楼层;默认覆盖所有合并语义实例和 Facilities,并按包围盒贴到楼层平面;传入 `kind` 时只作用于该语义类型,`kind` 可包含 `Facilities`。 |
60
+ | `planishFloors(kind?)` / `unplanishFloors(kind?)` | 压扁或恢复所有楼层;默认覆盖所有合并语义实例和 Facilities,并按包围盒贴到楼层平面附近,同时添加稳定的轻微 Y 偏移以减少共面闪烁;传入 `kind` 时只作用于该语义类型,`kind` 可包含 `Facilities`。 |
61
61
 
62
62
  ### `BuildingGroup`
63
63
 
@@ -73,10 +73,13 @@ floor.addEventListener('click', ({ event }) => {
73
73
  | `addFloor(floor)` | 添加楼层,并同步加入 group。 |
74
74
  | `removeFloor(floor)` | 移除楼层,并同步从 group 移除。 |
75
75
  | `getFloorAt(index)` | 按楼层下标获取 `FloorMesh`。 |
76
+ | `getFloorById(id)` | 按楼层语义 ID 获取 `FloorMesh`,内部使用 `Map` 索引。 |
76
77
  | `addElevator(elevator)` / `removeElevator(elevator)` | 添加或移除电梯井,并同步更新 group。 |
77
78
  | `getElevatorAt(index)` | 按下标获取电梯井 `ExtrudeMesh`。 |
79
+ | `getElevatorById(id)` | 按电梯井语义 ID 获取电梯井,内部使用 `Map` 索引。 |
78
80
  | `addVent(vent)` / `removeVent(vent)` | 添加或移除通风井,并同步更新 group。 |
79
81
  | `getVentAt(index)` | 按下标获取通风井 `ExtrudeMesh`。 |
82
+ | `getVentById(id)` | 按通风井语义 ID 获取通风井,内部使用 `Map` 索引。 |
80
83
  | `isolateFloor(floor \| index)` | 只显示指定楼层,隐藏其他楼层;也兼容传入楼层数组。 |
81
84
  | `isolateFloors(floors \| indexes)` | 只显示指定多个楼层,隐藏其他楼层。 |
82
85
  | `showAllFloors()` | 显示全部楼层。 |
@@ -88,7 +91,7 @@ floor.addEventListener('click', ({ event }) => {
88
91
  | `setFacilityOpacity(id \| object, opacity)` | 设置当前建筑内单个设备材质透明度。 |
89
92
  | `showAllFacilities()` | 显示整栋建筑内全部楼层设备。 |
90
93
  | `hideAllFacilities()` | 隐藏整栋建筑内全部楼层设备。 |
91
- | `planishFloors(kind?)` | 压扁所有楼层;默认覆盖所有合并语义实例和 Facilities,并按包围盒贴到楼层平面;传入 `kind` 时只压扁该语义类型,`kind` 可包含 `Facilities`。 |
94
+ | `planishFloors(kind?)` | 压扁所有楼层;默认覆盖所有合并语义实例和 Facilities,并按包围盒贴到楼层平面附近,同时添加稳定的轻微 Y 偏移以减少共面闪烁;传入 `kind` 时只压扁该语义类型,`kind` 可包含 `Facilities`。 |
92
95
  | `unplanishFloors(kind?)` | 恢复所有楼层;默认恢复所有合并语义实例和 Facilities;传入 `kind` 时只恢复该语义类型,`kind` 可包含 `Facilities`。 |
93
96
 
94
97
  ### 电梯井和通风井
@@ -97,7 +100,9 @@ floor.addEventListener('click', ({ event }) => {
97
100
 
98
101
  ```typescript
99
102
  const elevator = building.getElevatorAt(0);
103
+ const elevatorById = building.getElevatorById('ELEVATOR_001');
100
104
  const vent = building.getVentAt(0);
105
+ const ventById = building.getVentById('VENT_001');
101
106
 
102
107
  if (elevator) {
103
108
  await viewer.controls.flyToObject(elevator);
@@ -120,18 +125,29 @@ Facilities 会在单次 `SemanticParser` 解析范围内按模型 URL 自动分
120
125
 
121
126
  `facilityLayer` 使用 `FacilityInstancedLayer`,并继承自 `BaseGroup`。当 `facilityLayer.visible = false` 时,layer 会阻止内部 `InstancedMesh` 继续参与射线检测,避免隐藏的批处理设备仍被点击命中;单个设备的显隐仍应通过楼层下的设备引用或 `showFacility()` / `hideFacility()` 控制。
122
127
 
123
- Instanced 设备为了支持单个设备的透明度控制,会把 batch 材质保持为 `transparent: true`,并通过 per-instance opacity attribute 控制每个设备的可见透明度;`setFacilityOpacity(id, 1)` 会恢复完全不透明显示,但不会把 batch 材质的 `transparent` 属性改回 `false`。`resetFacilityColor()` 只清除颜色 override,不影响透明度 override。普通 fallback `Model` 的颜色和透明度控制依赖 `MaterialEffects`,当前支持单材质 Mesh;多材质 Mesh fallback 会保留原始材质数组,支持挂载、检索和显隐,但不会改写颜色或透明度 override。
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。
124
129
 
125
130
  加载成功后,设备引用会写入 `userData.facilityId`、`semanticId`、`semanticKind: 'Facilities'`、`semanticName`、`spaces`、`twinsIdentifier`、`storyId` 和 `floorIndex`,并按 `Facility.ID` 注册到 `viewer.objectManager`。自动 instancing 的引用还会带有 `userData.instanced: true` 和 `userData.modelUrl`。
126
131
 
127
132
  ```typescript
133
+ import { Box3 } from 'three';
134
+
128
135
  const floor = building.getFloorAt(0);
136
+ const box = new Box3();
129
137
 
130
138
  const facility = floor.getFacilityById('FACILITY_001') ?? viewer.objectManager.getById('FACILITY_001');
131
139
  const facilityEntity = floor.getSemanticById('FACILITY_001');
132
140
 
133
141
  if (facility && facilityEntity?.type === 'object') {
134
- await viewer.controls.flyToObject(facility);
142
+ const facilityBox =
143
+ typeof facility.getFacilityBoundingBox === 'function'
144
+ ? facility.getFacilityBoundingBox(box, true)
145
+ : box.setFromObject(facility);
146
+
147
+ await viewer.controls.flyToBox(facilityBox, {
148
+ viewpoint: 'rightFrontTop',
149
+ padding: 0.2,
150
+ });
135
151
  }
136
152
 
137
153
  const facilities = floor.getSemanticsByKind('Facilities');
@@ -144,9 +160,11 @@ semanticScene
144
160
  .showFacility('FACILITY_001');
145
161
  ```
146
162
 
163
+ 对 Facilities 推荐使用 `controls.flyToBox()` 飞向设备包围盒。instanced 设备引用是楼层下的轻量 `FacilityInstanceObject`,可通过 `getFacilityBoundingBox(box, true)` 取得完整世界包围盒;普通 fallback `Model` 也可以直接使用 `viewer.controls.flyToObject(facility)`。
164
+
147
165
  ### `FloorMesh`
148
166
 
149
- `FloorMesh` 继承自 `BaseMesh`,表示一个楼层。它不是 `BatchedMesh`,而是把同一楼层内的语义对象合并为一份几何,并通过 TSL + `DataTexture` 在 GPU 侧按 `semanticIndex` 控制颜色、透明度、显示隐藏和实例矩阵。Facilities 通过楼层下的引用对象进行检索和控制;实际渲染可能来自 `SemanticSceneGroup.facilityLayer` 的 `InstancedMesh` batch,也可能是 fallback 的普通 `Model`。
167
+ `FloorMesh` 继承自 `BaseMesh`,表示一个楼层。它不是 `BatchedMesh`,而是把同一楼层内的语义对象合并为一份几何,并通过 TSL + `DataTexture` 在 GPU 侧按 `semanticIndex` 控制颜色、透明度、显示隐藏和实例矩阵。楼层语义网格会按实例透明度拆分为不透明和透明两个材质通道;墙、柱、门等默认不透明实例会进入 opaque pipeline,`Spaces`、`Windows` 或通过 `setOpacityAt()` 改成半透明的实例会进入 transparent pipeline。Facilities 通过楼层下的引用对象进行检索和控制;实际渲染可能来自 `SemanticSceneGroup.facilityLayer` 的 `InstancedMesh` batch,也可能是 fallback 的普通 `Model`。
150
168
 
151
169
  这种结构的目标是让每个楼层通常只占一个主渲染 draw call,同时仍保留实例级控制能力。`xxxAt` 方法全部作用于单个语义实例;没有 `At` 后缀的方法作用于整个楼层 mesh。
152
170
 
@@ -187,7 +205,7 @@ semanticScene
187
205
  | `getBoundingBoxAt(index, target?, world?)` | 获取实例包围盒;`world = true` 时包含 `FloorMesh.matrixWorld`。 |
188
206
  | `getBoundingSphereAt(index, target?, world?)` | 获取实例包围球;`world = true` 时包含 `FloorMesh.matrixWorld`。 |
189
207
  | `computeBoundingBox()` / `computeBoundingSphere()` | 计算整个楼层的包围体。 |
190
- | `planish(kind?)` / `unplanish(kind?)` | 压扁或恢复楼层内所有合并语义实例和 Facilities;压平时按各自包围盒贴到楼层平面;传入 `kind` 时只作用于该语义类型,`kind` 可包含 `Facilities`。 |
208
+ | `planish(kind?)` / `unplanish(kind?)` | 压扁或恢复楼层内所有合并语义实例和 Facilities;压平时按各自包围盒贴到楼层平面附近,并加入稳定的轻微 Y 偏移;传入 `kind` 时只作用于该语义类型,`kind` 可包含 `Facilities`。 |
191
209
 
192
210
  ### 飞向语义实例
193
211
 
@@ -204,7 +222,15 @@ floor.addEventListener('click', async ({ event }) => {
204
222
 
205
223
  if (intersect.semanticKind === 'Facilities') {
206
224
  const facility = intersect.facility ?? event.target;
207
- await viewer.controls.flyToObject(facility);
225
+ const facilityBox =
226
+ typeof facility.getFacilityBoundingBox === 'function'
227
+ ? facility.getFacilityBoundingBox(box, true)
228
+ : box.setFromObject(facility);
229
+
230
+ await viewer.controls.flyToBox(facilityBox, {
231
+ viewpoint: 'rightFrontTop',
232
+ padding: 0.2,
233
+ });
208
234
  return;
209
235
  }
210
236
 
@@ -232,7 +258,7 @@ if (index !== undefined) {
232
258
 
233
259
  ### 楼层压扁
234
260
 
235
- `planish()` / `unplanish()` 默认作用于当前楼层的全部合并语义实例,包含墙、柱、空间、门窗、楼梯等语义几何,也包含楼层下的 Facilities 引用;传入 `kind` 时只处理该类型,例如 `floor.planish('Walls')` 或 `floor.planish('Facilities')`。所有被压平的实例都会按自身包围盒把压平后的薄片贴到楼层平面 `FloorMesh.pivot.y`,而不是围绕实例自身基点压缩;`unplanish()` 会恢复合并几何实例的原始矩阵,`unplanish('Facilities')` 会恢复设备原始 `position.y` 和 `scale.y`。instanced Facilities 会通过楼层下的 `FacilityInstanceObject` transform 同步到 `SemanticSceneGroup.facilityLayer`。
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`。
236
262
 
237
263
  如果只想调整单个合并几何实例,可以使用 `setScaleAt()` 或 `setScaleYAt()`,它们会保留该语义对象自己的基点矩阵,因此压扁墙、柱、房间时不会被压到世界原点。
238
264
 
package/docs/changelog.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  ## 未发布
4
4
 
5
+ ### 修复
6
+
7
+ - **u-manager semantics** — `FloorMesh.planish()` 会在压平后的语义实例和 Facilities 上加入稳定的轻微 Y 偏移,减少门、墙等重叠薄片之间的共面闪烁。
8
+ - **u-manager semantics** — `FloorMesh` 会按实例透明度把合并语义网格拆分到不透明和透明材质通道,避免墙、柱、门等完全不透明实例也走透明渲染队列,降低近距离压平场景的 overdraw 成本。
9
+ - **u-manager semantics** — `BuildingGroup` 新增 `getFloorById()`、`getElevatorById()` 和 `getVentById()`,内部通过 `Map` 维护楼层、电梯井和通风井语义 ID 索引。
10
+ - **u-manager semantics** — `FacilityInstancedLayer` 只在实例矩阵、颜色或透明度变化时上传对应 instance buffer,静态 Facilities 不再每帧重复提交全部实例数据;渲染前会按每个 Facility 实例的世界包围盒和当前相机视锥压缩本帧 active instance count,不再通过相机距离阈值裁剪设备。
11
+ - **u-manager semantics** — 修复 Facilities 视锥裁剪在视角切换后可能沿用上一帧 active subset 的 `visible` / `boundingSphere` 状态,导致某些视角下设备整批隐藏的问题;instanced batch 会保留完整包围球,同时用 `count` 控制本帧 active 实例。
12
+ - **u-manager semantics** — Instanced Facilities 的 batch 材质默认保持不透明,只有源材质本身透明或当前渲染实例存在半透明 opacity 时才切到透明队列,减少完全不透明设备在近距离视角下的 overdraw。
13
+ - **MaterialEffects** — 高亮、呼吸色、线框和淡入淡出效果支持材质数组,确保多材质 Mesh 和 `FloorMesh` 双材质通道都能正确应用并恢复材质状态。
14
+ - **docs/mcp** — 补充 Facilities auto instancing 的动态材质透明度、包围盒视锥裁剪、`controls.flyToBox()` 飞向设备,以及多材质 fallback 对颜色/透明度 override 的支持说明。
15
+
16
+ ## 0.0.25
17
+
5
18
  ### 新增
6
19
 
7
20
  - **u-manager semantics** — 新增 `SemanticSceneGroup`,作为单次 `SemanticParser` 解析结果的顶层容器;`SemanticLoader.loadAsync()` / `SemanticParser.parseAsync()` 直接返回完整语义场景。
@@ -18,6 +31,10 @@
18
31
  - **u-manager semantics** — `FacilityInstancedLayer` 继承 `BaseGroup`,避免 `facilityLayer.visible = false` 后内部 `InstancedMesh` 仍参与射线检测并触发隐藏设备点击事件。
19
32
  - **u-manager semantics** — 移除语义设备解析里的 `sid` 检索和注册逻辑;Facilities 只按 `Facility.ID` 以及显式传入的别名进入楼层索引和 `viewer.objectManager`。
20
33
 
34
+ ### 示例与文档
35
+
36
+ - **在线示例** — `examples/importmap.js` 的 CDN fallback 版本更新为 `0.0.25`,Vercel 部署会继续按 `package.json` 注入当前版本。
37
+
21
38
  ## 0.0.23
22
39
 
23
40
  ### 新增
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## 运行方式与版本
6
6
 
7
- 示例统一通过 `examples/importmap.js` 注入 Import Map。本地通过 `localhost`、`127.0.0.1`、`0.0.0.0` 或 `192.168.x.x` 访问时会加载仓库里的 `../dist/` 构建产物;在线部署或非本地域名访问时会从 jsDelivr 加载当前发布版本 `u-space@0.0.23`。Vercel 文档部署会继续把 `__VERSION__` 占位符替换为 `package.json` 中的版本号,源码里的 `0.0.23` 作为直接托管 `examples/` 时的 fallback。
7
+ 示例统一通过 `examples/importmap.js` 注入 Import Map。本地通过 `localhost`、`127.0.0.1`、`0.0.0.0` 或 `192.168.x.x` 访问时会加载仓库里的 `../dist/` 构建产物;在线部署或非本地域名访问时会从 jsDelivr 加载当前发布版本 `u-space@0.0.25`。Vercel 文档部署会继续把 `__VERSION__` 占位符替换为 `package.json` 中的版本号,源码里的 `0.0.25` 作为直接托管 `examples/` 时的 fallback。
8
8
 
9
9
  插件示例可以在页面加载 `importmap.js` 前通过 `window.__IMPORTS__` 声明额外依赖。将 `u-space/plugins/<name>` 的值设为 `true` 时,`importmap.js` 会自动在本地和 CDN 路径之间切换。
10
10
 
@@ -125,7 +125,7 @@ box.addEventListener('pointerleave', (e) => {
125
125
 
126
126
  ```typescript
127
127
  import { version } from 'u-space';
128
- console.log(version); // e.g. '0.0.23'
128
+ console.log(version); // e.g. '0.0.25'
129
129
 
130
130
  // 也可以通过全局变量访问
131
131
  console.log(window.__USPACE__.version);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u-space",
3
- "version": "0.0.25",
3
+ "version": "0.0.26",
4
4
  "type": "module",
5
5
  "types": "dist/src/index.d.ts",
6
6
  "module": "dist/index.js",