@ifc-lite/viewer 1.17.6 → 1.18.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 (143) hide show
  1. package/.turbo/turbo-build.log +17 -14
  2. package/.turbo/turbo-typecheck.log +1 -1
  3. package/CHANGELOG.md +513 -0
  4. package/dist/assets/{basketViewActivator-86rgogji.js → basketViewActivator-Cm1QEk_R.js} +1 -1
  5. package/dist/assets/{exporters-CcPS9MK5.js → exporters-B_OBqIyD.js} +3235 -2648
  6. package/dist/assets/{geometry.worker-BFUYA08u.js → geometry.worker-xHHy-9DV.js} +1 -1
  7. package/dist/assets/{ifc-lite_bg-BINvzoCP.wasm → ifc-lite_bg-ADjKXSms.wasm} +0 -0
  8. package/dist/assets/{index-Bfms9I4A.js → index-BKq-M3Mk.js} +44124 -30920
  9. package/dist/assets/index-COnQRuqY.css +1 -0
  10. package/dist/assets/{native-bridge-DUyLCMZS.js → native-bridge-SHXiQwFW.js} +1 -1
  11. package/dist/assets/sandbox-jez21HtV.js +9627 -0
  12. package/dist/assets/{server-client-BuZK7OST.js → server-client-ncOQVNso.js} +1 -1
  13. package/dist/assets/{wasm-bridge-JsqEGDV8.js → wasm-bridge-DyfBSB8z.js} +1 -1
  14. package/dist/index.html +6 -6
  15. package/package.json +10 -10
  16. package/src/apache-arrow.d.ts +30 -0
  17. package/src/components/viewer/AddElementPanel.tsx +758 -0
  18. package/src/components/viewer/BulkPropertyEditor.tsx +7 -0
  19. package/src/components/viewer/ChatPanel.tsx +64 -2
  20. package/src/components/viewer/CommandPalette.tsx +56 -7
  21. package/src/components/viewer/EntityContextMenu.tsx +168 -4
  22. package/src/components/viewer/ExportChangesButton.tsx +25 -5
  23. package/src/components/viewer/ExportDialog.tsx +19 -1
  24. package/src/components/viewer/MainToolbar.tsx +69 -10
  25. package/src/components/viewer/PropertiesPanel.tsx +222 -22
  26. package/src/components/viewer/SearchInline.tsx +669 -0
  27. package/src/components/viewer/SearchModal.filter.builder.tsx +766 -0
  28. package/src/components/viewer/SearchModal.filter.tsx +514 -0
  29. package/src/components/viewer/SearchModal.text.tsx +388 -0
  30. package/src/components/viewer/SearchModal.tsx +235 -0
  31. package/src/components/viewer/ToolOverlays.tsx +5 -0
  32. package/src/components/viewer/ViewerLayout.tsx +24 -4
  33. package/src/components/viewer/Viewport.tsx +11 -1
  34. package/src/components/viewer/ViewportContainer.tsx +2 -0
  35. package/src/components/viewer/annotations/AnnotationDropInput.tsx +203 -0
  36. package/src/components/viewer/annotations/AnnotationLayer.tsx +287 -0
  37. package/src/components/viewer/annotations/AnnotationPin.tsx +90 -0
  38. package/src/components/viewer/annotations/AnnotationPopover.tsx +296 -0
  39. package/src/components/viewer/bcf/BCFTopicDetail.tsx +1 -1
  40. package/src/components/viewer/lists/ListPanel.tsx +14 -21
  41. package/src/components/viewer/properties/RawStepCard.tsx +332 -0
  42. package/src/components/viewer/properties/RawStepRow.tsx +261 -0
  43. package/src/components/viewer/properties/ScheduleCard.tsx +224 -0
  44. package/src/components/viewer/properties/TaskEditCard.tsx +510 -0
  45. package/src/components/viewer/properties/raw-step-format.ts +193 -0
  46. package/src/components/viewer/schedule/AnimationSettingsPopover.tsx +542 -0
  47. package/src/components/viewer/schedule/GanttDependencyArrows.tsx +89 -0
  48. package/src/components/viewer/schedule/GanttDragTooltip.tsx +48 -0
  49. package/src/components/viewer/schedule/GanttEmptyState.tsx +97 -0
  50. package/src/components/viewer/schedule/GanttPanel.tsx +295 -0
  51. package/src/components/viewer/schedule/GanttTaskBar.tsx +199 -0
  52. package/src/components/viewer/schedule/GanttTaskTree.tsx +250 -0
  53. package/src/components/viewer/schedule/GanttTimeline.tsx +305 -0
  54. package/src/components/viewer/schedule/GanttToolbar.tsx +406 -0
  55. package/src/components/viewer/schedule/GenerateAdvancedPanel.tsx +147 -0
  56. package/src/components/viewer/schedule/GenerateScheduleDialog.tsx +392 -0
  57. package/src/components/viewer/schedule/HeightStrategyPanel.tsx +120 -0
  58. package/src/components/viewer/schedule/generate-schedule.test.ts +439 -0
  59. package/src/components/viewer/schedule/generate-schedule.ts +648 -0
  60. package/src/components/viewer/schedule/schedule-animator.test.ts +452 -0
  61. package/src/components/viewer/schedule/schedule-animator.ts +488 -0
  62. package/src/components/viewer/schedule/schedule-selection.test.ts +148 -0
  63. package/src/components/viewer/schedule/schedule-selection.ts +163 -0
  64. package/src/components/viewer/schedule/schedule-utils.ts +223 -0
  65. package/src/components/viewer/schedule/useConstructionSequence.ts +156 -0
  66. package/src/components/viewer/schedule/useGanttBarDrag.test.ts +90 -0
  67. package/src/components/viewer/schedule/useGanttBarDrag.ts +305 -0
  68. package/src/components/viewer/schedule/useGanttSelection3DHighlight.ts +152 -0
  69. package/src/components/viewer/schedule/useOverlayCompositor.ts +108 -0
  70. package/src/components/viewer/selectionHandlers.ts +446 -0
  71. package/src/components/viewer/tools/AddElementOverlay.tsx +540 -0
  72. package/src/components/viewer/useDuplicateShortcut.ts +77 -0
  73. package/src/components/viewer/useMouseControls.ts +9 -1
  74. package/src/hooks/useIfcLoader.ts +22 -10
  75. package/src/hooks/useKeyboardShortcuts.ts +25 -0
  76. package/src/hooks/useSandbox.ts +1 -1
  77. package/src/hooks/useSearchIndex.ts +125 -0
  78. package/src/index.css +66 -0
  79. package/src/lib/llm/system-prompt.test.ts +14 -0
  80. package/src/lib/llm/system-prompt.ts +102 -1
  81. package/src/lib/llm/types.ts +6 -0
  82. package/src/lib/recent-files.ts +38 -4
  83. package/src/lib/scripts/templates/bim-globals.d.ts +136 -114
  84. package/src/lib/scripts/templates/construction-schedule.ts +223 -0
  85. package/src/lib/scripts/templates.ts +7 -0
  86. package/src/lib/search/common-ifc-types.ts +36 -0
  87. package/src/lib/search/filter-evaluate.test.ts +537 -0
  88. package/src/lib/search/filter-evaluate.ts +610 -0
  89. package/src/lib/search/filter-rules.test.ts +119 -0
  90. package/src/lib/search/filter-rules.ts +198 -0
  91. package/src/lib/search/filter-schema.test.ts +233 -0
  92. package/src/lib/search/filter-schema.ts +146 -0
  93. package/src/lib/search/recent-searches.test.ts +116 -0
  94. package/src/lib/search/recent-searches.ts +93 -0
  95. package/src/lib/search/result-export.test.ts +101 -0
  96. package/src/lib/search/result-export.ts +104 -0
  97. package/src/lib/search/saved-filters.test.ts +118 -0
  98. package/src/lib/search/saved-filters.ts +154 -0
  99. package/src/lib/search/tier0-scan.test.ts +196 -0
  100. package/src/lib/search/tier0-scan.ts +237 -0
  101. package/src/lib/search/tier1-index.test.ts +242 -0
  102. package/src/lib/search/tier1-index.ts +448 -0
  103. package/src/sdk/adapters/export-adapter.test.ts +434 -1
  104. package/src/sdk/adapters/export-adapter.ts +404 -1
  105. package/src/sdk/adapters/export-schedule-splice.test.ts +127 -0
  106. package/src/sdk/adapters/export-schedule-splice.ts +87 -0
  107. package/src/sdk/adapters/model-compat.ts +8 -2
  108. package/src/sdk/adapters/schedule-adapter.ts +73 -0
  109. package/src/sdk/adapters/store-adapter.ts +201 -0
  110. package/src/sdk/adapters/visibility-adapter.ts +3 -0
  111. package/src/sdk/local-backend.ts +16 -8
  112. package/src/services/desktop-export.ts +3 -1
  113. package/src/services/desktop-native-metadata.ts +41 -18
  114. package/src/services/file-dialog.ts +4 -1
  115. package/src/services/tauri-modules.d.ts +25 -0
  116. package/src/store/basketVisibleSet.ts +3 -0
  117. package/src/store/globalId.ts +4 -1
  118. package/src/store/index.ts +70 -1
  119. package/src/store/slices/addElementMeshes.ts +365 -0
  120. package/src/store/slices/addElementSlice.ts +275 -0
  121. package/src/store/slices/annotationsSlice.test.ts +133 -0
  122. package/src/store/slices/annotationsSlice.ts +251 -0
  123. package/src/store/slices/dataSlice.test.ts +23 -4
  124. package/src/store/slices/dataSlice.ts +1 -1
  125. package/src/store/slices/modelSlice.test.ts +67 -9
  126. package/src/store/slices/modelSlice.ts +39 -7
  127. package/src/store/slices/mutationSlice.ts +964 -3
  128. package/src/store/slices/overlayCompositor.test.ts +164 -0
  129. package/src/store/slices/overlaySlice.test.ts +93 -0
  130. package/src/store/slices/overlaySlice.ts +151 -0
  131. package/src/store/slices/pinboardSlice.test.ts +6 -1
  132. package/src/store/slices/playbackSlice.ts +128 -0
  133. package/src/store/slices/schedule-edit-helpers.test.ts +97 -0
  134. package/src/store/slices/schedule-edit-helpers.ts +179 -0
  135. package/src/store/slices/scheduleSlice.test.ts +694 -0
  136. package/src/store/slices/scheduleSlice.ts +1330 -0
  137. package/src/store/slices/searchSlice.test.ts +342 -0
  138. package/src/store/slices/searchSlice.ts +341 -0
  139. package/src/store/slices/selectionSlice.test.ts +46 -0
  140. package/src/store/slices/selectionSlice.ts +20 -0
  141. package/src/store.ts +14 -0
  142. package/dist/assets/index-_bfZsDCC.css +0 -1
  143. package/dist/assets/sandbox-C8575tul.js +0 -5951
@@ -0,0 +1,73 @@
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
+ * Schedule backend adapter — drives the `bim.schedule.*` API by calling
7
+ * `extractScheduleOnDemand` against the viewer's active (or requested) model.
8
+ *
9
+ * Results are cached per-model until the underlying `ifcDataStore` identity
10
+ * changes, so repeated script / panel queries don't re-parse the STEP source.
11
+ */
12
+
13
+ import type { ScheduleBackendMethods, ScheduleExtractionData } from '@ifc-lite/sdk';
14
+ import { extractScheduleOnDemand, type IfcDataStore } from '@ifc-lite/parser';
15
+ import type { StoreApi } from './types.js';
16
+ import { getModelForRef } from './model-compat.js';
17
+
18
+ const EMPTY_EXTRACTION: ScheduleExtractionData = {
19
+ workSchedules: [],
20
+ tasks: [],
21
+ sequences: [],
22
+ hasSchedule: false,
23
+ };
24
+
25
+ /**
26
+ * Best-effort resolution of the data store to extract from: explicit modelId,
27
+ * then the legacy single-model store, then the first federated model.
28
+ */
29
+ function resolveStore(store: StoreApi, modelId?: string): IfcDataStore | null {
30
+ const state = store.getState();
31
+ if (modelId) {
32
+ const model = getModelForRef(state, modelId);
33
+ return (model?.ifcDataStore as IfcDataStore | undefined) ?? null;
34
+ }
35
+ if (state.ifcDataStore) return state.ifcDataStore as IfcDataStore;
36
+ // Respect the user's active model selection before falling back to the
37
+ // first federated entry — other namespaces (query, selection, viewer)
38
+ // follow the same pattern.
39
+ const activeId = state.activeModelId as string | null | undefined;
40
+ if (activeId) {
41
+ const active = getModelForRef(state, activeId);
42
+ if (active?.ifcDataStore) return active.ifcDataStore as IfcDataStore;
43
+ }
44
+ const firstFederated = state.models?.values().next().value;
45
+ return (firstFederated?.ifcDataStore as IfcDataStore | undefined) ?? null;
46
+ }
47
+
48
+ export function createScheduleAdapter(store: StoreApi): ScheduleBackendMethods {
49
+ /** Cache keyed by IfcDataStore identity (WeakMap avoids leaks on model swap). */
50
+ const cache = new WeakMap<IfcDataStore, ScheduleExtractionData>();
51
+
52
+ const extract = (modelId?: string): ScheduleExtractionData => {
53
+ const ds = resolveStore(store, modelId);
54
+ if (!ds) return EMPTY_EXTRACTION;
55
+ const cached = cache.get(ds);
56
+ if (cached) return cached;
57
+ try {
58
+ const result = extractScheduleOnDemand(ds) as ScheduleExtractionData;
59
+ cache.set(ds, result);
60
+ return result;
61
+ } catch (err) {
62
+ console.warn('[schedule-adapter] extraction failed', err);
63
+ return EMPTY_EXTRACTION;
64
+ }
65
+ };
66
+
67
+ return {
68
+ data: (modelId) => extract(modelId),
69
+ tasks: (modelId) => extract(modelId).tasks,
70
+ workSchedules: (modelId) => extract(modelId).workSchedules,
71
+ sequences: (modelId) => extract(modelId).sequences,
72
+ };
73
+ }
@@ -0,0 +1,201 @@
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
+ * `bim.store.*` adapter — implements StoreBackendMethods on top of the
7
+ * viewer's per-model MutablePropertyView. Routes through the same overlay
8
+ * that bim.mutate.* uses, so document-level edits and property edits stack
9
+ * coherently into a single export.
10
+ */
11
+
12
+ import { StoreEditor } from '@ifc-lite/mutations';
13
+ import {
14
+ addBeamToStore,
15
+ addColumnToStore,
16
+ addDoorToStore,
17
+ addMemberToStore,
18
+ addPlateToStore,
19
+ addRoofToStore,
20
+ addSlabToStore,
21
+ addSpaceToStore,
22
+ addWallToStore,
23
+ addWindowToStore,
24
+ resolveSpatialAnchor,
25
+ type BeamInStoreParams,
26
+ type ColumnInStoreParams,
27
+ type DoorInStoreParams,
28
+ type MemberInStoreParams,
29
+ type PlateInStoreParams,
30
+ type RoofInStoreParams,
31
+ type SlabInStoreParams,
32
+ type SpaceInStoreParams,
33
+ type WallInStoreParams,
34
+ type WindowInStoreParams,
35
+ } from '@ifc-lite/create';
36
+ import type {
37
+ AddBeamInStoreParams,
38
+ AddColumnInStoreParams,
39
+ AddDoorInStoreParams,
40
+ AddMemberInStoreParams,
41
+ AddPlateInStoreParams,
42
+ AddRoofInStoreParams,
43
+ AddSlabInStoreParams,
44
+ AddSpaceInStoreParams,
45
+ AddWallInStoreParams,
46
+ AddWindowInStoreParams,
47
+ EntityRef,
48
+ StoreBackendMethods,
49
+ } from '@ifc-lite/sdk';
50
+ import type { StoreApi } from './types.js';
51
+ import { getModelForRef, LEGACY_MODEL_ID } from './model-compat.js';
52
+ import { getOrCreateMutationView, normalizeMutationModelId } from './mutation-view.js';
53
+
54
+ export function createStoreAdapter(store: StoreApi): StoreBackendMethods {
55
+ // One StoreEditor per (modelId, MutablePropertyView) pair. Editors are
56
+ // cheap, but caching avoids re-scanning the entity index on every call.
57
+ const editors = new WeakMap<object, StoreEditor>();
58
+
59
+ function resolveDataStore(modelId: string) {
60
+ const state = store.getState();
61
+ const refModelId = modelId === 'legacy' ? LEGACY_MODEL_ID : modelId;
62
+ const model = getModelForRef(state, refModelId);
63
+ return model?.ifcDataStore ?? null;
64
+ }
65
+
66
+ function getEditor(modelId: string): StoreEditor | null {
67
+ const view = getOrCreateMutationView(store, modelId);
68
+ if (!view) return null;
69
+ let editor = editors.get(view);
70
+ if (editor) return editor;
71
+
72
+ const dataStore = resolveDataStore(modelId);
73
+ if (!dataStore) return null;
74
+
75
+ editor = new StoreEditor(dataStore, view);
76
+ editors.set(view, editor);
77
+ return editor;
78
+ }
79
+
80
+ return {
81
+ addEntity(modelId: string, def: { type: string; attributes: unknown[] }): EntityRef {
82
+ const normalizedId = normalizeMutationModelId(store.getState(), modelId);
83
+ const editor = getEditor(modelId);
84
+ if (!editor) {
85
+ throw new Error(`bim.store.addEntity: no model loaded for id "${modelId}"`);
86
+ }
87
+ const ref = editor.addEntity(def.type, def.attributes as Parameters<StoreEditor['addEntity']>[1]);
88
+ return { modelId: normalizedId, expressId: ref.expressId };
89
+ },
90
+ removeEntity(ref: EntityRef): boolean {
91
+ const editor = getEditor(ref.modelId);
92
+ if (!editor) return false;
93
+ return editor.removeEntity(ref.expressId);
94
+ },
95
+ setPositionalAttribute(ref: EntityRef, index: number, value: unknown): void {
96
+ const editor = getEditor(ref.modelId);
97
+ if (!editor) {
98
+ throw new Error(`bim.store.setPositionalAttribute: no model loaded for id "${ref.modelId}"`);
99
+ }
100
+ editor.setPositionalAttribute(ref.expressId, index, value as Parameters<StoreEditor['setPositionalAttribute']>[2]);
101
+ },
102
+ addColumn(modelId: string, storeyExpressId: number, params: AddColumnInStoreParams): EntityRef {
103
+ const editor = getEditor(modelId);
104
+ const dataStore = resolveDataStore(modelId);
105
+ if (!editor || !dataStore) {
106
+ throw new Error(`bim.store.addColumn: no model loaded for id "${modelId}"`);
107
+ }
108
+ const anchor = resolveSpatialAnchor(dataStore, storeyExpressId);
109
+ const normalizedModelId = normalizeMutationModelId(store.getState(), modelId);
110
+ const result = addColumnToStore(editor, anchor, params as ColumnInStoreParams);
111
+ return { modelId: normalizedModelId, expressId: result.columnId };
112
+ },
113
+ addWall(modelId: string, storeyExpressId: number, params: AddWallInStoreParams): EntityRef {
114
+ const editor = getEditor(modelId);
115
+ const dataStore = resolveDataStore(modelId);
116
+ if (!editor || !dataStore) {
117
+ throw new Error(`bim.store.addWall: no model loaded for id "${modelId}"`);
118
+ }
119
+ const anchor = resolveSpatialAnchor(dataStore, storeyExpressId);
120
+ const normalizedModelId = normalizeMutationModelId(store.getState(), modelId);
121
+ const result = addWallToStore(editor, anchor, params as WallInStoreParams);
122
+ return { modelId: normalizedModelId, expressId: result.wallId };
123
+ },
124
+ addSlab(modelId: string, storeyExpressId: number, params: AddSlabInStoreParams): EntityRef {
125
+ const editor = getEditor(modelId);
126
+ const dataStore = resolveDataStore(modelId);
127
+ if (!editor || !dataStore) {
128
+ throw new Error(`bim.store.addSlab: no model loaded for id "${modelId}"`);
129
+ }
130
+ const anchor = resolveSpatialAnchor(dataStore, storeyExpressId);
131
+ const normalizedModelId = normalizeMutationModelId(store.getState(), modelId);
132
+ const result = addSlabToStore(editor, anchor, params as SlabInStoreParams);
133
+ return { modelId: normalizedModelId, expressId: result.slabId };
134
+ },
135
+ addBeam(modelId: string, storeyExpressId: number, params: AddBeamInStoreParams): EntityRef {
136
+ const editor = getEditor(modelId);
137
+ const dataStore = resolveDataStore(modelId);
138
+ if (!editor || !dataStore) {
139
+ throw new Error(`bim.store.addBeam: no model loaded for id "${modelId}"`);
140
+ }
141
+ const anchor = resolveSpatialAnchor(dataStore, storeyExpressId);
142
+ const normalizedModelId = normalizeMutationModelId(store.getState(), modelId);
143
+ const result = addBeamToStore(editor, anchor, params as BeamInStoreParams);
144
+ return { modelId: normalizedModelId, expressId: result.beamId };
145
+ },
146
+ addDoor(modelId: string, storeyExpressId: number, params: AddDoorInStoreParams): EntityRef {
147
+ const editor = getEditor(modelId);
148
+ const dataStore = resolveDataStore(modelId);
149
+ if (!editor || !dataStore) throw new Error(`bim.store.addDoor: no model loaded for id "${modelId}"`);
150
+ const anchor = resolveSpatialAnchor(dataStore, storeyExpressId);
151
+ const normalizedModelId = normalizeMutationModelId(store.getState(), modelId);
152
+ const result = addDoorToStore(editor, anchor, params as DoorInStoreParams);
153
+ return { modelId: normalizedModelId, expressId: result.doorId };
154
+ },
155
+ addWindow(modelId: string, storeyExpressId: number, params: AddWindowInStoreParams): EntityRef {
156
+ const editor = getEditor(modelId);
157
+ const dataStore = resolveDataStore(modelId);
158
+ if (!editor || !dataStore) throw new Error(`bim.store.addWindow: no model loaded for id "${modelId}"`);
159
+ const anchor = resolveSpatialAnchor(dataStore, storeyExpressId);
160
+ const normalizedModelId = normalizeMutationModelId(store.getState(), modelId);
161
+ const result = addWindowToStore(editor, anchor, params as WindowInStoreParams);
162
+ return { modelId: normalizedModelId, expressId: result.windowId };
163
+ },
164
+ addSpace(modelId: string, storeyExpressId: number, params: AddSpaceInStoreParams): EntityRef {
165
+ const editor = getEditor(modelId);
166
+ const dataStore = resolveDataStore(modelId);
167
+ if (!editor || !dataStore) throw new Error(`bim.store.addSpace: no model loaded for id "${modelId}"`);
168
+ const anchor = resolveSpatialAnchor(dataStore, storeyExpressId);
169
+ const normalizedModelId = normalizeMutationModelId(store.getState(), modelId);
170
+ const result = addSpaceToStore(editor, anchor, params as SpaceInStoreParams);
171
+ return { modelId: normalizedModelId, expressId: result.spaceId };
172
+ },
173
+ addRoof(modelId: string, storeyExpressId: number, params: AddRoofInStoreParams): EntityRef {
174
+ const editor = getEditor(modelId);
175
+ const dataStore = resolveDataStore(modelId);
176
+ if (!editor || !dataStore) throw new Error(`bim.store.addRoof: no model loaded for id "${modelId}"`);
177
+ const anchor = resolveSpatialAnchor(dataStore, storeyExpressId);
178
+ const normalizedModelId = normalizeMutationModelId(store.getState(), modelId);
179
+ const result = addRoofToStore(editor, anchor, params as RoofInStoreParams);
180
+ return { modelId: normalizedModelId, expressId: result.roofId };
181
+ },
182
+ addPlate(modelId: string, storeyExpressId: number, params: AddPlateInStoreParams): EntityRef {
183
+ const editor = getEditor(modelId);
184
+ const dataStore = resolveDataStore(modelId);
185
+ if (!editor || !dataStore) throw new Error(`bim.store.addPlate: no model loaded for id "${modelId}"`);
186
+ const anchor = resolveSpatialAnchor(dataStore, storeyExpressId);
187
+ const normalizedModelId = normalizeMutationModelId(store.getState(), modelId);
188
+ const result = addPlateToStore(editor, anchor, params as PlateInStoreParams);
189
+ return { modelId: normalizedModelId, expressId: result.plateId };
190
+ },
191
+ addMember(modelId: string, storeyExpressId: number, params: AddMemberInStoreParams): EntityRef {
192
+ const editor = getEditor(modelId);
193
+ const dataStore = resolveDataStore(modelId);
194
+ if (!editor || !dataStore) throw new Error(`bim.store.addMember: no model loaded for id "${modelId}"`);
195
+ const anchor = resolveSpatialAnchor(dataStore, storeyExpressId);
196
+ const normalizedModelId = normalizeMutationModelId(store.getState(), modelId);
197
+ const result = addMemberToStore(editor, anchor, params as MemberInStoreParams);
198
+ return { modelId: normalizedModelId, expressId: result.memberId };
199
+ },
200
+ };
201
+ }
@@ -28,6 +28,9 @@ function findDescendantNode(root: SpatialNode, expressId: number): SpatialNode |
28
28
  */
29
29
  function expandSpatialRef(ref: EntityRef, model: ModelLike): number[] {
30
30
  const dataStore = model.ifcDataStore;
31
+ // Native-metadata-only models have no parsed data store — visibility
32
+ // expansion isn't possible, fall back to the single ref.
33
+ if (!dataStore) return [ref.expressId];
31
34
  const typeName = dataStore.entities.getTypeName(ref.expressId) || '';
32
35
  if (!isSpatialStructureTypeName(typeName) || isSpaceLikeSpatialTypeName(typeName)) {
33
36
  return [ref.expressId];
@@ -18,10 +18,12 @@ import type {
18
18
  VisibilityBackendMethods,
19
19
  ViewerBackendMethods,
20
20
  MutateBackendMethods,
21
+ StoreBackendMethods,
21
22
  SpatialBackendMethods,
22
23
  ExportBackendMethods,
23
24
  LensBackendMethods,
24
25
  FilesBackendMethods,
26
+ ScheduleBackendMethods,
25
27
  } from '@ifc-lite/sdk';
26
28
  import type { StoreApi } from './adapters/types.js';
27
29
  import { LEGACY_MODEL_ID } from './adapters/model-compat.js';
@@ -31,10 +33,12 @@ import { createSelectionAdapter } from './adapters/selection-adapter.js';
31
33
  import { createVisibilityAdapter } from './adapters/visibility-adapter.js';
32
34
  import { createViewerAdapter } from './adapters/viewer-adapter.js';
33
35
  import { createMutateAdapter } from './adapters/mutate-adapter.js';
36
+ import { createStoreAdapter } from './adapters/store-adapter.js';
34
37
  import { createSpatialAdapter } from './adapters/spatial-adapter.js';
35
38
  import { createLensAdapter } from './adapters/lens-adapter.js';
36
39
  import { createExportAdapter } from './adapters/export-adapter.js';
37
40
  import { createFilesAdapter } from './adapters/files-adapter.js';
41
+ import { createScheduleAdapter } from './adapters/schedule-adapter.js';
38
42
 
39
43
  export class LocalBackend implements BimBackend {
40
44
  readonly model: ModelBackendMethods;
@@ -43,38 +47,42 @@ export class LocalBackend implements BimBackend {
43
47
  readonly visibility: VisibilityBackendMethods;
44
48
  readonly viewer: ViewerBackendMethods;
45
49
  readonly mutate: MutateBackendMethods;
50
+ readonly store: StoreBackendMethods;
46
51
  readonly spatial: SpatialBackendMethods;
47
52
  readonly export: ExportBackendMethods;
48
53
  readonly lens: LensBackendMethods;
49
54
  readonly files: FilesBackendMethods;
55
+ readonly schedule: ScheduleBackendMethods;
50
56
 
51
- private store: StoreApi;
57
+ private storeApi: StoreApi;
52
58
 
53
59
  constructor(store: StoreApi) {
54
- this.store = store;
60
+ this.storeApi = store;
55
61
  this.model = createModelAdapter(store);
56
62
  this.query = createQueryAdapter(store);
57
63
  this.selection = createSelectionAdapter(store);
58
64
  this.visibility = createVisibilityAdapter(store);
59
65
  this.viewer = createViewerAdapter(store);
60
66
  this.mutate = createMutateAdapter(store);
67
+ this.store = createStoreAdapter(store);
61
68
  this.spatial = createSpatialAdapter(store);
62
69
  this.lens = createLensAdapter(store);
63
70
  this.export = createExportAdapter(store);
64
71
  this.files = createFilesAdapter(store);
72
+ this.schedule = createScheduleAdapter(store);
65
73
  }
66
74
 
67
75
  subscribe(event: BimEventType, handler: (data: unknown) => void): () => void {
68
76
  switch (event) {
69
77
  case 'selection:changed':
70
- return this.store.subscribe((state, prev) => {
78
+ return this.storeApi.subscribe((state, prev) => {
71
79
  if (state.selectedEntities !== prev.selectedEntities) {
72
80
  handler({ refs: state.selectedEntities ?? [] });
73
81
  }
74
82
  });
75
83
 
76
84
  case 'model:loaded':
77
- return this.store.subscribe((state, prev) => {
85
+ return this.storeApi.subscribe((state, prev) => {
78
86
  if (state.models.size > prev.models.size) {
79
87
  for (const [id, model] of state.models) {
80
88
  if (!prev.models.has(id)) {
@@ -108,7 +116,7 @@ export class LocalBackend implements BimBackend {
108
116
  });
109
117
 
110
118
  case 'model:removed':
111
- return this.store.subscribe((state, prev) => {
119
+ return this.storeApi.subscribe((state, prev) => {
112
120
  if (state.models.size < prev.models.size) {
113
121
  for (const id of prev.models.keys()) {
114
122
  if (!state.models.has(id)) {
@@ -119,7 +127,7 @@ export class LocalBackend implements BimBackend {
119
127
  });
120
128
 
121
129
  case 'visibility:changed':
122
- return this.store.subscribe((state, prev) => {
130
+ return this.storeApi.subscribe((state, prev) => {
123
131
  if (
124
132
  state.hiddenEntities !== prev.hiddenEntities ||
125
133
  state.isolatedEntities !== prev.isolatedEntities ||
@@ -130,14 +138,14 @@ export class LocalBackend implements BimBackend {
130
138
  });
131
139
 
132
140
  case 'mutation:changed':
133
- return this.store.subscribe((state, prev) => {
141
+ return this.storeApi.subscribe((state, prev) => {
134
142
  if (state.mutationVersion !== prev.mutationVersion) {
135
143
  handler({});
136
144
  }
137
145
  });
138
146
 
139
147
  case 'lens:changed':
140
- return this.store.subscribe((state, prev) => {
148
+ return this.storeApi.subscribe((state, prev) => {
141
149
  if (state.activeLensId !== prev.activeLensId) {
142
150
  handler({ lensId: state.activeLensId });
143
151
  }
@@ -10,7 +10,9 @@ import { readNativeFile } from '@/services/file-dialog';
10
10
  const exportHydrationByModel = new Map<string, Promise<IfcDataStore | null>>();
11
11
 
12
12
  function isDesktopRuntime(): boolean {
13
- const win = globalThis as Window & { __TAURI_INTERNALS__?: { invoke?: unknown } };
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 } };
14
16
  return typeof win.__TAURI_INTERNALS__?.invoke === 'function';
15
17
  }
16
18
 
@@ -6,10 +6,41 @@ import type {
6
6
  NativeMetadataEntityDetails,
7
7
  NativeMetadataEntitySummary,
8
8
  NativeMetadataSnapshot,
9
+ NativeMetadataSpatialNode,
9
10
  } from '@/store/types';
10
- import type { MetadataBootstrapPayload } from '@ifc-lite/geometry';
11
+ import type {
12
+ MetadataBootstrapPayload,
13
+ MetadataBootstrapEntitySummary,
14
+ MetadataBootstrapSpatialNode,
15
+ } from '@ifc-lite/geometry';
11
16
  import { getNativeModelSnapshot, setNativeModelSnapshot } from './desktop-cache';
12
17
 
18
+ function bootstrapKindToNative(kind: string): 'spatial' | 'element' {
19
+ return kind === 'spatial' ? 'spatial' : 'element';
20
+ }
21
+
22
+ function summaryFromBootstrap(node: MetadataBootstrapEntitySummary): NativeMetadataEntitySummary {
23
+ return {
24
+ expressId: node.expressId,
25
+ type: node.typeName,
26
+ name: node.name,
27
+ globalId: node.globalId ?? null,
28
+ kind: bootstrapKindToNative(node.kind),
29
+ hasChildren: node.hasChildren,
30
+ elementCount: node.elementCount,
31
+ elevation: node.elevation ?? null,
32
+ };
33
+ }
34
+
35
+ function spatialNodeFromBootstrap(node: MetadataBootstrapSpatialNode | null): NativeMetadataSpatialNode | null {
36
+ if (!node) return null;
37
+ return {
38
+ ...summaryFromBootstrap(node),
39
+ children: node.children.map((c) => spatialNodeFromBootstrap(c)).filter((n): n is NativeMetadataSpatialNode => n !== null),
40
+ elements: node.elements.map(summaryFromBootstrap),
41
+ };
42
+ }
43
+
13
44
  type InvokeFn = <T>(cmd: string, args?: Record<string, unknown>) => Promise<T>;
14
45
 
15
46
  async function getInvoke(): Promise<InvokeFn> {
@@ -21,13 +52,6 @@ async function getInvoke(): Promise<InvokeFn> {
21
52
  return core.invoke as InvokeFn;
22
53
  }
23
54
 
24
- interface NativeMetadataBootstrapPayload {
25
- cacheKey: string;
26
- schemaVersion: string;
27
- entityCount: number;
28
- spatialTree: NativeMetadataSnapshot['spatialTree'];
29
- }
30
-
31
55
  function toSchemaVersion(schemaVersion: string): NativeMetadataSnapshot['schemaVersion'] {
32
56
  if (schemaVersion === 'IFC4X3' || schemaVersion === 'IFC4' || schemaVersion === 'IFC5') {
33
57
  return schemaVersion;
@@ -45,24 +69,23 @@ export function nativeMetadataSnapshotFromBootstrap(
45
69
  filePath: path,
46
70
  schemaVersion: toSchemaVersion(payload.schemaVersion),
47
71
  entityCount: payload.entityCount,
48
- spatialTree: payload.spatialTree ?? null,
72
+ spatialTree: spatialNodeFromBootstrap(payload.spatialTree ?? null),
49
73
  };
50
74
  }
51
75
 
52
76
  export async function bootstrapNativeMetadata(path: string, cacheKey: string): Promise<NativeMetadataSnapshot> {
53
77
  const invoke = await getInvoke();
54
- const result = await invoke<NativeMetadataBootstrapPayload>('bootstrap_native_metadata', {
78
+ // The Tauri command returns a bootstrap-shaped payload (typeName /
79
+ // kind unfolded). Route through the shared `nativeMetadataSnapshotFromBootstrap`
80
+ // helper so both constructors apply the same `spatialNodeFromBootstrap`
81
+ // normalization — without this the `from cached snapshot` and
82
+ // `bootstrap fresh` paths produce subtly different shapes and the
83
+ // property panel reads break for the freshly-bootstrapped case.
84
+ const result = await invoke<MetadataBootstrapPayload>('bootstrap_native_metadata', {
55
85
  path,
56
86
  cacheKey,
57
87
  });
58
- return {
59
- mode: 'desktop-lazy',
60
- cacheKey: result.cacheKey,
61
- filePath: path,
62
- schemaVersion: toSchemaVersion(result.schemaVersion),
63
- entityCount: result.entityCount,
64
- spatialTree: result.spatialTree ?? null,
65
- };
88
+ return nativeMetadataSnapshotFromBootstrap(path, result);
66
89
  }
67
90
 
68
91
  export async function restoreNativeMetadataSnapshot(cacheKey: string): Promise<NativeMetadataSnapshot | null> {
@@ -139,7 +139,10 @@ export async function openGenericFileDialog(options: GenericFileDialogOptions =
139
139
  const bytes = await readNativeFile(normalizedPath);
140
140
  const pathSegments = normalizedPath.split(/[\\/]/);
141
141
  const name = pathSegments[pathSegments.length - 1] || 'document';
142
- return new File([bytes], name, { type: 'application/octet-stream' });
142
+ // Slice to a fresh ArrayBuffer view — TS5+ narrows `Uint8Array` to
143
+ // `Uint8Array<ArrayBufferLike>` which `BlobPart` doesn't accept.
144
+ const blobPart = new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength).slice();
145
+ return new File([blobPart], name, { type: 'application/octet-stream' });
143
146
  } catch (error) {
144
147
  console.warn('[FileDialog] Failed to open generic native file dialog:', error);
145
148
  return null;
@@ -17,9 +17,34 @@ declare module '@tauri-apps/plugin-fs' {
17
17
  export function exists(path: string): Promise<boolean>;
18
18
  export function remove(path: string): Promise<void>;
19
19
  export function readDir(path: string): Promise<Array<{ name: string | null }>>;
20
+ export function stat(path: string): Promise<{ size: number; mtime?: number | null }>;
20
21
  }
21
22
 
22
23
  declare module '@tauri-apps/api/path' {
23
24
  export function appDataDir(): Promise<string>;
24
25
  export function join(...paths: string[]): Promise<string>;
25
26
  }
27
+
28
+ declare module '@tauri-apps/api/core' {
29
+ /**
30
+ * Tauri's IPC entry point. Generic by command name.
31
+ */
32
+ export function invoke<T = unknown>(cmd: string, args?: Record<string, unknown>): Promise<T>;
33
+ }
34
+
35
+ declare module '@tauri-apps/plugin-dialog' {
36
+ export interface OpenDialogOptions {
37
+ title?: string;
38
+ multiple?: boolean;
39
+ directory?: boolean;
40
+ defaultPath?: string;
41
+ filters?: Array<{ name: string; extensions: string[] }>;
42
+ }
43
+ export interface SaveDialogOptions {
44
+ title?: string;
45
+ defaultPath?: string;
46
+ filters?: Array<{ name: string; extensions: string[] }>;
47
+ }
48
+ export function open(options?: OpenDialogOptions): Promise<string | string[] | null>;
49
+ export function save(options?: SaveDialogOptions): Promise<string | null>;
50
+ }
@@ -341,6 +341,9 @@ function collectVisibleCandidates(state: ViewerStateSnapshot): VisibleCandidate[
341
341
  if (state.models.size > 0) {
342
342
  for (const [modelId, model] of state.models) {
343
343
  if (!model.visible) continue;
344
+ // Native-metadata models have no parsed geometry result. Skip them
345
+ // — they can't contribute mesh-level visible candidates.
346
+ if (!model.geometryResult) continue;
344
347
  const offset = model.idOffset ?? 0;
345
348
  for (const mesh of model.geometryResult.meshes) {
346
349
  if (!matchesTypeVisibility(mesh.ifcType, state.typeVisibility)) continue;
@@ -4,7 +4,10 @@
4
4
 
5
5
  import type { EntityRef, FederatedModel } from './types.js';
6
6
 
7
- type ForwardModelMapLike = ReadonlyMap<string, { idOffset?: number }>;
7
+ /** Shape accepted by `toGlobalIdFromModels` re-export it so consumers can
8
+ * drop the `as unknown as Map<...>` cast when threading the store's
9
+ * federated `models` map through downstream helpers. */
10
+ export type ForwardModelMapLike = ReadonlyMap<string, { idOffset?: number }>;
8
11
  type ReverseModelMapLike = ReadonlyMap<string, Pick<FederatedModel, 'idOffset' | 'maxExpressId'>>;
9
12
 
10
13
  /**