@ifc-lite/viewer 1.10.0 → 1.11.1
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 +52 -0
- package/dist/assets/{Arrow.dom-Bw5JMdDs.js → Arrow.dom-p9ppgFLr.js} +1 -1
- package/dist/assets/{browser-DdRf3aWl.js → browser-lKzgHsnJ.js} +1 -1
- package/dist/assets/{ifc-lite_bg-C1-gLAHo.wasm → ifc-lite_bg-B6s-pcv0.wasm} +0 -0
- package/dist/assets/index-BoYyWYAu.css +1 -0
- package/dist/assets/{index-1ff6P0kc.js → index-CF854G-8.js} +42703 -41097
- package/dist/assets/{index-Bz7vHRxl.js → index-DQlpY6aJ.js} +4 -4
- package/dist/assets/{native-bridge-C5hD5vae.js → native-bridge-BgRWyawy.js} +1 -1
- package/dist/assets/{wasm-bridge-CaNKXFGM.js → wasm-bridge-BZxGtE7z.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +20 -19
- package/src/components/viewer/BasketPresentationDock.tsx +422 -0
- package/src/components/viewer/CommandPalette.tsx +29 -32
- package/src/components/viewer/EntityContextMenu.tsx +37 -22
- package/src/components/viewer/HierarchyPanel.tsx +19 -1
- package/src/components/viewer/MainToolbar.tsx +56 -113
- package/src/components/viewer/Section2DPanel.tsx +8 -1
- package/src/components/viewer/ThemeSwitch.tsx +55 -0
- package/src/components/viewer/Viewport.tsx +66 -105
- package/src/components/viewer/ViewportContainer.tsx +2 -0
- package/src/components/viewer/ViewportOverlays.tsx +9 -3
- package/src/components/viewer/useGeometryStreaming.ts +25 -0
- package/src/components/viewer/useKeyboardControls.ts +2 -2
- package/src/components/viewer/useRenderUpdates.ts +10 -3
- package/src/hooks/meshColorUpdates.test.ts +56 -0
- package/src/hooks/meshColorUpdates.ts +20 -0
- package/src/hooks/useIDS.ts +7 -8
- package/src/hooks/useIfcLoader.ts +25 -1
- package/src/hooks/useKeyboardShortcuts.ts +51 -84
- package/src/hooks/useViewerSelectors.ts +4 -0
- package/src/store/basket/basketCommands.ts +81 -0
- package/src/store/basket/basketViewActivator.ts +54 -0
- package/src/store/basketSave.ts +122 -0
- package/src/store/basketVisibleSet.test.ts +161 -0
- package/src/store/basketVisibleSet.ts +487 -0
- package/src/store/homeView.ts +21 -0
- package/src/store/index.ts +8 -0
- package/src/store/slices/dataSlice.test.ts +53 -4
- package/src/store/slices/dataSlice.ts +13 -5
- package/src/store/slices/drawing2DSlice.ts +5 -0
- package/src/store/slices/pinboardSlice.test.ts +160 -0
- package/src/store/slices/pinboardSlice.ts +248 -18
- package/src/store/types.ts +11 -0
- package/dist/assets/index-mvbV6NHd.css +0 -1
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
import { IfcTypeEnum, type SpatialNode } from '@ifc-lite/data';
|
|
6
|
+
import type { IfcDataStore } from '@ifc-lite/parser';
|
|
7
|
+
import type { EntityRef } from './types.js';
|
|
8
|
+
import { entityRefToString, stringToEntityRef } from './types.js';
|
|
9
|
+
import { useViewerStore } from './index.js';
|
|
10
|
+
|
|
11
|
+
type ViewerStateSnapshot = ReturnType<typeof useViewerStore.getState>;
|
|
12
|
+
|
|
13
|
+
type VisibleCandidate = {
|
|
14
|
+
globalId: number;
|
|
15
|
+
modelId: string;
|
|
16
|
+
expressId: number;
|
|
17
|
+
ifcType?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type BasketVisibleStats = {
|
|
21
|
+
visibleCount: number;
|
|
22
|
+
addCount: number;
|
|
23
|
+
removeCount: number;
|
|
24
|
+
basketCount: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type BasketInputSource = 'selection' | 'hierarchy' | 'visible' | 'empty';
|
|
28
|
+
|
|
29
|
+
type CacheEntry = { key: string; refs: EntityRef[] };
|
|
30
|
+
let _visibleCache: CacheEntry | null = null;
|
|
31
|
+
|
|
32
|
+
function digestNumberSet(values: Iterable<number>): string {
|
|
33
|
+
let count = 0;
|
|
34
|
+
let xor = 0;
|
|
35
|
+
let sum = 0;
|
|
36
|
+
for (const v of values) {
|
|
37
|
+
const n = Number.isFinite(v) ? (v | 0) : 0;
|
|
38
|
+
count++;
|
|
39
|
+
xor ^= n;
|
|
40
|
+
sum = (sum + (n >>> 0)) >>> 0;
|
|
41
|
+
}
|
|
42
|
+
return `${count}:${xor >>> 0}:${sum >>> 0}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function digestModelEntityMap(map: Map<string, Set<number>>): string {
|
|
46
|
+
if (map.size === 0) return '0';
|
|
47
|
+
const parts: string[] = [];
|
|
48
|
+
for (const [modelId, ids] of map) {
|
|
49
|
+
parts.push(`${modelId}:${digestNumberSet(ids)}`);
|
|
50
|
+
}
|
|
51
|
+
parts.sort();
|
|
52
|
+
return parts.join('|');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function visibilityFingerprint(state: ViewerStateSnapshot): string {
|
|
56
|
+
const tv = state.typeVisibility;
|
|
57
|
+
return [
|
|
58
|
+
digestNumberSet(state.hiddenEntities),
|
|
59
|
+
state.isolatedEntities ? digestNumberSet(state.isolatedEntities) : 'none',
|
|
60
|
+
digestNumberSet(state.lensHiddenIds),
|
|
61
|
+
digestModelEntityMap(state.hiddenEntitiesByModel),
|
|
62
|
+
digestModelEntityMap(state.isolatedEntitiesByModel),
|
|
63
|
+
tv.spaces ? 1 : 0,
|
|
64
|
+
tv.openings ? 1 : 0,
|
|
65
|
+
tv.site ? 1 : 0,
|
|
66
|
+
state.models.size,
|
|
67
|
+
state.activeBasketViewId ?? 'none',
|
|
68
|
+
].join(':');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function invalidateVisibleBasketCache(): void {
|
|
72
|
+
_visibleCache = null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const STOREY_TYPE = 'IfcBuildingStorey';
|
|
76
|
+
const SPATIAL_CONTAINER_TYPES = new Set(['IfcProject', 'IfcSite', 'IfcBuilding']);
|
|
77
|
+
|
|
78
|
+
function dedupeRefs(refs: EntityRef[]): EntityRef[] {
|
|
79
|
+
const out: EntityRef[] = [];
|
|
80
|
+
const seen = new Set<string>();
|
|
81
|
+
for (const ref of refs) {
|
|
82
|
+
const key = entityRefToString(ref);
|
|
83
|
+
if (seen.has(key)) continue;
|
|
84
|
+
seen.add(key);
|
|
85
|
+
out.push(ref);
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function matchesTypeVisibility(ifcType: string | undefined, typeVisibility: ViewerStateSnapshot['typeVisibility']): boolean {
|
|
91
|
+
if (ifcType === 'IfcSpace' && !typeVisibility.spaces) return false;
|
|
92
|
+
if (ifcType === 'IfcOpeningElement' && !typeVisibility.openings) return false;
|
|
93
|
+
if (ifcType === 'IfcSite' && !typeVisibility.site) return false;
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getDataStoreForModel(state: ViewerStateSnapshot, modelId: string): IfcDataStore | null {
|
|
98
|
+
if (modelId === 'legacy') {
|
|
99
|
+
return state.ifcDataStore;
|
|
100
|
+
}
|
|
101
|
+
return state.models.get(modelId)?.ifcDataStore ?? null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getEntityTypeName(state: ViewerStateSnapshot, ref: EntityRef): string {
|
|
105
|
+
const dataStore = getDataStoreForModel(state, ref.modelId);
|
|
106
|
+
if (!dataStore) return '';
|
|
107
|
+
return dataStore.entities.getTypeName(ref.expressId) || '';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function findSpatialNode(root: SpatialNode, expressId: number): SpatialNode | null {
|
|
111
|
+
const stack: SpatialNode[] = [root];
|
|
112
|
+
while (stack.length > 0) {
|
|
113
|
+
const current = stack.pop()!;
|
|
114
|
+
if (current.expressId === expressId) {
|
|
115
|
+
return current;
|
|
116
|
+
}
|
|
117
|
+
for (const child of current.children || []) {
|
|
118
|
+
stack.push(child);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function getContainerElementIds(dataStore: IfcDataStore, containerExpressId: number): number[] {
|
|
125
|
+
const hierarchy = dataStore.spatialHierarchy;
|
|
126
|
+
if (!hierarchy?.project) return [];
|
|
127
|
+
|
|
128
|
+
const startNode = findSpatialNode(hierarchy.project, containerExpressId);
|
|
129
|
+
if (!startNode) return [];
|
|
130
|
+
|
|
131
|
+
const elementIds: number[] = [];
|
|
132
|
+
const seen = new Set<number>();
|
|
133
|
+
const stack: SpatialNode[] = [startNode];
|
|
134
|
+
|
|
135
|
+
while (stack.length > 0) {
|
|
136
|
+
const current = stack.pop()!;
|
|
137
|
+
if (current.type === IfcTypeEnum.IfcBuildingStorey) {
|
|
138
|
+
const storeyElements = hierarchy.byStorey.get(current.expressId) as number[] | undefined;
|
|
139
|
+
if (storeyElements) {
|
|
140
|
+
for (const id of storeyElements) {
|
|
141
|
+
if (seen.has(id)) continue;
|
|
142
|
+
seen.add(id);
|
|
143
|
+
elementIds.push(id);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
for (const child of current.children || []) {
|
|
148
|
+
stack.push(child);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return elementIds;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function expandRefToElements(state: ViewerStateSnapshot, ref: EntityRef): EntityRef[] {
|
|
156
|
+
const dataStore = getDataStoreForModel(state, ref.modelId);
|
|
157
|
+
if (!dataStore) return [ref];
|
|
158
|
+
|
|
159
|
+
const entityType = dataStore.entities.getTypeName(ref.expressId) || '';
|
|
160
|
+
if (entityType === STOREY_TYPE) {
|
|
161
|
+
const localIds = dataStore.spatialHierarchy?.byStorey.get(ref.expressId) as number[] | undefined;
|
|
162
|
+
if (!localIds || localIds.length === 0) return [];
|
|
163
|
+
return localIds.map((expressId) => ({ modelId: ref.modelId, expressId }));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (SPATIAL_CONTAINER_TYPES.has(entityType)) {
|
|
167
|
+
const localIds = getContainerElementIds(dataStore, ref.expressId);
|
|
168
|
+
if (localIds.length === 0) return [];
|
|
169
|
+
return localIds.map((expressId) => ({ modelId: ref.modelId, expressId }));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return [ref];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function toGlobalId(modelId: string, expressId: number, state: ViewerStateSnapshot): number {
|
|
176
|
+
if (modelId === 'legacy') return expressId;
|
|
177
|
+
const model = state.models.get(modelId);
|
|
178
|
+
return expressId + (model?.idOffset ?? 0);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function globalIdToRef(state: ViewerStateSnapshot, globalId: number): EntityRef | null {
|
|
182
|
+
const resolved = state.resolveGlobalIdFromModels(globalId);
|
|
183
|
+
if (resolved) {
|
|
184
|
+
return { modelId: resolved.modelId, expressId: resolved.expressId };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (state.models.size > 0) return null;
|
|
188
|
+
|
|
189
|
+
if (state.ifcDataStore) {
|
|
190
|
+
return { modelId: 'legacy', expressId: globalId };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function basketToGlobalIds(state: ViewerStateSnapshot): Set<number> {
|
|
197
|
+
const ids = new Set<number>();
|
|
198
|
+
for (const str of state.pinboardEntities) {
|
|
199
|
+
const ref = stringToEntityRef(str);
|
|
200
|
+
ids.add(toGlobalId(ref.modelId, ref.expressId, state));
|
|
201
|
+
}
|
|
202
|
+
return ids;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function getSelectedStoreyElementRefs(state: ViewerStateSnapshot): EntityRef[] {
|
|
206
|
+
if (state.selectedStoreys.size === 0) return [];
|
|
207
|
+
|
|
208
|
+
const refs: EntityRef[] = [];
|
|
209
|
+
|
|
210
|
+
if (state.models.size > 0) {
|
|
211
|
+
for (const [modelId, model] of state.models) {
|
|
212
|
+
const hierarchy = model.ifcDataStore?.spatialHierarchy;
|
|
213
|
+
if (!hierarchy) continue;
|
|
214
|
+
const offset = model.idOffset ?? 0;
|
|
215
|
+
for (const storeyId of state.selectedStoreys) {
|
|
216
|
+
const storeyElementIds = hierarchy.byStorey.get(storeyId) || hierarchy.byStorey.get(storeyId - offset);
|
|
217
|
+
if (!storeyElementIds) continue;
|
|
218
|
+
for (const localId of storeyElementIds) {
|
|
219
|
+
refs.push({ modelId, expressId: localId });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
} else if (state.ifcDataStore?.spatialHierarchy) {
|
|
224
|
+
for (const storeyId of state.selectedStoreys) {
|
|
225
|
+
const storeyElementIds = state.ifcDataStore.spatialHierarchy.byStorey.get(storeyId);
|
|
226
|
+
if (!storeyElementIds) continue;
|
|
227
|
+
for (const id of storeyElementIds) {
|
|
228
|
+
refs.push({ modelId: 'legacy', expressId: id });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return dedupeRefs(refs);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function getSelectionBaseRefs(state: ViewerStateSnapshot): EntityRef[] {
|
|
237
|
+
const refs: EntityRef[] = [];
|
|
238
|
+
|
|
239
|
+
if (state.selectedEntitiesSet.size > 0) {
|
|
240
|
+
for (const str of state.selectedEntitiesSet) {
|
|
241
|
+
refs.push(stringToEntityRef(str));
|
|
242
|
+
}
|
|
243
|
+
return refs;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (state.selectedEntityIds.size > 0) {
|
|
247
|
+
for (const globalId of state.selectedEntityIds) {
|
|
248
|
+
const resolved = globalIdToRef(state, globalId);
|
|
249
|
+
if (resolved) refs.push(resolved);
|
|
250
|
+
}
|
|
251
|
+
return refs;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (state.selectedEntities.length > 0) {
|
|
255
|
+
return [...state.selectedEntities];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (state.selectedEntity) {
|
|
259
|
+
return [state.selectedEntity];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (state.selectedEntityId !== null) {
|
|
263
|
+
const resolved = globalIdToRef(state, state.selectedEntityId);
|
|
264
|
+
if (resolved) refs.push(resolved);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return refs;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function getExpandedSelectionRefs(state: ViewerStateSnapshot): EntityRef[] {
|
|
271
|
+
const baseRefs = getSelectionBaseRefs(state);
|
|
272
|
+
if (baseRefs.length === 0) return [];
|
|
273
|
+
return dedupeRefs(baseRefs.flatMap((ref) => expandRefToElements(state, ref)));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function computeStoreyIsolation(state: ViewerStateSnapshot): Set<number> | null {
|
|
277
|
+
if (state.selectedStoreys.size === 0) return null;
|
|
278
|
+
|
|
279
|
+
const ids = new Set<number>();
|
|
280
|
+
|
|
281
|
+
if (state.models.size > 0) {
|
|
282
|
+
for (const [, model] of state.models) {
|
|
283
|
+
const hierarchy = model.ifcDataStore?.spatialHierarchy;
|
|
284
|
+
if (!hierarchy) continue;
|
|
285
|
+
const offset = model.idOffset ?? 0;
|
|
286
|
+
for (const storeyId of state.selectedStoreys) {
|
|
287
|
+
const storeyElementIds = hierarchy.byStorey.get(storeyId) || hierarchy.byStorey.get(storeyId - offset);
|
|
288
|
+
if (!storeyElementIds) continue;
|
|
289
|
+
for (const localId of storeyElementIds) {
|
|
290
|
+
ids.add(localId + offset);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
} else if (state.ifcDataStore?.spatialHierarchy) {
|
|
295
|
+
for (const storeyId of state.selectedStoreys) {
|
|
296
|
+
const storeyElementIds = state.ifcDataStore.spatialHierarchy.byStorey.get(storeyId);
|
|
297
|
+
if (!storeyElementIds) continue;
|
|
298
|
+
for (const id of storeyElementIds) {
|
|
299
|
+
ids.add(id);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return ids.size > 0 ? ids : null;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function collectVisibleCandidates(state: ViewerStateSnapshot): VisibleCandidate[] {
|
|
308
|
+
const candidates: VisibleCandidate[] = [];
|
|
309
|
+
|
|
310
|
+
if (state.models.size > 0) {
|
|
311
|
+
for (const [modelId, model] of state.models) {
|
|
312
|
+
if (!model.visible) continue;
|
|
313
|
+
const offset = model.idOffset ?? 0;
|
|
314
|
+
for (const mesh of model.geometryResult.meshes) {
|
|
315
|
+
if (!matchesTypeVisibility(mesh.ifcType, state.typeVisibility)) continue;
|
|
316
|
+
const globalId = mesh.expressId;
|
|
317
|
+
candidates.push({
|
|
318
|
+
globalId,
|
|
319
|
+
modelId,
|
|
320
|
+
expressId: globalId - offset,
|
|
321
|
+
ifcType: mesh.ifcType,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
} else if (state.geometryResult) {
|
|
326
|
+
for (const mesh of state.geometryResult.meshes) {
|
|
327
|
+
if (!matchesTypeVisibility(mesh.ifcType, state.typeVisibility)) continue;
|
|
328
|
+
candidates.push({
|
|
329
|
+
globalId: mesh.expressId,
|
|
330
|
+
modelId: 'legacy',
|
|
331
|
+
expressId: mesh.expressId,
|
|
332
|
+
ifcType: mesh.ifcType,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return candidates;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function getVisibleGlobalIds(state: ViewerStateSnapshot): Set<number> {
|
|
341
|
+
const candidates = collectVisibleCandidates(state);
|
|
342
|
+
|
|
343
|
+
const globalHidden = new Set<number>(state.hiddenEntities);
|
|
344
|
+
for (const id of state.lensHiddenIds) {
|
|
345
|
+
globalHidden.add(id);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const globalIsolation = state.isolatedEntities ?? computeStoreyIsolation(state);
|
|
349
|
+
|
|
350
|
+
const visible = new Set<number>();
|
|
351
|
+
for (const candidate of candidates) {
|
|
352
|
+
if (globalIsolation !== null && !globalIsolation.has(candidate.globalId)) continue;
|
|
353
|
+
if (globalHidden.has(candidate.globalId)) continue;
|
|
354
|
+
|
|
355
|
+
const modelHidden = state.hiddenEntitiesByModel.get(candidate.modelId);
|
|
356
|
+
if (modelHidden?.has(candidate.expressId)) continue;
|
|
357
|
+
|
|
358
|
+
const modelIsolated = state.isolatedEntitiesByModel.get(candidate.modelId);
|
|
359
|
+
if (modelIsolated && !modelIsolated.has(candidate.expressId)) continue;
|
|
360
|
+
|
|
361
|
+
visible.add(candidate.globalId);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return visible;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function getVisibleBasketEntityRefsFromStore(): EntityRef[] {
|
|
368
|
+
const state = useViewerStore.getState();
|
|
369
|
+
const key = visibilityFingerprint(state);
|
|
370
|
+
if (_visibleCache?.key === key) return _visibleCache.refs;
|
|
371
|
+
|
|
372
|
+
const visibleIds = getVisibleGlobalIds(state);
|
|
373
|
+
if (visibleIds.size === 0) {
|
|
374
|
+
_visibleCache = { key, refs: [] };
|
|
375
|
+
return [];
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const refs: EntityRef[] = [];
|
|
379
|
+
for (const globalId of visibleIds) {
|
|
380
|
+
const resolved = state.resolveGlobalIdFromModels(globalId);
|
|
381
|
+
if (resolved) {
|
|
382
|
+
refs.push({ modelId: resolved.modelId, expressId: resolved.expressId });
|
|
383
|
+
} else {
|
|
384
|
+
refs.push({ modelId: 'legacy', expressId: globalId });
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const result = dedupeRefs(refs);
|
|
388
|
+
_visibleCache = { key, refs: result };
|
|
389
|
+
return result;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Resolve active entity selection into basket refs.
|
|
394
|
+
* Explicit selected entities are preferred; if empty, selected storeys are expanded.
|
|
395
|
+
*/
|
|
396
|
+
export function getBasketSelectionRefsFromStore(): EntityRef[] {
|
|
397
|
+
const state = useViewerStore.getState();
|
|
398
|
+
|
|
399
|
+
const expandedSelection = getExpandedSelectionRefs(state);
|
|
400
|
+
if (expandedSelection.length > 0) {
|
|
401
|
+
return expandedSelection;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return getSelectedStoreyElementRefs(state);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Resolve hierarchy-derived basket source.
|
|
409
|
+
* Priority: explicit hierarchy source snapshot -> selected storeys -> selected hierarchy container/entity.
|
|
410
|
+
*/
|
|
411
|
+
export function getHierarchyBasketEntityRefsFromStore(): EntityRef[] {
|
|
412
|
+
const state = useViewerStore.getState();
|
|
413
|
+
|
|
414
|
+
if (state.hierarchyBasketSelection.size > 0) {
|
|
415
|
+
const hierarchyRefs = Array.from(state.hierarchyBasketSelection).map((key) => stringToEntityRef(key));
|
|
416
|
+
const expandedHierarchy = dedupeRefs(hierarchyRefs.flatMap((ref) => expandRefToElements(state, ref)));
|
|
417
|
+
if (expandedHierarchy.length > 0) {
|
|
418
|
+
return expandedHierarchy;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const storeyRefs = getSelectedStoreyElementRefs(state);
|
|
423
|
+
if (storeyRefs.length > 0) {
|
|
424
|
+
return storeyRefs;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const selectionRefs = getExpandedSelectionRefs(state);
|
|
428
|
+
if (selectionRefs.length > 0) {
|
|
429
|
+
const hasContainer = selectionRefs.some((ref) => {
|
|
430
|
+
const typeName = getEntityTypeName(state, ref);
|
|
431
|
+
return typeName === STOREY_TYPE || SPATIAL_CONTAINER_TYPES.has(typeName);
|
|
432
|
+
});
|
|
433
|
+
if (hasContainer || getSelectionBaseRefs(state).length > 0) {
|
|
434
|
+
return selectionRefs;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return [];
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export function getSmartBasketInputFromStore(): { refs: EntityRef[]; source: BasketInputSource } {
|
|
442
|
+
const selectionRefs = getBasketSelectionRefsFromStore();
|
|
443
|
+
if (selectionRefs.length > 0) {
|
|
444
|
+
return { refs: selectionRefs, source: 'selection' };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const hierarchyRefs = getHierarchyBasketEntityRefsFromStore();
|
|
448
|
+
if (hierarchyRefs.length > 0) {
|
|
449
|
+
return { refs: hierarchyRefs, source: 'hierarchy' };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const visibleRefs = getVisibleBasketEntityRefsFromStore();
|
|
453
|
+
if (visibleRefs.length > 0) {
|
|
454
|
+
return { refs: visibleRefs, source: 'visible' };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return { refs: [], source: 'empty' };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export function isBasketIsolationActiveFromStore(): boolean {
|
|
461
|
+
const state = useViewerStore.getState();
|
|
462
|
+
if (state.pinboardEntities.size === 0 || state.isolatedEntities === null) return false;
|
|
463
|
+
|
|
464
|
+
const basketIds = basketToGlobalIds(state);
|
|
465
|
+
if (basketIds.size !== state.isolatedEntities.size) return false;
|
|
466
|
+
for (const id of basketIds) {
|
|
467
|
+
if (!state.isolatedEntities.has(id)) return false;
|
|
468
|
+
}
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export function getVisibleBasketStatsFromStore(): BasketVisibleStats {
|
|
473
|
+
const state = useViewerStore.getState();
|
|
474
|
+
const visibleRefs = getVisibleBasketEntityRefsFromStore();
|
|
475
|
+
const visibleKeys = new Set<string>(visibleRefs.map(entityRefToString));
|
|
476
|
+
let removeCount = 0;
|
|
477
|
+
for (const key of state.pinboardEntities) {
|
|
478
|
+
if (visibleKeys.has(key)) removeCount++;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return {
|
|
482
|
+
visibleCount: visibleKeys.size,
|
|
483
|
+
addCount: Math.max(0, visibleKeys.size - removeCount),
|
|
484
|
+
removeCount,
|
|
485
|
+
basketCount: state.pinboardEntities.size,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
import { useViewerStore } from './index.js';
|
|
6
|
+
|
|
7
|
+
export function resetVisibilityForHomeFromStore(): void {
|
|
8
|
+
const state = useViewerStore.getState();
|
|
9
|
+
state.showAllInAllModels();
|
|
10
|
+
state.clearStoreySelection();
|
|
11
|
+
state.clearHierarchyBasketSelection();
|
|
12
|
+
state.clearEntitySelection();
|
|
13
|
+
state.clearBasket();
|
|
14
|
+
useViewerStore.setState({ activeBasketViewId: null });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function goHomeFromStore(): void {
|
|
18
|
+
resetVisibilityForHomeFromStore();
|
|
19
|
+
const state = useViewerStore.getState();
|
|
20
|
+
state.cameraCallbacks.home?.();
|
|
21
|
+
}
|
package/src/store/index.ts
CHANGED
|
@@ -31,6 +31,7 @@ import { createListSlice, type ListSlice } from './slices/listSlice.js';
|
|
|
31
31
|
import { createPinboardSlice, type PinboardSlice } from './slices/pinboardSlice.js';
|
|
32
32
|
import { createLensSlice, type LensSlice } from './slices/lensSlice.js';
|
|
33
33
|
import { createScriptSlice, type ScriptSlice } from './slices/scriptSlice.js';
|
|
34
|
+
import { invalidateVisibleBasketCache } from './basketVisibleSet.js';
|
|
34
35
|
|
|
35
36
|
// Import constants for reset function
|
|
36
37
|
import { CAMERA_DEFAULTS, SECTION_PLANE_DEFAULTS, UI_DEFAULTS, TYPE_VISIBILITY_DEFAULTS } from './constants.js';
|
|
@@ -122,6 +123,7 @@ export const useViewerStore = create<ViewerState>()((...args) => ({
|
|
|
122
123
|
// Reset all viewer state when loading new file
|
|
123
124
|
// Note: Does NOT clear models - use clearAllModels() for that
|
|
124
125
|
resetViewerState: () => {
|
|
126
|
+
invalidateVisibleBasketCache();
|
|
125
127
|
const [set] = args;
|
|
126
128
|
set({
|
|
127
129
|
// Selection (legacy)
|
|
@@ -148,6 +150,7 @@ export const useViewerStore = create<ViewerState>()((...args) => ({
|
|
|
148
150
|
|
|
149
151
|
// Data
|
|
150
152
|
pendingColorUpdates: null,
|
|
153
|
+
pendingMeshColorUpdates: null,
|
|
151
154
|
|
|
152
155
|
// Hover/Context
|
|
153
156
|
hoverState: { entityId: null, screenX: 0, screenY: 0 },
|
|
@@ -202,6 +205,7 @@ export const useViewerStore = create<ViewerState>()((...args) => ({
|
|
|
202
205
|
drawing2DPhase: '',
|
|
203
206
|
drawing2DError: null,
|
|
204
207
|
drawing2DPanelVisible: false,
|
|
208
|
+
suppressNextSection2DPanelAutoOpen: false,
|
|
205
209
|
drawing2DSvgContent: null,
|
|
206
210
|
drawing2DDisplayOptions: {
|
|
207
211
|
showHiddenLines: true,
|
|
@@ -266,6 +270,10 @@ export const useViewerStore = create<ViewerState>()((...args) => ({
|
|
|
266
270
|
|
|
267
271
|
// Pinboard - clear pinned entities on new file
|
|
268
272
|
pinboardEntities: new Set<string>(),
|
|
273
|
+
basketViews: [],
|
|
274
|
+
activeBasketViewId: null,
|
|
275
|
+
basketPresentationVisible: false,
|
|
276
|
+
hierarchyBasketSelection: new Set<string>(),
|
|
269
277
|
|
|
270
278
|
// Script - reset execution state but keep saved scripts and editor content
|
|
271
279
|
scriptPanelVisible: false,
|
|
@@ -46,6 +46,10 @@ describe('DataSlice', () => {
|
|
|
46
46
|
it('should have null pendingColorUpdates', () => {
|
|
47
47
|
assert.strictEqual(state.pendingColorUpdates, null);
|
|
48
48
|
});
|
|
49
|
+
|
|
50
|
+
it('should have null pendingMeshColorUpdates', () => {
|
|
51
|
+
assert.strictEqual(state.pendingMeshColorUpdates, null);
|
|
52
|
+
});
|
|
49
53
|
});
|
|
50
54
|
|
|
51
55
|
describe('setIfcDataStore', () => {
|
|
@@ -136,9 +140,11 @@ describe('DataSlice', () => {
|
|
|
136
140
|
state.updateMeshColors(updates);
|
|
137
141
|
|
|
138
142
|
assert.deepStrictEqual(state.geometryResult?.meshes[0].color, [0, 1, 0, 1]);
|
|
143
|
+
assert.strictEqual(state.pendingColorUpdates, null);
|
|
144
|
+
assert.deepStrictEqual(state.pendingMeshColorUpdates?.get(1), [0, 1, 0, 1]);
|
|
139
145
|
});
|
|
140
146
|
|
|
141
|
-
it('should clone
|
|
147
|
+
it('should clone updates and avoid mutating state from external map writes', () => {
|
|
142
148
|
const mesh = createMockMesh(1);
|
|
143
149
|
state.appendGeometryBatch([mesh] as any);
|
|
144
150
|
|
|
@@ -151,16 +157,20 @@ describe('DataSlice', () => {
|
|
|
151
157
|
updates.set(1, [1, 1, 1, 1]);
|
|
152
158
|
|
|
153
159
|
// State should not be affected
|
|
154
|
-
assert.
|
|
160
|
+
assert.deepStrictEqual(state.geometryResult?.meshes[0].color, [0, 1, 0, 1]);
|
|
161
|
+
assert.strictEqual(state.pendingColorUpdates, null);
|
|
162
|
+
assert.deepStrictEqual(state.pendingMeshColorUpdates?.get(1), [0, 1, 0, 1]);
|
|
155
163
|
});
|
|
156
164
|
|
|
157
|
-
it('should
|
|
165
|
+
it('should skip mesh mutation but still set pendingMeshColorUpdates when no geometry result', () => {
|
|
158
166
|
const updates = new Map<number, [number, number, number, number]>();
|
|
159
167
|
updates.set(1, [0, 1, 0, 1]);
|
|
160
168
|
|
|
161
169
|
state.updateMeshColors(updates);
|
|
162
170
|
|
|
163
171
|
assert.strictEqual(state.geometryResult, null);
|
|
172
|
+
assert.strictEqual(state.pendingColorUpdates, null);
|
|
173
|
+
assert.deepStrictEqual(state.pendingMeshColorUpdates?.get(1), [0, 1, 0, 1]);
|
|
164
174
|
});
|
|
165
175
|
|
|
166
176
|
it('should preserve unaffected meshes', () => {
|
|
@@ -185,7 +195,7 @@ describe('DataSlice', () => {
|
|
|
185
195
|
|
|
186
196
|
const updates = new Map<number, [number, number, number, number]>();
|
|
187
197
|
updates.set(1, [0, 1, 0, 1]);
|
|
188
|
-
state.
|
|
198
|
+
state.setPendingColorUpdates(updates);
|
|
189
199
|
|
|
190
200
|
state.clearPendingColorUpdates();
|
|
191
201
|
|
|
@@ -193,6 +203,45 @@ describe('DataSlice', () => {
|
|
|
193
203
|
});
|
|
194
204
|
});
|
|
195
205
|
|
|
206
|
+
describe('clearPendingMeshColorUpdates', () => {
|
|
207
|
+
it('should clear pending mesh color updates', () => {
|
|
208
|
+
const mesh = createMockMesh(1);
|
|
209
|
+
state.appendGeometryBatch([mesh] as any);
|
|
210
|
+
|
|
211
|
+
const updates = new Map<number, [number, number, number, number]>();
|
|
212
|
+
updates.set(1, [0, 1, 0, 1]);
|
|
213
|
+
state.updateMeshColors(updates);
|
|
214
|
+
|
|
215
|
+
state.clearPendingMeshColorUpdates();
|
|
216
|
+
|
|
217
|
+
assert.strictEqual(state.pendingMeshColorUpdates, null);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe('setPendingColorUpdates', () => {
|
|
222
|
+
it('should clone pending color updates map', () => {
|
|
223
|
+
const updates = new Map<number, [number, number, number, number]>();
|
|
224
|
+
updates.set(1, [0, 1, 0, 1]);
|
|
225
|
+
state.setPendingColorUpdates(updates);
|
|
226
|
+
|
|
227
|
+
updates.set(1, [1, 1, 1, 1]);
|
|
228
|
+
assert.notStrictEqual(state.pendingColorUpdates, updates);
|
|
229
|
+
assert.deepStrictEqual(state.pendingColorUpdates?.get(1), [0, 1, 0, 1]);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should not mutate persisted geometry colors', () => {
|
|
233
|
+
const mesh = createMockMesh(1, [0.2, 0.2, 0.2, 1]);
|
|
234
|
+
state.appendGeometryBatch([mesh] as any);
|
|
235
|
+
const updates = new Map<number, [number, number, number, number]>();
|
|
236
|
+
updates.set(1, [1, 0, 1, 0.5]);
|
|
237
|
+
|
|
238
|
+
state.setPendingColorUpdates(updates);
|
|
239
|
+
|
|
240
|
+
assert.deepStrictEqual(state.geometryResult?.meshes[0].color, [0.2, 0.2, 0.2, 1]);
|
|
241
|
+
assert.deepStrictEqual(state.pendingColorUpdates?.get(1), [1, 0, 1, 0.5]);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
196
245
|
describe('updateCoordinateInfo', () => {
|
|
197
246
|
it('should update coordinate info', () => {
|
|
198
247
|
const mesh = createMockMesh(1);
|