@ifc-lite/viewer 1.14.3 → 1.15.0

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/CHANGELOG.md +32 -0
  2. package/dist/assets/{Arrow.dom-BgkZDIQm.js → Arrow.dom-OVBBPqOB.js} +1 -1
  3. package/dist/assets/{basketViewActivator-h_M3YbMW.js → basketViewActivator-Bx6QU4ma.js} +1 -1
  4. package/dist/assets/{browser-CRQ0bPh1.js → browser-BMqEoJw4.js} +1 -1
  5. package/dist/assets/ifc-lite_bg-CyWQTvp5.wasm +0 -0
  6. package/dist/assets/index-CJr7Itua.css +1 -0
  7. package/dist/assets/index-DZY6uD8A.js +185948 -0
  8. package/dist/assets/{index-C4VVJRL-.js → index-DsX-NCtx.js} +4 -4
  9. package/dist/assets/{native-bridge-DtcJqlOi.js → native-bridge-D6tKFqGO.js} +1 -1
  10. package/dist/assets/{wasm-bridge-BJJVu9P2.js → wasm-bridge-D4kvZVDw.js} +1 -1
  11. package/dist/index.html +2 -2
  12. package/package.json +7 -7
  13. package/src/components/viewer/CommandPalette.tsx +1 -0
  14. package/src/components/viewer/ExportDialog.tsx +40 -2
  15. package/src/components/viewer/HierarchyPanel.tsx +127 -35
  16. package/src/components/viewer/MainToolbar.tsx +113 -95
  17. package/src/components/viewer/ViewportContainer.tsx +30 -25
  18. package/src/components/viewer/hierarchy/HierarchyNode.tsx +10 -3
  19. package/src/components/viewer/hierarchy/treeDataBuilder.test.ts +126 -0
  20. package/src/components/viewer/hierarchy/treeDataBuilder.ts +139 -38
  21. package/src/components/viewer/hierarchy/types.ts +6 -1
  22. package/src/components/viewer/hierarchy/useHierarchyTree.ts +27 -12
  23. package/src/sdk/adapters/visibility-adapter.ts +82 -2
  24. package/src/store/basketVisibleSet.ts +72 -4
  25. package/src/store/index.ts +11 -1
  26. package/src/store/slices/visibilitySlice.ts +28 -2
  27. package/dist/assets/ifc-lite_bg-DyBKoGgk.wasm +0 -0
  28. package/dist/assets/index-Be6XjVeM.js +0 -116717
  29. package/dist/assets/index-DdwD4c-E.css +0 -1
@@ -2,7 +2,7 @@
2
2
  * License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
4
 
5
- import { IfcTypeEnum, type SpatialNode } from '@ifc-lite/data';
5
+ import { IfcTypeEnum, type SpatialNode, type SpatialHierarchy } from '@ifc-lite/data';
6
6
  import type { IfcDataStore } from '@ifc-lite/parser';
7
7
  import type { EntityRef } from './types.js';
8
8
  import { entityRefToString, stringToEntityRef } from './types.js';
@@ -57,6 +57,7 @@ function visibilityFingerprint(state: ViewerStateSnapshot): string {
57
57
  return [
58
58
  digestNumberSet(state.hiddenEntities),
59
59
  state.isolatedEntities ? digestNumberSet(state.isolatedEntities) : 'none',
60
+ state.classFilter ? digestNumberSet(state.classFilter.ids) : 'none',
60
61
  digestNumberSet(state.lensHiddenIds),
61
62
  digestModelEntityMap(state.hiddenEntitiesByModel),
62
63
  digestModelEntityMap(state.isolatedEntitiesByModel),
@@ -273,6 +274,52 @@ function getExpandedSelectionRefs(state: ViewerStateSnapshot): EntityRef[] {
273
274
  return dedupeRefs(baseRefs.flatMap((ref) => expandRefToElements(state, ref)));
274
275
  }
275
276
 
277
+ /**
278
+ * Collect all descendant IfcSpace expressIds from a spatial node.
279
+ */
280
+ function collectDescendantSpaceIds(node: SpatialNode): number[] {
281
+ const spaceIds: number[] = [];
282
+ for (const child of node.children || []) {
283
+ if (child.type === IfcTypeEnum.IfcSpace) {
284
+ spaceIds.push(child.expressId);
285
+ }
286
+ // Recurse into all children (spaces can nest under other spatial nodes)
287
+ spaceIds.push(...collectDescendantSpaceIds(child));
288
+ }
289
+ return spaceIds;
290
+ }
291
+
292
+ /**
293
+ * Collect all element IDs for an IfcBuildingStorey, including elements
294
+ * contained in descendant IfcSpace nodes and the space geometry itself.
295
+ */
296
+ export function collectIfcBuildingStoreyElementsWithIfcSpace(
297
+ hierarchy: SpatialHierarchy,
298
+ storeyId: number
299
+ ): number[] | null {
300
+ const storeyElements = hierarchy.byStorey.get(storeyId);
301
+ if (!storeyElements) return null;
302
+
303
+ const storeyNode = findSpatialNode(hierarchy.project, storeyId);
304
+ if (!storeyNode) return storeyElements;
305
+
306
+ const spaceIds = collectDescendantSpaceIds(storeyNode);
307
+ if (spaceIds.length === 0) return storeyElements;
308
+
309
+ // Combine storey elements + space expressIds + elements inside spaces
310
+ const combined = [...storeyElements];
311
+ for (const spaceId of spaceIds) {
312
+ combined.push(spaceId); // The space geometry itself
313
+ const spaceElements = hierarchy.bySpace.get(spaceId);
314
+ if (spaceElements) {
315
+ for (const elemId of spaceElements) {
316
+ combined.push(elemId);
317
+ }
318
+ }
319
+ }
320
+ return combined;
321
+ }
322
+
276
323
  function computeStoreyIsolation(state: ViewerStateSnapshot): Set<number> | null {
277
324
  if (state.selectedStoreys.size === 0) return null;
278
325
 
@@ -284,7 +331,8 @@ function computeStoreyIsolation(state: ViewerStateSnapshot): Set<number> | null
284
331
  if (!hierarchy) continue;
285
332
  const offset = model.idOffset ?? 0;
286
333
  for (const storeyId of state.selectedStoreys) {
287
- const storeyElementIds = hierarchy.byStorey.get(storeyId) || hierarchy.byStorey.get(storeyId - offset);
334
+ const localStoreyId = hierarchy.byStorey.has(storeyId) ? storeyId : storeyId - offset;
335
+ const storeyElementIds = collectIfcBuildingStoreyElementsWithIfcSpace(hierarchy, localStoreyId);
288
336
  if (!storeyElementIds) continue;
289
337
  for (const localId of storeyElementIds) {
290
338
  ids.add(localId + offset);
@@ -292,8 +340,9 @@ function computeStoreyIsolation(state: ViewerStateSnapshot): Set<number> | null
292
340
  }
293
341
  }
294
342
  } else if (state.ifcDataStore?.spatialHierarchy) {
343
+ const hierarchy = state.ifcDataStore.spatialHierarchy;
295
344
  for (const storeyId of state.selectedStoreys) {
296
- const storeyElementIds = state.ifcDataStore.spatialHierarchy.byStorey.get(storeyId);
345
+ const storeyElementIds = collectIfcBuildingStoreyElementsWithIfcSpace(hierarchy, storeyId);
297
346
  if (!storeyElementIds) continue;
298
347
  for (const id of storeyElementIds) {
299
348
  ids.add(id);
@@ -345,7 +394,26 @@ function getVisibleGlobalIds(state: ViewerStateSnapshot): Set<number> {
345
394
  globalHidden.add(id);
346
395
  }
347
396
 
348
- const globalIsolation = state.isolatedEntities ?? computeStoreyIsolation(state);
397
+ // Collect all active filter sets and intersect them
398
+ const filters: Set<number>[] = [];
399
+ const storeyIsolation = computeStoreyIsolation(state);
400
+ if (storeyIsolation !== null) filters.push(storeyIsolation);
401
+ if (state.classFilter !== null) filters.push(state.classFilter.ids);
402
+ if (state.isolatedEntities !== null) filters.push(state.isolatedEntities);
403
+
404
+ let globalIsolation: Set<number> | null = null;
405
+ if (filters.length === 1) {
406
+ globalIsolation = filters[0];
407
+ } else if (filters.length > 1) {
408
+ // Intersect all active filters — start from smallest for efficiency
409
+ const sorted = filters.sort((a, b) => a.size - b.size);
410
+ globalIsolation = new Set<number>();
411
+ for (const id of sorted[0]) {
412
+ if (sorted.every(s => s.has(id))) {
413
+ globalIsolation.add(id);
414
+ }
415
+ }
416
+ }
349
417
 
350
418
  const visible = new Set<number>();
351
419
  for (const candidate of candidates) {
@@ -130,7 +130,7 @@ export const useViewerStore = create<ViewerState>()((...args) => ({
130
130
  // Note: Does NOT clear models - use clearAllModels() for that
131
131
  resetViewerState: () => {
132
132
  invalidateVisibleBasketCache();
133
- const [set] = args;
133
+ const [set, get] = args;
134
134
  set({
135
135
  // Selection (legacy)
136
136
  selectedEntityId: null,
@@ -144,6 +144,7 @@ export const useViewerStore = create<ViewerState>()((...args) => ({
144
144
  // Visibility (legacy)
145
145
  hiddenEntities: new Set(),
146
146
  isolatedEntities: null,
147
+ classFilter: null,
147
148
  typeVisibility: {
148
149
  spaces: TYPE_VISIBILITY_DEFAULTS.SPACES,
149
150
  openings: TYPE_VISIBILITY_DEFAULTS.OPENINGS,
@@ -303,6 +304,15 @@ export const useViewerStore = create<ViewerState>()((...args) => ({
303
304
  chatStreamingContent: '',
304
305
  chatError: null,
305
306
  chatAbortController: null,
307
+
308
+ // Mutations - clear all mutation state so stale changes don't carry over
309
+ mutationViews: new Map(),
310
+ changeSets: new Map(),
311
+ activeChangeSetId: null,
312
+ undoStacks: new Map(),
313
+ redoStacks: new Map(),
314
+ dirtyModels: new Set(),
315
+ mutationVersion: get().mutationVersion + 1,
306
316
  });
307
317
  },
308
318
  }));
@@ -17,6 +17,8 @@ export interface VisibilitySlice {
17
17
  // State (legacy - single model)
18
18
  hiddenEntities: Set<number>;
19
19
  isolatedEntities: Set<number> | null;
20
+ /** Class-level filter (from Class tab type-group clicks) — independent of isolatedEntities */
21
+ classFilter: { ids: Set<number>; label: string } | null;
20
22
  typeVisibility: TypeVisibility;
21
23
 
22
24
  // State (multi-model)
@@ -34,6 +36,11 @@ export interface VisibilitySlice {
34
36
  isolateEntity: (id: number) => void;
35
37
  isolateEntities: (ids: number[]) => void;
36
38
  clearIsolation: () => void;
39
+ /** Set class-level filter (IFC class isolation from Class tab) */
40
+ setClassFilter: (ids: number[], label: string) => void;
41
+ clearClassFilter: () => void;
42
+ /** Clear all isolation and class filters */
43
+ clearAllFilters: () => void;
37
44
  showAll: () => void;
38
45
  isEntityVisible: (id: number) => boolean;
39
46
  toggleTypeVisibility: (type: 'spaces' | 'openings' | 'site') => void;
@@ -67,6 +74,7 @@ export const createVisibilitySlice: StateCreator<VisibilitySlice, [], [], Visibi
67
74
  // Initial state (legacy)
68
75
  hiddenEntities: new Set(),
69
76
  isolatedEntities: null,
77
+ classFilter: null,
70
78
  typeVisibility: {
71
79
  spaces: TYPE_VISIBILITY_DEFAULTS.SPACES,
72
80
  openings: TYPE_VISIBILITY_DEFAULTS.OPENINGS,
@@ -153,9 +161,25 @@ export const createVisibilitySlice: StateCreator<VisibilitySlice, [], [], Visibi
153
161
 
154
162
  clearIsolation: () => set({ isolatedEntities: null }),
155
163
 
156
- showAll: () => set({ hiddenEntities: new Set(), isolatedEntities: null }),
164
+ setClassFilter: (ids, label) => set((state) => {
165
+ const idsSet = new Set(ids);
166
+ // Toggle: if same class already filtered, clear it
167
+ const isAlready = state.classFilter !== null &&
168
+ state.classFilter.ids.size === idsSet.size &&
169
+ ids.every(id => state.classFilter!.ids.has(id));
170
+ if (isAlready) {
171
+ return { classFilter: null };
172
+ }
173
+ return { classFilter: { ids: idsSet, label } };
174
+ }),
175
+
176
+ clearClassFilter: () => set({ classFilter: null }),
177
+
178
+ clearAllFilters: () => set({ isolatedEntities: null, classFilter: null }),
179
+
180
+ showAll: () => set({ hiddenEntities: new Set(), isolatedEntities: null, classFilter: null }),
157
181
 
158
- setHiddenEntities: (ids) => set({ hiddenEntities: new Set(ids), isolatedEntities: null }),
182
+ setHiddenEntities: (ids) => set({ hiddenEntities: new Set(ids), isolatedEntities: null, classFilter: null }),
159
183
 
160
184
  setIsolatedEntities: (ids) => set({
161
185
  isolatedEntities: ids ? new Set(ids) : null,
@@ -166,6 +190,7 @@ export const createVisibilitySlice: StateCreator<VisibilitySlice, [], [], Visibi
166
190
  const state = get();
167
191
  if (state.hiddenEntities.has(id)) return false;
168
192
  if (state.isolatedEntities !== null && !state.isolatedEntities.has(id)) return false;
193
+ if (state.classFilter !== null && !state.classFilter.ids.has(id)) return false;
169
194
  return true;
170
195
  },
171
196
 
@@ -271,6 +296,7 @@ export const createVisibilitySlice: StateCreator<VisibilitySlice, [], [], Visibi
271
296
  showAllInAllModels: () => set({
272
297
  hiddenEntities: new Set(),
273
298
  isolatedEntities: null,
299
+ classFilter: null,
274
300
  hiddenEntitiesByModel: new Map(),
275
301
  isolatedEntitiesByModel: new Map(),
276
302
  }),