@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.
- package/CHANGELOG.md +32 -0
- package/dist/assets/{Arrow.dom-BgkZDIQm.js → Arrow.dom-OVBBPqOB.js} +1 -1
- package/dist/assets/{basketViewActivator-h_M3YbMW.js → basketViewActivator-Bx6QU4ma.js} +1 -1
- package/dist/assets/{browser-CRQ0bPh1.js → browser-BMqEoJw4.js} +1 -1
- package/dist/assets/ifc-lite_bg-CyWQTvp5.wasm +0 -0
- package/dist/assets/index-CJr7Itua.css +1 -0
- package/dist/assets/index-DZY6uD8A.js +185948 -0
- package/dist/assets/{index-C4VVJRL-.js → index-DsX-NCtx.js} +4 -4
- package/dist/assets/{native-bridge-DtcJqlOi.js → native-bridge-D6tKFqGO.js} +1 -1
- package/dist/assets/{wasm-bridge-BJJVu9P2.js → wasm-bridge-D4kvZVDw.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +7 -7
- package/src/components/viewer/CommandPalette.tsx +1 -0
- package/src/components/viewer/ExportDialog.tsx +40 -2
- package/src/components/viewer/HierarchyPanel.tsx +127 -35
- package/src/components/viewer/MainToolbar.tsx +113 -95
- package/src/components/viewer/ViewportContainer.tsx +30 -25
- package/src/components/viewer/hierarchy/HierarchyNode.tsx +10 -3
- package/src/components/viewer/hierarchy/treeDataBuilder.test.ts +126 -0
- package/src/components/viewer/hierarchy/treeDataBuilder.ts +139 -38
- package/src/components/viewer/hierarchy/types.ts +6 -1
- package/src/components/viewer/hierarchy/useHierarchyTree.ts +27 -12
- package/src/sdk/adapters/visibility-adapter.ts +82 -2
- package/src/store/basketVisibleSet.ts +72 -4
- package/src/store/index.ts +11 -1
- package/src/store/slices/visibilitySlice.ts +28 -2
- package/dist/assets/ifc-lite_bg-DyBKoGgk.wasm +0 -0
- package/dist/assets/index-Be6XjVeM.js +0 -116717
- 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
|
|
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 =
|
|
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
|
-
|
|
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) {
|
package/src/store/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}),
|
|
Binary file
|