@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.
- package/CHANGELOG.md +88 -0
- package/dist/assets/{Arrow.dom-BGPQieQQ.js → Arrow.dom-CusgkT03.js} +1 -1
- package/dist/assets/browser-BXNIkE8a.js +694 -0
- package/dist/assets/emscripten-module-BTRCZGcB.wasm +0 -0
- package/dist/assets/emscripten-module-CGIn_cMh.wasm +0 -0
- package/dist/assets/emscripten-module-DYvzWiHh.wasm +0 -0
- package/dist/assets/emscripten-module-NWak2PoB.wasm +0 -0
- package/dist/assets/emscripten-module.browser-CY5t0Vfq.js +1 -0
- package/dist/assets/esbuild-COv63sf-.js +1 -0
- package/dist/assets/esbuild-Cpd5nU_H.wasm +0 -0
- package/dist/assets/ffi-DlhRHxHv.js +1 -0
- package/dist/assets/index-6Mr3byM-.js +216 -0
- package/dist/assets/index-CGbokkQ9.css +1 -0
- package/dist/assets/index-huvR-kGC.js +98305 -0
- package/dist/assets/module-6F3E5H7Y-tx0BadV3.js +6 -0
- package/dist/assets/{native-bridge-DD0SNyQ5.js → native-bridge-DsHOKdgD.js} +1 -1
- package/dist/assets/{wasm-bridge-D54YMO7X.js → wasm-bridge-Bd73HXn-.js} +1 -1
- package/dist/index.html +12 -3
- package/index.html +10 -1
- package/package.json +30 -21
- package/src/App.tsx +6 -1
- package/src/components/ui/dialog.tsx +8 -6
- package/src/components/viewer/CodeEditor.tsx +309 -0
- package/src/components/viewer/CommandPalette.tsx +597 -0
- package/src/components/viewer/Drawing2DCanvas.tsx +364 -1
- package/src/components/viewer/EntityContextMenu.tsx +47 -20
- package/src/components/viewer/ExportDialog.tsx +166 -17
- package/src/components/viewer/HierarchyPanel.tsx +3 -1
- package/src/components/viewer/LensPanel.tsx +848 -85
- package/src/components/viewer/MainToolbar.tsx +145 -84
- package/src/components/viewer/ScriptPanel.tsx +416 -0
- package/src/components/viewer/Section2DPanel.tsx +269 -29
- package/src/components/viewer/TextAnnotationEditor.tsx +112 -0
- package/src/components/viewer/ViewerLayout.tsx +63 -11
- package/src/components/viewer/Viewport.tsx +58 -23
- package/src/components/viewer/ViewportContainer.tsx +2 -0
- package/src/components/viewer/hierarchy/HierarchyNode.tsx +1 -1
- package/src/components/viewer/hierarchy/types.ts +1 -1
- package/src/components/viewer/lists/ListResultsTable.tsx +53 -19
- package/src/components/viewer/tools/cloudPathGenerator.test.ts +118 -0
- package/src/components/viewer/tools/cloudPathGenerator.ts +275 -0
- package/src/components/viewer/tools/computePolygonArea.test.ts +165 -0
- package/src/components/viewer/tools/computePolygonArea.ts +72 -0
- package/src/components/viewer/useGeometryStreaming.ts +25 -5
- package/src/hooks/ids/idsExportService.ts +1 -1
- package/src/hooks/useAnnotation2D.ts +551 -0
- package/src/hooks/useDrawingExport.ts +83 -1
- package/src/hooks/useKeyboardShortcuts.ts +114 -14
- package/src/hooks/useLens.ts +40 -55
- package/src/hooks/useLensDiscovery.ts +46 -0
- package/src/hooks/useModelSelection.ts +5 -22
- package/src/hooks/useSandbox.ts +113 -0
- package/src/index.css +7 -1
- package/src/lib/lens/adapter.ts +127 -1
- package/src/lib/lists/columnToAutoColor.ts +33 -0
- package/src/lib/recent-files.ts +122 -0
- package/src/lib/scripts/persistence.ts +132 -0
- package/src/lib/scripts/templates/bim-globals.d.ts +111 -0
- package/src/lib/scripts/templates/data-quality-audit.ts +149 -0
- package/src/lib/scripts/templates/envelope-check.ts +164 -0
- package/src/lib/scripts/templates/federation-compare.ts +189 -0
- package/src/lib/scripts/templates/fire-safety-check.ts +161 -0
- package/src/lib/scripts/templates/mep-equipment-schedule.ts +175 -0
- package/src/lib/scripts/templates/quantity-takeoff.ts +145 -0
- package/src/lib/scripts/templates/reset-view.ts +6 -0
- package/src/lib/scripts/templates/space-validation.ts +189 -0
- package/src/lib/scripts/templates/tsconfig.json +13 -0
- package/src/lib/scripts/templates.ts +86 -0
- package/src/sdk/BimProvider.tsx +50 -0
- package/src/sdk/adapters/export-adapter.ts +283 -0
- package/src/sdk/adapters/lens-adapter.ts +44 -0
- package/src/sdk/adapters/model-adapter.ts +32 -0
- package/src/sdk/adapters/model-compat.ts +80 -0
- package/src/sdk/adapters/mutate-adapter.ts +45 -0
- package/src/sdk/adapters/query-adapter.ts +241 -0
- package/src/sdk/adapters/selection-adapter.ts +29 -0
- package/src/sdk/adapters/spatial-adapter.ts +37 -0
- package/src/sdk/adapters/types.ts +11 -0
- package/src/sdk/adapters/viewer-adapter.ts +103 -0
- package/src/sdk/adapters/visibility-adapter.ts +61 -0
- package/src/sdk/local-backend.ts +144 -0
- package/src/sdk/useBimHost.ts +69 -0
- package/src/store/constants.ts +10 -2
- package/src/store/index.ts +28 -2
- package/src/store/resolveEntityRef.ts +44 -0
- package/src/store/slices/drawing2DSlice.ts +321 -0
- package/src/store/slices/lensSlice.ts +46 -4
- package/src/store/slices/pinboardSlice.ts +171 -42
- package/src/store/slices/scriptSlice.ts +218 -0
- package/src/store/slices/uiSlice.ts +2 -0
- package/src/store.ts +3 -0
- package/tsconfig.json +5 -2
- package/vite.config.ts +8 -0
- package/dist/assets/index-dgdgiQ9p.js +0 -75456
- 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
|
+
}
|