@ifc-lite/viewer 1.7.0 → 1.9.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 +88 -0
- package/dist/assets/{Arrow.dom-BGPQieQQ.js → Arrow.dom-CusgkT03.js} +1 -1
- package/dist/assets/browser-BXNIkE8a.js +694 -0
- package/dist/assets/emscripten-module-BTRCZGcB.wasm +0 -0
- package/dist/assets/emscripten-module-CGIn_cMh.wasm +0 -0
- package/dist/assets/emscripten-module-DYvzWiHh.wasm +0 -0
- package/dist/assets/emscripten-module-NWak2PoB.wasm +0 -0
- package/dist/assets/emscripten-module.browser-CY5t0Vfq.js +1 -0
- package/dist/assets/esbuild-COv63sf-.js +1 -0
- package/dist/assets/esbuild-Cpd5nU_H.wasm +0 -0
- package/dist/assets/ffi-DlhRHxHv.js +1 -0
- package/dist/assets/index-6Mr3byM-.js +216 -0
- package/dist/assets/index-CGbokkQ9.css +1 -0
- package/dist/assets/index-huvR-kGC.js +98305 -0
- package/dist/assets/module-6F3E5H7Y-tx0BadV3.js +6 -0
- package/dist/assets/{native-bridge-DD0SNyQ5.js → native-bridge-DsHOKdgD.js} +1 -1
- package/dist/assets/{wasm-bridge-D54YMO7X.js → wasm-bridge-Bd73HXn-.js} +1 -1
- package/dist/index.html +12 -3
- package/index.html +10 -1
- package/package.json +30 -21
- package/src/App.tsx +6 -1
- package/src/components/ui/dialog.tsx +8 -6
- package/src/components/viewer/CodeEditor.tsx +309 -0
- package/src/components/viewer/CommandPalette.tsx +597 -0
- package/src/components/viewer/Drawing2DCanvas.tsx +364 -1
- package/src/components/viewer/EntityContextMenu.tsx +47 -20
- package/src/components/viewer/ExportDialog.tsx +166 -17
- package/src/components/viewer/HierarchyPanel.tsx +3 -1
- package/src/components/viewer/LensPanel.tsx +848 -85
- package/src/components/viewer/MainToolbar.tsx +145 -84
- package/src/components/viewer/ScriptPanel.tsx +416 -0
- package/src/components/viewer/Section2DPanel.tsx +269 -29
- package/src/components/viewer/TextAnnotationEditor.tsx +112 -0
- package/src/components/viewer/ViewerLayout.tsx +63 -11
- package/src/components/viewer/Viewport.tsx +58 -23
- package/src/components/viewer/ViewportContainer.tsx +2 -0
- package/src/components/viewer/hierarchy/HierarchyNode.tsx +1 -1
- package/src/components/viewer/hierarchy/types.ts +1 -1
- package/src/components/viewer/lists/ListResultsTable.tsx +53 -19
- package/src/components/viewer/tools/cloudPathGenerator.test.ts +118 -0
- package/src/components/viewer/tools/cloudPathGenerator.ts +275 -0
- package/src/components/viewer/tools/computePolygonArea.test.ts +165 -0
- package/src/components/viewer/tools/computePolygonArea.ts +72 -0
- package/src/components/viewer/useGeometryStreaming.ts +25 -5
- package/src/hooks/ids/idsExportService.ts +1 -1
- package/src/hooks/useAnnotation2D.ts +551 -0
- package/src/hooks/useDrawingExport.ts +83 -1
- package/src/hooks/useKeyboardShortcuts.ts +114 -14
- package/src/hooks/useLens.ts +40 -55
- package/src/hooks/useLensDiscovery.ts +46 -0
- package/src/hooks/useModelSelection.ts +5 -22
- package/src/hooks/useSandbox.ts +113 -0
- package/src/index.css +7 -1
- package/src/lib/lens/adapter.ts +127 -1
- package/src/lib/lists/columnToAutoColor.ts +33 -0
- package/src/lib/recent-files.ts +122 -0
- package/src/lib/scripts/persistence.ts +132 -0
- package/src/lib/scripts/templates/bim-globals.d.ts +111 -0
- package/src/lib/scripts/templates/data-quality-audit.ts +149 -0
- package/src/lib/scripts/templates/envelope-check.ts +164 -0
- package/src/lib/scripts/templates/federation-compare.ts +189 -0
- package/src/lib/scripts/templates/fire-safety-check.ts +161 -0
- package/src/lib/scripts/templates/mep-equipment-schedule.ts +175 -0
- package/src/lib/scripts/templates/quantity-takeoff.ts +145 -0
- package/src/lib/scripts/templates/reset-view.ts +6 -0
- package/src/lib/scripts/templates/space-validation.ts +189 -0
- package/src/lib/scripts/templates/tsconfig.json +13 -0
- package/src/lib/scripts/templates.ts +86 -0
- package/src/sdk/BimProvider.tsx +50 -0
- package/src/sdk/adapters/export-adapter.ts +283 -0
- package/src/sdk/adapters/lens-adapter.ts +44 -0
- package/src/sdk/adapters/model-adapter.ts +32 -0
- package/src/sdk/adapters/model-compat.ts +80 -0
- package/src/sdk/adapters/mutate-adapter.ts +45 -0
- package/src/sdk/adapters/query-adapter.ts +241 -0
- package/src/sdk/adapters/selection-adapter.ts +29 -0
- package/src/sdk/adapters/spatial-adapter.ts +37 -0
- package/src/sdk/adapters/types.ts +11 -0
- package/src/sdk/adapters/viewer-adapter.ts +103 -0
- package/src/sdk/adapters/visibility-adapter.ts +61 -0
- package/src/sdk/local-backend.ts +144 -0
- package/src/sdk/useBimHost.ts +69 -0
- package/src/store/constants.ts +10 -2
- package/src/store/index.ts +28 -2
- package/src/store/resolveEntityRef.ts +44 -0
- package/src/store/slices/drawing2DSlice.ts +321 -0
- package/src/store/slices/lensSlice.ts +46 -4
- package/src/store/slices/pinboardSlice.ts +171 -42
- package/src/store/slices/scriptSlice.ts +218 -0
- package/src/store/slices/uiSlice.ts +2 -0
- package/src/store.ts +3 -0
- package/tsconfig.json +5 -2
- package/vite.config.ts +8 -0
- package/dist/assets/index-dgdgiQ9p.js +0 -75456
- package/dist/assets/index-yTqs8kgX.css +0 -1
|
@@ -7,12 +7,49 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { useEffect, useCallback } from 'react';
|
|
10
|
-
import { useViewerStore } from '@/store';
|
|
10
|
+
import { useViewerStore, stringToEntityRef } from '@/store';
|
|
11
|
+
import type { EntityRef } from '@/store';
|
|
11
12
|
|
|
12
13
|
interface KeyboardShortcutsOptions {
|
|
13
14
|
enabled?: boolean;
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
/** Clear multi-select state so subsequent operations use single-entity selectedEntity */
|
|
18
|
+
function clearMultiSelect(): void {
|
|
19
|
+
const state = useViewerStore.getState();
|
|
20
|
+
if (state.selectedEntitiesSet.size > 0) {
|
|
21
|
+
useViewerStore.setState({ selectedEntitiesSet: new Set(), selectedEntityIds: new Set() });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Get all selected global IDs — multi-select if available, else single selectedEntityId */
|
|
26
|
+
function getAllSelectedGlobalIds(): number[] {
|
|
27
|
+
const state = useViewerStore.getState();
|
|
28
|
+
if (state.selectedEntityIds.size > 0) {
|
|
29
|
+
return Array.from(state.selectedEntityIds);
|
|
30
|
+
}
|
|
31
|
+
if (state.selectedEntityId !== null) {
|
|
32
|
+
return [state.selectedEntityId];
|
|
33
|
+
}
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Get current selection as EntityRef[] — multi-select if available, else single */
|
|
38
|
+
function getSelectionRefsFromStore(): EntityRef[] {
|
|
39
|
+
const state = useViewerStore.getState();
|
|
40
|
+
if (state.selectedEntitiesSet.size > 0) {
|
|
41
|
+
const refs: EntityRef[] = [];
|
|
42
|
+
for (const str of state.selectedEntitiesSet) {
|
|
43
|
+
refs.push(stringToEntityRef(str));
|
|
44
|
+
}
|
|
45
|
+
return refs;
|
|
46
|
+
}
|
|
47
|
+
if (state.selectedEntity) {
|
|
48
|
+
return [state.selectedEntity];
|
|
49
|
+
}
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
|
|
16
53
|
export function useKeyboardShortcuts(options: KeyboardShortcutsOptions = {}) {
|
|
17
54
|
const { enabled = true } = options;
|
|
18
55
|
|
|
@@ -20,12 +57,18 @@ export function useKeyboardShortcuts(options: KeyboardShortcutsOptions = {}) {
|
|
|
20
57
|
const setSelectedEntityId = useViewerStore((s) => s.setSelectedEntityId);
|
|
21
58
|
const activeTool = useViewerStore((s) => s.activeTool);
|
|
22
59
|
const setActiveTool = useViewerStore((s) => s.setActiveTool);
|
|
23
|
-
const
|
|
24
|
-
const hideEntity = useViewerStore((s) => s.hideEntity);
|
|
60
|
+
const hideEntities = useViewerStore((s) => s.hideEntities);
|
|
25
61
|
const showAll = useViewerStore((s) => s.showAll);
|
|
26
62
|
const clearStoreySelection = useViewerStore((s) => s.clearStoreySelection);
|
|
27
63
|
const toggleTheme = useViewerStore((s) => s.toggleTheme);
|
|
28
64
|
|
|
65
|
+
// Basket actions
|
|
66
|
+
const setBasket = useViewerStore((s) => s.setBasket);
|
|
67
|
+
const addToBasket = useViewerStore((s) => s.addToBasket);
|
|
68
|
+
const removeFromBasket = useViewerStore((s) => s.removeFromBasket);
|
|
69
|
+
const clearBasket = useViewerStore((s) => s.clearBasket);
|
|
70
|
+
const showPinboard = useViewerStore((s) => s.showPinboard);
|
|
71
|
+
|
|
29
72
|
// Measure tool specific actions
|
|
30
73
|
const activeMeasurement = useViewerStore((s) => s.activeMeasurement);
|
|
31
74
|
const cancelMeasurement = useViewerStore((s) => s.cancelMeasurement);
|
|
@@ -74,18 +117,67 @@ export function useKeyboardShortcuts(options: KeyboardShortcutsOptions = {}) {
|
|
|
74
117
|
setActiveTool('section');
|
|
75
118
|
}
|
|
76
119
|
|
|
77
|
-
// Visibility controls
|
|
78
|
-
|
|
120
|
+
// Basket / Visibility controls
|
|
121
|
+
// I = Set basket (isolate selection as basket), or re-apply basket if no selection
|
|
122
|
+
if (key === 'i' && !ctrl && !shift) {
|
|
123
|
+
const state = useViewerStore.getState();
|
|
124
|
+
// If basket already exists and user hasn't explicitly multi-selected,
|
|
125
|
+
// re-apply the basket instead of replacing it with a stale single selection.
|
|
126
|
+
if (state.pinboardEntities.size > 0 && state.selectedEntitiesSet.size === 0) {
|
|
127
|
+
e.preventDefault();
|
|
128
|
+
showPinboard();
|
|
129
|
+
} else {
|
|
130
|
+
const refs = getSelectionRefsFromStore();
|
|
131
|
+
if (refs.length > 0) {
|
|
132
|
+
e.preventDefault();
|
|
133
|
+
setBasket(refs);
|
|
134
|
+
// Consume multi-select so subsequent − removes a single entity
|
|
135
|
+
clearMultiSelect();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// + or = (with shift) = Add to basket
|
|
141
|
+
if ((e.key === '+' || (e.key === '=' && shift)) && !ctrl) {
|
|
142
|
+
e.preventDefault();
|
|
143
|
+
const refs = getSelectionRefsFromStore();
|
|
144
|
+
if (refs.length > 0) {
|
|
145
|
+
addToBasket(refs);
|
|
146
|
+
// Consume multi-select so subsequent − removes a single entity
|
|
147
|
+
clearMultiSelect();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// - or _ = Remove from basket
|
|
152
|
+
if ((e.key === '-' || e.key === '_') && !ctrl) {
|
|
79
153
|
e.preventDefault();
|
|
80
|
-
|
|
154
|
+
const refs = getSelectionRefsFromStore();
|
|
155
|
+
if (refs.length > 0) {
|
|
156
|
+
removeFromBasket(refs);
|
|
157
|
+
// Consume multi-select after removal
|
|
158
|
+
clearMultiSelect();
|
|
159
|
+
}
|
|
81
160
|
}
|
|
161
|
+
|
|
82
162
|
if ((key === 'delete' || key === 'backspace') && !ctrl && !shift && selectedEntityId) {
|
|
83
163
|
e.preventDefault();
|
|
84
|
-
|
|
164
|
+
const ids = getAllSelectedGlobalIds();
|
|
165
|
+
hideEntities(ids);
|
|
166
|
+
clearMultiSelect();
|
|
167
|
+
}
|
|
168
|
+
// Space to hide — skip when focused on buttons/selects/links where Space has native behavior
|
|
169
|
+
if (key === ' ' && !ctrl && !shift && selectedEntityId) {
|
|
170
|
+
const tag = document.activeElement?.tagName;
|
|
171
|
+
if (tag !== 'BUTTON' && tag !== 'SELECT' && tag !== 'A') {
|
|
172
|
+
e.preventDefault();
|
|
173
|
+
const ids = getAllSelectedGlobalIds();
|
|
174
|
+
hideEntities(ids);
|
|
175
|
+
clearMultiSelect();
|
|
176
|
+
}
|
|
85
177
|
}
|
|
86
178
|
if (key === 'a' && !ctrl && !shift) {
|
|
87
179
|
e.preventDefault();
|
|
88
|
-
showAll();
|
|
180
|
+
showAll(); // Clear hiddenEntities + isolatedEntities (basket preserved)
|
|
89
181
|
clearStoreySelection(); // Also clear storey filtering
|
|
90
182
|
}
|
|
91
183
|
|
|
@@ -121,6 +213,7 @@ export function useKeyboardShortcuts(options: KeyboardShortcutsOptions = {}) {
|
|
|
121
213
|
if (key === 'escape') {
|
|
122
214
|
e.preventDefault();
|
|
123
215
|
setSelectedEntityId(null);
|
|
216
|
+
clearBasket();
|
|
124
217
|
showAll();
|
|
125
218
|
clearStoreySelection(); // Also clear storey filtering
|
|
126
219
|
setActiveTool('select');
|
|
@@ -139,8 +232,12 @@ export function useKeyboardShortcuts(options: KeyboardShortcutsOptions = {}) {
|
|
|
139
232
|
setSelectedEntityId,
|
|
140
233
|
activeTool,
|
|
141
234
|
setActiveTool,
|
|
142
|
-
|
|
143
|
-
|
|
235
|
+
setBasket,
|
|
236
|
+
addToBasket,
|
|
237
|
+
removeFromBasket,
|
|
238
|
+
clearBasket,
|
|
239
|
+
showPinboard,
|
|
240
|
+
hideEntities,
|
|
144
241
|
showAll,
|
|
145
242
|
clearStoreySelection,
|
|
146
243
|
toggleTheme,
|
|
@@ -171,14 +268,17 @@ export const KEYBOARD_SHORTCUTS = [
|
|
|
171
268
|
{ key: 'S', description: 'Toggle snapping (Measure tool)', category: 'Tools' },
|
|
172
269
|
{ key: 'Esc', description: 'Cancel measurement (Measure tool)', category: 'Tools' },
|
|
173
270
|
{ key: 'Ctrl+C', description: 'Clear measurements (Measure tool)', category: 'Tools' },
|
|
174
|
-
{ key: 'I', description: '
|
|
175
|
-
{ key: '
|
|
176
|
-
{ key: '
|
|
271
|
+
{ key: 'I', description: 'Set basket (isolate selection)', category: 'Visibility' },
|
|
272
|
+
{ key: '+', description: 'Add selection to basket', category: 'Visibility' },
|
|
273
|
+
{ key: '−', description: 'Remove selection from basket', category: 'Visibility' },
|
|
274
|
+
{ key: 'Del / Space', description: 'Hide selection', category: 'Visibility' },
|
|
275
|
+
{ key: 'A', description: 'Show all (clear filters, keep basket)', category: 'Visibility' },
|
|
177
276
|
{ key: 'H', description: 'Home (Isometric view)', category: 'Camera' },
|
|
178
277
|
{ key: 'Z', description: 'Fit all (zoom extents)', category: 'Camera' },
|
|
179
278
|
{ key: 'F', description: 'Frame selection', category: 'Camera' },
|
|
180
279
|
{ key: '1-6', description: 'Preset views', category: 'Camera' },
|
|
181
280
|
{ key: 'T', description: 'Toggle theme', category: 'UI' },
|
|
182
|
-
{ key: 'Esc', description: 'Reset all (clear selection,
|
|
281
|
+
{ key: 'Esc', description: 'Reset all (clear selection, basket, isolation)', category: 'Selection' },
|
|
282
|
+
{ key: 'Ctrl+K', description: 'Command palette', category: 'UI' },
|
|
183
283
|
{ key: '?', description: 'Show info panel', category: 'Help' },
|
|
184
284
|
] as const;
|
package/src/hooks/useLens.ts
CHANGED
|
@@ -10,78 +10,55 @@
|
|
|
10
10
|
* Unmatched entities with geometry are ghosted (semi-transparent).
|
|
11
11
|
*
|
|
12
12
|
* The pure evaluation logic lives in @ifc-lite/lens — this hook handles
|
|
13
|
-
* React lifecycle
|
|
13
|
+
* React lifecycle and Zustand integration.
|
|
14
14
|
*
|
|
15
15
|
* Performance notes:
|
|
16
16
|
* - Does NOT subscribe to `models` or `ifcDataStore` — reads them from
|
|
17
17
|
* getState() only when the active lens changes. This prevents re-evaluation
|
|
18
18
|
* during model loading.
|
|
19
|
-
* - Uses
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* - Uses color overlay system: pendingColorUpdates triggers
|
|
20
|
+
* scene.setColorOverrides() which builds overlay batches rendered on top
|
|
21
|
+
* of original geometry. Original batches are NEVER modified — clearing
|
|
22
|
+
* lens is instant (no batch rebuild).
|
|
22
23
|
*/
|
|
23
24
|
|
|
24
|
-
import { useEffect, useRef,
|
|
25
|
-
import { evaluateLens, rgbaToHex, isGhostColor } from '@ifc-lite/lens';
|
|
26
|
-
import type {
|
|
25
|
+
import { useEffect, useRef, useMemo } from 'react';
|
|
26
|
+
import { evaluateLens, evaluateAutoColorLens, rgbaToHex, isGhostColor } from '@ifc-lite/lens';
|
|
27
|
+
import type { AutoColorEvaluationResult } from '@ifc-lite/lens';
|
|
27
28
|
import { useViewerStore } from '@/store';
|
|
28
29
|
import { createLensDataProvider } from '@/lib/lens';
|
|
30
|
+
import { useLensDiscovery } from './useLensDiscovery';
|
|
29
31
|
|
|
30
32
|
export function useLens() {
|
|
31
33
|
const activeLensId = useViewerStore((s) => s.activeLensId);
|
|
32
34
|
const savedLenses = useViewerStore((s) => s.savedLenses);
|
|
33
35
|
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const captureOriginalColors = useCallback(() => {
|
|
41
|
-
const state = useViewerStore.getState();
|
|
42
|
-
const originals = new Map<number, RGBAColor>();
|
|
43
|
-
|
|
44
|
-
// Federation mode: collect from all model geometries
|
|
45
|
-
if (state.models.size > 0) {
|
|
46
|
-
for (const [, model] of state.models) {
|
|
47
|
-
if (model.geometryResult?.meshes) {
|
|
48
|
-
for (const mesh of model.geometryResult.meshes) {
|
|
49
|
-
if (mesh.color) {
|
|
50
|
-
originals.set(mesh.expressId, mesh.color as RGBAColor);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
36
|
+
// Derive the active lens object — only re-evaluates when activeLensId or
|
|
37
|
+
// the active lens entry itself changes, not when unrelated lenses are edited.
|
|
38
|
+
const activeLens = useMemo(
|
|
39
|
+
() => savedLenses.find(l => l.id === activeLensId) ?? null,
|
|
40
|
+
[activeLensId, savedLenses],
|
|
41
|
+
);
|
|
56
42
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
for (const mesh of state.geometryResult.meshes) {
|
|
60
|
-
if (mesh.color) {
|
|
61
|
-
originals.set(mesh.expressId, mesh.color as RGBAColor);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
43
|
+
// Run data discovery when models change (populates discoveredLensData in store)
|
|
44
|
+
useLensDiscovery();
|
|
65
45
|
|
|
66
|
-
|
|
67
|
-
|
|
46
|
+
// Track the previously active lens to detect deactivation
|
|
47
|
+
const prevLensIdRef = useRef<string | null>(null);
|
|
68
48
|
|
|
69
49
|
useEffect(() => {
|
|
70
|
-
const activeLens = savedLenses.find(l => l.id === activeLensId) ?? null;
|
|
71
50
|
|
|
72
|
-
// Lens deactivated —
|
|
51
|
+
// Lens deactivated — clear overlay (instant, no batch rebuild)
|
|
73
52
|
if (!activeLens && prevLensIdRef.current !== null) {
|
|
74
53
|
prevLensIdRef.current = null;
|
|
75
54
|
useViewerStore.getState().setLensColorMap(new Map());
|
|
76
55
|
useViewerStore.getState().setLensHiddenIds(new Set());
|
|
77
56
|
useViewerStore.getState().setLensRuleCounts(new Map());
|
|
78
57
|
useViewerStore.getState().setLensRuleEntityIds(new Map());
|
|
58
|
+
useViewerStore.getState().setLensAutoColorLegend([]);
|
|
79
59
|
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
useViewerStore.getState().setPendingColorUpdates(originalColorsRef.current);
|
|
83
|
-
}
|
|
84
|
-
originalColorsRef.current = null;
|
|
60
|
+
// Send empty map to signal "clear overlays" to useGeometryStreaming
|
|
61
|
+
useViewerStore.getState().setPendingColorUpdates(new Map());
|
|
85
62
|
return;
|
|
86
63
|
}
|
|
87
64
|
|
|
@@ -92,16 +69,18 @@ export function useLens() {
|
|
|
92
69
|
const { models, ifcDataStore } = useViewerStore.getState();
|
|
93
70
|
if (models.size === 0 && !ifcDataStore) return;
|
|
94
71
|
|
|
95
|
-
// Save original colors before first lens application
|
|
96
|
-
if (prevLensIdRef.current === null) {
|
|
97
|
-
originalColorsRef.current = captureOriginalColors();
|
|
98
|
-
}
|
|
99
|
-
|
|
100
72
|
prevLensIdRef.current = activeLensId;
|
|
101
73
|
|
|
102
74
|
// Create data provider and evaluate lens using @ifc-lite/lens package
|
|
103
75
|
const provider = createLensDataProvider(models, ifcDataStore);
|
|
104
|
-
|
|
76
|
+
|
|
77
|
+
// Dispatch: auto-color mode vs. rule-based mode
|
|
78
|
+
const isAutoColor = !!activeLens.autoColor;
|
|
79
|
+
const result = isAutoColor
|
|
80
|
+
? evaluateAutoColorLens(activeLens.autoColor!, provider)
|
|
81
|
+
: evaluateLens(activeLens, provider);
|
|
82
|
+
|
|
83
|
+
const { colorMap, hiddenIds, ruleCounts, ruleEntityIds } = result;
|
|
105
84
|
|
|
106
85
|
// Build hex color map for UI legend (exclude ghost entries)
|
|
107
86
|
const hexColorMap = new Map<number, string>();
|
|
@@ -115,12 +94,18 @@ export function useLens() {
|
|
|
115
94
|
useViewerStore.getState().setLensRuleCounts(ruleCounts);
|
|
116
95
|
useViewerStore.getState().setLensRuleEntityIds(ruleEntityIds);
|
|
117
96
|
|
|
118
|
-
//
|
|
119
|
-
|
|
97
|
+
// Store auto-color legend entries for UI display
|
|
98
|
+
if (isAutoColor && 'legend' in result) {
|
|
99
|
+
useViewerStore.getState().setLensAutoColorLegend((result as AutoColorEvaluationResult).legend);
|
|
100
|
+
} else {
|
|
101
|
+
useViewerStore.getState().setLensAutoColorLegend([]);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Apply colors via overlay system — original batches are never modified
|
|
120
105
|
if (colorMap.size > 0) {
|
|
121
106
|
useViewerStore.getState().setPendingColorUpdates(colorMap);
|
|
122
107
|
}
|
|
123
|
-
}, [activeLensId,
|
|
108
|
+
}, [activeLensId, activeLens]);
|
|
124
109
|
|
|
125
110
|
return {
|
|
126
111
|
activeLensId,
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
/**
|
|
6
|
+
* Lens class discovery hook — INSTANT, zero loading impact.
|
|
7
|
+
*
|
|
8
|
+
* Only discovers IFC class names from the entity table (O(n) array scan,
|
|
9
|
+
* no STEP buffer parsing). Property sets, quantities, materials, and
|
|
10
|
+
* classifications are discovered lazily on-demand when the user opens
|
|
11
|
+
* a dropdown that needs them — see `useLazyDiscovery` in LensPanel.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useEffect } from 'react';
|
|
15
|
+
import { discoverClasses } from '@ifc-lite/lens';
|
|
16
|
+
import { useViewerStore } from '@/store';
|
|
17
|
+
import { createLensDataProvider } from '@/lib/lens';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Discover IFC classes when models change (instant).
|
|
21
|
+
* Stores result in `discoveredLensData.classes` on the lens slice.
|
|
22
|
+
*/
|
|
23
|
+
export function useLensDiscovery(): void {
|
|
24
|
+
const modelCount = useViewerStore((s) => s.models.size);
|
|
25
|
+
const ifcDataStore = useViewerStore((s) => s.ifcDataStore);
|
|
26
|
+
const setDiscoveredLensData = useViewerStore((s) => s.setDiscoveredLensData);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const { models, ifcDataStore: ds } = useViewerStore.getState();
|
|
30
|
+
if (models.size === 0 && !ds) {
|
|
31
|
+
setDiscoveredLensData(null);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Instant: just reads type names from entity arrays, no STEP parsing
|
|
36
|
+
const provider = createLensDataProvider(models, ds);
|
|
37
|
+
const classes = discoverClasses(provider);
|
|
38
|
+
setDiscoveredLensData({
|
|
39
|
+
classes,
|
|
40
|
+
propertySets: null, // lazy — discovered on-demand
|
|
41
|
+
quantitySets: null, // lazy — discovered on-demand
|
|
42
|
+
classificationSystems: null, // lazy — discovered on-demand
|
|
43
|
+
materials: null, // lazy — discovered on-demand
|
|
44
|
+
});
|
|
45
|
+
}, [modelCount, ifcDataStore, setDiscoveredLensData]);
|
|
46
|
+
}
|
|
@@ -20,14 +20,13 @@
|
|
|
20
20
|
|
|
21
21
|
import { useEffect } from 'react';
|
|
22
22
|
import { useViewerStore } from '../store.js';
|
|
23
|
+
import { resolveEntityRef } from '../store/resolveEntityRef.js';
|
|
23
24
|
|
|
24
25
|
export function useModelSelection() {
|
|
25
26
|
const selectedEntityId = useViewerStore((s) => s.selectedEntityId);
|
|
26
27
|
const setSelectedEntity = useViewerStore((s) => s.setSelectedEntity);
|
|
27
28
|
// Subscribe to models for reactivity (when models are added/removed)
|
|
28
29
|
const models = useViewerStore((s) => s.models);
|
|
29
|
-
// Use the bulletproof store-based resolver
|
|
30
|
-
const resolveGlobalIdFromModels = useViewerStore((s) => s.resolveGlobalIdFromModels);
|
|
31
30
|
|
|
32
31
|
useEffect(() => {
|
|
33
32
|
if (selectedEntityId === null) {
|
|
@@ -38,24 +37,8 @@ export function useModelSelection() {
|
|
|
38
37
|
return;
|
|
39
38
|
}
|
|
40
39
|
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (resolved) {
|
|
46
|
-
// Set EntityRef with ORIGINAL expressId (for property lookup in IfcDataStore)
|
|
47
|
-
setSelectedEntity({ modelId: resolved.modelId, expressId: resolved.expressId });
|
|
48
|
-
} else {
|
|
49
|
-
// Fallback for single-model mode (offset = 0, globalId = expressId)
|
|
50
|
-
// In this case, try to find the first model and use the globalId as expressId
|
|
51
|
-
if (models.size > 0) {
|
|
52
|
-
const firstModelId = Array.from(models.keys())[0];
|
|
53
|
-
setSelectedEntity({ modelId: firstModelId, expressId: selectedEntityId });
|
|
54
|
-
} else {
|
|
55
|
-
// Legacy single-model mode: use 'legacy' as modelId
|
|
56
|
-
// This allows PropertiesPanel to fall back to the legacy query
|
|
57
|
-
setSelectedEntity({ modelId: 'legacy', expressId: selectedEntityId });
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}, [selectedEntityId, setSelectedEntity, models, resolveGlobalIdFromModels]);
|
|
40
|
+
// Single source of truth: resolveEntityRef handles globalId → EntityRef
|
|
41
|
+
// including fallback for single-model mode (offset 0). Always returns an EntityRef.
|
|
42
|
+
setSelectedEntity(resolveEntityRef(selectedEntityId));
|
|
43
|
+
}, [selectedEntityId, setSelectedEntity, models]);
|
|
61
44
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
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
|
+
/**
|
|
6
|
+
* useSandbox — React hook for executing scripts in a QuickJS sandbox.
|
|
7
|
+
*
|
|
8
|
+
* Creates a fresh sandbox context per execution for full isolation.
|
|
9
|
+
* The WASM module is cached across the session (cheap to reuse),
|
|
10
|
+
* but each script runs in a clean context with no leaked state.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
14
|
+
import { useBim } from '../sdk/BimProvider.js';
|
|
15
|
+
import { useViewerStore } from '../store/index.js';
|
|
16
|
+
import type { Sandbox, ScriptResult, SandboxConfig } from '@ifc-lite/sandbox';
|
|
17
|
+
|
|
18
|
+
/** Type guard for ScriptError shape (has logs + durationMs) */
|
|
19
|
+
function isScriptError(err: unknown): err is { message: string; logs: Array<{ level: string; args: unknown[]; timestamp: number }>; durationMs: number } {
|
|
20
|
+
return (
|
|
21
|
+
err !== null &&
|
|
22
|
+
typeof err === 'object' &&
|
|
23
|
+
'logs' in err &&
|
|
24
|
+
Array.isArray((err as Record<string, unknown>).logs) &&
|
|
25
|
+
'durationMs' in err &&
|
|
26
|
+
typeof (err as Record<string, unknown>).durationMs === 'number'
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Hook that provides a sandbox execution interface.
|
|
32
|
+
*
|
|
33
|
+
* Each execute() call creates a fresh QuickJS context for full isolation —
|
|
34
|
+
* scripts cannot leak global state between runs. The WASM module itself
|
|
35
|
+
* is cached (loaded once per app lifetime, ~1ms context creation overhead).
|
|
36
|
+
*/
|
|
37
|
+
export function useSandbox(config?: SandboxConfig) {
|
|
38
|
+
const bim = useBim();
|
|
39
|
+
const activeSandboxRef = useRef<Sandbox | null>(null);
|
|
40
|
+
|
|
41
|
+
const setExecutionState = useViewerStore((s) => s.setScriptExecutionState);
|
|
42
|
+
const setResult = useViewerStore((s) => s.setScriptResult);
|
|
43
|
+
const setError = useViewerStore((s) => s.setScriptError);
|
|
44
|
+
|
|
45
|
+
/** Execute a script in an isolated sandbox context */
|
|
46
|
+
const execute = useCallback(async (code: string): Promise<ScriptResult | null> => {
|
|
47
|
+
setExecutionState('running');
|
|
48
|
+
setError(null);
|
|
49
|
+
|
|
50
|
+
let sandbox: Sandbox | null = null;
|
|
51
|
+
try {
|
|
52
|
+
// Create a fresh sandbox for every execution — full isolation
|
|
53
|
+
const { createSandbox } = await import('@ifc-lite/sandbox');
|
|
54
|
+
sandbox = await createSandbox(bim, {
|
|
55
|
+
permissions: { model: true, query: true, viewer: true, mutate: true, lens: true, export: true, ...config?.permissions },
|
|
56
|
+
limits: { timeoutMs: 30_000, ...config?.limits },
|
|
57
|
+
});
|
|
58
|
+
activeSandboxRef.current = sandbox;
|
|
59
|
+
|
|
60
|
+
const result = await sandbox.eval(code);
|
|
61
|
+
setResult({
|
|
62
|
+
value: result.value,
|
|
63
|
+
logs: result.logs,
|
|
64
|
+
durationMs: result.durationMs,
|
|
65
|
+
});
|
|
66
|
+
return result;
|
|
67
|
+
} catch (err: unknown) {
|
|
68
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
69
|
+
setError(message);
|
|
70
|
+
|
|
71
|
+
// If the error is a ScriptError with captured logs, preserve them
|
|
72
|
+
if (isScriptError(err)) {
|
|
73
|
+
setResult({
|
|
74
|
+
value: undefined,
|
|
75
|
+
logs: err.logs as ScriptResult['logs'],
|
|
76
|
+
durationMs: err.durationMs,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
} finally {
|
|
81
|
+
// Always dispose the sandbox after execution
|
|
82
|
+
if (sandbox) {
|
|
83
|
+
sandbox.dispose();
|
|
84
|
+
}
|
|
85
|
+
if (activeSandboxRef.current === sandbox) {
|
|
86
|
+
activeSandboxRef.current = null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}, [bim, config?.permissions, config?.limits, setExecutionState, setResult, setError]);
|
|
90
|
+
|
|
91
|
+
/** Reset clears any active sandbox (no-op if none running) */
|
|
92
|
+
const reset = useCallback(() => {
|
|
93
|
+
if (activeSandboxRef.current) {
|
|
94
|
+
activeSandboxRef.current.dispose();
|
|
95
|
+
activeSandboxRef.current = null;
|
|
96
|
+
}
|
|
97
|
+
setExecutionState('idle');
|
|
98
|
+
setResult(null);
|
|
99
|
+
setError(null);
|
|
100
|
+
}, [setExecutionState, setResult, setError]);
|
|
101
|
+
|
|
102
|
+
// Cleanup on unmount
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
return () => {
|
|
105
|
+
if (activeSandboxRef.current) {
|
|
106
|
+
activeSandboxRef.current.dispose();
|
|
107
|
+
activeSandboxRef.current = null;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}, []);
|
|
111
|
+
|
|
112
|
+
return { execute, reset };
|
|
113
|
+
}
|
package/src/index.css
CHANGED
|
@@ -187,10 +187,16 @@ body {
|
|
|
187
187
|
color: var(--tokyo-comment) !important;
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
.dark .text-zinc-200
|
|
190
|
+
.dark .text-zinc-200,
|
|
191
|
+
.dark .text-zinc-300 {
|
|
191
192
|
color: var(--tokyo-fg-dark) !important;
|
|
192
193
|
}
|
|
193
194
|
|
|
195
|
+
.dark .text-zinc-700,
|
|
196
|
+
.dark .text-zinc-800 {
|
|
197
|
+
color: var(--tokyo-fg) !important;
|
|
198
|
+
}
|
|
199
|
+
|
|
194
200
|
/* Primary accent */
|
|
195
201
|
.dark .bg-primary {
|
|
196
202
|
background-color: var(--tokyo-blue) !important;
|