@ifc-lite/viewer 1.6.0 → 1.7.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 +78 -0
- package/dist/assets/{Arrow.dom-BjDQoB2M.js → Arrow.dom-BGPQieQQ.js} +1 -1
- package/dist/assets/ifc-lite_bg-DyIN_nBM.wasm +0 -0
- package/dist/assets/{index-YBtrHPu3.js → index-dgdgiQ9p.js} +40212 -30008
- package/dist/assets/index-yTqs8kgX.css +1 -0
- package/dist/assets/{native-bridge-CULtTDX3.js → native-bridge-DD0SNyQ5.js} +1 -1
- package/dist/assets/{wasm-bridge-CjL-lSak.js → wasm-bridge-D54YMO7X.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +18 -15
- package/src/components/viewer/BCFPanel.tsx +7 -789
- package/src/components/viewer/Drawing2DCanvas.tsx +1048 -0
- package/src/components/viewer/DrawingSettingsPanel.tsx +3 -3
- package/src/components/viewer/HierarchyPanel.tsx +110 -842
- package/src/components/viewer/IDSExportDialog.tsx +281 -0
- package/src/components/viewer/IDSPanel.tsx +126 -17
- package/src/components/viewer/KeyboardShortcutsDialog.tsx +9 -0
- package/src/components/viewer/LensPanel.tsx +603 -0
- package/src/components/viewer/MainToolbar.tsx +188 -21
- package/src/components/viewer/PropertiesPanel.tsx +171 -663
- package/src/components/viewer/PropertyEditor.tsx +866 -77
- package/src/components/viewer/Section2DPanel.tsx +76 -2648
- package/src/components/viewer/ToolOverlays.tsx +3 -1097
- package/src/components/viewer/ViewerLayout.tsx +132 -45
- package/src/components/viewer/Viewport.tsx +237 -1659
- package/src/components/viewer/ViewportContainer.tsx +11 -3
- package/src/components/viewer/bcf/BCFCreateTopicForm.tsx +134 -0
- package/src/components/viewer/bcf/BCFTopicDetail.tsx +388 -0
- package/src/components/viewer/bcf/BCFTopicList.tsx +239 -0
- package/src/components/viewer/bcf/bcfHelpers.tsx +109 -0
- package/src/components/viewer/hierarchy/HierarchyNode.tsx +328 -0
- package/src/components/viewer/hierarchy/treeDataBuilder.ts +464 -0
- package/src/components/viewer/hierarchy/types.ts +54 -0
- package/src/components/viewer/hierarchy/useHierarchyTree.ts +280 -0
- package/src/components/viewer/lists/ListBuilder.tsx +486 -0
- package/src/components/viewer/lists/ListPanel.tsx +540 -0
- package/src/components/viewer/lists/ListResultsTable.tsx +193 -0
- package/src/components/viewer/properties/ClassificationCard.tsx +70 -0
- package/src/components/viewer/properties/CoordinateDisplay.tsx +49 -0
- package/src/components/viewer/properties/DocumentCard.tsx +89 -0
- package/src/components/viewer/properties/MaterialCard.tsx +201 -0
- package/src/components/viewer/properties/ModelMetadataPanel.tsx +335 -0
- package/src/components/viewer/properties/PropertySetCard.tsx +132 -0
- package/src/components/viewer/properties/QuantitySetCard.tsx +79 -0
- package/src/components/viewer/properties/RelationshipsCard.tsx +100 -0
- package/src/components/viewer/properties/encodingUtils.ts +29 -0
- package/src/components/viewer/tools/MeasurePanel.tsx +218 -0
- package/src/components/viewer/tools/MeasurementVisuals.tsx +644 -0
- package/src/components/viewer/tools/SectionPanel.tsx +183 -0
- package/src/components/viewer/tools/SectionVisualization.tsx +78 -0
- package/src/components/viewer/tools/formatDistance.ts +18 -0
- package/src/components/viewer/tools/sectionConstants.ts +14 -0
- package/src/components/viewer/useAnimationLoop.ts +166 -0
- package/src/components/viewer/useGeometryStreaming.ts +398 -0
- package/src/components/viewer/useKeyboardControls.ts +221 -0
- package/src/components/viewer/useMouseControls.ts +1009 -0
- package/src/components/viewer/useRenderUpdates.ts +165 -0
- package/src/components/viewer/useTouchControls.ts +245 -0
- package/src/hooks/ids/idsColorSystem.ts +125 -0
- package/src/hooks/ids/idsDataAccessor.ts +237 -0
- package/src/hooks/ids/idsExportService.ts +444 -0
- package/src/hooks/useBCF.ts +7 -0
- package/src/hooks/useDrawingExport.ts +627 -0
- package/src/hooks/useDrawingGeneration.ts +627 -0
- package/src/hooks/useFloorplanView.ts +108 -0
- package/src/hooks/useIDS.ts +270 -463
- package/src/hooks/useIfc.ts +26 -1628
- package/src/hooks/useIfcFederation.ts +803 -0
- package/src/hooks/useIfcLoader.ts +508 -0
- package/src/hooks/useIfcServer.ts +465 -0
- package/src/hooks/useKeyboardShortcuts.ts +1 -1
- package/src/hooks/useLens.ts +129 -0
- package/src/hooks/useMeasure2D.ts +365 -0
- package/src/hooks/useViewControls.ts +218 -0
- package/src/lib/ifc4-pset-definitions.test.ts +161 -0
- package/src/lib/ifc4-pset-definitions.ts +621 -0
- package/src/lib/ifc4-qto-definitions.ts +315 -0
- package/src/lib/lens/adapter.ts +138 -0
- package/src/lib/lens/index.ts +5 -0
- package/src/lib/lists/adapter.ts +69 -0
- package/src/lib/lists/index.ts +28 -0
- package/src/lib/lists/persistence.ts +64 -0
- package/src/services/fs-cache.ts +1 -1
- package/src/services/tauri-modules.d.ts +25 -0
- package/src/store/index.ts +38 -2
- package/src/store/slices/cameraSlice.ts +14 -1
- package/src/store/slices/dataSlice.ts +14 -1
- package/src/store/slices/lensSlice.ts +184 -0
- package/src/store/slices/listSlice.ts +74 -0
- package/src/store/slices/pinboardSlice.ts +114 -0
- package/src/store/types.ts +5 -0
- package/src/utils/ifcConfig.ts +16 -3
- package/src/utils/serverDataModel.ts +64 -101
- package/src/vite-env.d.ts +3 -0
- package/dist/assets/ifc-lite_bg-C6kblxf9.wasm +0 -0
- package/dist/assets/index-v3mcCUPN.css +0 -1
|
@@ -0,0 +1,803 @@
|
|
|
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
|
+
* Hook for multi-model federation operations
|
|
7
|
+
* Handles addModel, removeModel, ID offset management, RTC alignment,
|
|
8
|
+
* IFCX federated layer composition, and legacy model migration
|
|
9
|
+
*
|
|
10
|
+
* Extracted from useIfc.ts for better separation of concerns
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { useCallback } from 'react';
|
|
14
|
+
import { useViewerStore, type FederatedModel, type SchemaVersion } from '../store.js';
|
|
15
|
+
import { IfcParser, detectFormat, parseIfcx, parseFederatedIfcx, type IfcDataStore, type FederatedIfcxParseResult } from '@ifc-lite/parser';
|
|
16
|
+
import { GeometryProcessor, GeometryQuality, type MeshData, type CoordinateInfo } from '@ifc-lite/geometry';
|
|
17
|
+
import { IfcQuery } from '@ifc-lite/query';
|
|
18
|
+
import { buildSpatialIndex } from '@ifc-lite/spatial';
|
|
19
|
+
import { loadGLBToMeshData } from '@ifc-lite/cache';
|
|
20
|
+
|
|
21
|
+
import { getDynamicBatchConfig } from '../utils/ifcConfig.js';
|
|
22
|
+
import {
|
|
23
|
+
calculateMeshBounds,
|
|
24
|
+
createCoordinateInfo,
|
|
25
|
+
calculateStoreyHeights,
|
|
26
|
+
normalizeColor,
|
|
27
|
+
} from '../utils/localParsingUtils.js';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Extended data store type for IFCX (IFC5) files.
|
|
31
|
+
* IFCX uses schemaVersion 'IFC5' and may include federated composition metadata.
|
|
32
|
+
*/
|
|
33
|
+
export interface IfcxDataStore extends IfcDataStore {
|
|
34
|
+
schemaVersion: 'IFC5';
|
|
35
|
+
/** Federated layer info for re-composition */
|
|
36
|
+
_federatedLayers?: Array<{ id: string; name: string; enabled: boolean }>;
|
|
37
|
+
/** Original buffers for re-composition when adding overlays */
|
|
38
|
+
_federatedBuffers?: Array<{ buffer: ArrayBuffer; name: string }>;
|
|
39
|
+
/** Composition statistics */
|
|
40
|
+
_compositionStats?: { totalNodes: number; layersUsed: number; inheritanceResolutions: number; crossLayerReferences: number };
|
|
41
|
+
/** Layer info for display */
|
|
42
|
+
_layerInfo?: Array<{ id: string; name: string; meshCount: number }>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Shape of raw meshes returned from IFCX parsers before conversion to MeshData */
|
|
46
|
+
interface RawIfcxMesh {
|
|
47
|
+
expressId?: number;
|
|
48
|
+
express_id?: number;
|
|
49
|
+
id?: number;
|
|
50
|
+
positions: Float32Array | number[];
|
|
51
|
+
indices: Uint32Array | number[];
|
|
52
|
+
normals: Float32Array | number[];
|
|
53
|
+
color?: [number, number, number, number] | [number, number, number];
|
|
54
|
+
ifcType?: string;
|
|
55
|
+
ifc_type?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Convert raw IFCX parser meshes to viewer-compatible MeshData[].
|
|
60
|
+
* Ensures typed arrays, normalizes colors, and filters out empty meshes.
|
|
61
|
+
*/
|
|
62
|
+
function convertIfcxMeshes(rawMeshes: RawIfcxMesh[]): MeshData[] {
|
|
63
|
+
return rawMeshes.map((m) => {
|
|
64
|
+
const positions = m.positions instanceof Float32Array ? m.positions : new Float32Array(m.positions || []);
|
|
65
|
+
const indices = m.indices instanceof Uint32Array ? m.indices : new Uint32Array(m.indices || []);
|
|
66
|
+
const normals = m.normals instanceof Float32Array ? m.normals : new Float32Array(m.normals || []);
|
|
67
|
+
const color = normalizeColor(m.color);
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
expressId: m.expressId ?? m.express_id ?? m.id ?? 0,
|
|
71
|
+
positions,
|
|
72
|
+
indices,
|
|
73
|
+
normals,
|
|
74
|
+
color,
|
|
75
|
+
ifcType: m.ifcType ?? m.ifc_type ?? 'IfcProduct',
|
|
76
|
+
};
|
|
77
|
+
}).filter((m) => m.positions.length > 0 && m.indices.length > 0);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Hook providing multi-model federation operations
|
|
82
|
+
* Includes addModel, removeModel, federated IFCX loading, overlay management,
|
|
83
|
+
* and ID resolution helpers
|
|
84
|
+
*/
|
|
85
|
+
export function useIfcFederation() {
|
|
86
|
+
const {
|
|
87
|
+
setLoading,
|
|
88
|
+
setError,
|
|
89
|
+
setProgress,
|
|
90
|
+
setIfcDataStore,
|
|
91
|
+
setGeometryResult,
|
|
92
|
+
// Multi-model state and actions
|
|
93
|
+
addModel: storeAddModel,
|
|
94
|
+
removeModel: storeRemoveModel,
|
|
95
|
+
clearAllModels,
|
|
96
|
+
getModel,
|
|
97
|
+
hasModels,
|
|
98
|
+
// Federation Registry helpers
|
|
99
|
+
registerModelOffset,
|
|
100
|
+
fromGlobalId,
|
|
101
|
+
findModelForGlobalId,
|
|
102
|
+
} = useViewerStore();
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Add a model to the federation (multi-model support)
|
|
106
|
+
* Uses FederationRegistry to assign unique ID offsets - BULLETPROOF against ID collisions
|
|
107
|
+
* Returns the model ID on success, null on failure
|
|
108
|
+
*/
|
|
109
|
+
const addModel = useCallback(async (
|
|
110
|
+
file: File,
|
|
111
|
+
options?: { name?: string }
|
|
112
|
+
): Promise<string | null> => {
|
|
113
|
+
const modelId = crypto.randomUUID();
|
|
114
|
+
const totalStartTime = performance.now();
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
// IMPORTANT: Before adding a new model, check if there's a legacy model
|
|
118
|
+
// (loaded via loadFile) that's not in the Map yet. If so, migrate it first.
|
|
119
|
+
const currentModels = useViewerStore.getState().models;
|
|
120
|
+
const currentIfcDataStore = useViewerStore.getState().ifcDataStore;
|
|
121
|
+
const currentGeometryResult = useViewerStore.getState().geometryResult;
|
|
122
|
+
|
|
123
|
+
if (currentModels.size === 0 && currentIfcDataStore && currentGeometryResult) {
|
|
124
|
+
// Migrate the legacy model to the Map
|
|
125
|
+
// Legacy model has offset 0 (IDs are unchanged)
|
|
126
|
+
const legacyModelId = crypto.randomUUID();
|
|
127
|
+
const legacyName = currentIfcDataStore.spatialHierarchy?.project?.name || 'Model 1';
|
|
128
|
+
|
|
129
|
+
// Find max expressId in legacy model for registry
|
|
130
|
+
// IMPORTANT: Include ALL entities, not just meshes, for proper globalId resolution
|
|
131
|
+
const legacyMeshes = currentGeometryResult.meshes || [];
|
|
132
|
+
const legacyMaxExpressIdFromMeshes = legacyMeshes.reduce((max: number, m: MeshData) => Math.max(max, m.expressId), 0);
|
|
133
|
+
// FIXED: Use iteration instead of spread to avoid stack overflow with large Maps
|
|
134
|
+
let legacyMaxExpressIdFromEntities = 0;
|
|
135
|
+
if (currentIfcDataStore.entityIndex?.byId) {
|
|
136
|
+
for (const key of currentIfcDataStore.entityIndex.byId.keys()) {
|
|
137
|
+
if (key > legacyMaxExpressIdFromEntities) legacyMaxExpressIdFromEntities = key;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const legacyMaxExpressId = Math.max(legacyMaxExpressIdFromMeshes, legacyMaxExpressIdFromEntities);
|
|
141
|
+
|
|
142
|
+
// Register legacy model with offset 0 (IDs already in use as-is)
|
|
143
|
+
const legacyOffset = registerModelOffset(legacyModelId, legacyMaxExpressId);
|
|
144
|
+
|
|
145
|
+
const legacyModel: FederatedModel = {
|
|
146
|
+
id: legacyModelId,
|
|
147
|
+
name: legacyName,
|
|
148
|
+
ifcDataStore: currentIfcDataStore,
|
|
149
|
+
geometryResult: currentGeometryResult,
|
|
150
|
+
visible: true,
|
|
151
|
+
collapsed: false,
|
|
152
|
+
schemaVersion: 'IFC4',
|
|
153
|
+
loadedAt: Date.now() - 1000,
|
|
154
|
+
fileSize: 0,
|
|
155
|
+
idOffset: legacyOffset,
|
|
156
|
+
maxExpressId: legacyMaxExpressId,
|
|
157
|
+
};
|
|
158
|
+
storeAddModel(legacyModel);
|
|
159
|
+
console.log(`[useIfc] Migrated legacy model "${legacyModel.name}" to federation (offset: ${legacyOffset}, maxId: ${legacyMaxExpressId})`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
setLoading(true);
|
|
163
|
+
setError(null);
|
|
164
|
+
setProgress({ phase: 'Loading file', percent: 0 });
|
|
165
|
+
|
|
166
|
+
// Read file from disk
|
|
167
|
+
const buffer = await file.arrayBuffer();
|
|
168
|
+
const fileSizeMB = buffer.byteLength / (1024 * 1024);
|
|
169
|
+
|
|
170
|
+
// Detect file format
|
|
171
|
+
const format = detectFormat(buffer);
|
|
172
|
+
|
|
173
|
+
let parsedDataStore: IfcDataStore | null = null;
|
|
174
|
+
let parsedGeometry: { meshes: MeshData[]; totalVertices: number; totalTriangles: number; coordinateInfo: CoordinateInfo } | null = null;
|
|
175
|
+
let schemaVersion: SchemaVersion = 'IFC4';
|
|
176
|
+
|
|
177
|
+
// IFCX files must be parsed client-side
|
|
178
|
+
if (format === 'ifcx') {
|
|
179
|
+
setProgress({ phase: 'Parsing IFCX (client-side)', percent: 10 });
|
|
180
|
+
|
|
181
|
+
const ifcxResult = await parseIfcx(buffer, {
|
|
182
|
+
onProgress: (prog: { phase: string; percent: number }) => {
|
|
183
|
+
setProgress({ phase: `IFCX ${prog.phase}`, percent: 10 + (prog.percent * 0.8) });
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Convert IFCX meshes to viewer format
|
|
188
|
+
const meshes: MeshData[] = convertIfcxMeshes(ifcxResult.meshes);
|
|
189
|
+
|
|
190
|
+
// Check if this is an overlay-only IFCX file (no geometry)
|
|
191
|
+
if (meshes.length === 0 && ifcxResult.entityCount > 0) {
|
|
192
|
+
console.warn(`[useIfc] IFCX file "${file.name}" has no geometry - this is an overlay file.`);
|
|
193
|
+
setError(`"${file.name}" is an overlay file with no geometry. Please load it together with a base IFCX file (select all files at once for federated loading).`);
|
|
194
|
+
setLoading(false);
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const { bounds, stats } = calculateMeshBounds(meshes);
|
|
199
|
+
const coordinateInfo = createCoordinateInfo(bounds);
|
|
200
|
+
|
|
201
|
+
parsedGeometry = {
|
|
202
|
+
meshes,
|
|
203
|
+
totalVertices: stats.totalVertices,
|
|
204
|
+
totalTriangles: stats.totalTriangles,
|
|
205
|
+
coordinateInfo,
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
parsedDataStore = {
|
|
209
|
+
fileSize: ifcxResult.fileSize,
|
|
210
|
+
schemaVersion: 'IFC5' as const,
|
|
211
|
+
entityCount: ifcxResult.entityCount,
|
|
212
|
+
parseTime: ifcxResult.parseTime,
|
|
213
|
+
source: new Uint8Array(buffer),
|
|
214
|
+
entityIndex: { byId: new Map(), byType: new Map() },
|
|
215
|
+
strings: ifcxResult.strings,
|
|
216
|
+
entities: ifcxResult.entities,
|
|
217
|
+
properties: ifcxResult.properties,
|
|
218
|
+
quantities: ifcxResult.quantities,
|
|
219
|
+
relationships: ifcxResult.relationships,
|
|
220
|
+
spatialHierarchy: ifcxResult.spatialHierarchy,
|
|
221
|
+
} as IfcxDataStore;
|
|
222
|
+
|
|
223
|
+
schemaVersion = 'IFC5';
|
|
224
|
+
|
|
225
|
+
} else if (format === 'glb') {
|
|
226
|
+
// GLB files: parse directly to MeshData (geometry only, no IFC data model)
|
|
227
|
+
setProgress({ phase: 'Parsing GLB', percent: 10 });
|
|
228
|
+
|
|
229
|
+
const meshes = loadGLBToMeshData(new Uint8Array(buffer));
|
|
230
|
+
|
|
231
|
+
if (meshes.length === 0) {
|
|
232
|
+
setError('GLB file contains no geometry');
|
|
233
|
+
setLoading(false);
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const { bounds, stats } = calculateMeshBounds(meshes);
|
|
238
|
+
const coordinateInfo = createCoordinateInfo(bounds);
|
|
239
|
+
|
|
240
|
+
parsedGeometry = {
|
|
241
|
+
meshes,
|
|
242
|
+
totalVertices: stats.totalVertices,
|
|
243
|
+
totalTriangles: stats.totalTriangles,
|
|
244
|
+
coordinateInfo,
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// Create a minimal data store for GLB (no IFC properties)
|
|
248
|
+
parsedDataStore = {
|
|
249
|
+
fileSize: buffer.byteLength,
|
|
250
|
+
schemaVersion: 'IFC4' as const,
|
|
251
|
+
entityCount: meshes.length,
|
|
252
|
+
parseTime: 0,
|
|
253
|
+
source: new Uint8Array(0),
|
|
254
|
+
entityIndex: { byId: new Map(), byType: new Map() },
|
|
255
|
+
strings: { getString: () => undefined, getStringId: () => undefined, count: 0 } as unknown as IfcDataStore['strings'],
|
|
256
|
+
entities: { count: 0, getId: () => 0, getType: () => 0, getName: () => undefined, getGlobalId: () => undefined } as unknown as IfcDataStore['entities'],
|
|
257
|
+
properties: { count: 0, getPropertiesForEntity: () => [], getPropertySetForEntity: () => [] } as unknown as IfcDataStore['properties'],
|
|
258
|
+
quantities: { count: 0, getQuantitiesForEntity: () => [] } as unknown as IfcDataStore['quantities'],
|
|
259
|
+
relationships: { count: 0, getRelationships: () => [], getRelated: () => [] } as unknown as IfcDataStore['relationships'],
|
|
260
|
+
spatialHierarchy: null as unknown as IfcDataStore['spatialHierarchy'],
|
|
261
|
+
} as unknown as IfcDataStore;
|
|
262
|
+
|
|
263
|
+
schemaVersion = 'IFC4'; // GLB doesn't have a schema version, use IFC4 as default
|
|
264
|
+
|
|
265
|
+
} else {
|
|
266
|
+
// IFC4/IFC2X3 STEP format - use WASM parsing
|
|
267
|
+
setProgress({ phase: 'Starting geometry streaming', percent: 10 });
|
|
268
|
+
|
|
269
|
+
const geometryProcessor = new GeometryProcessor({ quality: GeometryQuality.Balanced });
|
|
270
|
+
await geometryProcessor.init();
|
|
271
|
+
|
|
272
|
+
// Parse data model
|
|
273
|
+
const parser = new IfcParser();
|
|
274
|
+
const wasmApi = geometryProcessor.getApi();
|
|
275
|
+
|
|
276
|
+
const dataStorePromise = parser.parseColumnar(buffer, { wasmApi });
|
|
277
|
+
|
|
278
|
+
// Process geometry
|
|
279
|
+
const allMeshes: MeshData[] = [];
|
|
280
|
+
let finalCoordinateInfo: CoordinateInfo | null = null;
|
|
281
|
+
// Capture RTC offset from WASM for proper multi-model alignment
|
|
282
|
+
let capturedRtcOffset: { x: number; y: number; z: number } | null = null;
|
|
283
|
+
|
|
284
|
+
const dynamicBatchConfig = getDynamicBatchConfig(fileSizeMB);
|
|
285
|
+
|
|
286
|
+
for await (const event of geometryProcessor.processAdaptive(new Uint8Array(buffer), {
|
|
287
|
+
sizeThreshold: 2 * 1024 * 1024,
|
|
288
|
+
batchSize: dynamicBatchConfig,
|
|
289
|
+
})) {
|
|
290
|
+
switch (event.type) {
|
|
291
|
+
case 'batch': {
|
|
292
|
+
for (let i = 0; i < event.meshes.length; i++) allMeshes.push(event.meshes[i]);
|
|
293
|
+
finalCoordinateInfo = event.coordinateInfo ?? null;
|
|
294
|
+
const progressPercent = 10 + Math.min(80, (allMeshes.length / 1000) * 0.8);
|
|
295
|
+
setProgress({ phase: `Processing geometry (${allMeshes.length} meshes)`, percent: progressPercent });
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
case 'rtcOffset': {
|
|
299
|
+
// Capture RTC offset from WASM for multi-model alignment
|
|
300
|
+
if (event.hasRtc) {
|
|
301
|
+
capturedRtcOffset = event.rtcOffset;
|
|
302
|
+
}
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
case 'complete':
|
|
306
|
+
finalCoordinateInfo = event.coordinateInfo ?? null;
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
parsedDataStore = await dataStorePromise;
|
|
312
|
+
|
|
313
|
+
// Calculate storey heights
|
|
314
|
+
if (parsedDataStore.spatialHierarchy && parsedDataStore.spatialHierarchy.storeyHeights.size === 0 && parsedDataStore.spatialHierarchy.storeyElevations.size > 1) {
|
|
315
|
+
const calculatedHeights = calculateStoreyHeights(parsedDataStore.spatialHierarchy.storeyElevations);
|
|
316
|
+
for (const [storeyId, height] of calculatedHeights) {
|
|
317
|
+
parsedDataStore.spatialHierarchy.storeyHeights.set(storeyId, height);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Build spatial index
|
|
322
|
+
if (allMeshes.length > 0) {
|
|
323
|
+
try {
|
|
324
|
+
const spatialIndex = buildSpatialIndex(allMeshes);
|
|
325
|
+
parsedDataStore.spatialIndex = spatialIndex;
|
|
326
|
+
} catch (err) {
|
|
327
|
+
console.warn('[useIfc] Failed to build spatial index:', err);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
parsedGeometry = {
|
|
332
|
+
meshes: allMeshes,
|
|
333
|
+
totalVertices: allMeshes.reduce((sum, m) => sum + m.positions.length / 3, 0),
|
|
334
|
+
totalTriangles: allMeshes.reduce((sum, m) => sum + m.indices.length / 3, 0),
|
|
335
|
+
coordinateInfo: finalCoordinateInfo || createCoordinateInfo(calculateMeshBounds(allMeshes).bounds),
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
// Store captured RTC offset in coordinate info for multi-model alignment
|
|
339
|
+
if (parsedGeometry.coordinateInfo && capturedRtcOffset) {
|
|
340
|
+
parsedGeometry.coordinateInfo.wasmRtcOffset = capturedRtcOffset;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
schemaVersion = parsedDataStore.schemaVersion === 'IFC4X3' ? 'IFC4X3' :
|
|
344
|
+
parsedDataStore.schemaVersion === 'IFC4' ? 'IFC4' : 'IFC2X3';
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (!parsedDataStore || !parsedGeometry) {
|
|
348
|
+
throw new Error('Failed to parse file');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// =========================================================================
|
|
352
|
+
// FEDERATION REGISTRY: Transform expressIds to globally unique IDs
|
|
353
|
+
// This is the BULLETPROOF fix for multi-model ID collisions
|
|
354
|
+
// =========================================================================
|
|
355
|
+
|
|
356
|
+
// Step 1: Find max expressId in this model
|
|
357
|
+
// IMPORTANT: Use ALL entities from data store, not just meshes
|
|
358
|
+
// Spatial containers (IfcProject, IfcSite, etc.) don't have geometry but need valid globalId resolution
|
|
359
|
+
const maxExpressIdFromMeshes = parsedGeometry.meshes.reduce((max, m) => Math.max(max, m.expressId), 0);
|
|
360
|
+
// FIXED: Use iteration instead of spread to avoid stack overflow with large Maps
|
|
361
|
+
let maxExpressIdFromEntities = 0;
|
|
362
|
+
if (parsedDataStore.entityIndex?.byId) {
|
|
363
|
+
for (const key of parsedDataStore.entityIndex.byId.keys()) {
|
|
364
|
+
if (key > maxExpressIdFromEntities) maxExpressIdFromEntities = key;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
const maxExpressId = Math.max(maxExpressIdFromMeshes, maxExpressIdFromEntities);
|
|
368
|
+
|
|
369
|
+
// Step 2: Register with federation registry to get unique offset
|
|
370
|
+
const idOffset = registerModelOffset(modelId, maxExpressId);
|
|
371
|
+
|
|
372
|
+
// Step 3: Transform ALL mesh expressIds to globalIds
|
|
373
|
+
// globalId = originalExpressId + offset
|
|
374
|
+
// This ensures no two models can have the same ID
|
|
375
|
+
if (idOffset > 0) {
|
|
376
|
+
for (const mesh of parsedGeometry.meshes) {
|
|
377
|
+
mesh.expressId = mesh.expressId + idOffset;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// =========================================================================
|
|
382
|
+
// COORDINATE ALIGNMENT: Align new model with existing models using RTC delta
|
|
383
|
+
// WASM applies per-model RTC offsets. To align models from the same project,
|
|
384
|
+
// we calculate the difference in RTC offsets and apply it to the new model.
|
|
385
|
+
//
|
|
386
|
+
// RTC offset is in IFC coordinates (Z-up). After Z-up to Y-up conversion:
|
|
387
|
+
// - IFC X → WebGL X
|
|
388
|
+
// - IFC Y → WebGL -Z
|
|
389
|
+
// - IFC Z → WebGL Y (vertical)
|
|
390
|
+
// =========================================================================
|
|
391
|
+
const existingModels = Array.from(useViewerStore.getState().models.values()) as FederatedModel[];
|
|
392
|
+
if (existingModels.length > 0) {
|
|
393
|
+
const firstModel = existingModels[0];
|
|
394
|
+
const firstRtc = firstModel.geometryResult?.coordinateInfo?.wasmRtcOffset;
|
|
395
|
+
const newRtc = parsedGeometry.coordinateInfo?.wasmRtcOffset;
|
|
396
|
+
|
|
397
|
+
// If both models have RTC offsets, use RTC delta for precise alignment
|
|
398
|
+
if (firstRtc && newRtc) {
|
|
399
|
+
// Calculate what adjustment is needed to align new model with first model
|
|
400
|
+
// First model: pos = original - firstRtc
|
|
401
|
+
// New model: pos = original - newRtc
|
|
402
|
+
// To align: newPos + adjustment = firstPos (assuming same original)
|
|
403
|
+
// adjustment = firstRtc - newRtc (add back new's RTC, subtract first's RTC)
|
|
404
|
+
const adjustX = firstRtc.x - newRtc.x; // IFC X adjustment
|
|
405
|
+
const adjustY = firstRtc.y - newRtc.y; // IFC Y adjustment
|
|
406
|
+
const adjustZ = firstRtc.z - newRtc.z; // IFC Z adjustment (vertical)
|
|
407
|
+
|
|
408
|
+
// Convert to WebGL coordinates:
|
|
409
|
+
// IFC X → WebGL X (no change)
|
|
410
|
+
// IFC Y → WebGL -Z (swap and negate)
|
|
411
|
+
// IFC Z → WebGL Y (vertical)
|
|
412
|
+
const webglAdjustX = adjustX;
|
|
413
|
+
const webglAdjustY = adjustZ; // IFC Z is WebGL Y (vertical)
|
|
414
|
+
const webglAdjustZ = -adjustY; // IFC Y is WebGL -Z
|
|
415
|
+
|
|
416
|
+
const hasSignificantAdjust = Math.abs(webglAdjustX) > 0.01 ||
|
|
417
|
+
Math.abs(webglAdjustY) > 0.01 ||
|
|
418
|
+
Math.abs(webglAdjustZ) > 0.01;
|
|
419
|
+
|
|
420
|
+
if (hasSignificantAdjust) {
|
|
421
|
+
console.log(`[useIfc] Aligning model "${file.name}" using RTC adjustment: X=${webglAdjustX.toFixed(2)}m, Y=${webglAdjustY.toFixed(2)}m, Z=${webglAdjustZ.toFixed(2)}m`);
|
|
422
|
+
|
|
423
|
+
// Apply adjustment to all mesh vertices
|
|
424
|
+
// SUBTRACT adjustment: if firstRtc > newRtc, first was shifted MORE,
|
|
425
|
+
// so new model needs to be shifted in same direction (subtract more)
|
|
426
|
+
for (const mesh of parsedGeometry.meshes) {
|
|
427
|
+
const positions = mesh.positions;
|
|
428
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
429
|
+
positions[i] -= webglAdjustX;
|
|
430
|
+
positions[i + 1] -= webglAdjustY;
|
|
431
|
+
positions[i + 2] -= webglAdjustZ;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Update coordinate info bounds
|
|
436
|
+
if (parsedGeometry.coordinateInfo) {
|
|
437
|
+
parsedGeometry.coordinateInfo.shiftedBounds.min.x -= webglAdjustX;
|
|
438
|
+
parsedGeometry.coordinateInfo.shiftedBounds.max.x -= webglAdjustX;
|
|
439
|
+
parsedGeometry.coordinateInfo.shiftedBounds.min.y -= webglAdjustY;
|
|
440
|
+
parsedGeometry.coordinateInfo.shiftedBounds.max.y -= webglAdjustY;
|
|
441
|
+
parsedGeometry.coordinateInfo.shiftedBounds.min.z -= webglAdjustZ;
|
|
442
|
+
parsedGeometry.coordinateInfo.shiftedBounds.max.z -= webglAdjustZ;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
} else {
|
|
446
|
+
// No RTC info - can't align reliably. This happens with old cache entries.
|
|
447
|
+
console.warn(`[useIfc] Cannot align "${file.name}" - missing RTC offset. Clear cache and reload.`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Create the federated model with offset info
|
|
452
|
+
const federatedModel: FederatedModel = {
|
|
453
|
+
id: modelId,
|
|
454
|
+
name: options?.name ?? file.name,
|
|
455
|
+
ifcDataStore: parsedDataStore,
|
|
456
|
+
geometryResult: parsedGeometry,
|
|
457
|
+
visible: true,
|
|
458
|
+
collapsed: hasModels(), // Collapse if not first model
|
|
459
|
+
schemaVersion,
|
|
460
|
+
loadedAt: Date.now(),
|
|
461
|
+
fileSize: buffer.byteLength,
|
|
462
|
+
idOffset,
|
|
463
|
+
maxExpressId,
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
// Add to store
|
|
467
|
+
storeAddModel(federatedModel);
|
|
468
|
+
|
|
469
|
+
// Also set legacy single-model state for backward compatibility
|
|
470
|
+
setIfcDataStore(parsedDataStore);
|
|
471
|
+
setGeometryResult(parsedGeometry);
|
|
472
|
+
|
|
473
|
+
setProgress({ phase: 'Complete', percent: 100 });
|
|
474
|
+
setLoading(false);
|
|
475
|
+
|
|
476
|
+
const totalElapsedMs = performance.now() - totalStartTime;
|
|
477
|
+
console.log(`[useIfc] ✓ Added model ${file.name} (${fileSizeMB.toFixed(1)}MB) | ${totalElapsedMs.toFixed(0)}ms`);
|
|
478
|
+
|
|
479
|
+
return modelId;
|
|
480
|
+
|
|
481
|
+
} catch (err) {
|
|
482
|
+
console.error('[useIfc] addModel failed:', err);
|
|
483
|
+
setError(err instanceof Error ? err.message : 'Unknown error');
|
|
484
|
+
setLoading(false);
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
}, [setLoading, setError, setProgress, setIfcDataStore, setGeometryResult, storeAddModel, hasModels, registerModelOffset]);
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Remove a model from the federation
|
|
491
|
+
*/
|
|
492
|
+
const removeModel = useCallback((modelId: string) => {
|
|
493
|
+
storeRemoveModel(modelId);
|
|
494
|
+
|
|
495
|
+
// Read fresh state from store after removal to avoid stale closure
|
|
496
|
+
const freshModels = useViewerStore.getState().models;
|
|
497
|
+
const remaining = Array.from(freshModels.values()) as FederatedModel[];
|
|
498
|
+
if (remaining.length > 0) {
|
|
499
|
+
const newActive = remaining[0];
|
|
500
|
+
setIfcDataStore(newActive.ifcDataStore);
|
|
501
|
+
setGeometryResult(newActive.geometryResult);
|
|
502
|
+
} else {
|
|
503
|
+
setIfcDataStore(null);
|
|
504
|
+
setGeometryResult(null);
|
|
505
|
+
}
|
|
506
|
+
}, [storeRemoveModel, setIfcDataStore, setGeometryResult]);
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Get query instance for a specific model
|
|
510
|
+
*/
|
|
511
|
+
const getQueryForModel = useCallback((modelId: string): IfcQuery | null => {
|
|
512
|
+
const model = getModel(modelId);
|
|
513
|
+
if (!model) return null;
|
|
514
|
+
return new IfcQuery(model.ifcDataStore);
|
|
515
|
+
}, [getModel]);
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Load multiple files sequentially (WASM parser isn't thread-safe)
|
|
519
|
+
* Each file fully loads before the next one starts
|
|
520
|
+
*/
|
|
521
|
+
const loadFilesSequentially = useCallback(async (files: File[]): Promise<void> => {
|
|
522
|
+
for (const file of files) {
|
|
523
|
+
await addModel(file);
|
|
524
|
+
}
|
|
525
|
+
}, [addModel]);
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Load multiple IFCX files as federated layers
|
|
529
|
+
* Uses IFC5's layer composition system where later files override earlier ones.
|
|
530
|
+
* Properties from overlay files are merged with the base file(s).
|
|
531
|
+
*
|
|
532
|
+
* @param files - Array of IFCX files (first = base/weakest, last = strongest overlay)
|
|
533
|
+
*
|
|
534
|
+
* @example
|
|
535
|
+
* ```typescript
|
|
536
|
+
* // Load base model with property overlay
|
|
537
|
+
* await loadFederatedIfcx([
|
|
538
|
+
* baseFile, // hello-wall.ifcx
|
|
539
|
+
* fireRatingFile, // add-fire-rating.ifcx (adds FireRating property)
|
|
540
|
+
* ]);
|
|
541
|
+
* ```
|
|
542
|
+
*/
|
|
543
|
+
/**
|
|
544
|
+
* Internal: Load federated IFCX from buffers (used by both initial load and add overlay)
|
|
545
|
+
*/
|
|
546
|
+
const loadFederatedIfcxFromBuffers = useCallback(async (
|
|
547
|
+
buffers: Array<{ buffer: ArrayBuffer; name: string }>,
|
|
548
|
+
options: { resetState?: boolean } = {}
|
|
549
|
+
): Promise<void> => {
|
|
550
|
+
const { resetViewerState, clearAllModels } = useViewerStore.getState();
|
|
551
|
+
|
|
552
|
+
try {
|
|
553
|
+
// Always reset viewer state when geometry changes (selection, hidden entities, etc.)
|
|
554
|
+
// This ensures 3D highlighting works correctly after re-composition
|
|
555
|
+
resetViewerState();
|
|
556
|
+
|
|
557
|
+
// Clear legacy geometry BEFORE clearing models to prevent stale fallback
|
|
558
|
+
// This avoids a race condition where mergedGeometryResult uses old geometry
|
|
559
|
+
// during the brief moment when storeModels.size === 0
|
|
560
|
+
setGeometryResult(null);
|
|
561
|
+
clearAllModels();
|
|
562
|
+
|
|
563
|
+
setLoading(true);
|
|
564
|
+
setError(null);
|
|
565
|
+
setProgress({ phase: 'Parsing federated IFCX', percent: 0 });
|
|
566
|
+
|
|
567
|
+
// Parse federated IFCX files
|
|
568
|
+
const result = await parseFederatedIfcx(buffers, {
|
|
569
|
+
onProgress: (prog: { phase: string; percent: number }) => {
|
|
570
|
+
setProgress({ phase: `IFCX ${prog.phase}`, percent: prog.percent });
|
|
571
|
+
},
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
// Convert IFCX meshes to viewer format
|
|
575
|
+
const meshes: MeshData[] = convertIfcxMeshes(result.meshes);
|
|
576
|
+
|
|
577
|
+
// Calculate bounds
|
|
578
|
+
const { bounds, stats } = calculateMeshBounds(meshes);
|
|
579
|
+
const coordinateInfo = createCoordinateInfo(bounds);
|
|
580
|
+
|
|
581
|
+
const geometryResult = {
|
|
582
|
+
meshes,
|
|
583
|
+
totalVertices: stats.totalVertices,
|
|
584
|
+
totalTriangles: stats.totalTriangles,
|
|
585
|
+
coordinateInfo,
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
// NOTE: Do NOT call setGeometryResult() here!
|
|
589
|
+
// For federated loading, geometry comes from the models Map via mergedGeometryResult.
|
|
590
|
+
// Calling setGeometryResult() before models are added causes a race condition where
|
|
591
|
+
// meshes are added to the scene WITHOUT modelIndex, breaking selection highlighting.
|
|
592
|
+
|
|
593
|
+
// Get layer info with mesh counts
|
|
594
|
+
const layers = result.layerStack.getLayers();
|
|
595
|
+
|
|
596
|
+
// Create data store from federated result
|
|
597
|
+
const dataStore = {
|
|
598
|
+
fileSize: result.fileSize,
|
|
599
|
+
schemaVersion: 'IFC5' as const,
|
|
600
|
+
entityCount: result.entityCount,
|
|
601
|
+
parseTime: result.parseTime,
|
|
602
|
+
source: new Uint8Array(buffers[0].buffer),
|
|
603
|
+
entityIndex: {
|
|
604
|
+
byId: new Map(),
|
|
605
|
+
byType: new Map(),
|
|
606
|
+
},
|
|
607
|
+
strings: result.strings,
|
|
608
|
+
entities: result.entities,
|
|
609
|
+
properties: result.properties,
|
|
610
|
+
quantities: result.quantities,
|
|
611
|
+
relationships: result.relationships,
|
|
612
|
+
spatialHierarchy: result.spatialHierarchy,
|
|
613
|
+
// Federated-specific: store layer info and ORIGINAL BUFFERS for re-composition
|
|
614
|
+
_federatedLayers: layers.map((l: { id: string; name: string; enabled: boolean }) => ({
|
|
615
|
+
id: l.id,
|
|
616
|
+
name: l.name,
|
|
617
|
+
enabled: l.enabled,
|
|
618
|
+
})),
|
|
619
|
+
_federatedBuffers: buffers.map(b => ({
|
|
620
|
+
buffer: b.buffer.slice(0), // Clone buffer
|
|
621
|
+
name: b.name,
|
|
622
|
+
})),
|
|
623
|
+
_compositionStats: result.compositionStats,
|
|
624
|
+
} as IfcxDataStore;
|
|
625
|
+
|
|
626
|
+
// IfcxDataStore extends IfcDataStore (with schemaVersion: 'IFC5'), so this is safe
|
|
627
|
+
setIfcDataStore(dataStore);
|
|
628
|
+
|
|
629
|
+
// Clear existing models and add each layer as a "model" in the Models panel
|
|
630
|
+
// This shows users all the files that contributed to the composition
|
|
631
|
+
clearAllModels();
|
|
632
|
+
|
|
633
|
+
// Find max expressId for proper ID range tracking
|
|
634
|
+
// This is needed for resolveGlobalIdFromModels to work correctly
|
|
635
|
+
let maxExpressId = 0;
|
|
636
|
+
if (result.entities?.expressId) {
|
|
637
|
+
for (let i = 0; i < result.entities.count; i++) {
|
|
638
|
+
const id = result.entities.expressId[i];
|
|
639
|
+
if (id > maxExpressId) maxExpressId = id;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
for (let i = 0; i < layers.length; i++) {
|
|
644
|
+
const layer = layers[i];
|
|
645
|
+
const layerBuffer = buffers.find(b => b.name === layer.name);
|
|
646
|
+
|
|
647
|
+
// Count how many meshes came from this layer
|
|
648
|
+
// For base layers: count meshes, for overlays: show as data-only
|
|
649
|
+
const isBaseLayer = i === layers.length - 1; // Last layer (weakest) is typically base
|
|
650
|
+
|
|
651
|
+
const layerModel: FederatedModel = {
|
|
652
|
+
id: layer.id,
|
|
653
|
+
name: layer.name,
|
|
654
|
+
ifcDataStore: dataStore, // Share the composed data store
|
|
655
|
+
geometryResult: isBaseLayer ? geometryResult : {
|
|
656
|
+
meshes: [],
|
|
657
|
+
totalVertices: 0,
|
|
658
|
+
totalTriangles: 0,
|
|
659
|
+
coordinateInfo,
|
|
660
|
+
},
|
|
661
|
+
visible: true,
|
|
662
|
+
collapsed: i > 0, // Collapse overlays by default
|
|
663
|
+
schemaVersion: 'IFC5',
|
|
664
|
+
loadedAt: Date.now() - (layers.length - i) * 100, // Stagger timestamps
|
|
665
|
+
fileSize: layerBuffer?.buffer.byteLength || 0,
|
|
666
|
+
// For base layer: set proper ID range for resolveGlobalIdFromModels
|
|
667
|
+
// Overlays share the same data store so they don't need their own range
|
|
668
|
+
idOffset: 0,
|
|
669
|
+
maxExpressId: isBaseLayer ? maxExpressId : 0,
|
|
670
|
+
// Mark overlay-only layers
|
|
671
|
+
_isOverlay: !isBaseLayer,
|
|
672
|
+
_layerIndex: i,
|
|
673
|
+
} as FederatedModel & { _isOverlay?: boolean; _layerIndex?: number };
|
|
674
|
+
|
|
675
|
+
storeAddModel(layerModel);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
console.log(`[useIfc] Federated IFCX loaded: ${layers.length} layers, ${result.entityCount} entities, ${meshes.length} meshes`);
|
|
679
|
+
console.log(`[useIfc] Composition stats: ${result.compositionStats.inheritanceResolutions} inheritance resolutions, ${result.compositionStats.crossLayerReferences} cross-layer refs`);
|
|
680
|
+
console.log(`[useIfc] Layers in Models panel: ${layers.map((l: { name: string }) => l.name).join(', ')}`);
|
|
681
|
+
|
|
682
|
+
setProgress({ phase: 'Complete', percent: 100 });
|
|
683
|
+
setLoading(false);
|
|
684
|
+
} catch (err: unknown) {
|
|
685
|
+
console.error('[useIfc] Federated IFCX loading failed:', err);
|
|
686
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
687
|
+
setError(`Federated IFCX loading failed: ${message}`);
|
|
688
|
+
setLoading(false);
|
|
689
|
+
}
|
|
690
|
+
}, [setLoading, setError, setProgress, setGeometryResult, setIfcDataStore, storeAddModel, clearAllModels]);
|
|
691
|
+
|
|
692
|
+
const loadFederatedIfcx = useCallback(async (files: File[]): Promise<void> => {
|
|
693
|
+
if (files.length === 0) {
|
|
694
|
+
setError('No files provided for federated loading');
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Check that all files are IFCX format and read buffers
|
|
699
|
+
const buffers: Array<{ buffer: ArrayBuffer; name: string }> = [];
|
|
700
|
+
for (const file of files) {
|
|
701
|
+
const buffer = await file.arrayBuffer();
|
|
702
|
+
const format = detectFormat(buffer);
|
|
703
|
+
if (format !== 'ifcx') {
|
|
704
|
+
setError(`File "${file.name}" is not an IFCX file. Federated loading only supports IFCX files.`);
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
buffers.push({ buffer, name: file.name });
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
await loadFederatedIfcxFromBuffers(buffers);
|
|
711
|
+
}, [setError, loadFederatedIfcxFromBuffers]);
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Add IFCX overlay files to existing federated model
|
|
715
|
+
* Re-composes all layers including new overlays
|
|
716
|
+
* Also handles adding overlays to a single IFCX file that wasn't loaded via federated loading
|
|
717
|
+
*/
|
|
718
|
+
const addIfcxOverlays = useCallback(async (files: File[]): Promise<void> => {
|
|
719
|
+
const currentStore = useViewerStore.getState().ifcDataStore as IfcxDataStore | null;
|
|
720
|
+
const currentModels = useViewerStore.getState().models;
|
|
721
|
+
|
|
722
|
+
// Get existing buffers - either from federated loading or from single file load
|
|
723
|
+
let existingBuffers: Array<{ buffer: ArrayBuffer; name: string }> = [];
|
|
724
|
+
|
|
725
|
+
if (currentStore?._federatedBuffers) {
|
|
726
|
+
// Already federated - use stored buffers
|
|
727
|
+
existingBuffers = currentStore._federatedBuffers as Array<{ buffer: ArrayBuffer; name: string }>;
|
|
728
|
+
} else if (currentStore?.source && currentStore.schemaVersion === 'IFC5') {
|
|
729
|
+
// Single IFCX file loaded via loadFile() - reconstruct buffer from source
|
|
730
|
+
// Get the model name from the models map
|
|
731
|
+
let modelName = 'base.ifcx';
|
|
732
|
+
for (const [, model] of currentModels) {
|
|
733
|
+
// Compare object identity (cast needed due to IFC5 schema extension)
|
|
734
|
+
if ((model.ifcDataStore as unknown) === currentStore || model.schemaVersion === 'IFC5') {
|
|
735
|
+
modelName = model.name;
|
|
736
|
+
break;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Convert Uint8Array source back to ArrayBuffer
|
|
741
|
+
const sourceBuffer = currentStore.source.buffer.slice(
|
|
742
|
+
currentStore.source.byteOffset,
|
|
743
|
+
currentStore.source.byteOffset + currentStore.source.byteLength
|
|
744
|
+
) as ArrayBuffer;
|
|
745
|
+
|
|
746
|
+
existingBuffers = [{ buffer: sourceBuffer, name: modelName }];
|
|
747
|
+
console.log(`[useIfc] Converting single IFCX file "${modelName}" to federated mode`);
|
|
748
|
+
} else {
|
|
749
|
+
setError('Cannot add overlays: no IFCX model loaded');
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// Read new overlay buffers
|
|
754
|
+
const newBuffers: Array<{ buffer: ArrayBuffer; name: string }> = [];
|
|
755
|
+
for (const file of files) {
|
|
756
|
+
const buffer = await file.arrayBuffer();
|
|
757
|
+
const format = detectFormat(buffer);
|
|
758
|
+
if (format !== 'ifcx') {
|
|
759
|
+
setError(`File "${file.name}" is not an IFCX file.`);
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
newBuffers.push({ buffer, name: file.name });
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Combine: existing layers + new overlays (new overlays are strongest = first in array)
|
|
766
|
+
const allBuffers = [...newBuffers, ...existingBuffers];
|
|
767
|
+
|
|
768
|
+
console.log(`[useIfc] Re-composing federated IFCX with ${newBuffers.length} new overlay(s)`);
|
|
769
|
+
console.log(`[useIfc] Total layers: ${allBuffers.length} (${existingBuffers.length} existing + ${newBuffers.length} new)`);
|
|
770
|
+
|
|
771
|
+
await loadFederatedIfcxFromBuffers(allBuffers, { resetState: false });
|
|
772
|
+
}, [setError, loadFederatedIfcxFromBuffers]);
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Find which model contains a given globalId
|
|
776
|
+
* Uses FederationRegistry for O(log N) lookup - BULLETPROOF
|
|
777
|
+
* Returns the modelId or null if not found
|
|
778
|
+
*/
|
|
779
|
+
const findModelForEntity = useCallback((globalId: number): string | null => {
|
|
780
|
+
return findModelForGlobalId(globalId);
|
|
781
|
+
}, [findModelForGlobalId]);
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Convert a globalId back to the original (modelId, expressId) pair
|
|
785
|
+
* Use this when you need to look up properties in the IfcDataStore
|
|
786
|
+
*/
|
|
787
|
+
const resolveGlobalId = useCallback((globalId: number): { modelId: string; expressId: number } | null => {
|
|
788
|
+
return fromGlobalId(globalId);
|
|
789
|
+
}, [fromGlobalId]);
|
|
790
|
+
|
|
791
|
+
return {
|
|
792
|
+
addModel,
|
|
793
|
+
removeModel,
|
|
794
|
+
getQueryForModel,
|
|
795
|
+
loadFilesSequentially,
|
|
796
|
+
loadFederatedIfcx,
|
|
797
|
+
addIfcxOverlays,
|
|
798
|
+
findModelForEntity,
|
|
799
|
+
resolveGlobalId,
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
export default useIfcFederation;
|