@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
|
@@ -98,6 +98,13 @@ export interface Drawing2DState {
|
|
|
98
98
|
* to the IfcAnnotation text feature).
|
|
99
99
|
*/
|
|
100
100
|
showIfcAnnotations: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Construction projection (issue #979): project geometry beyond the cut
|
|
103
|
+
* as reference lines — thin solid for the visible floor side, dashed for
|
|
104
|
+
* overhead elements (beams, roofs, eaves). Plan ('down') sections only.
|
|
105
|
+
* Off by default; the section view stays cut-only until enabled.
|
|
106
|
+
*/
|
|
107
|
+
showConstructionProjection: boolean;
|
|
101
108
|
};
|
|
102
109
|
/** Available graphic override presets */
|
|
103
110
|
graphicOverridePresets: GraphicOverridePreset[];
|
|
@@ -244,6 +251,7 @@ const getDefaultDisplayOptions = (): Drawing2DState['drawing2DDisplayOptions'] =
|
|
|
244
251
|
scale: 100, // 1:100 default
|
|
245
252
|
useSymbolicRepresentations: false, // Default to section cut (Body geometry)
|
|
246
253
|
showIfcAnnotations: true, // Mirror the 3D Class Visibility default
|
|
254
|
+
showConstructionProjection: false, // Optional reference projection (issue #979), off by default
|
|
247
255
|
});
|
|
248
256
|
|
|
249
257
|
const getDefaultState = (): Drawing2DState => ({
|
|
@@ -98,6 +98,11 @@ export interface LensSlice {
|
|
|
98
98
|
lensPanelVisible: boolean;
|
|
99
99
|
/** Computed: globalId → hex color for entities matched by active lens */
|
|
100
100
|
lensColorMap: Map<number, string>;
|
|
101
|
+
/** The exact RGBA overlay the active lens last pushed to the shared color
|
|
102
|
+
* channel, or null when no lens is active. Lets another channel owner
|
|
103
|
+
* (e.g. the compare overlay) hand control back to the lens on teardown
|
|
104
|
+
* instead of clearing it. */
|
|
105
|
+
lensAppliedColors: Map<number, [number, number, number, number]> | null;
|
|
101
106
|
/** Computed: globalIds to hide via lens rules */
|
|
102
107
|
lensHiddenIds: Set<number>;
|
|
103
108
|
/** Computed: ruleId → matched entity count for the active lens */
|
|
@@ -117,6 +122,7 @@ export interface LensSlice {
|
|
|
117
122
|
toggleLensPanel: () => void;
|
|
118
123
|
setLensPanelVisible: (visible: boolean) => void;
|
|
119
124
|
setLensColorMap: (map: Map<number, string>) => void;
|
|
125
|
+
setLensAppliedColors: (map: Map<number, [number, number, number, number]> | null) => void;
|
|
120
126
|
setLensHiddenIds: (ids: Set<number>) => void;
|
|
121
127
|
setLensRuleCounts: (counts: Map<string, number>) => void;
|
|
122
128
|
setLensRuleEntityIds: (ids: Map<string, number[]>) => void;
|
|
@@ -147,6 +153,7 @@ export const createLensSlice: StateCreator<LensSlice, [], [], LensSlice> = (set,
|
|
|
147
153
|
activeLensId: null,
|
|
148
154
|
lensPanelVisible: false,
|
|
149
155
|
lensColorMap: new Map(),
|
|
156
|
+
lensAppliedColors: null,
|
|
150
157
|
lensHiddenIds: new Set(),
|
|
151
158
|
lensRuleCounts: new Map(),
|
|
152
159
|
lensRuleEntityIds: new Map(),
|
|
@@ -183,6 +190,7 @@ export const createLensSlice: StateCreator<LensSlice, [], [], LensSlice> = (set,
|
|
|
183
190
|
setLensPanelVisible: (lensPanelVisible) => set({ lensPanelVisible }),
|
|
184
191
|
|
|
185
192
|
setLensColorMap: (lensColorMap) => set({ lensColorMap }),
|
|
193
|
+
setLensAppliedColors: (lensAppliedColors) => set({ lensAppliedColors }),
|
|
186
194
|
setLensHiddenIds: (lensHiddenIds) => set({ lensHiddenIds }),
|
|
187
195
|
setLensRuleCounts: (lensRuleCounts) => set({ lensRuleCounts }),
|
|
188
196
|
setLensRuleEntityIds: (lensRuleEntityIds) => set({ lensRuleEntityIds }),
|
|
@@ -11,7 +11,14 @@
|
|
|
11
11
|
|
|
12
12
|
import type { StateCreator } from 'zustand';
|
|
13
13
|
import type { TypeVisibility, EntityRef } from '../types.js';
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
getPersistedTypeVisibility,
|
|
16
|
+
TYPE_VISIBILITY_STORAGE_KEYS,
|
|
17
|
+
TYPE_VISIBILITY_SEMANTIC_DEFAULTS,
|
|
18
|
+
getPersistedTypeViewMode,
|
|
19
|
+
TYPE_VIEW_MODE_STORAGE_KEY,
|
|
20
|
+
type TypeViewMode,
|
|
21
|
+
} from '../constants.js';
|
|
15
22
|
|
|
16
23
|
export interface VisibilitySlice {
|
|
17
24
|
// State (legacy - single model)
|
|
@@ -20,6 +27,9 @@ export interface VisibilitySlice {
|
|
|
20
27
|
/** Class-level filter (from Class tab type-group clicks) — independent of isolatedEntities */
|
|
21
28
|
classFilter: { ids: Set<number>; label: string } | null;
|
|
22
29
|
typeVisibility: TypeVisibility;
|
|
30
|
+
/** 3D view mode for the Model/Types switch (#957 follow-up). 'model' shows
|
|
31
|
+
* placed occurrences (default); 'types' shows the type-library shapes. */
|
|
32
|
+
typeViewMode: TypeViewMode;
|
|
23
33
|
|
|
24
34
|
// State (multi-model)
|
|
25
35
|
/** Hidden entities per model */
|
|
@@ -46,6 +56,8 @@ export interface VisibilitySlice {
|
|
|
46
56
|
toggleTypeVisibility: (type: 'spaces' | 'openings' | 'site' | 'ifcAnnotations' | 'ifcGrid') => void;
|
|
47
57
|
/** Restore every type-visibility toggle to its semantic default (and persist). */
|
|
48
58
|
resetTypeVisibility: () => void;
|
|
59
|
+
/** Set the Model/Types 3D view mode (and persist). */
|
|
60
|
+
setTypeViewMode: (mode: TypeViewMode) => void;
|
|
49
61
|
/** Set all hidden entities at once (for BCF viewpoint application) */
|
|
50
62
|
setHiddenEntities: (ids: Set<number>) => void;
|
|
51
63
|
/** Set all isolated entities at once (for BCF viewpoint with defaultVisibility=false) */
|
|
@@ -79,6 +91,7 @@ export const createVisibilitySlice: StateCreator<VisibilitySlice, [], [], Visibi
|
|
|
79
91
|
classFilter: null,
|
|
80
92
|
// Read persisted toggles fresh so the user's choices survive reloads.
|
|
81
93
|
typeVisibility: getPersistedTypeVisibility(),
|
|
94
|
+
typeViewMode: getPersistedTypeViewMode(),
|
|
82
95
|
|
|
83
96
|
// Initial state (multi-model)
|
|
84
97
|
hiddenEntitiesByModel: new Map(),
|
|
@@ -221,6 +234,14 @@ export const createVisibilitySlice: StateCreator<VisibilitySlice, [], [], Visibi
|
|
|
221
234
|
return { typeVisibility: { ...TYPE_VISIBILITY_SEMANTIC_DEFAULTS } };
|
|
222
235
|
}),
|
|
223
236
|
|
|
237
|
+
setTypeViewMode: (mode) => set(() => {
|
|
238
|
+
if (typeof window !== 'undefined') {
|
|
239
|
+
try { localStorage.setItem(TYPE_VIEW_MODE_STORAGE_KEY, mode); }
|
|
240
|
+
catch { /* private-mode storage rejection — non-fatal */ }
|
|
241
|
+
}
|
|
242
|
+
return { typeViewMode: mode };
|
|
243
|
+
}),
|
|
244
|
+
|
|
224
245
|
// Actions (multi-model)
|
|
225
246
|
hideEntityInModel: (modelId, expressId) => set((state) => {
|
|
226
247
|
const newMap = new Map(state.hiddenEntitiesByModel);
|
package/src/store/types.ts
CHANGED
|
@@ -298,75 +298,7 @@ export type MetadataLoadState =
|
|
|
298
298
|
| 'complete'
|
|
299
299
|
| 'error';
|
|
300
300
|
|
|
301
|
-
export
|
|
302
|
-
name: string;
|
|
303
|
-
value: string | number | boolean | null;
|
|
304
|
-
type?: number;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
export interface NativeMetadataPropertySet {
|
|
308
|
-
name: string;
|
|
309
|
-
globalId?: string;
|
|
310
|
-
properties: NativeMetadataProperty[];
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
export interface NativeMetadataQuantity {
|
|
314
|
-
name: string;
|
|
315
|
-
value: number;
|
|
316
|
-
type?: number;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
export interface NativeMetadataQuantitySet {
|
|
320
|
-
name: string;
|
|
321
|
-
quantities: NativeMetadataQuantity[];
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
export interface NativeMetadataEntitySummary {
|
|
325
|
-
expressId: number;
|
|
326
|
-
type: string;
|
|
327
|
-
name: string;
|
|
328
|
-
globalId?: string | null;
|
|
329
|
-
kind: 'spatial' | 'element';
|
|
330
|
-
hasChildren: boolean;
|
|
331
|
-
elementCount?: number;
|
|
332
|
-
elevation?: number | null;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
export interface NativeMetadataSpatialNode extends NativeMetadataEntitySummary {
|
|
336
|
-
children: NativeMetadataSpatialNode[];
|
|
337
|
-
elements: NativeMetadataEntitySummary[];
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
export interface NativeMetadataSpatialInfo {
|
|
341
|
-
storeyId?: number | null;
|
|
342
|
-
storeyName?: string | null;
|
|
343
|
-
elevation?: number | null;
|
|
344
|
-
height?: number | null;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
export interface NativeMetadataEntityDetails {
|
|
348
|
-
summary: NativeMetadataEntitySummary;
|
|
349
|
-
typeSummary?: NativeMetadataEntitySummary | null;
|
|
350
|
-
spatial?: NativeMetadataSpatialInfo | null;
|
|
351
|
-
properties: NativeMetadataPropertySet[];
|
|
352
|
-
quantities: NativeMetadataQuantitySet[];
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
export interface NativeMetadataSnapshot {
|
|
356
|
-
mode: 'desktop-lazy';
|
|
357
|
-
cacheKey: string;
|
|
358
|
-
filePath: string;
|
|
359
|
-
schemaVersion: SchemaVersion;
|
|
360
|
-
entityCount: number;
|
|
361
|
-
spatialTree: NativeMetadataSpatialNode | null;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
export type ModelSourceFile = File | {
|
|
365
|
-
path: string;
|
|
366
|
-
name: string;
|
|
367
|
-
size: number;
|
|
368
|
-
modifiedMs?: number | null;
|
|
369
|
-
};
|
|
301
|
+
export type ModelSourceFile = File;
|
|
370
302
|
|
|
371
303
|
/** Complete model container for federation */
|
|
372
304
|
export interface FederatedModel {
|
|
@@ -406,8 +338,6 @@ export interface FederatedModel {
|
|
|
406
338
|
metadataLoadState?: MetadataLoadState;
|
|
407
339
|
/** True once the model is visibly interactive in the viewport. */
|
|
408
340
|
interactiveReady?: boolean;
|
|
409
|
-
/** Optional sparse desktop metadata snapshot for huge native loads. */
|
|
410
|
-
nativeMetadata?: NativeMetadataSnapshot | null;
|
|
411
341
|
/** Cache state for the current load session. */
|
|
412
342
|
cacheState?: 'none' | 'hit' | 'miss' | 'writing';
|
|
413
343
|
/** Optional load error for this model. */
|
|
@@ -192,15 +192,23 @@ describe('acquireFileBuffer', () => {
|
|
|
192
192
|
);
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
-
// Sanity check: the IFC
|
|
196
|
-
// (
|
|
195
|
+
// Sanity check: the IFC/STEP path still SAB-streams. addModel now delegates
|
|
196
|
+
// to the canonical loadFile (one load path), so the acquireFileBuffer SAB
|
|
197
|
+
// streaming lives there — assert addModel routes through loadFile, and that
|
|
198
|
+
// loadFile keeps acquireFileBuffer for the STEP/IFC binary path. (IFCX is
|
|
199
|
+
// still guarded above: its federation entry points stay on file.arrayBuffer.)
|
|
197
200
|
const addModelStart = source.indexOf('const addModel = useCallback');
|
|
198
201
|
assert.ok(addModelStart >= 0, 'expected addModel declaration');
|
|
199
202
|
const addModelEnd = source.indexOf('}, [', addModelStart);
|
|
200
203
|
const addModelBody = source.slice(addModelStart, addModelEnd);
|
|
201
204
|
assert.ok(
|
|
202
|
-
addModelBody.includes('
|
|
203
|
-
'addModel
|
|
205
|
+
addModelBody.includes('loadFile('),
|
|
206
|
+
'addModel must delegate to the canonical loadFile (one load path)',
|
|
207
|
+
);
|
|
208
|
+
const loaderSource = readFileSync(join(here, '..', 'hooks', 'useIfcLoader.ts'), 'utf8');
|
|
209
|
+
assert.ok(
|
|
210
|
+
loaderSource.includes('acquireFileBuffer'),
|
|
211
|
+
'loadFile (IFC/STEP path) must keep using acquireFileBuffer() for SAB streaming',
|
|
204
212
|
);
|
|
205
213
|
});
|
|
206
214
|
|
package/src/utils/ifcConfig.ts
CHANGED
|
@@ -46,9 +46,6 @@ export const CACHE_SIZE_THRESHOLD = 10 * 1024 * 1024;
|
|
|
46
46
|
* and including it would make the IndexedDB write prohibitively large. */
|
|
47
47
|
export const CACHE_MAX_SOURCE_SIZE = 150 * 1024 * 1024;
|
|
48
48
|
|
|
49
|
-
/** Route desktop IFCs above this threshold through the bounded-memory path. */
|
|
50
|
-
export const HUGE_NATIVE_FILE_THRESHOLD = 128 * 1024 * 1024;
|
|
51
|
-
|
|
52
49
|
/**
|
|
53
50
|
* File size at which the browser-File-API entry path streams directly into a
|
|
54
51
|
* `SharedArrayBuffer` instead of going through `await file.arrayBuffer()`.
|
|
@@ -87,15 +84,6 @@ export const THRESHOLDS = {
|
|
|
87
84
|
CACHE_MIN_MB: 10,
|
|
88
85
|
} as const;
|
|
89
86
|
|
|
90
|
-
// ============================================================================
|
|
91
|
-
// Platform Detection
|
|
92
|
-
// ============================================================================
|
|
93
|
-
|
|
94
|
-
/** Detect if running in Tauri (desktop) environment */
|
|
95
|
-
export function isTauri(): boolean {
|
|
96
|
-
return typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
87
|
// ============================================================================
|
|
100
88
|
// Dynamic Batch Configuration
|
|
101
89
|
// ============================================================================
|
|
@@ -44,3 +44,35 @@ export function buildSpatialIndexGuarded(
|
|
|
44
44
|
console.warn('[loadingUtils] Failed to build spatial index:', err);
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Build a spatial index for a specific (e.g. federated) model.
|
|
50
|
+
*
|
|
51
|
+
* Unlike {@link buildSpatialIndexGuarded}, this never touches the active-model
|
|
52
|
+
* slot: a federated model is usually not the active one, so guarding on / writing
|
|
53
|
+
* through `ifcDataStore` (`setIfcDataStore`) would either discard the index or
|
|
54
|
+
* mutate the wrong model. Instead it guards on the target model still holding the
|
|
55
|
+
* same store and publishes through `updateModel(modelId, ...)`.
|
|
56
|
+
*
|
|
57
|
+
* @param meshes - Final mesh array with correct IDs and world-space positions
|
|
58
|
+
* @param modelId - The federated model to attach the spatial index to
|
|
59
|
+
* @param dataStore - That model's IfcDataStore (mutated in place)
|
|
60
|
+
*/
|
|
61
|
+
export function buildSpatialIndexForModel(
|
|
62
|
+
meshes: MeshData[],
|
|
63
|
+
modelId: string,
|
|
64
|
+
dataStore: IfcDataStore,
|
|
65
|
+
): void {
|
|
66
|
+
if (meshes.length === 0) return;
|
|
67
|
+
|
|
68
|
+
buildSpatialIndexAsync(meshes).then(spatialIndex => {
|
|
69
|
+
const state = useViewerStore.getState();
|
|
70
|
+
const model = state.models.get(modelId);
|
|
71
|
+
// Model removed, or its store was replaced since this build started.
|
|
72
|
+
if (!model || model.ifcDataStore !== dataStore) return;
|
|
73
|
+
dataStore.spatialIndex = spatialIndex;
|
|
74
|
+
state.updateModel(modelId, { ifcDataStore: dataStore });
|
|
75
|
+
}).catch(err => {
|
|
76
|
+
console.warn('[loadingUtils] Failed to build spatial index for model:', err);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
RelationshipType,
|
|
12
12
|
StringTable,
|
|
13
13
|
} from '@ifc-lite/data';
|
|
14
|
-
import { rebuildSpatialHierarchy } from './spatialHierarchy';
|
|
14
|
+
import { rebuildSpatialHierarchy, rebuildOnDemandMaps } from './spatialHierarchy';
|
|
15
15
|
|
|
16
16
|
describe('rebuildSpatialHierarchy', () => {
|
|
17
17
|
it('preserves IFC4.3 facility-part trees during cache rebuilds', () => {
|
|
@@ -152,3 +152,55 @@ describe('rebuildSpatialHierarchy', () => {
|
|
|
152
152
|
assert.equal(hierarchy.elementToStorey.get(6), 3);
|
|
153
153
|
});
|
|
154
154
|
});
|
|
155
|
+
|
|
156
|
+
describe('rebuildOnDemandMaps', () => {
|
|
157
|
+
const makeEntityIndex = (byType: Map<string, number[]>) => ({
|
|
158
|
+
byId: { get: () => undefined, has: () => false, size: 0 },
|
|
159
|
+
byType,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('rebuilds onDemandMaterialMap from AssociatesMaterial edges (cache parity)', () => {
|
|
163
|
+
const strings = new StringTable();
|
|
164
|
+
const entities = new EntityTableBuilder(2, strings);
|
|
165
|
+
entities.add(5, 'IFCBEAM', 'b0', 'Beam', '', '', true);
|
|
166
|
+
entities.add(10, 'IFCMATERIAL', 'm0', 'Concrete', '', '');
|
|
167
|
+
|
|
168
|
+
const builder = new RelationshipGraphBuilder();
|
|
169
|
+
// material(10) -> element(5) forward, matching the columnar parser.
|
|
170
|
+
builder.addEdge(10, 5, RelationshipType.AssociatesMaterial, 100);
|
|
171
|
+
// pset(20) -> element(5), so the property map still rebuilds too.
|
|
172
|
+
builder.addEdge(20, 5, RelationshipType.DefinesByProperties, 101);
|
|
173
|
+
|
|
174
|
+
const entityIndex = makeEntityIndex(new Map<string, number[]>([
|
|
175
|
+
['IFCMATERIAL', [10]],
|
|
176
|
+
['IFCPROPERTYSET', [20]],
|
|
177
|
+
]));
|
|
178
|
+
|
|
179
|
+
const { onDemandMaterialMap, onDemandPropertyMap } = rebuildOnDemandMaps(
|
|
180
|
+
entities.build(),
|
|
181
|
+
builder.build(),
|
|
182
|
+
entityIndex,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
assert.equal(onDemandMaterialMap.size, 1);
|
|
186
|
+
assert.equal(onDemandMaterialMap.get(5), 10);
|
|
187
|
+
assert.deepEqual(onDemandPropertyMap.get(5), [20]);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('matches material definitions case-insensitively (mixed-case byType keys)', () => {
|
|
191
|
+
const strings = new StringTable();
|
|
192
|
+
const entities = new EntityTableBuilder(2, strings);
|
|
193
|
+
entities.add(5, 'IFCWALL', 'w0', 'Wall', '', '', true);
|
|
194
|
+
entities.add(40, 'IFCMATERIALLAYERSET', 'ls0', 'Buildup', '', '');
|
|
195
|
+
|
|
196
|
+
const builder = new RelationshipGraphBuilder();
|
|
197
|
+
builder.addEdge(40, 5, RelationshipType.AssociatesMaterial, 100);
|
|
198
|
+
|
|
199
|
+
const entityIndex = makeEntityIndex(new Map<string, number[]>([
|
|
200
|
+
['IfcMaterialLayerSet', [40]], // mixed-case, as some cache writers emit
|
|
201
|
+
]));
|
|
202
|
+
|
|
203
|
+
const { onDemandMaterialMap } = rebuildOnDemandMaps(entities.build(), builder.build(), entityIndex);
|
|
204
|
+
assert.equal(onDemandMaterialMap.get(5), 40);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -208,8 +208,22 @@ export interface EntityIndex {
|
|
|
208
208
|
export interface OnDemandMaps {
|
|
209
209
|
onDemandPropertyMap: Map<number, number[]>;
|
|
210
210
|
onDemandQuantityMap: Map<number, number[]>;
|
|
211
|
+
/** element/type expressId -> associated material definition expressId. */
|
|
212
|
+
onDemandMaterialMap: Map<number, number>;
|
|
211
213
|
}
|
|
212
214
|
|
|
215
|
+
/** IFC material *definition* classes that can be the RelatingMaterial of an
|
|
216
|
+
* IfcRelAssociatesMaterial — the source nodes of AssociatesMaterial edges. */
|
|
217
|
+
const MATERIAL_DEF_TYPES = new Set([
|
|
218
|
+
'IFCMATERIAL',
|
|
219
|
+
'IFCMATERIALLAYERSET',
|
|
220
|
+
'IFCMATERIALLAYERSETUSAGE',
|
|
221
|
+
'IFCMATERIALPROFILESET',
|
|
222
|
+
'IFCMATERIALPROFILESETUSAGE',
|
|
223
|
+
'IFCMATERIALCONSTITUENTSET',
|
|
224
|
+
'IFCMATERIALLIST',
|
|
225
|
+
]);
|
|
226
|
+
|
|
213
227
|
/**
|
|
214
228
|
* Rebuild on-demand property/quantity maps from relationships and entity types
|
|
215
229
|
* Uses FORWARD direction: pset -> elements (more efficient than inverse lookup)
|
|
@@ -228,6 +242,7 @@ export function rebuildOnDemandMaps(
|
|
|
228
242
|
): OnDemandMaps {
|
|
229
243
|
const onDemandPropertyMap = new Map<number, number[]>();
|
|
230
244
|
const onDemandQuantityMap = new Map<number, number[]>();
|
|
245
|
+
const onDemandMaterialMap = new Map<number, number>();
|
|
231
246
|
|
|
232
247
|
// Use entityIndex.byType if available (needed for cache loads where entity table
|
|
233
248
|
// doesn't include IfcPropertySet/IfcElementQuantity entities)
|
|
@@ -288,8 +303,33 @@ export function rebuildOnDemandMaps(
|
|
|
288
303
|
}
|
|
289
304
|
}
|
|
290
305
|
|
|
306
|
+
// Process material associations (FORWARD: material definition -> elements),
|
|
307
|
+
// mirroring the columnar parser's onDemandMaterialMap. Needed so cache-loaded
|
|
308
|
+
// models populate the Materials tab + per-material totals, which read this map
|
|
309
|
+
// (the relationship-graph fallback only covers single-element lookups, not the
|
|
310
|
+
// model-wide usage index). Requires entityIndex.byType to enumerate material
|
|
311
|
+
// definitions — the cached graph preserves AssociatesMaterial edges.
|
|
312
|
+
let materialDefCount = 0;
|
|
313
|
+
if (entityIndex?.byType) {
|
|
314
|
+
for (const [typeKey, ids] of entityIndex.byType) {
|
|
315
|
+
if (!MATERIAL_DEF_TYPES.has(typeKey.toUpperCase())) continue;
|
|
316
|
+
for (const materialId of ids) {
|
|
317
|
+
materialDefCount += 1;
|
|
318
|
+
const associated = relationships.getRelated(
|
|
319
|
+
materialId,
|
|
320
|
+
RelationshipType.AssociatesMaterial,
|
|
321
|
+
'forward'
|
|
322
|
+
);
|
|
323
|
+
for (const entityId of associated) {
|
|
324
|
+
// Last association wins, matching the columnar parser's `.set` build.
|
|
325
|
+
onDemandMaterialMap.set(entityId, materialId);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
291
331
|
console.log(
|
|
292
|
-
`[spatialHierarchy] Rebuilt on-demand maps: ${propertySets.length} psets, ${quantitySets.length} qsets -> ${onDemandPropertyMap.size} entities with properties, ${onDemandQuantityMap.size} with quantities`
|
|
332
|
+
`[spatialHierarchy] Rebuilt on-demand maps: ${propertySets.length} psets, ${quantitySets.length} qsets, ${materialDefCount} material defs -> ${onDemandPropertyMap.size} entities with properties, ${onDemandQuantityMap.size} with quantities, ${onDemandMaterialMap.size} with materials`
|
|
293
333
|
);
|
|
294
|
-
return { onDemandPropertyMap, onDemandQuantityMap };
|
|
334
|
+
return { onDemandPropertyMap, onDemandQuantityMap, onDemandMaterialMap };
|
|
295
335
|
}
|
package/src/vite-env.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ interface ImportMetaEnv {
|
|
|
17
17
|
readonly VITE_LLM_IMAGE_MODELS?: string;
|
|
18
18
|
/** Comma-separated model IDs that support file attachment context */
|
|
19
19
|
readonly VITE_LLM_FILE_ATTACHMENT_MODELS?: string;
|
|
20
|
+
/** Build-time default Cesium ion access token */
|
|
21
|
+
readonly VITE_CESIUM_ION_TOKEN?: string;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
interface ImportMeta {
|
package/vite.config.ts
CHANGED
|
@@ -267,9 +267,6 @@ export default defineConfig({
|
|
|
267
267
|
'@ifc-lite/encoding': path.resolve(__dirname, '../../packages/encoding/src'),
|
|
268
268
|
'@ifc-lite/ids': path.resolve(__dirname, '../../packages/ids/src'),
|
|
269
269
|
'@ifc-lite/lists': path.resolve(__dirname, '../../packages/lists/src'),
|
|
270
|
-
'@tauri-apps/api/core': path.resolve(__dirname, './src/services/tauri-core-stub.ts'),
|
|
271
|
-
'@tauri-apps/plugin-dialog': path.resolve(__dirname, './src/services/tauri-dialog-stub.ts'),
|
|
272
|
-
'@tauri-apps/plugin-fs': path.resolve(__dirname, './src/services/tauri-fs-stub.ts'),
|
|
273
270
|
},
|
|
274
271
|
},
|
|
275
272
|
server: {
|
|
@@ -301,6 +298,12 @@ export default defineConfig({
|
|
|
301
298
|
target: 'esnext',
|
|
302
299
|
chunkSizeWarningLimit: 6000,
|
|
303
300
|
rollupOptions: {
|
|
301
|
+
// @ifc-lite/geometry's NativeBridge does a dynamic `import('@tauri-apps/api/event')`
|
|
302
|
+
// (under isTauri(), never reached on web). Rollup still resolves it
|
|
303
|
+
// statically, so externalize it to prevent a build failure. ifc-lite no
|
|
304
|
+
// longer ships a desktop app; downstream desktop builders supply
|
|
305
|
+
// @tauri-apps in their own host layer.
|
|
306
|
+
external: ['@tauri-apps/api/event'],
|
|
304
307
|
output: {
|
|
305
308
|
manualChunks(id) {
|
|
306
309
|
if (id.includes('/packages/sandbox/')) return 'sandbox';
|
package/DESKTOP_CONTRACT_VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
2
|