@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.
Files changed (162) hide show
  1. package/.turbo/turbo-build.log +35 -42
  2. package/CHANGELOG.md +74 -0
  3. package/dist/assets/{basketViewActivator-B3CdrLsb.js → basketViewActivator-Ce38DhXd.js} +8 -8
  4. package/dist/assets/{bcf-QeHK_Aud.js → bcf-Cv_O3JfD.js} +56 -56
  5. package/dist/assets/{decode-worker-CgM1iNSK.js → decode-worker-Cjign7Zh.js} +1 -1
  6. package/dist/assets/{deflate-B-d0SYQM.js → deflate-HbyMq59o.js} +1 -1
  7. package/dist/assets/drawing-2d-DW98umlt.js +257 -0
  8. package/dist/assets/e57-source-2wI9jkCA.js +1 -0
  9. package/dist/assets/{exporters-B4LbZFeT.js → exporters-BuD3XRzB.js} +1309 -1153
  10. package/dist/assets/geometry.worker-TH3fCCoY.js +1 -0
  11. package/dist/assets/{geotiff-CrVtDRFq.js → geotiff-B2HA8Bwm.js} +10 -10
  12. package/dist/assets/{ids-DjsGFN10.js → ids-DYUFMd5f.js} +952 -945
  13. package/dist/assets/{ifc-lite_bg-DsYUIHm3.wasm → ifc-lite_bg-BEA5DLmg.wasm} +0 -0
  14. package/dist/assets/index-E9wB0zWt.css +1 -0
  15. package/dist/assets/{index-COYokSKc.js → index-n5O1QJMM.js} +37877 -38126
  16. package/dist/assets/{index.es-CY202jA3.js → index.es-BKVIpZgL.js} +9 -9
  17. package/dist/assets/{jpeg-D4wOkf5h.js → jpeg-C7hjKjPX.js} +1 -1
  18. package/dist/assets/{jspdf.es.min-DIGb9BHN.js → jspdf.es.min-oWlFc42Y.js} +4 -4
  19. package/dist/assets/lens-C4p1kQ0p.js +1 -0
  20. package/dist/assets/{lerc-DmW0_tgf.js → lerc-BfIOGhQz.js} +1 -1
  21. package/dist/assets/{lzw-oWetY-d6.js → lzw-B0jRuuW5.js} +1 -1
  22. package/dist/assets/{native-bridge-BX8_tHXE.js → native-bridge-DpB-dtEn.js} +6 -3
  23. package/dist/assets/{packbits-F8Nkp4NY.js → packbits-DVvBTC39.js} +1 -1
  24. package/dist/assets/parser.worker-BDsWQ6rc.js +182 -0
  25. package/dist/assets/{pdf-Dsh3HPZB.js → pdf-dVIqI5ac.js} +10 -10
  26. package/dist/assets/raw-C0ZJYGmN.js +1 -0
  27. package/dist/assets/{sandbox-BAC3a-eN.js → sandbox-qpJlrNN0.js} +2962 -2554
  28. package/dist/assets/server-client-DVZ2huNS.js +719 -0
  29. package/dist/assets/{webimage-BLV1dgmd.js → webimage-B394g0Tw.js} +1 -1
  30. package/dist/assets/{xlsx-Bc2HTrjC.js → xlsx-D-oHO76J.js} +8 -8
  31. package/dist/assets/{zstd-C_1HxVrA.js → zstd-Bf38MwV2.js} +1 -1
  32. package/dist/index.html +9 -9
  33. package/package.json +24 -23
  34. package/src/App.tsx +1 -3
  35. package/src/components/mcp/playground-dispatcher.ts +3 -0
  36. package/src/components/mcp/playground-files.ts +33 -1
  37. package/src/components/viewer/BCFPanel.tsx +1 -16
  38. package/src/components/viewer/ChatPanel.tsx +11 -46
  39. package/src/components/viewer/CommandPalette.tsx +6 -1
  40. package/src/components/viewer/ComparePanel.tsx +420 -0
  41. package/src/components/viewer/HierarchyPanel.tsx +48 -183
  42. package/src/components/viewer/IDSPanel.tsx +1 -26
  43. package/src/components/viewer/MainToolbar.tsx +94 -187
  44. package/src/components/viewer/MobileToolbar.tsx +1 -9
  45. package/src/components/viewer/PropertiesPanel.tsx +98 -127
  46. package/src/components/viewer/ScriptPanel.tsx +8 -34
  47. package/src/components/viewer/Section2DPanel.tsx +32 -1
  48. package/src/components/viewer/ViewerLayout.tsx +5 -2
  49. package/src/components/viewer/Viewport.tsx +3 -0
  50. package/src/components/viewer/ViewportContainer.tsx +24 -42
  51. package/src/components/viewer/ViewportOverlays.tsx +1 -4
  52. package/src/components/viewer/hierarchy/HierarchyNode.tsx +3 -3
  53. package/src/components/viewer/hierarchy/ifc-icons.ts +9 -0
  54. package/src/components/viewer/hierarchy/treeDataBuilder.ts +87 -0
  55. package/src/components/viewer/hierarchy/types.ts +1 -0
  56. package/src/components/viewer/hierarchy/useHierarchyTree.ts +6 -2
  57. package/src/components/viewer/properties/MaterialTotalsPanel.tsx +283 -0
  58. package/src/components/viewer/useGeometryStreaming.ts +0 -2
  59. package/src/hooks/federationLoadGate.test.ts +12 -2
  60. package/src/hooks/federationLoadGate.ts +9 -2
  61. package/src/hooks/ingest/federationAlign.ts +488 -0
  62. package/src/hooks/ingest/viewerModelIngest.ts +3 -212
  63. package/src/hooks/useCompare.ts +0 -0
  64. package/src/hooks/useCompareOverlay.ts +119 -0
  65. package/src/hooks/useDrawingGeneration.ts +234 -14
  66. package/src/hooks/useIfc.ts +1 -1
  67. package/src/hooks/useIfcCache.ts +100 -24
  68. package/src/hooks/useIfcFederation.ts +42 -811
  69. package/src/hooks/useIfcLoader.ts +349 -1517
  70. package/src/hooks/useIfcServer.ts +3 -0
  71. package/src/hooks/useLens.ts +5 -1
  72. package/src/hooks/useSymbolicAnnotations.ts +70 -38
  73. package/src/lib/compare/buildFingerprints.ts +173 -0
  74. package/src/lib/compare/describeChange.ts +0 -0
  75. package/src/lib/compare/geometricData.test.ts +54 -0
  76. package/src/lib/compare/geometricData.ts +37 -0
  77. package/src/lib/compare/overlay.test.ts +99 -0
  78. package/src/lib/compare/overlay.ts +91 -0
  79. package/src/lib/geo/cesium-placement.ts +1 -1
  80. package/src/lib/geo/reproject.ts +4 -1
  81. package/src/lib/llm/script-edit-ops.ts +23 -0
  82. package/src/lib/llm/stream-client.ts +8 -1
  83. package/src/lib/search/result-export.ts +7 -1
  84. package/src/sdk/adapters/export-adapter.ts +6 -1
  85. package/src/services/cacheService.ts +9 -25
  86. package/src/services/desktop-export.ts +2 -59
  87. package/src/services/file-dialog.ts +8 -142
  88. package/src/store/constants.ts +23 -0
  89. package/src/store/globalId.ts +15 -13
  90. package/src/store/index.ts +19 -6
  91. package/src/store/slices/cesiumSlice.ts +8 -1
  92. package/src/store/slices/compareSlice.ts +96 -0
  93. package/src/store/slices/drawing2DSlice.ts +8 -0
  94. package/src/store/slices/lensSlice.ts +8 -0
  95. package/src/store/slices/visibilitySlice.ts +22 -1
  96. package/src/store/types.ts +1 -71
  97. package/src/utils/acquireFileBuffer.test.ts +12 -4
  98. package/src/utils/ifcConfig.ts +0 -12
  99. package/src/utils/loadingUtils.ts +32 -0
  100. package/src/utils/spatialHierarchy.test.ts +53 -1
  101. package/src/utils/spatialHierarchy.ts +42 -2
  102. package/src/vite-env.d.ts +2 -0
  103. package/vite.config.ts +6 -3
  104. package/DESKTOP_CONTRACT_VERSION +0 -1
  105. package/dist/assets/drawing-2d-C71b8Ugx.js +0 -257
  106. package/dist/assets/e57-source-CQHxE8n3.js +0 -1
  107. package/dist/assets/event-B0kAzHa-.js +0 -1
  108. package/dist/assets/geometry.worker-BdH-E6NB.js +0 -1
  109. package/dist/assets/index-ajK6D32J.css +0 -1
  110. package/dist/assets/lens-PYsLu_MA.js +0 -1
  111. package/dist/assets/parser.worker-D591Zu_-.js +0 -182
  112. package/dist/assets/raw-D9iw0tmc.js +0 -1
  113. package/dist/assets/server-client-Cjwnm7il.js +0 -706
  114. package/dist/assets/tauri-core-stub-D8Fa-u43.js +0 -1
  115. package/dist/assets/tauri-dialog-stub-r7Wksg7o.js +0 -1
  116. package/dist/assets/tauri-fs-stub-BdeRC7aK.js +0 -1
  117. package/src/components/viewer/DesktopEntitlementBanner.tsx +0 -74
  118. package/src/components/viewer/SettingsPage.tsx +0 -581
  119. package/src/hooks/ingest/resolveDataStoreOrAbort.test.ts +0 -61
  120. package/src/hooks/ingest/resolveDataStoreOrAbort.ts +0 -28
  121. package/src/hooks/ingest/watchedGeometryStream.test.ts +0 -78
  122. package/src/hooks/ingest/watchedGeometryStream.ts +0 -76
  123. package/src/lib/desktop/desktopEntitlementEvents.ts +0 -39
  124. package/src/lib/desktop-entitlement.ts +0 -43
  125. package/src/lib/desktop-product.ts +0 -130
  126. package/src/lib/platform.ts +0 -23
  127. package/src/services/desktop-cache.ts +0 -186
  128. package/src/services/desktop-harness.ts +0 -196
  129. package/src/services/desktop-logger.ts +0 -20
  130. package/src/services/desktop-native-metadata.ts +0 -230
  131. package/src/services/desktop-panel-actions.ts +0 -43
  132. package/src/services/desktop-preferences.ts +0 -44
  133. package/src/services/fs-cache.ts +0 -212
  134. package/src/services/tauri-core-stub.ts +0 -7
  135. package/src/services/tauri-dialog-stub.ts +0 -7
  136. package/src/services/tauri-fs-stub.ts +0 -7
  137. package/src/services/tauri-modules.d.ts +0 -50
  138. package/src/store/slices/desktopEntitlementSlice.ts +0 -86
  139. package/src/utils/desktopModelSnapshot.ts +0 -358
  140. package/src/utils/nativeSpatialDataStore.ts +0 -277
  141. package/src-tauri/Cargo.toml +0 -29
  142. package/src-tauri/build.rs +0 -7
  143. package/src-tauri/capabilities/default.json +0 -18
  144. package/src-tauri/icons/128x128.png +0 -0
  145. package/src-tauri/icons/128x128@2x.png +0 -0
  146. package/src-tauri/icons/32x32.png +0 -0
  147. package/src-tauri/icons/Square107x107Logo.png +0 -0
  148. package/src-tauri/icons/Square142x142Logo.png +0 -0
  149. package/src-tauri/icons/Square150x150Logo.png +0 -0
  150. package/src-tauri/icons/Square284x284Logo.png +0 -0
  151. package/src-tauri/icons/Square30x30Logo.png +0 -0
  152. package/src-tauri/icons/Square310x310Logo.png +0 -0
  153. package/src-tauri/icons/Square44x44Logo.png +0 -0
  154. package/src-tauri/icons/Square71x71Logo.png +0 -0
  155. package/src-tauri/icons/Square89x89Logo.png +0 -0
  156. package/src-tauri/icons/StoreLogo.png +0 -0
  157. package/src-tauri/icons/icon.icns +0 -0
  158. package/src-tauri/icons/icon.ico +0 -0
  159. package/src-tauri/icons/icon.png +0 -0
  160. package/src-tauri/src/lib.rs +0 -21
  161. package/src-tauri/src/main.rs +0 -10
  162. 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 { getPersistedTypeVisibility, TYPE_VISIBILITY_STORAGE_KEYS, TYPE_VISIBILITY_SEMANTIC_DEFAULTS } from '../constants.js';
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);
@@ -298,75 +298,7 @@ export type MetadataLoadState =
298
298
  | 'complete'
299
299
  | 'error';
300
300
 
301
- export interface NativeMetadataProperty {
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 addModel path SHOULD still use acquireFileBuffer
196
- // (STEP/IFC binary path benefits from SAB streaming).
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('acquireFileBuffer'),
203
- 'addModel (IFC/STEP path) must keep using acquireFileBuffer() for SAB streaming',
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
 
@@ -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';
@@ -1 +0,0 @@
1
- 2