@ifc-lite/viewer 1.27.0 → 1.28.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/.turbo/turbo-build.log +35 -42
- package/CHANGELOG.md +74 -0
- package/dist/assets/{basketViewActivator-B3CdrLsb.js → basketViewActivator-Ce38DhXd.js} +8 -8
- package/dist/assets/{bcf-QeHK_Aud.js → bcf-Cv_O3JfD.js} +56 -56
- package/dist/assets/{decode-worker-CgM1iNSK.js → decode-worker-Cjign7Zh.js} +1 -1
- package/dist/assets/{deflate-B-d0SYQM.js → deflate-HbyMq59o.js} +1 -1
- package/dist/assets/drawing-2d-DW98umlt.js +257 -0
- package/dist/assets/e57-source-2wI9jkCA.js +1 -0
- package/dist/assets/{exporters-B4LbZFeT.js → exporters-BuD3XRzB.js} +1309 -1153
- package/dist/assets/geometry.worker-TH3fCCoY.js +1 -0
- package/dist/assets/{geotiff-CrVtDRFq.js → geotiff-B2HA8Bwm.js} +10 -10
- package/dist/assets/{ids-DjsGFN10.js → ids-DYUFMd5f.js} +952 -945
- package/dist/assets/{ifc-lite_bg-DsYUIHm3.wasm → ifc-lite_bg-BEA5DLmg.wasm} +0 -0
- package/dist/assets/index-E9wB0zWt.css +1 -0
- package/dist/assets/{index-COYokSKc.js → index-n5O1QJMM.js} +37877 -38126
- package/dist/assets/{index.es-CY202jA3.js → index.es-BKVIpZgL.js} +9 -9
- package/dist/assets/{jpeg-D4wOkf5h.js → jpeg-C7hjKjPX.js} +1 -1
- package/dist/assets/{jspdf.es.min-DIGb9BHN.js → jspdf.es.min-oWlFc42Y.js} +4 -4
- package/dist/assets/lens-C4p1kQ0p.js +1 -0
- package/dist/assets/{lerc-DmW0_tgf.js → lerc-BfIOGhQz.js} +1 -1
- package/dist/assets/{lzw-oWetY-d6.js → lzw-B0jRuuW5.js} +1 -1
- package/dist/assets/{native-bridge-BX8_tHXE.js → native-bridge-DpB-dtEn.js} +6 -3
- package/dist/assets/{packbits-F8Nkp4NY.js → packbits-DVvBTC39.js} +1 -1
- package/dist/assets/parser.worker-BDsWQ6rc.js +182 -0
- package/dist/assets/{pdf-Dsh3HPZB.js → pdf-dVIqI5ac.js} +10 -10
- package/dist/assets/raw-C0ZJYGmN.js +1 -0
- package/dist/assets/{sandbox-BAC3a-eN.js → sandbox-qpJlrNN0.js} +2962 -2554
- package/dist/assets/server-client-DVZ2huNS.js +719 -0
- package/dist/assets/{webimage-BLV1dgmd.js → webimage-B394g0Tw.js} +1 -1
- package/dist/assets/{xlsx-Bc2HTrjC.js → xlsx-D-oHO76J.js} +8 -8
- package/dist/assets/{zstd-C_1HxVrA.js → zstd-Bf38MwV2.js} +1 -1
- package/dist/index.html +9 -9
- package/package.json +24 -23
- package/src/App.tsx +1 -3
- package/src/components/mcp/playground-dispatcher.ts +3 -0
- package/src/components/mcp/playground-files.ts +33 -1
- package/src/components/viewer/BCFPanel.tsx +1 -16
- package/src/components/viewer/ChatPanel.tsx +11 -46
- package/src/components/viewer/CommandPalette.tsx +6 -1
- package/src/components/viewer/ComparePanel.tsx +420 -0
- package/src/components/viewer/HierarchyPanel.tsx +48 -183
- package/src/components/viewer/IDSPanel.tsx +1 -26
- package/src/components/viewer/MainToolbar.tsx +94 -187
- package/src/components/viewer/MobileToolbar.tsx +1 -9
- package/src/components/viewer/PropertiesPanel.tsx +98 -127
- package/src/components/viewer/ScriptPanel.tsx +8 -34
- package/src/components/viewer/Section2DPanel.tsx +32 -1
- package/src/components/viewer/ViewerLayout.tsx +5 -2
- package/src/components/viewer/Viewport.tsx +3 -0
- package/src/components/viewer/ViewportContainer.tsx +24 -42
- package/src/components/viewer/ViewportOverlays.tsx +1 -4
- package/src/components/viewer/hierarchy/HierarchyNode.tsx +3 -3
- package/src/components/viewer/hierarchy/ifc-icons.ts +9 -0
- package/src/components/viewer/hierarchy/treeDataBuilder.ts +87 -0
- package/src/components/viewer/hierarchy/types.ts +1 -0
- package/src/components/viewer/hierarchy/useHierarchyTree.ts +6 -2
- package/src/components/viewer/properties/MaterialTotalsPanel.tsx +283 -0
- package/src/components/viewer/useGeometryStreaming.ts +0 -2
- package/src/hooks/federationLoadGate.test.ts +12 -2
- package/src/hooks/federationLoadGate.ts +9 -2
- package/src/hooks/ingest/federationAlign.ts +488 -0
- package/src/hooks/ingest/viewerModelIngest.ts +3 -212
- package/src/hooks/useCompare.ts +0 -0
- package/src/hooks/useCompareOverlay.ts +119 -0
- package/src/hooks/useDrawingGeneration.ts +234 -14
- package/src/hooks/useIfc.ts +1 -1
- package/src/hooks/useIfcCache.ts +100 -24
- package/src/hooks/useIfcFederation.ts +42 -811
- package/src/hooks/useIfcLoader.ts +349 -1517
- package/src/hooks/useIfcServer.ts +3 -0
- package/src/hooks/useLens.ts +5 -1
- package/src/hooks/useSymbolicAnnotations.ts +70 -38
- package/src/lib/compare/buildFingerprints.ts +173 -0
- package/src/lib/compare/describeChange.ts +0 -0
- package/src/lib/compare/geometricData.test.ts +54 -0
- package/src/lib/compare/geometricData.ts +37 -0
- package/src/lib/compare/overlay.test.ts +99 -0
- package/src/lib/compare/overlay.ts +91 -0
- package/src/lib/geo/cesium-placement.ts +1 -1
- package/src/lib/geo/reproject.ts +4 -1
- package/src/lib/llm/script-edit-ops.ts +23 -0
- package/src/lib/llm/stream-client.ts +8 -1
- package/src/lib/search/result-export.ts +7 -1
- package/src/sdk/adapters/export-adapter.ts +6 -1
- package/src/services/cacheService.ts +9 -25
- package/src/services/desktop-export.ts +2 -59
- package/src/services/file-dialog.ts +8 -142
- package/src/store/constants.ts +23 -0
- package/src/store/globalId.ts +15 -13
- package/src/store/index.ts +19 -6
- package/src/store/slices/cesiumSlice.ts +8 -1
- package/src/store/slices/compareSlice.ts +96 -0
- package/src/store/slices/drawing2DSlice.ts +8 -0
- package/src/store/slices/lensSlice.ts +8 -0
- package/src/store/slices/visibilitySlice.ts +22 -1
- package/src/store/types.ts +1 -71
- package/src/utils/acquireFileBuffer.test.ts +12 -4
- package/src/utils/ifcConfig.ts +0 -12
- package/src/utils/loadingUtils.ts +32 -0
- package/src/utils/spatialHierarchy.test.ts +53 -1
- package/src/utils/spatialHierarchy.ts +42 -2
- package/src/vite-env.d.ts +2 -0
- package/vite.config.ts +6 -3
- package/DESKTOP_CONTRACT_VERSION +0 -1
- package/dist/assets/drawing-2d-C71b8Ugx.js +0 -257
- package/dist/assets/e57-source-CQHxE8n3.js +0 -1
- package/dist/assets/event-B0kAzHa-.js +0 -1
- package/dist/assets/geometry.worker-BdH-E6NB.js +0 -1
- package/dist/assets/index-ajK6D32J.css +0 -1
- package/dist/assets/lens-PYsLu_MA.js +0 -1
- package/dist/assets/parser.worker-D591Zu_-.js +0 -182
- package/dist/assets/raw-D9iw0tmc.js +0 -1
- package/dist/assets/server-client-Cjwnm7il.js +0 -706
- package/dist/assets/tauri-core-stub-D8Fa-u43.js +0 -1
- package/dist/assets/tauri-dialog-stub-r7Wksg7o.js +0 -1
- package/dist/assets/tauri-fs-stub-BdeRC7aK.js +0 -1
- package/src/components/viewer/DesktopEntitlementBanner.tsx +0 -74
- package/src/components/viewer/SettingsPage.tsx +0 -581
- package/src/hooks/ingest/resolveDataStoreOrAbort.test.ts +0 -61
- package/src/hooks/ingest/resolveDataStoreOrAbort.ts +0 -28
- package/src/hooks/ingest/watchedGeometryStream.test.ts +0 -78
- package/src/hooks/ingest/watchedGeometryStream.ts +0 -76
- package/src/lib/desktop/desktopEntitlementEvents.ts +0 -39
- package/src/lib/desktop-entitlement.ts +0 -43
- package/src/lib/desktop-product.ts +0 -130
- package/src/lib/platform.ts +0 -23
- package/src/services/desktop-cache.ts +0 -186
- package/src/services/desktop-harness.ts +0 -196
- package/src/services/desktop-logger.ts +0 -20
- package/src/services/desktop-native-metadata.ts +0 -230
- package/src/services/desktop-panel-actions.ts +0 -43
- package/src/services/desktop-preferences.ts +0 -44
- package/src/services/fs-cache.ts +0 -212
- package/src/services/tauri-core-stub.ts +0 -7
- package/src/services/tauri-dialog-stub.ts +0 -7
- package/src/services/tauri-fs-stub.ts +0 -7
- package/src/services/tauri-modules.d.ts +0 -50
- package/src/store/slices/desktopEntitlementSlice.ts +0 -86
- package/src/utils/desktopModelSnapshot.ts +0 -358
- package/src/utils/nativeSpatialDataStore.ts +0 -277
- package/src-tauri/Cargo.toml +0 -29
- package/src-tauri/build.rs +0 -7
- package/src-tauri/capabilities/default.json +0 -18
- package/src-tauri/icons/128x128.png +0 -0
- package/src-tauri/icons/128x128@2x.png +0 -0
- package/src-tauri/icons/32x32.png +0 -0
- package/src-tauri/icons/Square107x107Logo.png +0 -0
- package/src-tauri/icons/Square142x142Logo.png +0 -0
- package/src-tauri/icons/Square150x150Logo.png +0 -0
- package/src-tauri/icons/Square284x284Logo.png +0 -0
- package/src-tauri/icons/Square30x30Logo.png +0 -0
- package/src-tauri/icons/Square310x310Logo.png +0 -0
- package/src-tauri/icons/Square44x44Logo.png +0 -0
- package/src-tauri/icons/Square71x71Logo.png +0 -0
- package/src-tauri/icons/Square89x89Logo.png +0 -0
- package/src-tauri/icons/StoreLogo.png +0 -0
- package/src-tauri/icons/icon.icns +0 -0
- package/src-tauri/icons/icon.ico +0 -0
- package/src-tauri/icons/icon.png +0 -0
- package/src-tauri/src/lib.rs +0 -21
- package/src-tauri/src/main.rs +0 -10
- package/src-tauri/tauri.conf.json +0 -39
|
@@ -456,6 +456,29 @@ export function applyScriptEditOperations(params: {
|
|
|
456
456
|
return { ok: true, content, selection, revision, appliedOpIds, changes, status: 'ok' };
|
|
457
457
|
}
|
|
458
458
|
|
|
459
|
+
if (operations.length > 1 && operations.some((op) => op.type === 'replaceAll')) {
|
|
460
|
+
const diagnostic = createPatchDiagnostic(
|
|
461
|
+
'patch_semantic_error',
|
|
462
|
+
'A replaceAll edit must be the only operation in its batch; it cannot be combined with positional ops.',
|
|
463
|
+
'error',
|
|
464
|
+
{
|
|
465
|
+
failureKind: 'mixed_repair_scopes',
|
|
466
|
+
fixHint:
|
|
467
|
+
'Emit replaceAll on its own, or use only positional ops (insert/replaceRange/append) in one batch.',
|
|
468
|
+
},
|
|
469
|
+
);
|
|
470
|
+
return {
|
|
471
|
+
ok: false,
|
|
472
|
+
content: params.content,
|
|
473
|
+
selection: params.selection,
|
|
474
|
+
revision,
|
|
475
|
+
appliedOpIds: [],
|
|
476
|
+
status: 'semantic_error',
|
|
477
|
+
error: diagnostic.message,
|
|
478
|
+
diagnostic,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
459
482
|
if (params.intent === 'repair') {
|
|
460
483
|
const metadataError = validateRepairBatchMetadata(operations);
|
|
461
484
|
if (metadataError) {
|
|
@@ -139,7 +139,14 @@ export async function readSseStream(
|
|
|
139
139
|
if (!line.startsWith('data: ')) continue;
|
|
140
140
|
const data = line.slice(6);
|
|
141
141
|
if (data === '[DONE]') continue;
|
|
142
|
-
try {
|
|
142
|
+
try {
|
|
143
|
+
onEvent(data);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
// Malformed JSON payloads are expected and skipped, but a genuine
|
|
146
|
+
// callback failure (onChunk/onUsageInfo/logCacheHit/fullText) would
|
|
147
|
+
// otherwise be silently dropped — surface it for diagnosability.
|
|
148
|
+
console.debug('[sse] skipped event', err);
|
|
149
|
+
}
|
|
143
150
|
}
|
|
144
151
|
}
|
|
145
152
|
};
|
|
@@ -33,9 +33,15 @@ function cellToString(v: unknown): string {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/** RFC-4180-style escaping: quote any cell containing comma, quote, or
|
|
36
|
-
* newline; double-up embedded quotes inside the wrapped cell.
|
|
36
|
+
* newline; double-up embedded quotes inside the wrapped cell. Also
|
|
37
|
+
* neutralises spreadsheet formula triggers (CWE-1236) so user/model-
|
|
38
|
+
* controlled cell values are treated as text on open. */
|
|
37
39
|
function escapeCsvCell(raw: string): string {
|
|
38
40
|
if (raw.length === 0) return '';
|
|
41
|
+
// CWE-1236: neutralise spreadsheet formula triggers in the leading
|
|
42
|
+
// position. Prefixing first ensures the needsQuotes check below still
|
|
43
|
+
// wraps values that also contain comma/quote/newline.
|
|
44
|
+
if (/^[=+\-@\t\r]/.test(raw)) raw = `'${raw}`;
|
|
39
45
|
const needsQuotes = raw.includes(',') || raw.includes('"') || raw.includes('\n') || raw.includes('\r');
|
|
40
46
|
if (!needsQuotes) return raw;
|
|
41
47
|
return `"${raw.replace(/"/g, '""')}"`;
|
|
@@ -108,7 +108,12 @@ export function resolveVisibilityFilterSets(
|
|
|
108
108
|
* double-quotes, or newlines.
|
|
109
109
|
*/
|
|
110
110
|
function escapeCsv(value: string, sep: string): string {
|
|
111
|
-
|
|
111
|
+
// Neutralize spreadsheet formula injection (CWE-1236): a leading
|
|
112
|
+
// =, +, -, @, TAB or CR makes a cell execute as a formula in Excel/
|
|
113
|
+
// LibreOffice/Sheets. IFC values are attacker-controllable, so prefix
|
|
114
|
+
// such cells with an apostrophe.
|
|
115
|
+
if (/^[=+\-@\t\r]/.test(value)) value = `'${value}`;
|
|
116
|
+
if (value.includes(sep) || value.includes('"') || value.includes('\n') || value.includes('\r')) {
|
|
112
117
|
return `"${value.replace(/"/g, '""')}"`;
|
|
113
118
|
}
|
|
114
119
|
return value;
|
|
@@ -3,16 +3,11 @@
|
|
|
3
3
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
* Dynamically loads the appropriate cache implementation based on platform:
|
|
8
|
-
* - Tauri (desktop): Uses native filesystem via desktop-cache.ts
|
|
9
|
-
* - Web: Uses IndexedDB via ifc-cache.ts
|
|
6
|
+
* Cache service — IndexedDB-backed cache (ifc-cache.ts) for the web build.
|
|
10
7
|
*
|
|
11
8
|
* Extracted from useIfc.ts for reusability and testability
|
|
12
9
|
*/
|
|
13
10
|
|
|
14
|
-
import { isTauri } from '../utils/ifcConfig.js';
|
|
15
|
-
|
|
16
11
|
// ============================================================================
|
|
17
12
|
// Types
|
|
18
13
|
// ============================================================================
|
|
@@ -65,29 +60,18 @@ export interface ICacheService {
|
|
|
65
60
|
let cacheService: ICacheService | null = null;
|
|
66
61
|
|
|
67
62
|
/**
|
|
68
|
-
* Get the cache service
|
|
69
|
-
* Lazily loads the
|
|
63
|
+
* Get the cache service.
|
|
64
|
+
* Lazily loads the IndexedDB implementation.
|
|
70
65
|
*/
|
|
71
66
|
export async function getCacheService(): Promise<ICacheService> {
|
|
72
67
|
if (cacheService) return cacheService;
|
|
73
68
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
deleteCached: mod.deleteCached,
|
|
81
|
-
};
|
|
82
|
-
} else {
|
|
83
|
-
// Web: Use IndexedDB
|
|
84
|
-
const mod = await import('./ifc-cache.js');
|
|
85
|
-
cacheService = {
|
|
86
|
-
getCached: mod.getCached,
|
|
87
|
-
setCached: mod.setCached,
|
|
88
|
-
deleteCached: mod.deleteCached,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
69
|
+
const mod = await import('./ifc-cache.js');
|
|
70
|
+
cacheService = {
|
|
71
|
+
getCached: mod.getCached,
|
|
72
|
+
setCached: mod.setCached,
|
|
73
|
+
deleteCached: mod.deleteCached,
|
|
74
|
+
};
|
|
91
75
|
|
|
92
76
|
return cacheService;
|
|
93
77
|
}
|
|
@@ -2,34 +2,8 @@
|
|
|
2
2
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { type IfcDataStore } from '@ifc-lite/parser';
|
|
6
6
|
import { getViewerStoreApi } from '@/store';
|
|
7
|
-
import { toast } from '@/components/ui/toast';
|
|
8
|
-
import { readNativeFile } from '@/services/file-dialog';
|
|
9
|
-
|
|
10
|
-
const exportHydrationByModel = new Map<string, Promise<IfcDataStore | null>>();
|
|
11
|
-
|
|
12
|
-
function isDesktopRuntime(): boolean {
|
|
13
|
-
// `globalThis` and `Window` aren't structurally compatible per TS, so
|
|
14
|
-
// route through `unknown` first — the cast is intentional.
|
|
15
|
-
const win = globalThis as unknown as Window & { __TAURI_INTERNALS__?: { invoke?: unknown } };
|
|
16
|
-
return typeof win.__TAURI_INTERNALS__?.invoke === 'function';
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function hasFullStepSource(dataStore: IfcDataStore | null | undefined): dataStore is IfcDataStore {
|
|
20
|
-
return Boolean(dataStore?.source?.length && dataStore.entityIndex?.byId?.size);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function toExactArrayBuffer(bytes: Uint8Array): ArrayBuffer {
|
|
24
|
-
if (
|
|
25
|
-
bytes.buffer instanceof ArrayBuffer
|
|
26
|
-
&& bytes.byteOffset === 0
|
|
27
|
-
&& bytes.byteLength === bytes.buffer.byteLength
|
|
28
|
-
) {
|
|
29
|
-
return bytes.buffer;
|
|
30
|
-
}
|
|
31
|
-
return bytes.slice().buffer;
|
|
32
|
-
}
|
|
33
7
|
|
|
34
8
|
export async function ensureModelExportReady(modelId: string): Promise<IfcDataStore | null> {
|
|
35
9
|
const store = getViewerStoreApi();
|
|
@@ -44,36 +18,5 @@ export async function ensureModelExportReady(modelId: string): Promise<IfcDataSt
|
|
|
44
18
|
return null;
|
|
45
19
|
}
|
|
46
20
|
|
|
47
|
-
|
|
48
|
-
return model.ifcDataStore;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (!isDesktopRuntime() || !model.nativeMetadata?.filePath) {
|
|
52
|
-
return model.ifcDataStore;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const pending = exportHydrationByModel.get(modelId);
|
|
56
|
-
if (pending) {
|
|
57
|
-
return pending;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const hydrationPromise = (async () => {
|
|
61
|
-
toast.info(`Preparing ${model.name} for IFC export...`);
|
|
62
|
-
const bytes = await readNativeFile(model.nativeMetadata!.filePath);
|
|
63
|
-
const parser = new IfcParser();
|
|
64
|
-
const hydratedStore = await parser.parseColumnar(toExactArrayBuffer(bytes));
|
|
65
|
-
|
|
66
|
-
store.getState().updateModel(modelId, {
|
|
67
|
-
ifcDataStore: hydratedStore,
|
|
68
|
-
schemaVersion: hydratedStore.schemaVersion,
|
|
69
|
-
metadataLoadState: 'complete',
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
return hydratedStore;
|
|
73
|
-
})().finally(() => {
|
|
74
|
-
exportHydrationByModel.delete(modelId);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
exportHydrationByModel.set(modelId, hydrationPromise);
|
|
78
|
-
return hydrationPromise;
|
|
21
|
+
return model.ifcDataStore;
|
|
79
22
|
}
|
|
@@ -2,151 +2,17 @@
|
|
|
2
2
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
4
|
|
|
5
|
-
type InvokeFn = <T>(cmd: string, args?: Record<string, unknown>) => Promise<T>;
|
|
6
|
-
|
|
7
|
-
export interface NativeFileHandle {
|
|
8
|
-
path: string;
|
|
9
|
-
name: string;
|
|
10
|
-
size: number;
|
|
11
|
-
modifiedMs?: number | null;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
5
|
export interface GenericFileDialogOptions {
|
|
15
6
|
title?: string;
|
|
16
7
|
filters?: Array<{ name: string; extensions: string[] }>;
|
|
17
8
|
}
|
|
18
9
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
async function loadDialogModule(): Promise<{
|
|
29
|
-
open: (options?: {
|
|
30
|
-
multiple?: boolean;
|
|
31
|
-
directory?: boolean;
|
|
32
|
-
filters?: Array<{ name: string; extensions: string[] }>;
|
|
33
|
-
title?: string;
|
|
34
|
-
}) => Promise<string | string[] | null>;
|
|
35
|
-
} | null> {
|
|
36
|
-
try {
|
|
37
|
-
return await import('@tauri-apps/plugin-dialog');
|
|
38
|
-
} catch {
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function loadFsModule(): Promise<{
|
|
44
|
-
stat: (path: string) => Promise<{ size: number }>;
|
|
45
|
-
} | null> {
|
|
46
|
-
try {
|
|
47
|
-
return await import('@tauri-apps/plugin-fs');
|
|
48
|
-
} catch {
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async function getInvoke(): Promise<InvokeFn> {
|
|
54
|
-
const win = globalThis as unknown as { __TAURI_INTERNALS__?: { invoke: InvokeFn } };
|
|
55
|
-
if (win.__TAURI_INTERNALS__?.invoke) {
|
|
56
|
-
return win.__TAURI_INTERNALS__.invoke;
|
|
57
|
-
}
|
|
58
|
-
const moduleInvoke = await loadInvokeFromTauriModule();
|
|
59
|
-
if (moduleInvoke) {
|
|
60
|
-
return moduleInvoke;
|
|
61
|
-
}
|
|
62
|
-
throw new Error('Tauri API not available');
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export async function openIfcFileDialog(): Promise<NativeFileHandle | null> {
|
|
66
|
-
try {
|
|
67
|
-
const invoke = await getInvoke();
|
|
68
|
-
return await invoke<NativeFileHandle | null>('open_ifc_file');
|
|
69
|
-
} catch {
|
|
70
|
-
// Expected in browser builds — fall through to plugin fallback, then browser file input.
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
const dialog = await loadDialogModule();
|
|
75
|
-
const fs = await loadFsModule();
|
|
76
|
-
if (!dialog || !fs) {
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const selected = await dialog.open({
|
|
81
|
-
multiple: false,
|
|
82
|
-
directory: false,
|
|
83
|
-
title: 'Open IFC, Mesh or Point Cloud File',
|
|
84
|
-
filters: [
|
|
85
|
-
{ name: 'IFC Files', extensions: ['ifc', 'ifczip', 'ifcxml', 'ifcx'] },
|
|
86
|
-
{ name: 'Mesh Files', extensions: ['glb'] },
|
|
87
|
-
{ name: 'Point Clouds', extensions: ['las', 'laz', 'ply', 'pcd', 'e57'] },
|
|
88
|
-
{ name: 'All Files', extensions: ['*'] },
|
|
89
|
-
],
|
|
90
|
-
});
|
|
91
|
-
if (!selected || Array.isArray(selected)) {
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const metadata = await fs.stat(selected);
|
|
96
|
-
const normalizedPath = selected.toString();
|
|
97
|
-
const pathSegments = normalizedPath.split(/[\\/]/);
|
|
98
|
-
const name = pathSegments[pathSegments.length - 1] || 'unknown.ifc';
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
path: normalizedPath,
|
|
102
|
-
name,
|
|
103
|
-
size: metadata.size,
|
|
104
|
-
};
|
|
105
|
-
} catch {
|
|
106
|
-
// No Tauri plugin available — caller falls back to browser <input type="file">.
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export async function readNativeFile(path: string): Promise<Uint8Array> {
|
|
112
|
-
try {
|
|
113
|
-
const invoke = await getInvoke();
|
|
114
|
-
const bytes = await invoke<number[]>('read_native_file', { path });
|
|
115
|
-
return Uint8Array.from(bytes);
|
|
116
|
-
} catch (error) {
|
|
117
|
-
console.warn('[FileDialog] Falling back to plugin-fs read for native file:', error);
|
|
118
|
-
const fs = await import('@tauri-apps/plugin-fs');
|
|
119
|
-
return fs.readFile(path);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export async function openGenericFileDialog(options: GenericFileDialogOptions = {}): Promise<File | null> {
|
|
124
|
-
try {
|
|
125
|
-
const dialog = await loadDialogModule();
|
|
126
|
-
if (!dialog) {
|
|
127
|
-
return null;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const selected = await dialog.open({
|
|
131
|
-
multiple: false,
|
|
132
|
-
directory: false,
|
|
133
|
-
title: options.title,
|
|
134
|
-
filters: options.filters,
|
|
135
|
-
});
|
|
136
|
-
if (!selected || Array.isArray(selected)) {
|
|
137
|
-
return null;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const normalizedPath = selected.toString();
|
|
141
|
-
const bytes = await readNativeFile(normalizedPath);
|
|
142
|
-
const pathSegments = normalizedPath.split(/[\\/]/);
|
|
143
|
-
const name = pathSegments[pathSegments.length - 1] || 'document';
|
|
144
|
-
// Slice to a fresh ArrayBuffer view — TS5+ narrows `Uint8Array` to
|
|
145
|
-
// `Uint8Array<ArrayBufferLike>` which `BlobPart` doesn't accept.
|
|
146
|
-
const blobPart = new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength).slice();
|
|
147
|
-
return new File([blobPart], name, { type: 'application/octet-stream' });
|
|
148
|
-
} catch (error) {
|
|
149
|
-
console.warn('[FileDialog] Failed to open generic native file dialog:', error);
|
|
150
|
-
return null;
|
|
151
|
-
}
|
|
10
|
+
/**
|
|
11
|
+
* Browser-only build: there is no native OS file dialog, so this resolves to
|
|
12
|
+
* `null` and callers fall back to a browser `<input type="file">`. (ifc-lite no
|
|
13
|
+
* longer ships a desktop app; third parties building their own desktop shell on
|
|
14
|
+
* the published packages supply native file access in their own host layer.)
|
|
15
|
+
*/
|
|
16
|
+
export async function openGenericFileDialog(_options: GenericFileDialogOptions = {}): Promise<null> {
|
|
17
|
+
return null;
|
|
152
18
|
}
|
package/src/store/constants.ts
CHANGED
|
@@ -222,6 +222,29 @@ export function getPersistedTypeVisibility(): TypeVisibility {
|
|
|
222
222
|
};
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
/**
|
|
226
|
+
* The 3D view mode for the Model/Types switch (#957 follow-up).
|
|
227
|
+
* 'model' — show placed occurrences (the default; the building as designed).
|
|
228
|
+
* 'types' — show the type-library shapes (each IfcTypeProduct's
|
|
229
|
+
* RepresentationMap at its MappingOrigin), hiding occurrences.
|
|
230
|
+
* Orphan type geometry (a type with no occurrence, e.g. annex-E showcase files)
|
|
231
|
+
* shows in BOTH modes since it is the only geometry the file has.
|
|
232
|
+
*/
|
|
233
|
+
export type TypeViewMode = 'model' | 'types';
|
|
234
|
+
|
|
235
|
+
export const TYPE_VIEW_MODE_STORAGE_KEY = 'ifc-lite-type-view-mode';
|
|
236
|
+
export const TYPE_VIEW_MODE_DEFAULT: TypeViewMode = 'model';
|
|
237
|
+
|
|
238
|
+
/** Resolve the persisted Model/Types view mode (read fresh, like type visibility). */
|
|
239
|
+
export function getPersistedTypeViewMode(): TypeViewMode {
|
|
240
|
+
if (typeof window === 'undefined') return TYPE_VIEW_MODE_DEFAULT;
|
|
241
|
+
try {
|
|
242
|
+
return localStorage.getItem(TYPE_VIEW_MODE_STORAGE_KEY) === 'types' ? 'types' : 'model';
|
|
243
|
+
} catch {
|
|
244
|
+
return TYPE_VIEW_MODE_DEFAULT;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
225
248
|
// ============================================================================
|
|
226
249
|
// Data Defaults
|
|
227
250
|
// ============================================================================
|
package/src/store/globalId.ts
CHANGED
|
@@ -44,23 +44,17 @@ export function fromGlobalIdFromModels(
|
|
|
44
44
|
models: ReverseModelMapLike,
|
|
45
45
|
globalId: number,
|
|
46
46
|
): EntityRef | undefined {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
modelId: firstModelId,
|
|
52
|
-
expressId: globalId,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
return {
|
|
56
|
-
modelId: 'legacy',
|
|
57
|
-
expressId: globalId,
|
|
58
|
-
};
|
|
47
|
+
// No models loaded — legacy single-store fallback (expressId === globalId).
|
|
48
|
+
if (models.size === 0) {
|
|
49
|
+
return { modelId: 'legacy', expressId: globalId };
|
|
59
50
|
}
|
|
60
51
|
|
|
52
|
+
// Resolve through every model by its offset range, regardless of count.
|
|
53
|
+
// For a true single model with idOffset 0 this still yields expressId === globalId.
|
|
54
|
+
// The `>= 0` boundary matches the canonical resolveGlobalIdFromModels (modelSlice.ts).
|
|
61
55
|
for (const [modelId, model] of models.entries()) {
|
|
62
56
|
const localExpressId = globalId - model.idOffset;
|
|
63
|
-
if (localExpressId
|
|
57
|
+
if (localExpressId >= 0 && localExpressId <= model.maxExpressId) {
|
|
64
58
|
return {
|
|
65
59
|
modelId,
|
|
66
60
|
expressId: localExpressId,
|
|
@@ -68,6 +62,14 @@ export function fromGlobalIdFromModels(
|
|
|
68
62
|
}
|
|
69
63
|
}
|
|
70
64
|
|
|
65
|
+
// Single-model graceful fallback: if exactly one model and the offset
|
|
66
|
+
// range check missed (e.g. overlay-allocated id above maxExpressId),
|
|
67
|
+
// still return that model with the offset-corrected id rather than undefined.
|
|
68
|
+
if (models.size === 1) {
|
|
69
|
+
const [modelId, model] = models.entries().next().value!;
|
|
70
|
+
return { modelId, expressId: globalId - model.idOffset };
|
|
71
|
+
}
|
|
72
|
+
|
|
71
73
|
return undefined;
|
|
72
74
|
}
|
|
73
75
|
|
package/src/store/index.ts
CHANGED
|
@@ -34,10 +34,10 @@ import { createListSlice, type ListSlice } from './slices/listSlice.js';
|
|
|
34
34
|
import { createPinboardSlice, type PinboardSlice } from './slices/pinboardSlice.js';
|
|
35
35
|
import { createLensSlice, type LensSlice } from './slices/lensSlice.js';
|
|
36
36
|
import { createClashSlice, type ClashSlice } from './slices/clashSlice.js';
|
|
37
|
+
import { createCompareSlice, type CompareSlice } from './slices/compareSlice.js';
|
|
37
38
|
import { createScriptSlice, type ScriptSlice } from './slices/scriptSlice.js';
|
|
38
39
|
import { createChatSlice, type ChatSlice } from './slices/chatSlice.js';
|
|
39
40
|
import { createCesiumSlice, type CesiumSlice } from './slices/cesiumSlice.js';
|
|
40
|
-
import { createDesktopEntitlementSlice, type DesktopEntitlementSlice } from './slices/desktopEntitlementSlice.js';
|
|
41
41
|
import { createScheduleSlice, type ScheduleSlice } from './slices/scheduleSlice.js';
|
|
42
42
|
import { createPlaybackSlice, type PlaybackSlice } from './slices/playbackSlice.js';
|
|
43
43
|
import { createOverlaySlice, type OverlaySlice } from './slices/overlaySlice.js';
|
|
@@ -50,7 +50,7 @@ import { createPointCloudSlice, type PointCloudSlice, POINT_CLOUD_DEFAULTS } fro
|
|
|
50
50
|
import { invalidateVisibleBasketCache } from './basketVisibleSet.js';
|
|
51
51
|
|
|
52
52
|
// Import constants for reset function
|
|
53
|
-
import { CAMERA_DEFAULTS, SECTION_PLANE_DEFAULTS, UI_DEFAULTS, getPersistedTypeVisibility } from './constants.js';
|
|
53
|
+
import { CAMERA_DEFAULTS, SECTION_PLANE_DEFAULTS, UI_DEFAULTS, getPersistedTypeVisibility, getPersistedTypeViewMode } from './constants.js';
|
|
54
54
|
|
|
55
55
|
// Re-export types for consumers
|
|
56
56
|
export type * from './types.js';
|
|
@@ -85,13 +85,13 @@ export type { PinboardSlice } from './slices/pinboardSlice.js';
|
|
|
85
85
|
|
|
86
86
|
// Re-export Lens types
|
|
87
87
|
export type { LensSlice, Lens, LensRule, LensCriteria } from './slices/lensSlice.js';
|
|
88
|
+
export type { CompareSlice, CompareResult } from './slices/compareSlice.js';
|
|
88
89
|
|
|
89
90
|
// Re-export Script types
|
|
90
91
|
export type { ScriptSlice } from './slices/scriptSlice.js';
|
|
91
92
|
|
|
92
93
|
// Re-export Chat types
|
|
93
94
|
export type { ChatSlice } from './slices/chatSlice.js';
|
|
94
|
-
export type { DesktopEntitlementSlice } from './slices/desktopEntitlementSlice.js';
|
|
95
95
|
|
|
96
96
|
// Re-export Cesium types
|
|
97
97
|
export type { CesiumSlice, CesiumDataSource, CesiumPlacementDraft } from './slices/cesiumSlice.js';
|
|
@@ -131,10 +131,10 @@ export type ViewerState = LoadingSlice &
|
|
|
131
131
|
PinboardSlice &
|
|
132
132
|
LensSlice &
|
|
133
133
|
ClashSlice &
|
|
134
|
+
CompareSlice &
|
|
134
135
|
ScriptSlice &
|
|
135
136
|
ChatSlice &
|
|
136
137
|
CesiumSlice &
|
|
137
|
-
DesktopEntitlementSlice &
|
|
138
138
|
ScheduleSlice &
|
|
139
139
|
PlaybackSlice &
|
|
140
140
|
OverlaySlice &
|
|
@@ -155,7 +155,7 @@ export type ViewerState = LoadingSlice &
|
|
|
155
155
|
* the right panel. Routed through by the toolbar, command palette, and the
|
|
156
156
|
* BCF overlay so every entry point behaves identically.
|
|
157
157
|
*/
|
|
158
|
-
openWorkspacePanel: (panel: 'bcf' | 'ids' | 'lens' | 'clash' | 'extensions') => void;
|
|
158
|
+
openWorkspacePanel: (panel: 'bcf' | 'ids' | 'lens' | 'clash' | 'compare' | 'extensions') => void;
|
|
159
159
|
};
|
|
160
160
|
|
|
161
161
|
/**
|
|
@@ -182,10 +182,10 @@ const createViewerStore = () => create<ViewerState>()((...args) => ({
|
|
|
182
182
|
...createPinboardSlice(...args),
|
|
183
183
|
...createLensSlice(...args),
|
|
184
184
|
...createClashSlice(...args),
|
|
185
|
+
...createCompareSlice(...args),
|
|
185
186
|
...createScriptSlice(...args),
|
|
186
187
|
...createChatSlice(...args),
|
|
187
188
|
...createCesiumSlice(...args),
|
|
188
|
-
...createDesktopEntitlementSlice(...args),
|
|
189
189
|
...createScheduleSlice(...args),
|
|
190
190
|
...createPlaybackSlice(...args),
|
|
191
191
|
...createOverlaySlice(...args),
|
|
@@ -211,6 +211,8 @@ const createViewerStore = () => create<ViewerState>()((...args) => ({
|
|
|
211
211
|
// Selection (multi-model)
|
|
212
212
|
selectedEntity: null,
|
|
213
213
|
selectedEntitiesSet: new Set(),
|
|
214
|
+
selectedEntities: [],
|
|
215
|
+
selectedModelId: null,
|
|
214
216
|
|
|
215
217
|
// Visibility (legacy)
|
|
216
218
|
hiddenEntities: new Set(),
|
|
@@ -219,6 +221,7 @@ const createViewerStore = () => create<ViewerState>()((...args) => ({
|
|
|
219
221
|
// Re-read persisted toggles on every file load so a new model never
|
|
220
222
|
// reverts the user's visibility choices (e.g. "Show Annotations").
|
|
221
223
|
typeVisibility: getPersistedTypeVisibility(),
|
|
224
|
+
typeViewMode: getPersistedTypeViewMode(),
|
|
222
225
|
|
|
223
226
|
// Visibility (multi-model)
|
|
224
227
|
hiddenEntitiesByModel: new Map(),
|
|
@@ -235,6 +238,14 @@ const createViewerStore = () => create<ViewerState>()((...args) => ({
|
|
|
235
238
|
pendingColorUpdates: null,
|
|
236
239
|
pendingMeshColorUpdates: null,
|
|
237
240
|
|
|
241
|
+
// Compare (#924): drop any stale diff result — it references models by
|
|
242
|
+
// id and the loaded set is changing. Keep panel visibility + A/B/scope
|
|
243
|
+
// choices (UI prefs); the user re-runs against the new set.
|
|
244
|
+
compareResult: null,
|
|
245
|
+
compareSelectedKey: null,
|
|
246
|
+
compareRunning: false,
|
|
247
|
+
compareError: null,
|
|
248
|
+
|
|
238
249
|
// Hover/Context
|
|
239
250
|
hoverState: { entityId: null, screenX: 0, screenY: 0 },
|
|
240
251
|
contextMenu: { isOpen: false, entityId: null, screenX: 0, screenY: 0 },
|
|
@@ -316,6 +327,7 @@ const createViewerStore = () => create<ViewerState>()((...args) => ({
|
|
|
316
327
|
scale: 100,
|
|
317
328
|
useSymbolicRepresentations: false,
|
|
318
329
|
showIfcAnnotations: true,
|
|
330
|
+
showConstructionProjection: false,
|
|
319
331
|
},
|
|
320
332
|
// Graphic overrides (keep presets, reset active and custom)
|
|
321
333
|
activePresetId: 'preset-3d-colors',
|
|
@@ -460,6 +472,7 @@ const createViewerStore = () => create<ViewerState>()((...args) => ({
|
|
|
460
472
|
idsPanelVisible: panel === 'ids',
|
|
461
473
|
lensPanelVisible: panel === 'lens',
|
|
462
474
|
clashPanelVisible: panel === 'clash',
|
|
475
|
+
comparePanelVisible: panel === 'compare',
|
|
463
476
|
extensionsPanelVisible: panel === 'extensions',
|
|
464
477
|
rightPanelCollapsed: false,
|
|
465
478
|
});
|
|
@@ -108,8 +108,15 @@ const STORAGE_KEY_DATA_SOURCE = 'ifc-lite:cesium-data-source';
|
|
|
108
108
|
* Default Cesium ion token provided at build time.
|
|
109
109
|
* Set via VITE_CESIUM_ION_TOKEN in .env or CI environment.
|
|
110
110
|
* This means users never need to configure a token manually.
|
|
111
|
+
*
|
|
112
|
+
* NOTE: `import.meta.env` is undefined under the Vitest/Node test runner (the
|
|
113
|
+
* Vite define plugin doesn't run there), so this module-top-level read would
|
|
114
|
+
* crash with "Cannot read properties of undefined" — every viewer test imports
|
|
115
|
+
* the store, which imports this slice. The optional chaining on `.env` keeps the
|
|
116
|
+
* read safe in that environment. `import.meta.env` is typed via vite-env.d.ts so
|
|
117
|
+
* no `as any` cast is needed. Do NOT drop the optional chaining.
|
|
111
118
|
*/
|
|
112
|
-
const DEFAULT_ION_TOKEN: string =
|
|
119
|
+
const DEFAULT_ION_TOKEN: string = import.meta.env?.VITE_CESIUM_ION_TOKEN ?? '';
|
|
113
120
|
|
|
114
121
|
function loadFromStorage(key: string, fallback: string): string {
|
|
115
122
|
try {
|
|
@@ -0,0 +1,96 @@
|
|
|
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
|
+
* Model-comparison panel state (issue #924). Holds the panel's UI state, the
|
|
7
|
+
* A/B model selection, the data-vs-geometry scope, and the last `@ifc-lite/diff`
|
|
8
|
+
* result. The orchestration (building per-entity fingerprints from each model's
|
|
9
|
+
* `IfcDataStore` + geometry hashes, running `diffModels`, applying the 3D
|
|
10
|
+
* colour/visibility overlay) lives in the `useCompare` hook — this slice is
|
|
11
|
+
* deliberately dumb, mirroring `clashSlice` + `useClash`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { StateCreator } from 'zustand';
|
|
15
|
+
import type { DiffScope, ModelDiff } from '@ifc-lite/diff';
|
|
16
|
+
import type { CompareRef } from '@/lib/compare/buildFingerprints';
|
|
17
|
+
|
|
18
|
+
/** A completed comparison: the engine result plus the A/B context it ran on. */
|
|
19
|
+
export interface CompareResult {
|
|
20
|
+
/** Federation model id chosen as the base (version A). */
|
|
21
|
+
baseModelId: string;
|
|
22
|
+
/** Federation model id chosen as the head (version B). */
|
|
23
|
+
headModelId: string;
|
|
24
|
+
/** Display name of the base model. */
|
|
25
|
+
baseName: string;
|
|
26
|
+
/** Display name of the head model. */
|
|
27
|
+
headName: string;
|
|
28
|
+
/** The scope the diff was computed with. */
|
|
29
|
+
scope: DiffScope;
|
|
30
|
+
/** True when a compared model carries no geometry hashes (loaded outside the
|
|
31
|
+
* WASM mesh path), so geometry-scope changes can't be detected. */
|
|
32
|
+
geometryUnavailable: boolean;
|
|
33
|
+
/** The engine output — entries keyed by GlobalId, with per-entity refs. */
|
|
34
|
+
diff: ModelDiff<CompareRef>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CompareSlice {
|
|
38
|
+
comparePanelVisible: boolean;
|
|
39
|
+
/** Selected base (A) / head (B) federation model ids. */
|
|
40
|
+
compareBaseModelId: string | null;
|
|
41
|
+
compareHeadModelId: string | null;
|
|
42
|
+
/** What counts as a change: data, geometry, or both. */
|
|
43
|
+
compareScope: DiffScope;
|
|
44
|
+
/** Whether unchanged elements are drawn (ghosted) or hidden. */
|
|
45
|
+
compareShowUnchanged: boolean;
|
|
46
|
+
/** Last comparison result (null when idle / not yet run). */
|
|
47
|
+
compareResult: CompareResult | null;
|
|
48
|
+
compareRunning: boolean;
|
|
49
|
+
compareError: string | null;
|
|
50
|
+
/** GlobalId of the entry focused in the list (for highlight). */
|
|
51
|
+
compareSelectedKey: string | null;
|
|
52
|
+
|
|
53
|
+
setComparePanelVisible: (visible: boolean) => void;
|
|
54
|
+
toggleComparePanel: () => void;
|
|
55
|
+
setCompareBaseModelId: (id: string | null) => void;
|
|
56
|
+
setCompareHeadModelId: (id: string | null) => void;
|
|
57
|
+
setCompareScope: (scope: DiffScope) => void;
|
|
58
|
+
setCompareShowUnchanged: (show: boolean) => void;
|
|
59
|
+
setCompareResult: (result: CompareResult | null) => void;
|
|
60
|
+
setCompareRunning: (running: boolean) => void;
|
|
61
|
+
setCompareError: (error: string | null) => void;
|
|
62
|
+
setCompareSelectedKey: (key: string | null) => void;
|
|
63
|
+
/** Clear the run result + selection; keeps the A/B + scope choices. */
|
|
64
|
+
clearCompare: () => void;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const createCompareSlice: StateCreator<CompareSlice, [], [], CompareSlice> = (set) => ({
|
|
68
|
+
comparePanelVisible: false,
|
|
69
|
+
compareBaseModelId: null,
|
|
70
|
+
compareHeadModelId: null,
|
|
71
|
+
compareScope: 'both',
|
|
72
|
+
compareShowUnchanged: false,
|
|
73
|
+
compareResult: null,
|
|
74
|
+
compareRunning: false,
|
|
75
|
+
compareError: null,
|
|
76
|
+
compareSelectedKey: null,
|
|
77
|
+
|
|
78
|
+
setComparePanelVisible: (comparePanelVisible) => set({ comparePanelVisible }),
|
|
79
|
+
toggleComparePanel: () => set((s) => ({ comparePanelVisible: !s.comparePanelVisible })),
|
|
80
|
+
setCompareBaseModelId: (compareBaseModelId) => set({ compareBaseModelId }),
|
|
81
|
+
setCompareHeadModelId: (compareHeadModelId) => set({ compareHeadModelId }),
|
|
82
|
+
setCompareScope: (compareScope) => set({ compareScope }),
|
|
83
|
+
setCompareShowUnchanged: (compareShowUnchanged) => set({ compareShowUnchanged }),
|
|
84
|
+
setCompareResult: (compareResult) => set({ compareResult }),
|
|
85
|
+
setCompareRunning: (compareRunning) => set({ compareRunning }),
|
|
86
|
+
setCompareError: (compareError) => set({ compareError }),
|
|
87
|
+
setCompareSelectedKey: (compareSelectedKey) => set({ compareSelectedKey }),
|
|
88
|
+
|
|
89
|
+
clearCompare: () =>
|
|
90
|
+
set({
|
|
91
|
+
compareResult: null,
|
|
92
|
+
compareRunning: false,
|
|
93
|
+
compareError: null,
|
|
94
|
+
compareSelectedKey: null,
|
|
95
|
+
}),
|
|
96
|
+
});
|