@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
|
@@ -3,20 +3,26 @@
|
|
|
3
3
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Pinboard state slice
|
|
6
|
+
* Pinboard (Basket) state slice
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* The basket is an incremental isolation set. Users build it with:
|
|
9
|
+
* = (set) — replace basket with current selection
|
|
10
|
+
* + (add) — add current selection to basket
|
|
11
|
+
* − (remove) — remove current selection from basket
|
|
12
|
+
*
|
|
13
|
+
* When the basket is non-empty, only basket entities are visible (isolation).
|
|
14
|
+
* The basket also syncs to isolatedEntities for renderer consumption.
|
|
10
15
|
*/
|
|
11
16
|
|
|
12
17
|
import type { StateCreator } from 'zustand';
|
|
13
18
|
import type { EntityRef } from '../types.js';
|
|
14
19
|
import { entityRefToString, stringToEntityRef } from '../types.js';
|
|
15
20
|
|
|
16
|
-
/**
|
|
17
|
-
interface
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
/** Cross-slice state that pinboard reads/writes via the combined store */
|
|
22
|
+
interface PinboardCrossSliceState {
|
|
23
|
+
isolatedEntities: Set<number> | null;
|
|
24
|
+
hiddenEntities: Set<number>;
|
|
25
|
+
models: Map<string, { idOffset: number }>;
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
export interface PinboardSlice {
|
|
@@ -25,46 +31,94 @@ export interface PinboardSlice {
|
|
|
25
31
|
pinboardEntities: Set<string>;
|
|
26
32
|
|
|
27
33
|
// Actions
|
|
28
|
-
/** Add entities to pinboard */
|
|
34
|
+
/** Add entities to pinboard/basket */
|
|
29
35
|
addToPinboard: (refs: EntityRef[]) => void;
|
|
30
|
-
/** Remove entities from pinboard */
|
|
36
|
+
/** Remove entities from pinboard/basket */
|
|
31
37
|
removeFromPinboard: (refs: EntityRef[]) => void;
|
|
32
|
-
/** Replace pinboard contents */
|
|
38
|
+
/** Replace pinboard/basket contents (= operation) */
|
|
33
39
|
setPinboard: (refs: EntityRef[]) => void;
|
|
34
|
-
/** Clear pinboard */
|
|
40
|
+
/** Clear pinboard/basket and isolation */
|
|
35
41
|
clearPinboard: () => void;
|
|
36
|
-
/** Isolate pinboard entities (
|
|
42
|
+
/** Isolate pinboard entities (sync basket → isolatedEntities) */
|
|
37
43
|
showPinboard: () => void;
|
|
38
|
-
/** Check if entity is
|
|
44
|
+
/** Check if entity is in basket */
|
|
39
45
|
isInPinboard: (ref: EntityRef) => boolean;
|
|
40
|
-
/** Get
|
|
46
|
+
/** Get basket count */
|
|
41
47
|
getPinboardCount: () => number;
|
|
42
|
-
/** Get all
|
|
48
|
+
/** Get all basket entities as EntityRef array */
|
|
43
49
|
getPinboardEntities: () => EntityRef[];
|
|
50
|
+
|
|
51
|
+
// Basket actions (semantic aliases that also sync isolation)
|
|
52
|
+
/** = Set basket to exactly these entities and isolate them */
|
|
53
|
+
setBasket: (refs: EntityRef[]) => void;
|
|
54
|
+
/** + Add entities to basket and update isolation */
|
|
55
|
+
addToBasket: (refs: EntityRef[]) => void;
|
|
56
|
+
/** − Remove entities from basket and update isolation */
|
|
57
|
+
removeFromBasket: (refs: EntityRef[]) => void;
|
|
58
|
+
/** Clear basket and clear isolation */
|
|
59
|
+
clearBasket: () => void;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Convert basket EntityRefs to global IDs using model offsets */
|
|
63
|
+
function basketToGlobalIds(
|
|
64
|
+
basketEntities: Set<string>,
|
|
65
|
+
models: Map<string, { idOffset: number }>,
|
|
66
|
+
): Set<number> {
|
|
67
|
+
const globalIds = new Set<number>();
|
|
68
|
+
for (const str of basketEntities) {
|
|
69
|
+
const ref = stringToEntityRef(str);
|
|
70
|
+
const model = models.get(ref.modelId);
|
|
71
|
+
const offset = model?.idOffset ?? 0;
|
|
72
|
+
globalIds.add(ref.expressId + offset);
|
|
73
|
+
}
|
|
74
|
+
return globalIds;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Compute a single EntityRef's global ID */
|
|
78
|
+
function refToGlobalId(ref: EntityRef, models: Map<string, { idOffset: number }>): number {
|
|
79
|
+
const model = models.get(ref.modelId);
|
|
80
|
+
return ref.expressId + (model?.idOffset ?? 0);
|
|
44
81
|
}
|
|
45
82
|
|
|
46
|
-
export const createPinboardSlice: StateCreator<
|
|
83
|
+
export const createPinboardSlice: StateCreator<
|
|
84
|
+
PinboardSlice & PinboardCrossSliceState,
|
|
85
|
+
[],
|
|
86
|
+
[],
|
|
87
|
+
PinboardSlice
|
|
88
|
+
> = (set, get) => ({
|
|
47
89
|
// Initial state
|
|
48
90
|
pinboardEntities: new Set(),
|
|
49
91
|
|
|
50
|
-
//
|
|
92
|
+
// Legacy actions (kept for backward compat, but now they also sync isolation)
|
|
51
93
|
addToPinboard: (refs) => {
|
|
52
94
|
set((state) => {
|
|
53
|
-
const next = new Set(state.pinboardEntities);
|
|
95
|
+
const next = new Set<string>(state.pinboardEntities);
|
|
54
96
|
for (const ref of refs) {
|
|
55
97
|
next.add(entityRefToString(ref));
|
|
56
98
|
}
|
|
57
|
-
|
|
99
|
+
const isolatedEntities = basketToGlobalIds(next, state.models);
|
|
100
|
+
const hiddenEntities = new Set<number>(state.hiddenEntities);
|
|
101
|
+
// Unhide any entities being added to basket
|
|
102
|
+
for (const ref of refs) {
|
|
103
|
+
const model = state.models.get(ref.modelId);
|
|
104
|
+
const offset = model?.idOffset ?? 0;
|
|
105
|
+
hiddenEntities.delete(ref.expressId + offset);
|
|
106
|
+
}
|
|
107
|
+
return { pinboardEntities: next, isolatedEntities, hiddenEntities };
|
|
58
108
|
});
|
|
59
109
|
},
|
|
60
110
|
|
|
61
111
|
removeFromPinboard: (refs) => {
|
|
62
112
|
set((state) => {
|
|
63
|
-
const next = new Set(state.pinboardEntities);
|
|
113
|
+
const next = new Set<string>(state.pinboardEntities);
|
|
64
114
|
for (const ref of refs) {
|
|
65
115
|
next.delete(entityRefToString(ref));
|
|
66
116
|
}
|
|
67
|
-
|
|
117
|
+
if (next.size === 0) {
|
|
118
|
+
return { pinboardEntities: next, isolatedEntities: null };
|
|
119
|
+
}
|
|
120
|
+
const isolatedEntities = basketToGlobalIds(next, state.models);
|
|
121
|
+
return { pinboardEntities: next, isolatedEntities };
|
|
68
122
|
});
|
|
69
123
|
},
|
|
70
124
|
|
|
@@ -73,31 +127,29 @@ export const createPinboardSlice: StateCreator<PinboardSlice, [], [], PinboardSl
|
|
|
73
127
|
for (const ref of refs) {
|
|
74
128
|
next.add(entityRefToString(ref));
|
|
75
129
|
}
|
|
76
|
-
|
|
130
|
+
if (next.size === 0) {
|
|
131
|
+
set({ pinboardEntities: next, isolatedEntities: null });
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const s = get();
|
|
135
|
+
const hiddenEntities = new Set<number>(s.hiddenEntities);
|
|
136
|
+
// Unhide basket entities
|
|
137
|
+
for (const ref of refs) {
|
|
138
|
+
const model = s.models.get(ref.modelId);
|
|
139
|
+
const offset = model?.idOffset ?? 0;
|
|
140
|
+
hiddenEntities.delete(ref.expressId + offset);
|
|
141
|
+
}
|
|
142
|
+
const isolatedEntities = basketToGlobalIds(next, s.models);
|
|
143
|
+
set({ pinboardEntities: next, isolatedEntities, hiddenEntities });
|
|
77
144
|
},
|
|
78
145
|
|
|
79
|
-
clearPinboard: () => set({ pinboardEntities: new Set() }),
|
|
146
|
+
clearPinboard: () => set({ pinboardEntities: new Set(), isolatedEntities: null }),
|
|
80
147
|
|
|
81
148
|
showPinboard: () => {
|
|
82
|
-
const
|
|
83
|
-
if (
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const store = get() as unknown as CombinedStoreAccess;
|
|
87
|
-
if (!store.isolateEntities) return;
|
|
88
|
-
|
|
89
|
-
// Convert EntityRef to global IDs for isolation
|
|
90
|
-
const globalIds: number[] = [];
|
|
91
|
-
for (const ref of entities) {
|
|
92
|
-
if (store.models) {
|
|
93
|
-
const model = store.models.get(ref.modelId);
|
|
94
|
-
const offset = model?.idOffset ?? 0;
|
|
95
|
-
globalIds.push(ref.expressId + offset);
|
|
96
|
-
} else {
|
|
97
|
-
globalIds.push(ref.expressId);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
store.isolateEntities(globalIds);
|
|
149
|
+
const state = get();
|
|
150
|
+
if (state.pinboardEntities.size === 0) return;
|
|
151
|
+
const isolatedEntities = basketToGlobalIds(state.pinboardEntities, state.models);
|
|
152
|
+
set({ isolatedEntities });
|
|
101
153
|
},
|
|
102
154
|
|
|
103
155
|
isInPinboard: (ref) => get().pinboardEntities.has(entityRefToString(ref)),
|
|
@@ -111,4 +163,81 @@ export const createPinboardSlice: StateCreator<PinboardSlice, [], [], PinboardSl
|
|
|
111
163
|
}
|
|
112
164
|
return result;
|
|
113
165
|
},
|
|
166
|
+
|
|
167
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
168
|
+
// Basket actions (= + −)
|
|
169
|
+
// These are the primary API for the new basket-based isolation UX.
|
|
170
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
/** = Set basket to exactly these entities and isolate them */
|
|
173
|
+
setBasket: (refs) => {
|
|
174
|
+
if (refs.length === 0) {
|
|
175
|
+
set({ pinboardEntities: new Set(), isolatedEntities: null });
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const next = new Set<string>();
|
|
179
|
+
for (const ref of refs) {
|
|
180
|
+
next.add(entityRefToString(ref));
|
|
181
|
+
}
|
|
182
|
+
const s = get();
|
|
183
|
+
const hiddenEntities = new Set<number>(s.hiddenEntities);
|
|
184
|
+
// Unhide basket entities
|
|
185
|
+
for (const ref of refs) {
|
|
186
|
+
const model = s.models.get(ref.modelId);
|
|
187
|
+
const offset = model?.idOffset ?? 0;
|
|
188
|
+
hiddenEntities.delete(ref.expressId + offset);
|
|
189
|
+
}
|
|
190
|
+
const isolatedEntities = basketToGlobalIds(next, s.models);
|
|
191
|
+
set({ pinboardEntities: next, isolatedEntities, hiddenEntities });
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
/** + Add entities to basket and update isolation (incremental — avoids re-parsing all strings) */
|
|
195
|
+
addToBasket: (refs) => {
|
|
196
|
+
if (refs.length === 0) return;
|
|
197
|
+
set((state) => {
|
|
198
|
+
const next = new Set<string>(state.pinboardEntities);
|
|
199
|
+
for (const ref of refs) {
|
|
200
|
+
next.add(entityRefToString(ref));
|
|
201
|
+
}
|
|
202
|
+
const hiddenEntities = new Set<number>(state.hiddenEntities);
|
|
203
|
+
// Incrementally add new globalIds to existing isolation set instead of re-parsing all
|
|
204
|
+
const prevIsolated = state.isolatedEntities;
|
|
205
|
+
const isolatedEntities = prevIsolated ? new Set<number>(prevIsolated) : basketToGlobalIds(state.pinboardEntities, state.models);
|
|
206
|
+
for (const ref of refs) {
|
|
207
|
+
const gid = refToGlobalId(ref, state.models);
|
|
208
|
+
isolatedEntities.add(gid);
|
|
209
|
+
hiddenEntities.delete(gid);
|
|
210
|
+
}
|
|
211
|
+
return { pinboardEntities: next, isolatedEntities, hiddenEntities };
|
|
212
|
+
});
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
/** − Remove entities from basket and update isolation (incremental — avoids re-parsing all strings) */
|
|
216
|
+
removeFromBasket: (refs) => {
|
|
217
|
+
if (refs.length === 0) return;
|
|
218
|
+
set((state) => {
|
|
219
|
+
const next = new Set<string>(state.pinboardEntities);
|
|
220
|
+
for (const ref of refs) {
|
|
221
|
+
next.delete(entityRefToString(ref));
|
|
222
|
+
}
|
|
223
|
+
if (next.size === 0) {
|
|
224
|
+
return { pinboardEntities: next, isolatedEntities: null };
|
|
225
|
+
}
|
|
226
|
+
// Incrementally remove globalIds from existing isolation set instead of re-parsing all
|
|
227
|
+
const prevIsolated = state.isolatedEntities;
|
|
228
|
+
if (prevIsolated) {
|
|
229
|
+
const isolatedEntities = new Set<number>(prevIsolated);
|
|
230
|
+
for (const ref of refs) {
|
|
231
|
+
isolatedEntities.delete(refToGlobalId(ref, state.models));
|
|
232
|
+
}
|
|
233
|
+
return { pinboardEntities: next, isolatedEntities };
|
|
234
|
+
}
|
|
235
|
+
// Fallback: full recompute if no existing isolation set
|
|
236
|
+
const isolatedEntities = basketToGlobalIds(next, state.models);
|
|
237
|
+
return { pinboardEntities: next, isolatedEntities };
|
|
238
|
+
});
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
/** Clear basket and clear isolation */
|
|
242
|
+
clearBasket: () => set({ pinboardEntities: new Set(), isolatedEntities: null }),
|
|
114
243
|
});
|
|
@@ -0,0 +1,218 @@
|
|
|
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
|
+
* Script state slice — manages script editor state, saved scripts,
|
|
7
|
+
* and execution results.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { StateCreator } from 'zustand';
|
|
11
|
+
import type { SavedScript } from '../../lib/scripts/persistence.js';
|
|
12
|
+
import { loadSavedScripts, saveScripts, validateScriptName, canCreateScript, isScriptWithinSizeLimit } from '../../lib/scripts/persistence.js';
|
|
13
|
+
|
|
14
|
+
export type ScriptExecutionState = 'idle' | 'running' | 'error' | 'success';
|
|
15
|
+
|
|
16
|
+
export interface LogEntry {
|
|
17
|
+
level: 'log' | 'warn' | 'error' | 'info';
|
|
18
|
+
args: unknown[];
|
|
19
|
+
timestamp: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ScriptResult {
|
|
23
|
+
value: unknown;
|
|
24
|
+
logs: LogEntry[];
|
|
25
|
+
durationMs: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ScriptSlice {
|
|
29
|
+
// State
|
|
30
|
+
savedScripts: SavedScript[];
|
|
31
|
+
activeScriptId: string | null;
|
|
32
|
+
scriptEditorContent: string;
|
|
33
|
+
scriptEditorDirty: boolean;
|
|
34
|
+
scriptExecutionState: ScriptExecutionState;
|
|
35
|
+
scriptLastResult: ScriptResult | null;
|
|
36
|
+
scriptLastError: string | null;
|
|
37
|
+
scriptPanelVisible: boolean;
|
|
38
|
+
scriptDeleteConfirmId: string | null;
|
|
39
|
+
|
|
40
|
+
// Actions
|
|
41
|
+
createScript: (name: string, code?: string) => string;
|
|
42
|
+
saveActiveScript: () => void;
|
|
43
|
+
deleteScript: (id: string) => void;
|
|
44
|
+
renameScript: (id: string, name: string) => void;
|
|
45
|
+
setActiveScriptId: (id: string | null) => void;
|
|
46
|
+
setScriptEditorContent: (content: string) => void;
|
|
47
|
+
setScriptExecutionState: (state: ScriptExecutionState) => void;
|
|
48
|
+
setScriptResult: (result: ScriptResult | null) => void;
|
|
49
|
+
setScriptError: (error: string | null) => void;
|
|
50
|
+
setScriptPanelVisible: (visible: boolean) => void;
|
|
51
|
+
toggleScriptPanel: () => void;
|
|
52
|
+
setScriptDeleteConfirmId: (id: string | null) => void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const DEFAULT_CODE = `// Write your BIM script here
|
|
56
|
+
// The 'bim' object provides access to the SDK
|
|
57
|
+
const models = bim.model.list()
|
|
58
|
+
console.log('Loaded models:', models.length)
|
|
59
|
+
|
|
60
|
+
// Query all entities
|
|
61
|
+
const all = bim.query.all()
|
|
62
|
+
console.log('Total entities:', all.length)
|
|
63
|
+
|
|
64
|
+
// Count by type
|
|
65
|
+
const counts = {}
|
|
66
|
+
for (const e of all) {
|
|
67
|
+
counts[e.type] = (counts[e.type] || 0) + 1
|
|
68
|
+
}
|
|
69
|
+
for (const [type, count] of Object.entries(counts).sort((a, b) => b[1] - a[1])) {
|
|
70
|
+
console.log(' ' + type + ': ' + count)
|
|
71
|
+
}
|
|
72
|
+
`;
|
|
73
|
+
|
|
74
|
+
export const createScriptSlice: StateCreator<ScriptSlice, [], [], ScriptSlice> = (set, get) => ({
|
|
75
|
+
// Initial state
|
|
76
|
+
savedScripts: loadSavedScripts(),
|
|
77
|
+
activeScriptId: null,
|
|
78
|
+
scriptEditorContent: DEFAULT_CODE,
|
|
79
|
+
scriptEditorDirty: false,
|
|
80
|
+
scriptExecutionState: 'idle',
|
|
81
|
+
scriptLastResult: null,
|
|
82
|
+
scriptLastError: null,
|
|
83
|
+
scriptPanelVisible: false,
|
|
84
|
+
scriptDeleteConfirmId: null,
|
|
85
|
+
|
|
86
|
+
// Actions
|
|
87
|
+
createScript: (name, code) => {
|
|
88
|
+
const { savedScripts } = get();
|
|
89
|
+
if (!canCreateScript(savedScripts.length)) {
|
|
90
|
+
console.warn('[Scripts] Maximum script limit reached');
|
|
91
|
+
return '';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const validName = validateScriptName(name) ?? 'Untitled Script';
|
|
95
|
+
const scriptCode = code ?? DEFAULT_CODE;
|
|
96
|
+
if (!isScriptWithinSizeLimit(scriptCode)) {
|
|
97
|
+
console.warn('[Scripts] Script code exceeds maximum size limit');
|
|
98
|
+
return '';
|
|
99
|
+
}
|
|
100
|
+
const id = crypto.randomUUID();
|
|
101
|
+
const now = Date.now();
|
|
102
|
+
const script: SavedScript = {
|
|
103
|
+
id,
|
|
104
|
+
name: validName,
|
|
105
|
+
code: scriptCode,
|
|
106
|
+
createdAt: now,
|
|
107
|
+
updatedAt: now,
|
|
108
|
+
version: 1,
|
|
109
|
+
};
|
|
110
|
+
const updated = [...savedScripts, script];
|
|
111
|
+
set({
|
|
112
|
+
savedScripts: updated,
|
|
113
|
+
activeScriptId: id,
|
|
114
|
+
scriptEditorContent: script.code,
|
|
115
|
+
scriptEditorDirty: false,
|
|
116
|
+
});
|
|
117
|
+
const result = saveScripts(updated);
|
|
118
|
+
if (!result.ok) {
|
|
119
|
+
console.warn('[Scripts] Save failed:', result.message);
|
|
120
|
+
}
|
|
121
|
+
return id;
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
saveActiveScript: () => {
|
|
125
|
+
const { activeScriptId, scriptEditorContent, savedScripts } = get();
|
|
126
|
+
if (!activeScriptId) return;
|
|
127
|
+
const updated = savedScripts.map((s) =>
|
|
128
|
+
s.id === activeScriptId
|
|
129
|
+
? { ...s, code: scriptEditorContent, updatedAt: Date.now() }
|
|
130
|
+
: s,
|
|
131
|
+
);
|
|
132
|
+
set({ savedScripts: updated, scriptEditorDirty: false });
|
|
133
|
+
const result = saveScripts(updated);
|
|
134
|
+
if (!result.ok) {
|
|
135
|
+
console.warn('[Scripts] Save failed:', result.message);
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
deleteScript: (id) => {
|
|
140
|
+
const updated = get().savedScripts.filter((s) => s.id !== id);
|
|
141
|
+
const activeScriptId = get().activeScriptId === id ? null : get().activeScriptId;
|
|
142
|
+
const scriptEditorContent = activeScriptId === null ? DEFAULT_CODE : get().scriptEditorContent;
|
|
143
|
+
set({
|
|
144
|
+
savedScripts: updated,
|
|
145
|
+
activeScriptId,
|
|
146
|
+
scriptEditorContent,
|
|
147
|
+
scriptEditorDirty: false,
|
|
148
|
+
scriptDeleteConfirmId: null,
|
|
149
|
+
});
|
|
150
|
+
saveScripts(updated);
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
renameScript: (id, name) => {
|
|
154
|
+
const validName = validateScriptName(name);
|
|
155
|
+
if (!validName) return;
|
|
156
|
+
const updated = get().savedScripts.map((s) =>
|
|
157
|
+
s.id === id ? { ...s, name: validName, updatedAt: Date.now() } : s,
|
|
158
|
+
);
|
|
159
|
+
set({ savedScripts: updated });
|
|
160
|
+
saveScripts(updated);
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
setActiveScriptId: (activeScriptId) => {
|
|
164
|
+
// Save current before switching
|
|
165
|
+
const { activeScriptId: current, scriptEditorDirty } = get();
|
|
166
|
+
if (current && scriptEditorDirty) {
|
|
167
|
+
get().saveActiveScript();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (activeScriptId) {
|
|
171
|
+
const script = get().savedScripts.find((s) => s.id === activeScriptId);
|
|
172
|
+
if (script) {
|
|
173
|
+
set({
|
|
174
|
+
activeScriptId,
|
|
175
|
+
scriptEditorContent: script.code,
|
|
176
|
+
scriptEditorDirty: false,
|
|
177
|
+
scriptLastResult: null,
|
|
178
|
+
scriptLastError: null,
|
|
179
|
+
scriptExecutionState: 'idle',
|
|
180
|
+
});
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
set({
|
|
185
|
+
activeScriptId: null,
|
|
186
|
+
scriptEditorContent: DEFAULT_CODE,
|
|
187
|
+
scriptEditorDirty: false,
|
|
188
|
+
scriptLastResult: null,
|
|
189
|
+
scriptLastError: null,
|
|
190
|
+
scriptExecutionState: 'idle',
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
setScriptEditorContent: (scriptEditorContent) => {
|
|
195
|
+
set({ scriptEditorContent, scriptEditorDirty: true });
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
setScriptExecutionState: (scriptExecutionState) => set({ scriptExecutionState }),
|
|
199
|
+
|
|
200
|
+
setScriptResult: (scriptLastResult) =>
|
|
201
|
+
set({ scriptLastResult, scriptLastError: null, scriptExecutionState: 'success' }),
|
|
202
|
+
|
|
203
|
+
// Error and execution state are set independently — clearing an error
|
|
204
|
+
// does NOT change execution state unless explicitly transitioned
|
|
205
|
+
setScriptError: (scriptLastError) => {
|
|
206
|
+
if (scriptLastError) {
|
|
207
|
+
set({ scriptLastError, scriptExecutionState: 'error' });
|
|
208
|
+
} else {
|
|
209
|
+
set({ scriptLastError: null });
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
setScriptPanelVisible: (scriptPanelVisible) => set({ scriptPanelVisible }),
|
|
214
|
+
|
|
215
|
+
toggleScriptPanel: () => set((state) => ({ scriptPanelVisible: !state.scriptPanelVisible })),
|
|
216
|
+
|
|
217
|
+
setScriptDeleteConfirmId: (scriptDeleteConfirmId) => set({ scriptDeleteConfirmId }),
|
|
218
|
+
});
|
|
@@ -44,12 +44,14 @@ export const createUISlice: StateCreator<UISlice, [], [], UISlice> = (set, get)
|
|
|
44
44
|
|
|
45
45
|
setTheme: (theme) => {
|
|
46
46
|
document.documentElement.classList.toggle('dark', theme === 'dark');
|
|
47
|
+
localStorage.setItem('ifc-lite-theme', theme);
|
|
47
48
|
set({ theme });
|
|
48
49
|
},
|
|
49
50
|
|
|
50
51
|
toggleTheme: () => {
|
|
51
52
|
const newTheme = get().theme === 'dark' ? 'light' : 'dark';
|
|
52
53
|
document.documentElement.classList.toggle('dark', newTheme === 'dark');
|
|
54
|
+
localStorage.setItem('ifc-lite-theme', newTheme);
|
|
53
55
|
set({ theme: newTheme });
|
|
54
56
|
},
|
|
55
57
|
|
package/src/store.ts
CHANGED
|
@@ -45,3 +45,6 @@ export type {
|
|
|
45
45
|
|
|
46
46
|
// Re-export utility functions for multi-model federation
|
|
47
47
|
export { entityRefToString, stringToEntityRef, entityRefEquals, isIfcxDataStore } from './store/types.js';
|
|
48
|
+
|
|
49
|
+
// Re-export single source of truth for globalId → EntityRef resolution
|
|
50
|
+
export { resolveEntityRef } from './store/resolveEntityRef.js';
|
package/tsconfig.json
CHANGED
|
@@ -13,8 +13,11 @@
|
|
|
13
13
|
"@ifc-lite/spatial": ["../../packages/spatial/src"],
|
|
14
14
|
"@ifc-lite/data": ["../../packages/data/src"],
|
|
15
15
|
"@ifc-lite/export": ["../../packages/export/src"],
|
|
16
|
-
"@ifc-lite/cache": ["../../packages/cache/src"]
|
|
16
|
+
"@ifc-lite/cache": ["../../packages/cache/src"],
|
|
17
|
+
"@ifc-lite/sdk": ["../../packages/sdk/src"],
|
|
18
|
+
"@ifc-lite/lens": ["../../packages/lens/src"]
|
|
17
19
|
}
|
|
18
20
|
},
|
|
19
|
-
"include": ["src/**/*", "../../packages/renderer/src/webgpu-types.d.ts"]
|
|
21
|
+
"include": ["src/**/*", "../../packages/renderer/src/webgpu-types.d.ts"],
|
|
22
|
+
"exclude": ["src/lib/scripts/templates"]
|
|
20
23
|
}
|
package/vite.config.ts
CHANGED
|
@@ -182,6 +182,14 @@ export default defineConfig({
|
|
|
182
182
|
'@ifc-lite/cache': path.resolve(__dirname, '../../packages/cache/src'),
|
|
183
183
|
'@ifc-lite/ifcx': path.resolve(__dirname, '../../packages/ifcx/src'),
|
|
184
184
|
'@ifc-lite/wasm': path.resolve(__dirname, '../../packages/wasm/pkg/ifc-lite.js'),
|
|
185
|
+
'@ifc-lite/sdk': path.resolve(__dirname, '../../packages/sdk/src'),
|
|
186
|
+
'@ifc-lite/lens': path.resolve(__dirname, '../../packages/lens/src'),
|
|
187
|
+
'@ifc-lite/mutations': path.resolve(__dirname, '../../packages/mutations/src'),
|
|
188
|
+
'@ifc-lite/bcf': path.resolve(__dirname, '../../packages/bcf/src'),
|
|
189
|
+
'@ifc-lite/drawing-2d': path.resolve(__dirname, '../../packages/drawing-2d/src'),
|
|
190
|
+
'@ifc-lite/encoding': path.resolve(__dirname, '../../packages/encoding/src'),
|
|
191
|
+
'@ifc-lite/ids': path.resolve(__dirname, '../../packages/ids/src'),
|
|
192
|
+
'@ifc-lite/lists': path.resolve(__dirname, '../../packages/lists/src'),
|
|
185
193
|
},
|
|
186
194
|
},
|
|
187
195
|
server: {
|