@ifc-lite/viewer 1.17.6 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/.turbo/turbo-build.log +20 -15
  2. package/.turbo/turbo-typecheck.log +1 -1
  3. package/CHANGELOG.md +949 -0
  4. package/dist/assets/{basketViewActivator-86rgogji.js → basketViewActivator-RZy5c3Td.js} +1 -1
  5. package/dist/assets/decode-worker-Collf_X_.js +1320 -0
  6. package/dist/assets/{exporters-CcPS9MK5.js → exporters-BraHBeoi.js} +4194 -3025
  7. package/dist/assets/{geometry.worker-BFUYA08u.js → geometry.worker-DQEZB2rB.js} +1 -1
  8. package/dist/assets/ifc-lite_bg-4yUkDRD8.wasm +0 -0
  9. package/dist/assets/index-0XpVr_S5.css +1 -0
  10. package/dist/assets/{index-Bfms9I4A.js → index-BOi3BuUI.js} +46423 -31181
  11. package/dist/assets/index-XwKzDuw6.js +22 -0
  12. package/dist/assets/{native-bridge-DUyLCMZS.js → native-bridge-CpBeOPQa.js} +1 -1
  13. package/dist/assets/sandbox-Baez7n-t.js +9682 -0
  14. package/dist/assets/{server-client-BuZK7OST.js → server-client-BB6cMAXE.js} +1 -1
  15. package/dist/assets/{wasm-bridge-JsqEGDV8.js → wasm-bridge-CAYCUHbE.js} +1 -1
  16. package/dist/index.html +6 -6
  17. package/package.json +11 -10
  18. package/src/apache-arrow.d.ts +30 -0
  19. package/src/components/viewer/AddElementPanel.tsx +758 -0
  20. package/src/components/viewer/BulkPropertyEditor.tsx +7 -0
  21. package/src/components/viewer/ChatPanel.tsx +64 -2
  22. package/src/components/viewer/CommandPalette.tsx +56 -7
  23. package/src/components/viewer/EntityContextMenu.tsx +168 -4
  24. package/src/components/viewer/ExportChangesButton.tsx +25 -5
  25. package/src/components/viewer/ExportDialog.tsx +19 -1
  26. package/src/components/viewer/MainToolbar.tsx +73 -12
  27. package/src/components/viewer/PointCloudPanel.tsx +174 -0
  28. package/src/components/viewer/PropertiesPanel.tsx +222 -22
  29. package/src/components/viewer/SearchInline.tsx +669 -0
  30. package/src/components/viewer/SearchModal.filter.builder.tsx +766 -0
  31. package/src/components/viewer/SearchModal.filter.tsx +514 -0
  32. package/src/components/viewer/SearchModal.text.tsx +388 -0
  33. package/src/components/viewer/SearchModal.tsx +235 -0
  34. package/src/components/viewer/ToolOverlays.tsx +5 -0
  35. package/src/components/viewer/ViewerLayout.tsx +24 -4
  36. package/src/components/viewer/Viewport.tsx +29 -2
  37. package/src/components/viewer/ViewportContainer.tsx +45 -5
  38. package/src/components/viewer/ViewportOverlays.tsx +13 -2
  39. package/src/components/viewer/annotations/AnnotationDropInput.tsx +203 -0
  40. package/src/components/viewer/annotations/AnnotationLayer.tsx +287 -0
  41. package/src/components/viewer/annotations/AnnotationPin.tsx +90 -0
  42. package/src/components/viewer/annotations/AnnotationPopover.tsx +296 -0
  43. package/src/components/viewer/bcf/BCFTopicDetail.tsx +1 -1
  44. package/src/components/viewer/lists/ListPanel.tsx +14 -21
  45. package/src/components/viewer/properties/RawStepCard.tsx +332 -0
  46. package/src/components/viewer/properties/RawStepRow.tsx +261 -0
  47. package/src/components/viewer/properties/ScheduleCard.tsx +224 -0
  48. package/src/components/viewer/properties/TaskEditCard.tsx +510 -0
  49. package/src/components/viewer/properties/raw-step-format.ts +193 -0
  50. package/src/components/viewer/schedule/AnimationSettingsPopover.tsx +542 -0
  51. package/src/components/viewer/schedule/GanttDependencyArrows.tsx +89 -0
  52. package/src/components/viewer/schedule/GanttDragTooltip.tsx +48 -0
  53. package/src/components/viewer/schedule/GanttEmptyState.tsx +97 -0
  54. package/src/components/viewer/schedule/GanttPanel.tsx +295 -0
  55. package/src/components/viewer/schedule/GanttTaskBar.tsx +199 -0
  56. package/src/components/viewer/schedule/GanttTaskTree.tsx +250 -0
  57. package/src/components/viewer/schedule/GanttTimeline.tsx +305 -0
  58. package/src/components/viewer/schedule/GanttToolbar.tsx +406 -0
  59. package/src/components/viewer/schedule/GenerateAdvancedPanel.tsx +147 -0
  60. package/src/components/viewer/schedule/GenerateScheduleDialog.tsx +392 -0
  61. package/src/components/viewer/schedule/HeightStrategyPanel.tsx +120 -0
  62. package/src/components/viewer/schedule/generate-schedule.test.ts +439 -0
  63. package/src/components/viewer/schedule/generate-schedule.ts +648 -0
  64. package/src/components/viewer/schedule/schedule-animator.test.ts +452 -0
  65. package/src/components/viewer/schedule/schedule-animator.ts +488 -0
  66. package/src/components/viewer/schedule/schedule-selection.test.ts +148 -0
  67. package/src/components/viewer/schedule/schedule-selection.ts +163 -0
  68. package/src/components/viewer/schedule/schedule-utils.ts +223 -0
  69. package/src/components/viewer/schedule/useConstructionSequence.ts +156 -0
  70. package/src/components/viewer/schedule/useGanttBarDrag.test.ts +90 -0
  71. package/src/components/viewer/schedule/useGanttBarDrag.ts +305 -0
  72. package/src/components/viewer/schedule/useGanttSelection3DHighlight.ts +152 -0
  73. package/src/components/viewer/schedule/useOverlayCompositor.ts +108 -0
  74. package/src/components/viewer/selectionHandlers.ts +446 -0
  75. package/src/components/viewer/tools/AddElementOverlay.tsx +581 -0
  76. package/src/components/viewer/useDuplicateShortcut.ts +77 -0
  77. package/src/components/viewer/useMouseControls.ts +9 -1
  78. package/src/components/viewer/usePointCloudLifecycle.ts +64 -0
  79. package/src/components/viewer/usePointCloudSync.ts +98 -0
  80. package/src/hooks/ingest/pointCloudIngest.ts +391 -0
  81. package/src/hooks/ingest/viewerModelIngest.ts +32 -3
  82. package/src/hooks/useIfcFederation.ts +72 -3
  83. package/src/hooks/useIfcLoader.ts +89 -13
  84. package/src/hooks/useKeyboardShortcuts.ts +25 -0
  85. package/src/hooks/useSandbox.ts +1 -1
  86. package/src/hooks/useSearchIndex.ts +125 -0
  87. package/src/index.css +66 -0
  88. package/src/lib/llm/system-prompt.test.ts +14 -0
  89. package/src/lib/llm/system-prompt.ts +102 -1
  90. package/src/lib/llm/types.ts +6 -0
  91. package/src/lib/recent-files.ts +38 -4
  92. package/src/lib/scripts/templates/bim-globals.d.ts +136 -114
  93. package/src/lib/scripts/templates/construction-schedule.ts +223 -0
  94. package/src/lib/scripts/templates.ts +7 -0
  95. package/src/lib/search/common-ifc-types.ts +36 -0
  96. package/src/lib/search/filter-evaluate.test.ts +537 -0
  97. package/src/lib/search/filter-evaluate.ts +610 -0
  98. package/src/lib/search/filter-rules.test.ts +119 -0
  99. package/src/lib/search/filter-rules.ts +198 -0
  100. package/src/lib/search/filter-schema.test.ts +233 -0
  101. package/src/lib/search/filter-schema.ts +146 -0
  102. package/src/lib/search/recent-searches.test.ts +116 -0
  103. package/src/lib/search/recent-searches.ts +93 -0
  104. package/src/lib/search/result-export.test.ts +101 -0
  105. package/src/lib/search/result-export.ts +104 -0
  106. package/src/lib/search/saved-filters.test.ts +118 -0
  107. package/src/lib/search/saved-filters.ts +154 -0
  108. package/src/lib/search/tier0-scan.test.ts +196 -0
  109. package/src/lib/search/tier0-scan.ts +237 -0
  110. package/src/lib/search/tier1-index.test.ts +242 -0
  111. package/src/lib/search/tier1-index.ts +448 -0
  112. package/src/sdk/adapters/export-adapter.test.ts +434 -1
  113. package/src/sdk/adapters/export-adapter.ts +404 -1
  114. package/src/sdk/adapters/export-schedule-splice.test.ts +127 -0
  115. package/src/sdk/adapters/export-schedule-splice.ts +87 -0
  116. package/src/sdk/adapters/model-compat.ts +8 -2
  117. package/src/sdk/adapters/schedule-adapter.ts +73 -0
  118. package/src/sdk/adapters/store-adapter.ts +201 -0
  119. package/src/sdk/adapters/visibility-adapter.ts +3 -0
  120. package/src/sdk/local-backend.ts +16 -8
  121. package/src/services/desktop-export.ts +3 -1
  122. package/src/services/desktop-native-metadata.ts +41 -18
  123. package/src/services/file-dialog.ts +8 -3
  124. package/src/services/tauri-modules.d.ts +25 -0
  125. package/src/store/basketVisibleSet.ts +3 -0
  126. package/src/store/globalId.ts +4 -1
  127. package/src/store/index.ts +79 -1
  128. package/src/store/slices/addElementMeshes.ts +365 -0
  129. package/src/store/slices/addElementSlice.ts +275 -0
  130. package/src/store/slices/annotationsSlice.test.ts +133 -0
  131. package/src/store/slices/annotationsSlice.ts +251 -0
  132. package/src/store/slices/dataSlice.test.ts +23 -4
  133. package/src/store/slices/dataSlice.ts +1 -1
  134. package/src/store/slices/modelSlice.test.ts +67 -9
  135. package/src/store/slices/modelSlice.ts +39 -7
  136. package/src/store/slices/mutationSlice.ts +964 -3
  137. package/src/store/slices/overlayCompositor.test.ts +164 -0
  138. package/src/store/slices/overlaySlice.test.ts +93 -0
  139. package/src/store/slices/overlaySlice.ts +151 -0
  140. package/src/store/slices/pinboardSlice.test.ts +6 -1
  141. package/src/store/slices/playbackSlice.ts +128 -0
  142. package/src/store/slices/pointCloudSlice.ts +102 -0
  143. package/src/store/slices/schedule-edit-helpers.test.ts +97 -0
  144. package/src/store/slices/schedule-edit-helpers.ts +179 -0
  145. package/src/store/slices/scheduleSlice.test.ts +694 -0
  146. package/src/store/slices/scheduleSlice.ts +1330 -0
  147. package/src/store/slices/searchSlice.test.ts +342 -0
  148. package/src/store/slices/searchSlice.ts +341 -0
  149. package/src/store/slices/selectionSlice.test.ts +46 -0
  150. package/src/store/slices/selectionSlice.ts +20 -0
  151. package/src/store/types.ts +7 -0
  152. package/src/store.ts +14 -0
  153. package/vite.config.ts +1 -0
  154. package/dist/assets/ifc-lite_bg-BINvzoCP.wasm +0 -0
  155. package/dist/assets/index-_bfZsDCC.css +0 -1
  156. package/dist/assets/sandbox-C8575tul.js +0 -5951
@@ -4,9 +4,11 @@
4
4
 
5
5
  import { describe, it, beforeEach } from 'node:test';
6
6
  import assert from 'node:assert';
7
- import { createDataSlice, type DataSlice } from './dataSlice.js';
7
+ import { createDataSlice, type DataSlice, type DataCrossSliceState } from './dataSlice.js';
8
8
  import { DATA_DEFAULTS } from '../constants.js';
9
9
 
10
+ type DataTestState = DataSlice & DataCrossSliceState;
11
+
10
12
  // Mock mesh data for testing
11
13
  const createMockMesh = (expressId: number, color: [number, number, number, number] = [1, 0, 0, 1]) => ({
12
14
  expressId,
@@ -17,9 +19,16 @@ const createMockMesh = (expressId: number, color: [number, number, number, numbe
17
19
  ifcType: 'IfcWall',
18
20
  });
19
21
 
22
+ type TestSetState = (
23
+ partial:
24
+ | Partial<DataTestState>
25
+ | ((state: DataTestState) => Partial<DataTestState>),
26
+ ) => void;
27
+ type TestGetState = () => DataTestState;
28
+
20
29
  describe('DataSlice', () => {
21
- let state: DataSlice;
22
- let setState: (partial: Partial<DataSlice> | ((state: DataSlice) => Partial<DataSlice>)) => void;
30
+ let state: DataTestState;
31
+ let setState: TestSetState;
23
32
 
24
33
  beforeEach(() => {
25
34
  setState = (partial) => {
@@ -31,7 +40,17 @@ describe('DataSlice', () => {
31
40
  }
32
41
  };
33
42
 
34
- state = createDataSlice(setState, () => state, {} as any);
43
+ const getState: TestGetState = () => state;
44
+
45
+ // Seed the cross-slice fields owned by ModelSlice. dataSlice's
46
+ // updaters look up the active model in this map, so the test mock
47
+ // has to provide it for the typed StateCreator to be satisfiable.
48
+ const slice = createDataSlice(
49
+ setState as Parameters<typeof createDataSlice>[0],
50
+ getState as Parameters<typeof createDataSlice>[1],
51
+ undefined as unknown as Parameters<typeof createDataSlice>[2],
52
+ );
53
+ state = { ...slice, activeModelId: null, models: new Map() };
35
54
  });
36
55
 
37
56
  describe('initial state', () => {
@@ -20,7 +20,7 @@ import { DATA_DEFAULTS } from '../constants.js';
20
20
  * consistent. The types below describe the minimal ModelSlice surface
21
21
  * that dataSlice accesses through the merged Zustand state.
22
22
  */
23
- interface DataCrossSliceState {
23
+ export interface DataCrossSliceState {
24
24
  activeModelId: string | null;
25
25
  models: Map<string, FederatedModel>;
26
26
  }
@@ -4,16 +4,34 @@
4
4
 
5
5
  import { describe, it, beforeEach } from 'node:test';
6
6
  import assert from 'node:assert';
7
- import { createModelSlice, type ModelSlice } from './modelSlice.js';
7
+ import type { IfcDataStore } from '@ifc-lite/parser';
8
+ import type { GeometryResult } from '@ifc-lite/geometry';
9
+ import { createModelSlice, type ModelSlice, type ModelCrossSliceState } from './modelSlice.js';
8
10
  import type { FederatedModel } from '../types.js';
9
11
 
10
- // Helper to create a mock model
12
+ type ModelTestState = ModelSlice & ModelCrossSliceState;
13
+
14
+ // Typed setter / getter shim that mirrors zustand's StateCreator
15
+ // signature without the broader middleware machinery the test doesn't
16
+ // need. Using StateCreator's exact types here would pull in the whole
17
+ // store; the local aliases below are tight enough for this test.
18
+ type TestSetState = (
19
+ partial:
20
+ | Partial<ModelTestState>
21
+ | ((state: ModelTestState) => Partial<ModelTestState>),
22
+ ) => void;
23
+ type TestGetState = () => ModelTestState;
24
+
25
+ // Helper to create a mock model. `IfcDataStore` and `GeometryResult` are
26
+ // large interfaces that the slice never inspects on these paths — the
27
+ // double-cast through `unknown` is the minimum that satisfies the
28
+ // compiler without an `any`.
11
29
  function createMockModel(id: string, name: string): FederatedModel {
12
30
  return {
13
31
  id,
14
32
  name,
15
- ifcDataStore: {} as any,
16
- geometryResult: {} as any,
33
+ ifcDataStore: {} as unknown as IfcDataStore,
34
+ geometryResult: {} as unknown as GeometryResult,
17
35
  visible: true,
18
36
  collapsed: false,
19
37
  schemaVersion: 'IFC4',
@@ -25,11 +43,10 @@ function createMockModel(id: string, name: string): FederatedModel {
25
43
  }
26
44
 
27
45
  describe('ModelSlice', () => {
28
- let state: ModelSlice;
29
- let setState: (partial: Partial<ModelSlice> | ((state: ModelSlice) => Partial<ModelSlice>)) => void;
46
+ let state: ModelTestState;
47
+ let setState: TestSetState;
30
48
 
31
49
  beforeEach(() => {
32
- // Create a mock set function that updates state
33
50
  setState = (partial) => {
34
51
  if (typeof partial === 'function') {
35
52
  const updates = partial(state);
@@ -39,8 +56,17 @@ describe('ModelSlice', () => {
39
56
  }
40
57
  };
41
58
 
42
- // Create slice with mock set function
43
- state = createModelSlice(setState, () => state, {} as any);
59
+ const getState: TestGetState = () => state;
60
+
61
+ // The slice's StateCreator signature includes a third middleware
62
+ // argument (store API) that the slice's body never reads. We pass
63
+ // `undefined` cast to the empty middleware shape rather than `any`.
64
+ const slice = createModelSlice(
65
+ setState as Parameters<typeof createModelSlice>[0],
66
+ getState as Parameters<typeof createModelSlice>[1],
67
+ undefined as unknown as Parameters<typeof createModelSlice>[2],
68
+ );
69
+ state = { ...slice, ifcDataStore: null, geometryResult: null };
44
70
  });
45
71
 
46
72
  describe('initial state', () => {
@@ -270,4 +296,36 @@ describe('ModelSlice', () => {
270
296
  assert.strictEqual(visible.length, 0);
271
297
  });
272
298
  });
299
+
300
+ describe('resolveGlobalIdFromModels — overlay-allocated ids', () => {
301
+ it('falls through to mutation views when the id is past maxExpressId', () => {
302
+ const model = createMockModel('model-1', 'First');
303
+ model.idOffset = 0;
304
+ model.maxExpressId = 10_000;
305
+ state.addModel(model);
306
+
307
+ // Seed a fake mutation view with a fresh overlay entity. The
308
+ // resolver only reads `getNewEntity` from each view, so we type
309
+ // the map narrowly and let it satisfy the slice's wider type via
310
+ // a single-property cast on the wrapping state object.
311
+ type StubView = { getNewEntity: (id: number) => { expressId: number } | null };
312
+ const stubViews: Map<string, StubView> = new Map([
313
+ ['model-1', { getNewEntity: (id: number) => (id === 11_001 ? { expressId: id } : null) }],
314
+ ]);
315
+ state = { ...state, mutationViews: stubViews } as typeof state & { mutationViews: Map<string, StubView> };
316
+
317
+ // Inside the parsed range — first pass resolves it.
318
+ const within = state.resolveGlobalIdFromModels(42);
319
+ assert.deepStrictEqual(within, { modelId: 'model-1', expressId: 42 });
320
+
321
+ // Above the parsed range but in the overlay — second pass resolves it.
322
+ const overlay = state.resolveGlobalIdFromModels(11_001);
323
+ assert.deepStrictEqual(overlay, { modelId: 'model-1', expressId: 11_001 });
324
+
325
+ // Above the parsed range and NOT in the overlay — returns null
326
+ // so callers can fall back to the legacy single-model path.
327
+ const phantom = state.resolveGlobalIdFromModels(99_999);
328
+ assert.strictEqual(phantom, null);
329
+ });
330
+ });
273
331
  });
@@ -13,8 +13,20 @@
13
13
 
14
14
  import type { StateCreator } from 'zustand';
15
15
  import type { FederatedModel } from '../types.js';
16
+ import type { IfcDataStore } from '@ifc-lite/parser';
17
+ import type { GeometryResult } from '@ifc-lite/geometry';
16
18
  import { federationRegistry, type GlobalIdLookup } from '@ifc-lite/renderer';
17
19
 
20
+ /**
21
+ * Cross-slice fields the model actions write to. `ifcDataStore` and
22
+ * `geometryResult` are owned by `dataSlice` but `modelSlice`'s set()
23
+ * calls need to keep them in sync with the active model.
24
+ */
25
+ export interface ModelCrossSliceState {
26
+ ifcDataStore: IfcDataStore | null;
27
+ geometryResult: GeometryResult | null;
28
+ }
29
+
18
30
  export interface ModelSlice {
19
31
  // State
20
32
  /** Map of all loaded models by ID */
@@ -72,7 +84,7 @@ export interface ModelSlice {
72
84
  resolveGlobalIdFromModels: (globalId: number) => GlobalIdLookup | null;
73
85
  }
74
86
 
75
- export const createModelSlice: StateCreator<ModelSlice, [], [], ModelSlice> = (set, get) => ({
87
+ export const createModelSlice: StateCreator<ModelSlice & ModelCrossSliceState, [], [], ModelSlice> = (set, get) => ({
76
88
  // Initial state
77
89
  models: new Map(),
78
90
  activeModelId: null,
@@ -245,19 +257,39 @@ export const createModelSlice: StateCreator<ModelSlice, [], [], ModelSlice> = (s
245
257
  */
246
258
  resolveGlobalIdFromModels: (globalId: number) => {
247
259
  const models = get().models;
260
+ const mutationViews = (get() as unknown as { mutationViews?: Map<string, { getNewEntity: (id: number) => unknown }> }).mutationViews;
248
261
 
249
262
  // Sort models by offset for correct range checking
250
263
  const sortedModels = Array.from(models.values()).sort((a, b) => a.idOffset - b.idOffset);
251
264
 
252
- // Find the model that contains this globalId
253
- // A model contains a globalId if: offset <= globalId <= offset + maxExpressId
265
+ // Find the model that contains this globalId.
266
+ //
267
+ // First pass — parse-time range. A model owns ids in
268
+ // `[offset, offset + maxExpressId]` from the original parse. This
269
+ // is the fast path covering 99% of selections.
270
+ //
271
+ // Second pass — overlay-allocated ids. Duplicates / scripted adds
272
+ // through StoreEditor land ABOVE the model's parse-time
273
+ // maxExpressId, so they fall outside the first-pass range. The
274
+ // federation resolver knows nothing about overlay state, so we
275
+ // consult each model's mutation view for the freshly-added
276
+ // entity. Falls back gracefully when no view is registered.
254
277
  for (const model of sortedModels) {
255
278
  const localId = globalId - model.idOffset;
256
279
  if (localId >= 0 && localId <= model.maxExpressId) {
257
- return {
258
- modelId: model.id,
259
- expressId: localId,
260
- };
280
+ return { modelId: model.id, expressId: localId };
281
+ }
282
+ }
283
+
284
+ if (mutationViews) {
285
+ for (const model of sortedModels) {
286
+ const localId = globalId - model.idOffset;
287
+ if (localId <= model.maxExpressId) continue; // already covered above
288
+ const view = mutationViews.get(model.id);
289
+ if (!view) continue;
290
+ if (view.getNewEntity(localId) !== null) {
291
+ return { modelId: model.id, expressId: localId };
292
+ }
261
293
  }
262
294
  }
263
295