@ifc-lite/viewer 1.7.0 → 1.9.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 (95) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/dist/assets/{Arrow.dom-BGPQieQQ.js → Arrow.dom-CusgkT03.js} +1 -1
  3. package/dist/assets/browser-BXNIkE8a.js +694 -0
  4. package/dist/assets/emscripten-module-BTRCZGcB.wasm +0 -0
  5. package/dist/assets/emscripten-module-CGIn_cMh.wasm +0 -0
  6. package/dist/assets/emscripten-module-DYvzWiHh.wasm +0 -0
  7. package/dist/assets/emscripten-module-NWak2PoB.wasm +0 -0
  8. package/dist/assets/emscripten-module.browser-CY5t0Vfq.js +1 -0
  9. package/dist/assets/esbuild-COv63sf-.js +1 -0
  10. package/dist/assets/esbuild-Cpd5nU_H.wasm +0 -0
  11. package/dist/assets/ffi-DlhRHxHv.js +1 -0
  12. package/dist/assets/index-6Mr3byM-.js +216 -0
  13. package/dist/assets/index-CGbokkQ9.css +1 -0
  14. package/dist/assets/index-huvR-kGC.js +98305 -0
  15. package/dist/assets/module-6F3E5H7Y-tx0BadV3.js +6 -0
  16. package/dist/assets/{native-bridge-DD0SNyQ5.js → native-bridge-DsHOKdgD.js} +1 -1
  17. package/dist/assets/{wasm-bridge-D54YMO7X.js → wasm-bridge-Bd73HXn-.js} +1 -1
  18. package/dist/index.html +12 -3
  19. package/index.html +10 -1
  20. package/package.json +30 -21
  21. package/src/App.tsx +6 -1
  22. package/src/components/ui/dialog.tsx +8 -6
  23. package/src/components/viewer/CodeEditor.tsx +309 -0
  24. package/src/components/viewer/CommandPalette.tsx +597 -0
  25. package/src/components/viewer/Drawing2DCanvas.tsx +364 -1
  26. package/src/components/viewer/EntityContextMenu.tsx +47 -20
  27. package/src/components/viewer/ExportDialog.tsx +166 -17
  28. package/src/components/viewer/HierarchyPanel.tsx +3 -1
  29. package/src/components/viewer/LensPanel.tsx +848 -85
  30. package/src/components/viewer/MainToolbar.tsx +145 -84
  31. package/src/components/viewer/ScriptPanel.tsx +416 -0
  32. package/src/components/viewer/Section2DPanel.tsx +269 -29
  33. package/src/components/viewer/TextAnnotationEditor.tsx +112 -0
  34. package/src/components/viewer/ViewerLayout.tsx +63 -11
  35. package/src/components/viewer/Viewport.tsx +58 -23
  36. package/src/components/viewer/ViewportContainer.tsx +2 -0
  37. package/src/components/viewer/hierarchy/HierarchyNode.tsx +1 -1
  38. package/src/components/viewer/hierarchy/types.ts +1 -1
  39. package/src/components/viewer/lists/ListResultsTable.tsx +53 -19
  40. package/src/components/viewer/tools/cloudPathGenerator.test.ts +118 -0
  41. package/src/components/viewer/tools/cloudPathGenerator.ts +275 -0
  42. package/src/components/viewer/tools/computePolygonArea.test.ts +165 -0
  43. package/src/components/viewer/tools/computePolygonArea.ts +72 -0
  44. package/src/components/viewer/useGeometryStreaming.ts +25 -5
  45. package/src/hooks/ids/idsExportService.ts +1 -1
  46. package/src/hooks/useAnnotation2D.ts +551 -0
  47. package/src/hooks/useDrawingExport.ts +83 -1
  48. package/src/hooks/useKeyboardShortcuts.ts +114 -14
  49. package/src/hooks/useLens.ts +40 -55
  50. package/src/hooks/useLensDiscovery.ts +46 -0
  51. package/src/hooks/useModelSelection.ts +5 -22
  52. package/src/hooks/useSandbox.ts +113 -0
  53. package/src/index.css +7 -1
  54. package/src/lib/lens/adapter.ts +127 -1
  55. package/src/lib/lists/columnToAutoColor.ts +33 -0
  56. package/src/lib/recent-files.ts +122 -0
  57. package/src/lib/scripts/persistence.ts +132 -0
  58. package/src/lib/scripts/templates/bim-globals.d.ts +111 -0
  59. package/src/lib/scripts/templates/data-quality-audit.ts +149 -0
  60. package/src/lib/scripts/templates/envelope-check.ts +164 -0
  61. package/src/lib/scripts/templates/federation-compare.ts +189 -0
  62. package/src/lib/scripts/templates/fire-safety-check.ts +161 -0
  63. package/src/lib/scripts/templates/mep-equipment-schedule.ts +175 -0
  64. package/src/lib/scripts/templates/quantity-takeoff.ts +145 -0
  65. package/src/lib/scripts/templates/reset-view.ts +6 -0
  66. package/src/lib/scripts/templates/space-validation.ts +189 -0
  67. package/src/lib/scripts/templates/tsconfig.json +13 -0
  68. package/src/lib/scripts/templates.ts +86 -0
  69. package/src/sdk/BimProvider.tsx +50 -0
  70. package/src/sdk/adapters/export-adapter.ts +283 -0
  71. package/src/sdk/adapters/lens-adapter.ts +44 -0
  72. package/src/sdk/adapters/model-adapter.ts +32 -0
  73. package/src/sdk/adapters/model-compat.ts +80 -0
  74. package/src/sdk/adapters/mutate-adapter.ts +45 -0
  75. package/src/sdk/adapters/query-adapter.ts +241 -0
  76. package/src/sdk/adapters/selection-adapter.ts +29 -0
  77. package/src/sdk/adapters/spatial-adapter.ts +37 -0
  78. package/src/sdk/adapters/types.ts +11 -0
  79. package/src/sdk/adapters/viewer-adapter.ts +103 -0
  80. package/src/sdk/adapters/visibility-adapter.ts +61 -0
  81. package/src/sdk/local-backend.ts +144 -0
  82. package/src/sdk/useBimHost.ts +69 -0
  83. package/src/store/constants.ts +10 -2
  84. package/src/store/index.ts +28 -2
  85. package/src/store/resolveEntityRef.ts +44 -0
  86. package/src/store/slices/drawing2DSlice.ts +321 -0
  87. package/src/store/slices/lensSlice.ts +46 -4
  88. package/src/store/slices/pinboardSlice.ts +171 -42
  89. package/src/store/slices/scriptSlice.ts +218 -0
  90. package/src/store/slices/uiSlice.ts +2 -0
  91. package/src/store.ts +3 -0
  92. package/tsconfig.json +5 -2
  93. package/vite.config.ts +8 -0
  94. package/dist/assets/index-dgdgiQ9p.js +0 -75456
  95. package/dist/assets/index-yTqs8kgX.css +0 -1
@@ -0,0 +1,283 @@
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
+ import type { StoreApi } from './types.js';
6
+ import type { EntityRef, EntityData, PropertySetData, QuantitySetData, ExportBackendMethods } from '@ifc-lite/sdk';
7
+ import { EntityNode } from '@ifc-lite/query';
8
+ import { getModelForRef } from './model-compat.js';
9
+
10
+ /** Options for CSV export */
11
+ interface CsvOptions {
12
+ columns: string[];
13
+ separator?: string;
14
+ filename?: string;
15
+ }
16
+
17
+ /**
18
+ * Validate that a value is a CsvOptions object.
19
+ */
20
+ function isCsvOptions(v: unknown): v is CsvOptions {
21
+ if (v === null || typeof v !== 'object' || !('columns' in v)) return false;
22
+ const columns = (v as CsvOptions).columns;
23
+ if (!Array.isArray(columns)) return false;
24
+ // Validate all column entries are strings
25
+ return columns.every((c): c is string => typeof c === 'string');
26
+ }
27
+
28
+ /**
29
+ * Validate that a value is an array of EntityRef objects.
30
+ */
31
+ function isEntityRefArray(v: unknown): v is EntityRef[] {
32
+ if (!Array.isArray(v)) return false;
33
+ if (v.length === 0) return true;
34
+ const first = v[0] as Record<string, unknown>;
35
+ // Accept both raw EntityRef and entity proxy objects with .ref
36
+ if ('modelId' in first && 'expressId' in first) {
37
+ return typeof first.modelId === 'string' && typeof first.expressId === 'number';
38
+ }
39
+ if ('ref' in first && first.ref !== null && typeof first.ref === 'object') {
40
+ const ref = first.ref as Record<string, unknown>;
41
+ return typeof ref.modelId === 'string' && typeof ref.expressId === 'number';
42
+ }
43
+ return false;
44
+ }
45
+
46
+ /**
47
+ * Normalize entity refs — entities from the sandbox may be EntityData
48
+ * objects with a .ref property, or raw EntityRef { modelId, expressId }.
49
+ */
50
+ function normalizeRefs(raw: unknown[]): EntityRef[] {
51
+ return raw.map((item) => {
52
+ const r = item as Record<string, unknown>;
53
+ if (r.ref && typeof r.ref === 'object') {
54
+ return r.ref as EntityRef;
55
+ }
56
+ return { modelId: r.modelId as string, expressId: r.expressId as number };
57
+ });
58
+ }
59
+
60
+ /**
61
+ * Escape a CSV cell value — wrap in quotes if it contains the separator,
62
+ * double-quotes, or newlines.
63
+ */
64
+ function escapeCsv(value: string, sep: string): string {
65
+ if (value.includes(sep) || value.includes('"') || value.includes('\n')) {
66
+ return `"${value.replace(/"/g, '""')}"`;
67
+ }
68
+ return value;
69
+ }
70
+
71
+ /**
72
+ * Export adapter — implements CSV and JSON export directly.
73
+ *
74
+ * This adapter resolves entity data by dispatching to the query adapter
75
+ * on the same LocalBackend, providing full export support for both
76
+ * direct dispatch calls and SDK namespace usage.
77
+ */
78
+ export function createExportAdapter(store: StoreApi): ExportBackendMethods {
79
+ /** Resolve entity data via the query subsystem */
80
+ function getEntityData(ref: EntityRef): EntityData | null {
81
+ const state = store.getState();
82
+ const model = getModelForRef(state, ref.modelId);
83
+ if (!model?.ifcDataStore) return null;
84
+
85
+ const node = new EntityNode(model.ifcDataStore, ref.expressId);
86
+ return {
87
+ ref,
88
+ globalId: node.globalId,
89
+ name: node.name,
90
+ type: node.type,
91
+ description: node.description,
92
+ objectType: node.objectType,
93
+ };
94
+ }
95
+
96
+ /** Resolve property sets for an entity */
97
+ function getProperties(ref: EntityRef): PropertySetData[] {
98
+ const state = store.getState();
99
+ const model = getModelForRef(state, ref.modelId);
100
+ if (!model?.ifcDataStore) return [];
101
+
102
+ const node = new EntityNode(model.ifcDataStore, ref.expressId);
103
+ return node.properties().map((pset: { name: string; globalId?: string; properties: Array<{ name: string; type: number; value: string | number | boolean | null }> }) => ({
104
+ name: pset.name,
105
+ globalId: pset.globalId,
106
+ properties: pset.properties.map((p: { name: string; type: number; value: string | number | boolean | null }) => ({
107
+ name: p.name,
108
+ type: p.type,
109
+ value: p.value,
110
+ })),
111
+ }));
112
+ }
113
+
114
+ /** Resolve quantity sets for an entity */
115
+ function getQuantities(ref: EntityRef): QuantitySetData[] {
116
+ const state = store.getState();
117
+ const model = getModelForRef(state, ref.modelId);
118
+ if (!model?.ifcDataStore) return [];
119
+
120
+ const node = new EntityNode(model.ifcDataStore, ref.expressId);
121
+ return node.quantities().map((qset: { name: string; quantities: Array<{ name: string; type: number; value: number }> }) => ({
122
+ name: qset.name,
123
+ quantities: qset.quantities.map((q: { name: string; type: number; value: number }) => ({
124
+ name: q.name,
125
+ type: q.type,
126
+ value: q.value,
127
+ })),
128
+ }));
129
+ }
130
+
131
+ /** Resolve a single column value from entity data + properties + quantities.
132
+ * Accepts both IFC PascalCase (Name, GlobalId) and legacy camelCase (name, globalId).
133
+ * Dot-path columns (e.g. "Pset_WallCommon.FireRating" or "Qto_WallBaseQuantities.GrossVolume")
134
+ * resolve against property sets first, then quantity sets. */
135
+ function resolveColumnValue(
136
+ data: EntityData,
137
+ col: string,
138
+ getProps: () => PropertySetData[],
139
+ getQties: () => QuantitySetData[],
140
+ ): string {
141
+ // IFC schema attribute names (PascalCase) + legacy camelCase
142
+ switch (col) {
143
+ case 'Name': case 'name': return data.name;
144
+ case 'Type': case 'type': return data.type;
145
+ case 'GlobalId': case 'globalId': return data.globalId;
146
+ case 'Description': case 'description': return data.description;
147
+ case 'ObjectType': case 'objectType': return data.objectType;
148
+ case 'modelId': return data.ref.modelId;
149
+ case 'expressId': return String(data.ref.expressId);
150
+ }
151
+
152
+ // Property/Quantity path: "SetName.ValueName"
153
+ const dotIdx = col.indexOf('.');
154
+ if (dotIdx > 0) {
155
+ const setName = col.slice(0, dotIdx);
156
+ const valueName = col.slice(dotIdx + 1);
157
+
158
+ // Try property sets first
159
+ const psets = getProps();
160
+ const pset = psets.find(p => p.name === setName);
161
+ if (pset) {
162
+ const prop = pset.properties.find(p => p.name === valueName);
163
+ if (prop?.value != null) return String(prop.value);
164
+ }
165
+
166
+ // Fall back to quantity sets
167
+ const qsets = getQties();
168
+ const qset = qsets.find(q => q.name === setName);
169
+ if (qset) {
170
+ const qty = qset.quantities.find(q => q.name === valueName);
171
+ if (qty?.value != null) return String(qty.value);
172
+ }
173
+
174
+ return '';
175
+ }
176
+
177
+ return '';
178
+ }
179
+
180
+ return {
181
+ csv(rawRefs: unknown, rawOptions: unknown) {
182
+ if (!isEntityRefArray(rawRefs)) {
183
+ throw new Error('export.csv: first argument must be an array of entity references');
184
+ }
185
+ if (!isCsvOptions(rawOptions)) {
186
+ throw new Error('export.csv: second argument must be { columns: string[], separator?: string }');
187
+ }
188
+
189
+ const refs = normalizeRefs(rawRefs);
190
+ const options = rawOptions;
191
+ const sep = options.separator ?? ',';
192
+ const rows: string[][] = [];
193
+
194
+ // Header row
195
+ rows.push(options.columns);
196
+
197
+ // Data rows
198
+ for (const ref of refs) {
199
+ const data = getEntityData(ref);
200
+ if (!data) continue;
201
+
202
+ // Lazy-load properties/quantities only if a column needs them
203
+ let cachedProps: PropertySetData[] | null = null;
204
+ const getProps = (): PropertySetData[] => {
205
+ if (!cachedProps) cachedProps = getProperties(ref);
206
+ return cachedProps;
207
+ };
208
+ let cachedQties: QuantitySetData[] | null = null;
209
+ const getQties = (): QuantitySetData[] => {
210
+ if (!cachedQties) cachedQties = getQuantities(ref);
211
+ return cachedQties;
212
+ };
213
+
214
+ const row = options.columns.map(col => resolveColumnValue(data, col, getProps, getQties));
215
+ rows.push(row);
216
+ }
217
+
218
+ const csvString = rows.map(r => r.map(cell => escapeCsv(cell, sep)).join(sep)).join('\n');
219
+
220
+ // If filename specified, trigger browser download
221
+ if (options.filename) {
222
+ triggerDownload(csvString, options.filename, 'text/csv;charset=utf-8;');
223
+ }
224
+
225
+ return csvString;
226
+ },
227
+
228
+ json(rawRefs: unknown, columns: unknown) {
229
+ if (!isEntityRefArray(rawRefs)) {
230
+ throw new Error('export.json: first argument must be an array of entity references');
231
+ }
232
+ if (!Array.isArray(columns)) {
233
+ throw new Error('export.json: second argument must be a string[] of column names');
234
+ }
235
+
236
+ const refs = normalizeRefs(rawRefs);
237
+ const result: Record<string, unknown>[] = [];
238
+
239
+ for (const ref of refs) {
240
+ const data = getEntityData(ref);
241
+ if (!data) continue;
242
+
243
+ let cachedProps: PropertySetData[] | null = null;
244
+ const getProps = (): PropertySetData[] => {
245
+ if (!cachedProps) cachedProps = getProperties(ref);
246
+ return cachedProps;
247
+ };
248
+ let cachedQties: QuantitySetData[] | null = null;
249
+ const getQties = (): QuantitySetData[] => {
250
+ if (!cachedQties) cachedQties = getQuantities(ref);
251
+ return cachedQties;
252
+ };
253
+
254
+ const row: Record<string, unknown> = {};
255
+ for (const col of columns as string[]) {
256
+ const value = resolveColumnValue(data, col, getProps, getQties);
257
+ // Try to parse numeric values
258
+ const numVal = Number(value);
259
+ row[col] = value === '' ? null : !isNaN(numVal) && value.trim() !== '' ? numVal : value;
260
+ }
261
+ result.push(row);
262
+ }
263
+
264
+ return result;
265
+ },
266
+
267
+ download(content: string, filename: string, mimeType?: string) {
268
+ triggerDownload(content, filename, mimeType ?? 'text/plain');
269
+ return undefined;
270
+ },
271
+ };
272
+ }
273
+
274
+ /** Trigger a browser file download */
275
+ function triggerDownload(content: string, filename: string, mimeType: string): void {
276
+ const blob = new Blob([content], { type: mimeType });
277
+ const url = URL.createObjectURL(blob);
278
+ const a = document.createElement('a');
279
+ a.href = url;
280
+ a.download = filename;
281
+ a.click();
282
+ URL.revokeObjectURL(url);
283
+ }
@@ -0,0 +1,44 @@
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
+ import type { LensBackendMethods } from '@ifc-lite/sdk';
6
+ import type { StoreApi } from './types.js';
7
+ import { BUILTIN_LENSES } from '@ifc-lite/lens';
8
+
9
+ /** Type guard for lens config object */
10
+ function isLensConfig(v: unknown): v is Record<string, unknown> {
11
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
12
+ }
13
+
14
+ export function createLensAdapter(store: StoreApi): LensBackendMethods {
15
+ return {
16
+ presets() {
17
+ return BUILTIN_LENSES;
18
+ },
19
+ create(config: unknown) {
20
+ if (!isLensConfig(config)) {
21
+ throw new Error('lens.create: argument must be a lens configuration object');
22
+ }
23
+ const id = crypto.randomUUID();
24
+ return { ...config, id };
25
+ },
26
+ activate(lensId: unknown) {
27
+ if (typeof lensId !== 'string') {
28
+ throw new Error('lens.activate: argument must be a lens ID string');
29
+ }
30
+ const state = store.getState();
31
+ state.setActiveLens?.(lensId);
32
+ return undefined;
33
+ },
34
+ deactivate() {
35
+ const state = store.getState();
36
+ state.setActiveLens?.(null);
37
+ return undefined;
38
+ },
39
+ getActive() {
40
+ const state = store.getState();
41
+ return state.activeLensId ?? null;
42
+ },
43
+ };
44
+ }
@@ -0,0 +1,32 @@
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
+ import type { ModelInfo, ModelBackendMethods } from '@ifc-lite/sdk';
6
+ import type { StoreApi } from './types.js';
7
+ import { getAllModelEntries, LEGACY_MODEL_ID } from './model-compat.js';
8
+
9
+ export function createModelAdapter(store: StoreApi): ModelBackendMethods {
10
+ return {
11
+ list() {
12
+ const state = store.getState();
13
+ const result: ModelInfo[] = [];
14
+ for (const [, model] of getAllModelEntries(state)) {
15
+ result.push({
16
+ id: model.id,
17
+ name: model.name,
18
+ schemaVersion: model.schemaVersion,
19
+ entityCount: model.ifcDataStore?.entities?.count ?? 0,
20
+ fileSize: model.fileSize,
21
+ loadedAt: model.loadedAt,
22
+ });
23
+ }
24
+ return result;
25
+ },
26
+ activeId() {
27
+ const state = store.getState();
28
+ // For legacy single-model, return the sentinel ID when no active model is set
29
+ return state.activeModelId ?? (state.models.size === 0 && state.ifcDataStore ? LEGACY_MODEL_ID : null);
30
+ },
31
+ };
32
+ }
@@ -0,0 +1,80 @@
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
+ * Legacy single-model compatibility layer.
7
+ *
8
+ * The viewer has two loading paths:
9
+ * - Single file: `loadFile()` stores data in `state.ifcDataStore` / `state.geometryResult`
10
+ * - Multi-model: `addModel()` stores each model in `state.models` Map
11
+ *
12
+ * SDK adapters need to query entities regardless of which path was used.
13
+ * These helpers provide a unified view by falling back to the legacy
14
+ * single-model state when the `models` Map is empty.
15
+ */
16
+
17
+ import type { IfcDataStore } from '@ifc-lite/parser';
18
+ import type { SchemaVersion } from '@ifc-lite/sdk';
19
+ import type { ViewerState } from '../../store/index.js';
20
+
21
+ /** Sentinel model ID used for the legacy single-model path */
22
+ export const LEGACY_MODEL_ID = 'default';
23
+
24
+ /** Minimal model shape needed by the SDK adapters */
25
+ export interface ModelLike {
26
+ id: string;
27
+ name: string;
28
+ ifcDataStore: IfcDataStore;
29
+ schemaVersion: SchemaVersion;
30
+ fileSize: number;
31
+ loadedAt: number;
32
+ idOffset: number;
33
+ maxExpressId: number;
34
+ }
35
+
36
+ /**
37
+ * Resolve a model by ID — checks the multi-model Map first,
38
+ * then falls back to the legacy single-model state.
39
+ */
40
+ export function getModelForRef(state: ViewerState, modelId: string): ModelLike | undefined {
41
+ const model = state.models.get(modelId);
42
+ if (model) return model;
43
+
44
+ // Legacy single-model fallback
45
+ if (modelId === LEGACY_MODEL_ID && state.models.size === 0 && state.ifcDataStore) {
46
+ return buildLegacyModel(state.ifcDataStore);
47
+ }
48
+
49
+ return undefined;
50
+ }
51
+
52
+ /**
53
+ * List all model entries — from the multi-model Map or the legacy state.
54
+ * Returns [modelId, model][] pairs.
55
+ */
56
+ export function getAllModelEntries(state: ViewerState): [string, ModelLike][] {
57
+ if (state.models.size > 0) {
58
+ return [...state.models.entries()];
59
+ }
60
+
61
+ // Legacy single-model fallback
62
+ if (state.ifcDataStore) {
63
+ return [[LEGACY_MODEL_ID, buildLegacyModel(state.ifcDataStore)]];
64
+ }
65
+
66
+ return [];
67
+ }
68
+
69
+ function buildLegacyModel(dataStore: IfcDataStore): ModelLike {
70
+ return {
71
+ id: LEGACY_MODEL_ID,
72
+ name: 'Model',
73
+ ifcDataStore: dataStore,
74
+ schemaVersion: dataStore.schemaVersion ?? 'IFC4',
75
+ fileSize: dataStore.source?.byteLength ?? 0,
76
+ loadedAt: 0,
77
+ idOffset: 0,
78
+ maxExpressId: dataStore.entities?.count ?? 0,
79
+ };
80
+ }
@@ -0,0 +1,45 @@
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
+ import type { EntityRef, MutateBackendMethods } from '@ifc-lite/sdk';
6
+ import type { StoreApi } from './types.js';
7
+
8
+ export function createMutateAdapter(store: StoreApi): MutateBackendMethods {
9
+ return {
10
+ setProperty(ref: EntityRef, psetName: string, propName: string, value: string | number | boolean) {
11
+ const state = store.getState();
12
+ state.setProperty?.(ref.modelId, ref.expressId, psetName, propName, value);
13
+ return undefined;
14
+ },
15
+ deleteProperty(ref: EntityRef, psetName: string, propName: string) {
16
+ const state = store.getState();
17
+ state.deleteProperty?.(ref.modelId, ref.expressId, psetName, propName);
18
+ return undefined;
19
+ },
20
+ undo(modelId: string) {
21
+ const state = store.getState();
22
+ if (state.canUndo?.(modelId)) {
23
+ state.undo?.(modelId);
24
+ return true;
25
+ }
26
+ return false;
27
+ },
28
+ redo(modelId: string) {
29
+ const state = store.getState();
30
+ if (state.canRedo?.(modelId)) {
31
+ state.redo?.(modelId);
32
+ return true;
33
+ }
34
+ return false;
35
+ },
36
+ batchBegin() {
37
+ // TODO: Implement batch grouping when the mutation store supports it.
38
+ // For now, individual mutations each create their own undo step.
39
+ return undefined;
40
+ },
41
+ batchEnd() {
42
+ return undefined;
43
+ },
44
+ };
45
+ }