@ifc-lite/viewer 1.8.0 → 1.10.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 +77 -0
- package/dist/assets/{Arrow.dom-CwcRxist.js → Arrow.dom-Bw5JMdDs.js} +1 -1
- package/dist/assets/browser-DdRf3aWl.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/ifc-lite_bg-C1-gLAHo.wasm +0 -0
- package/dist/assets/index-1ff6P0kc.js +100011 -0
- package/dist/assets/index-Bz7vHRxl.js +216 -0
- package/dist/assets/index-mvbV6NHd.css +1 -0
- package/dist/assets/module-6F3E5H7Y-tx0BadV3.js +6 -0
- package/dist/assets/{native-bridge-5LbrYh3R.js → native-bridge-C5hD5vae.js} +1 -1
- package/dist/assets/{wasm-bridge-CgpLtj1h.js → wasm-bridge-CaNKXFGM.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/MainToolbar.tsx +31 -3
- package/src/components/viewer/ScriptPanel.tsx +416 -0
- package/src/components/viewer/ViewerLayout.tsx +63 -11
- package/src/components/viewer/Viewport.tsx +58 -2
- package/src/components/viewer/hierarchy/treeDataBuilder.ts +3 -1
- package/src/components/viewer/useAnimationLoop.ts +4 -1
- package/src/components/viewer/useGeometryStreaming.ts +13 -1
- package/src/components/viewer/useRenderUpdates.ts +6 -1
- package/src/hooks/useKeyboardShortcuts.ts +1 -0
- package/src/hooks/useLens.ts +2 -1
- package/src/hooks/useSandbox.ts +113 -0
- package/src/hooks/useViewerSelectors.ts +22 -0
- package/src/index.css +6 -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 +30 -2
- package/src/store/index.ts +24 -1
- package/src/store/slices/pinboardSlice.ts +37 -41
- package/src/store/slices/scriptSlice.ts +218 -0
- package/src/store/slices/uiSlice.ts +43 -0
- package/tsconfig.json +5 -2
- package/vite.config.ts +8 -0
- package/dist/assets/ifc-lite_bg-DyIN_nBM.wasm +0 -0
- package/dist/assets/index-7WoQ-qVC.css +0 -1
- package/dist/assets/index-BSANf7-H.js +0 -78795
|
@@ -0,0 +1,144 @@
|
|
|
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
|
+
* LocalBackend — implements BimBackend via per-namespace adapters.
|
|
7
|
+
*
|
|
8
|
+
* This is the viewer's internal backend: zero serialization overhead.
|
|
9
|
+
* Each namespace is a typed property with named methods.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
BimBackend,
|
|
14
|
+
BimEventType,
|
|
15
|
+
ModelBackendMethods,
|
|
16
|
+
QueryBackendMethods,
|
|
17
|
+
SelectionBackendMethods,
|
|
18
|
+
VisibilityBackendMethods,
|
|
19
|
+
ViewerBackendMethods,
|
|
20
|
+
MutateBackendMethods,
|
|
21
|
+
SpatialBackendMethods,
|
|
22
|
+
ExportBackendMethods,
|
|
23
|
+
LensBackendMethods,
|
|
24
|
+
} from '@ifc-lite/sdk';
|
|
25
|
+
import type { StoreApi } from './adapters/types.js';
|
|
26
|
+
import { LEGACY_MODEL_ID } from './adapters/model-compat.js';
|
|
27
|
+
import { createModelAdapter } from './adapters/model-adapter.js';
|
|
28
|
+
import { createQueryAdapter } from './adapters/query-adapter.js';
|
|
29
|
+
import { createSelectionAdapter } from './adapters/selection-adapter.js';
|
|
30
|
+
import { createVisibilityAdapter } from './adapters/visibility-adapter.js';
|
|
31
|
+
import { createViewerAdapter } from './adapters/viewer-adapter.js';
|
|
32
|
+
import { createMutateAdapter } from './adapters/mutate-adapter.js';
|
|
33
|
+
import { createSpatialAdapter } from './adapters/spatial-adapter.js';
|
|
34
|
+
import { createLensAdapter } from './adapters/lens-adapter.js';
|
|
35
|
+
import { createExportAdapter } from './adapters/export-adapter.js';
|
|
36
|
+
|
|
37
|
+
export class LocalBackend implements BimBackend {
|
|
38
|
+
readonly model: ModelBackendMethods;
|
|
39
|
+
readonly query: QueryBackendMethods;
|
|
40
|
+
readonly selection: SelectionBackendMethods;
|
|
41
|
+
readonly visibility: VisibilityBackendMethods;
|
|
42
|
+
readonly viewer: ViewerBackendMethods;
|
|
43
|
+
readonly mutate: MutateBackendMethods;
|
|
44
|
+
readonly spatial: SpatialBackendMethods;
|
|
45
|
+
readonly export: ExportBackendMethods;
|
|
46
|
+
readonly lens: LensBackendMethods;
|
|
47
|
+
|
|
48
|
+
private store: StoreApi;
|
|
49
|
+
|
|
50
|
+
constructor(store: StoreApi) {
|
|
51
|
+
this.store = store;
|
|
52
|
+
this.model = createModelAdapter(store);
|
|
53
|
+
this.query = createQueryAdapter(store);
|
|
54
|
+
this.selection = createSelectionAdapter(store);
|
|
55
|
+
this.visibility = createVisibilityAdapter(store);
|
|
56
|
+
this.viewer = createViewerAdapter(store);
|
|
57
|
+
this.mutate = createMutateAdapter(store);
|
|
58
|
+
this.spatial = createSpatialAdapter(store);
|
|
59
|
+
this.lens = createLensAdapter(store);
|
|
60
|
+
this.export = createExportAdapter(store);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
subscribe(event: BimEventType, handler: (data: unknown) => void): () => void {
|
|
64
|
+
switch (event) {
|
|
65
|
+
case 'selection:changed':
|
|
66
|
+
return this.store.subscribe((state, prev) => {
|
|
67
|
+
if (state.selectedEntities !== prev.selectedEntities) {
|
|
68
|
+
handler({ refs: state.selectedEntities ?? [] });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
case 'model:loaded':
|
|
73
|
+
return this.store.subscribe((state, prev) => {
|
|
74
|
+
if (state.models.size > prev.models.size) {
|
|
75
|
+
for (const [id, model] of state.models) {
|
|
76
|
+
if (!prev.models.has(id)) {
|
|
77
|
+
handler({
|
|
78
|
+
model: {
|
|
79
|
+
id: model.id,
|
|
80
|
+
name: model.name,
|
|
81
|
+
schemaVersion: model.schemaVersion,
|
|
82
|
+
entityCount: model.ifcDataStore?.entities?.count ?? 0,
|
|
83
|
+
fileSize: model.fileSize,
|
|
84
|
+
loadedAt: model.loadedAt,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (state.ifcDataStore && !prev.ifcDataStore && state.models.size === 0) {
|
|
91
|
+
handler({
|
|
92
|
+
model: {
|
|
93
|
+
id: LEGACY_MODEL_ID,
|
|
94
|
+
name: 'Model',
|
|
95
|
+
schemaVersion: state.ifcDataStore.schemaVersion ?? 'IFC4',
|
|
96
|
+
entityCount: state.ifcDataStore.entities?.count ?? 0,
|
|
97
|
+
fileSize: state.ifcDataStore.source?.byteLength ?? 0,
|
|
98
|
+
loadedAt: 0,
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
case 'model:removed':
|
|
105
|
+
return this.store.subscribe((state, prev) => {
|
|
106
|
+
if (state.models.size < prev.models.size) {
|
|
107
|
+
for (const id of prev.models.keys()) {
|
|
108
|
+
if (!state.models.has(id)) {
|
|
109
|
+
handler({ modelId: id });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
case 'visibility:changed':
|
|
116
|
+
return this.store.subscribe((state, prev) => {
|
|
117
|
+
if (
|
|
118
|
+
state.hiddenEntities !== prev.hiddenEntities ||
|
|
119
|
+
state.isolatedEntities !== prev.isolatedEntities ||
|
|
120
|
+
state.hiddenEntitiesByModel !== prev.hiddenEntitiesByModel
|
|
121
|
+
) {
|
|
122
|
+
handler({});
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
case 'mutation:changed':
|
|
127
|
+
return this.store.subscribe((state, prev) => {
|
|
128
|
+
if (state.mutationVersion !== prev.mutationVersion) {
|
|
129
|
+
handler({});
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
case 'lens:changed':
|
|
134
|
+
return this.store.subscribe((state, prev) => {
|
|
135
|
+
if (state.activeLensId !== prev.activeLensId) {
|
|
136
|
+
handler({ lensId: state.activeLensId });
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
default:
|
|
141
|
+
return () => {};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
* useBimHost — React hook that initializes the SDK and BimHost.
|
|
7
|
+
*
|
|
8
|
+
* This hook:
|
|
9
|
+
* 1. Creates a LocalBackend backed by the Zustand store
|
|
10
|
+
* 2. Creates a BimContext (the `bim` object)
|
|
11
|
+
* 3. Starts a BimHost listening on BroadcastChannel 'ifc-lite'
|
|
12
|
+
* 4. External tools (ifc-scripts, ifc-flow) can connect to control the viewer
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* function App() {
|
|
16
|
+
* const bim = useBimHost();
|
|
17
|
+
* // bim is available for internal use
|
|
18
|
+
* // External tools can connect via BroadcastChannel 'ifc-lite'
|
|
19
|
+
* }
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { useRef, useEffect, useMemo } from 'react';
|
|
23
|
+
import { createBimContext, BimHost, type BimContext } from '@ifc-lite/sdk';
|
|
24
|
+
import { useViewerStore } from '../store/index.js';
|
|
25
|
+
import { LocalBackend } from './local-backend.js';
|
|
26
|
+
|
|
27
|
+
const BROADCAST_CHANNEL = 'ifc-lite';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Initialize the SDK with a local backend and start the BimHost.
|
|
31
|
+
* Returns the BimContext for internal use.
|
|
32
|
+
*/
|
|
33
|
+
export function useBimHost(): BimContext {
|
|
34
|
+
const hostRef = useRef<BimHost | null>(null);
|
|
35
|
+
const backendRef = useRef<LocalBackend | null>(null);
|
|
36
|
+
|
|
37
|
+
// Create local backend and BimContext once — single shared backend
|
|
38
|
+
const bim = useMemo(() => {
|
|
39
|
+
const storeApi = {
|
|
40
|
+
getState: useViewerStore.getState,
|
|
41
|
+
subscribe: useViewerStore.subscribe,
|
|
42
|
+
};
|
|
43
|
+
const backend = new LocalBackend(storeApi);
|
|
44
|
+
backendRef.current = backend;
|
|
45
|
+
return createBimContext({ backend });
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
// Start BimHost for external connections — reuse the same backend
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const backend = backendRef.current;
|
|
51
|
+
if (!backend) return;
|
|
52
|
+
const host = new BimHost(backend);
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
host.listenBroadcast(BROADCAST_CHANNEL);
|
|
56
|
+
} catch {
|
|
57
|
+
// BroadcastChannel not available (e.g., in some test environments)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
hostRef.current = host;
|
|
61
|
+
|
|
62
|
+
return () => {
|
|
63
|
+
host.close();
|
|
64
|
+
hostRef.current = null;
|
|
65
|
+
};
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
return bim;
|
|
69
|
+
}
|
package/src/store/constants.ts
CHANGED
|
@@ -51,13 +51,41 @@ export const EDGE_LOCK_DEFAULTS = {
|
|
|
51
51
|
// UI Defaults
|
|
52
52
|
// ============================================================================
|
|
53
53
|
|
|
54
|
+
/** Resolve the initial theme: localStorage override > system preference > dark fallback */
|
|
55
|
+
function getInitialTheme(): 'light' | 'dark' {
|
|
56
|
+
if (typeof window === 'undefined') return 'dark';
|
|
57
|
+
const saved = localStorage.getItem('ifc-lite-theme');
|
|
58
|
+
if (saved === 'light' || saved === 'dark') return saved;
|
|
59
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
60
|
+
}
|
|
61
|
+
|
|
54
62
|
export const UI_DEFAULTS = {
|
|
55
63
|
/** Default active tool */
|
|
56
64
|
ACTIVE_TOOL: 'select',
|
|
57
|
-
/** Default theme */
|
|
58
|
-
THEME:
|
|
65
|
+
/** Default theme – respects user's OS colour-scheme preference */
|
|
66
|
+
THEME: getInitialTheme(),
|
|
59
67
|
/** Default hover tooltips state */
|
|
60
68
|
HOVER_TOOLTIPS_ENABLED: false,
|
|
69
|
+
/** Global visual enhancement kill switch */
|
|
70
|
+
VISUAL_ENHANCEMENTS_ENABLED: true,
|
|
71
|
+
/** Edge contrast enhancement default */
|
|
72
|
+
EDGE_CONTRAST_ENABLED: true,
|
|
73
|
+
/** Edge contrast intensity */
|
|
74
|
+
EDGE_CONTRAST_INTENSITY: 1.2,
|
|
75
|
+
/** Contact shading quality preset */
|
|
76
|
+
CONTACT_SHADING_QUALITY: 'low' as const,
|
|
77
|
+
/** Contact shading intensity */
|
|
78
|
+
CONTACT_SHADING_INTENSITY: 0.35,
|
|
79
|
+
/** Contact shading radius in pixels */
|
|
80
|
+
CONTACT_SHADING_RADIUS: 1.5,
|
|
81
|
+
/** Separation-line overlay default */
|
|
82
|
+
SEPARATION_LINES_ENABLED: true,
|
|
83
|
+
/** Separation-line quality preset */
|
|
84
|
+
SEPARATION_LINES_QUALITY: 'low' as const,
|
|
85
|
+
/** Separation-line intensity */
|
|
86
|
+
SEPARATION_LINES_INTENSITY: 0.38,
|
|
87
|
+
/** Separation-line radius in pixels */
|
|
88
|
+
SEPARATION_LINES_RADIUS: 1.0,
|
|
61
89
|
} as const;
|
|
62
90
|
|
|
63
91
|
// ============================================================================
|
package/src/store/index.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { createIdsSlice, type IDSSlice } from './slices/idsSlice.js';
|
|
|
30
30
|
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
|
+
import { createScriptSlice, type ScriptSlice } from './slices/scriptSlice.js';
|
|
33
34
|
|
|
34
35
|
// Import constants for reset function
|
|
35
36
|
import { CAMERA_DEFAULTS, SECTION_PLANE_DEFAULTS, UI_DEFAULTS, TYPE_VISIBILITY_DEFAULTS } from './constants.js';
|
|
@@ -67,6 +68,9 @@ export type { PinboardSlice } from './slices/pinboardSlice.js';
|
|
|
67
68
|
// Re-export Lens types
|
|
68
69
|
export type { LensSlice, Lens, LensRule, LensCriteria } from './slices/lensSlice.js';
|
|
69
70
|
|
|
71
|
+
// Re-export Script types
|
|
72
|
+
export type { ScriptSlice } from './slices/scriptSlice.js';
|
|
73
|
+
|
|
70
74
|
// Combined store type
|
|
71
75
|
export type ViewerState = LoadingSlice &
|
|
72
76
|
SelectionSlice &
|
|
@@ -85,7 +89,8 @@ export type ViewerState = LoadingSlice &
|
|
|
85
89
|
IDSSlice &
|
|
86
90
|
ListSlice &
|
|
87
91
|
PinboardSlice &
|
|
88
|
-
LensSlice &
|
|
92
|
+
LensSlice &
|
|
93
|
+
ScriptSlice & {
|
|
89
94
|
resetViewerState: () => void;
|
|
90
95
|
};
|
|
91
96
|
|
|
@@ -112,6 +117,7 @@ export const useViewerStore = create<ViewerState>()((...args) => ({
|
|
|
112
117
|
...createListSlice(...args),
|
|
113
118
|
...createPinboardSlice(...args),
|
|
114
119
|
...createLensSlice(...args),
|
|
120
|
+
...createScriptSlice(...args),
|
|
115
121
|
|
|
116
122
|
// Reset all viewer state when loading new file
|
|
117
123
|
// Note: Does NOT clear models - use clearAllModels() for that
|
|
@@ -178,6 +184,16 @@ export const useViewerStore = create<ViewerState>()((...args) => ({
|
|
|
178
184
|
|
|
179
185
|
// UI
|
|
180
186
|
activeTool: UI_DEFAULTS.ACTIVE_TOOL,
|
|
187
|
+
visualEnhancementsEnabled: UI_DEFAULTS.VISUAL_ENHANCEMENTS_ENABLED,
|
|
188
|
+
edgeContrastEnabled: UI_DEFAULTS.EDGE_CONTRAST_ENABLED,
|
|
189
|
+
edgeContrastIntensity: UI_DEFAULTS.EDGE_CONTRAST_INTENSITY,
|
|
190
|
+
contactShadingQuality: UI_DEFAULTS.CONTACT_SHADING_QUALITY,
|
|
191
|
+
contactShadingIntensity: UI_DEFAULTS.CONTACT_SHADING_INTENSITY,
|
|
192
|
+
contactShadingRadius: UI_DEFAULTS.CONTACT_SHADING_RADIUS,
|
|
193
|
+
separationLinesEnabled: UI_DEFAULTS.SEPARATION_LINES_ENABLED,
|
|
194
|
+
separationLinesQuality: UI_DEFAULTS.SEPARATION_LINES_QUALITY,
|
|
195
|
+
separationLinesIntensity: UI_DEFAULTS.SEPARATION_LINES_INTENSITY,
|
|
196
|
+
separationLinesRadius: UI_DEFAULTS.SEPARATION_LINES_RADIUS,
|
|
181
197
|
|
|
182
198
|
// Drawing 2D
|
|
183
199
|
drawing2D: null,
|
|
@@ -251,6 +267,13 @@ export const useViewerStore = create<ViewerState>()((...args) => ({
|
|
|
251
267
|
// Pinboard - clear pinned entities on new file
|
|
252
268
|
pinboardEntities: new Set<string>(),
|
|
253
269
|
|
|
270
|
+
// Script - reset execution state but keep saved scripts and editor content
|
|
271
|
+
scriptPanelVisible: false,
|
|
272
|
+
scriptExecutionState: 'idle' as const,
|
|
273
|
+
scriptLastResult: null,
|
|
274
|
+
scriptLastError: null,
|
|
275
|
+
scriptDeleteConfirmId: null,
|
|
276
|
+
|
|
254
277
|
// Lens - deactivate but keep saved lenses
|
|
255
278
|
activeLensId: null,
|
|
256
279
|
lensPanelVisible: false,
|
|
@@ -18,11 +18,11 @@ import type { StateCreator } from 'zustand';
|
|
|
18
18
|
import type { EntityRef } from '../types.js';
|
|
19
19
|
import { entityRefToString, stringToEntityRef } from '../types.js';
|
|
20
20
|
|
|
21
|
-
/**
|
|
22
|
-
interface
|
|
23
|
-
isolatedEntities
|
|
24
|
-
hiddenEntities
|
|
25
|
-
models
|
|
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 }>;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export interface PinboardSlice {
|
|
@@ -62,29 +62,30 @@ export interface PinboardSlice {
|
|
|
62
62
|
/** Convert basket EntityRefs to global IDs using model offsets */
|
|
63
63
|
function basketToGlobalIds(
|
|
64
64
|
basketEntities: Set<string>,
|
|
65
|
-
models
|
|
65
|
+
models: Map<string, { idOffset: number }>,
|
|
66
66
|
): Set<number> {
|
|
67
67
|
const globalIds = new Set<number>();
|
|
68
68
|
for (const str of basketEntities) {
|
|
69
69
|
const ref = stringToEntityRef(str);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
globalIds.add(ref.expressId + offset);
|
|
74
|
-
} else {
|
|
75
|
-
globalIds.add(ref.expressId);
|
|
76
|
-
}
|
|
70
|
+
const model = models.get(ref.modelId);
|
|
71
|
+
const offset = model?.idOffset ?? 0;
|
|
72
|
+
globalIds.add(ref.expressId + offset);
|
|
77
73
|
}
|
|
78
74
|
return globalIds;
|
|
79
75
|
}
|
|
80
76
|
|
|
81
77
|
/** Compute a single EntityRef's global ID */
|
|
82
|
-
function refToGlobalId(ref: EntityRef, models
|
|
83
|
-
const model = models
|
|
78
|
+
function refToGlobalId(ref: EntityRef, models: Map<string, { idOffset: number }>): number {
|
|
79
|
+
const model = models.get(ref.modelId);
|
|
84
80
|
return ref.expressId + (model?.idOffset ?? 0);
|
|
85
81
|
}
|
|
86
82
|
|
|
87
|
-
export const createPinboardSlice: StateCreator<
|
|
83
|
+
export const createPinboardSlice: StateCreator<
|
|
84
|
+
PinboardSlice & PinboardCrossSliceState,
|
|
85
|
+
[],
|
|
86
|
+
[],
|
|
87
|
+
PinboardSlice
|
|
88
|
+
> = (set, get) => ({
|
|
88
89
|
// Initial state
|
|
89
90
|
pinboardEntities: new Set(),
|
|
90
91
|
|
|
@@ -95,12 +96,11 @@ export const createPinboardSlice: StateCreator<PinboardSlice, [], [], PinboardSl
|
|
|
95
96
|
for (const ref of refs) {
|
|
96
97
|
next.add(entityRefToString(ref));
|
|
97
98
|
}
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
const hiddenEntities = new Set<number>(store.hiddenEntities ?? []);
|
|
99
|
+
const isolatedEntities = basketToGlobalIds(next, state.models);
|
|
100
|
+
const hiddenEntities = new Set<number>(state.hiddenEntities);
|
|
101
101
|
// Unhide any entities being added to basket
|
|
102
102
|
for (const ref of refs) {
|
|
103
|
-
const model =
|
|
103
|
+
const model = state.models.get(ref.modelId);
|
|
104
104
|
const offset = model?.idOffset ?? 0;
|
|
105
105
|
hiddenEntities.delete(ref.expressId + offset);
|
|
106
106
|
}
|
|
@@ -117,8 +117,7 @@ export const createPinboardSlice: StateCreator<PinboardSlice, [], [], PinboardSl
|
|
|
117
117
|
if (next.size === 0) {
|
|
118
118
|
return { pinboardEntities: next, isolatedEntities: null };
|
|
119
119
|
}
|
|
120
|
-
const
|
|
121
|
-
const isolatedEntities = basketToGlobalIds(next, store.models);
|
|
120
|
+
const isolatedEntities = basketToGlobalIds(next, state.models);
|
|
122
121
|
return { pinboardEntities: next, isolatedEntities };
|
|
123
122
|
});
|
|
124
123
|
},
|
|
@@ -132,15 +131,15 @@ export const createPinboardSlice: StateCreator<PinboardSlice, [], [], PinboardSl
|
|
|
132
131
|
set({ pinboardEntities: next, isolatedEntities: null });
|
|
133
132
|
return;
|
|
134
133
|
}
|
|
135
|
-
const
|
|
136
|
-
const hiddenEntities = new Set<number>(
|
|
134
|
+
const s = get();
|
|
135
|
+
const hiddenEntities = new Set<number>(s.hiddenEntities);
|
|
137
136
|
// Unhide basket entities
|
|
138
137
|
for (const ref of refs) {
|
|
139
|
-
const model =
|
|
138
|
+
const model = s.models.get(ref.modelId);
|
|
140
139
|
const offset = model?.idOffset ?? 0;
|
|
141
140
|
hiddenEntities.delete(ref.expressId + offset);
|
|
142
141
|
}
|
|
143
|
-
const isolatedEntities = basketToGlobalIds(next,
|
|
142
|
+
const isolatedEntities = basketToGlobalIds(next, s.models);
|
|
144
143
|
set({ pinboardEntities: next, isolatedEntities, hiddenEntities });
|
|
145
144
|
},
|
|
146
145
|
|
|
@@ -149,8 +148,7 @@ export const createPinboardSlice: StateCreator<PinboardSlice, [], [], PinboardSl
|
|
|
149
148
|
showPinboard: () => {
|
|
150
149
|
const state = get();
|
|
151
150
|
if (state.pinboardEntities.size === 0) return;
|
|
152
|
-
const
|
|
153
|
-
const isolatedEntities = basketToGlobalIds(state.pinboardEntities, store.models);
|
|
151
|
+
const isolatedEntities = basketToGlobalIds(state.pinboardEntities, state.models);
|
|
154
152
|
set({ isolatedEntities });
|
|
155
153
|
},
|
|
156
154
|
|
|
@@ -181,15 +179,15 @@ export const createPinboardSlice: StateCreator<PinboardSlice, [], [], PinboardSl
|
|
|
181
179
|
for (const ref of refs) {
|
|
182
180
|
next.add(entityRefToString(ref));
|
|
183
181
|
}
|
|
184
|
-
const
|
|
185
|
-
const hiddenEntities = new Set<number>(
|
|
182
|
+
const s = get();
|
|
183
|
+
const hiddenEntities = new Set<number>(s.hiddenEntities);
|
|
186
184
|
// Unhide basket entities
|
|
187
185
|
for (const ref of refs) {
|
|
188
|
-
const model =
|
|
186
|
+
const model = s.models.get(ref.modelId);
|
|
189
187
|
const offset = model?.idOffset ?? 0;
|
|
190
188
|
hiddenEntities.delete(ref.expressId + offset);
|
|
191
189
|
}
|
|
192
|
-
const isolatedEntities = basketToGlobalIds(next,
|
|
190
|
+
const isolatedEntities = basketToGlobalIds(next, s.models);
|
|
193
191
|
set({ pinboardEntities: next, isolatedEntities, hiddenEntities });
|
|
194
192
|
},
|
|
195
193
|
|
|
@@ -201,13 +199,12 @@ export const createPinboardSlice: StateCreator<PinboardSlice, [], [], PinboardSl
|
|
|
201
199
|
for (const ref of refs) {
|
|
202
200
|
next.add(entityRefToString(ref));
|
|
203
201
|
}
|
|
204
|
-
const
|
|
205
|
-
const hiddenEntities = new Set<number>(store.hiddenEntities ?? []);
|
|
202
|
+
const hiddenEntities = new Set<number>(state.hiddenEntities);
|
|
206
203
|
// Incrementally add new globalIds to existing isolation set instead of re-parsing all
|
|
207
|
-
const prevIsolated =
|
|
208
|
-
const isolatedEntities = prevIsolated ? new Set<number>(prevIsolated) : basketToGlobalIds(state.pinboardEntities,
|
|
204
|
+
const prevIsolated = state.isolatedEntities;
|
|
205
|
+
const isolatedEntities = prevIsolated ? new Set<number>(prevIsolated) : basketToGlobalIds(state.pinboardEntities, state.models);
|
|
209
206
|
for (const ref of refs) {
|
|
210
|
-
const gid = refToGlobalId(ref,
|
|
207
|
+
const gid = refToGlobalId(ref, state.models);
|
|
211
208
|
isolatedEntities.add(gid);
|
|
212
209
|
hiddenEntities.delete(gid);
|
|
213
210
|
}
|
|
@@ -226,18 +223,17 @@ export const createPinboardSlice: StateCreator<PinboardSlice, [], [], PinboardSl
|
|
|
226
223
|
if (next.size === 0) {
|
|
227
224
|
return { pinboardEntities: next, isolatedEntities: null };
|
|
228
225
|
}
|
|
229
|
-
const store = state as unknown as CombinedStoreAccess;
|
|
230
226
|
// Incrementally remove globalIds from existing isolation set instead of re-parsing all
|
|
231
|
-
const prevIsolated =
|
|
227
|
+
const prevIsolated = state.isolatedEntities;
|
|
232
228
|
if (prevIsolated) {
|
|
233
229
|
const isolatedEntities = new Set<number>(prevIsolated);
|
|
234
230
|
for (const ref of refs) {
|
|
235
|
-
isolatedEntities.delete(refToGlobalId(ref,
|
|
231
|
+
isolatedEntities.delete(refToGlobalId(ref, state.models));
|
|
236
232
|
}
|
|
237
233
|
return { pinboardEntities: next, isolatedEntities };
|
|
238
234
|
}
|
|
239
235
|
// Fallback: full recompute if no existing isolation set
|
|
240
|
-
const isolatedEntities = basketToGlobalIds(next,
|
|
236
|
+
const isolatedEntities = basketToGlobalIds(next, state.models);
|
|
241
237
|
return { pinboardEntities: next, isolatedEntities };
|
|
242
238
|
});
|
|
243
239
|
},
|