@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
package/src/hooks/useIDS.ts
CHANGED
|
@@ -13,19 +13,12 @@
|
|
|
13
13
|
* - Isolate failed/passed entities
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { useCallback, useMemo, useEffect, useRef } from 'react';
|
|
16
|
+
import { useCallback, useMemo, useEffect, useRef, useState } from 'react';
|
|
17
17
|
import { useViewerStore } from '@/store';
|
|
18
18
|
import type {
|
|
19
19
|
IDSDocument,
|
|
20
20
|
IDSValidationReport,
|
|
21
21
|
IDSModelInfo,
|
|
22
|
-
IFCDataAccessor,
|
|
23
|
-
PropertyValueResult,
|
|
24
|
-
PropertySetInfo,
|
|
25
|
-
ClassificationInfo,
|
|
26
|
-
MaterialInfo,
|
|
27
|
-
ParentInfo,
|
|
28
|
-
PartOfRelation,
|
|
29
22
|
SupportedLocale,
|
|
30
23
|
ValidationProgress,
|
|
31
24
|
} from '@ifc-lite/ids';
|
|
@@ -35,6 +28,21 @@ import {
|
|
|
35
28
|
createTranslationService,
|
|
36
29
|
} from '@ifc-lite/ids';
|
|
37
30
|
import type { IfcDataStore } from '@ifc-lite/parser';
|
|
31
|
+
import { createBCFFromIDSReport, writeBCF } from '@ifc-lite/bcf';
|
|
32
|
+
import type { EntityBoundsInput, IDSBCFExportOptions } from '@ifc-lite/bcf';
|
|
33
|
+
import type { IDSBCFExportSettings, IDSExportProgress } from '@/components/viewer/IDSExportDialog';
|
|
34
|
+
import { getEntityBounds } from '@/utils/viewportUtils';
|
|
35
|
+
import { getGlobalRenderer } from '@/hooks/useBCF';
|
|
36
|
+
|
|
37
|
+
import { createDataAccessor } from './ids/idsDataAccessor';
|
|
38
|
+
import {
|
|
39
|
+
DEFAULT_FAILED_COLOR,
|
|
40
|
+
DEFAULT_PASSED_COLOR,
|
|
41
|
+
buildValidationColorUpdates,
|
|
42
|
+
buildRestoreColorUpdates,
|
|
43
|
+
} from './ids/idsColorSystem';
|
|
44
|
+
import type { ColorTuple } from './ids/idsColorSystem';
|
|
45
|
+
import { downloadReportJSON, downloadReportHTML } from './ids/idsExportService';
|
|
38
46
|
|
|
39
47
|
// ============================================================================
|
|
40
48
|
// Types
|
|
@@ -142,243 +150,18 @@ export interface UseIDSResult {
|
|
|
142
150
|
exportReportJSON: () => void;
|
|
143
151
|
/** Export validation report to HTML */
|
|
144
152
|
exportReportHTML: () => void;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
// ============================================================================
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Create an IFCDataAccessor from an IfcDataStore
|
|
153
|
-
* This bridges the viewer's data store to the IDS validator's interface
|
|
154
|
-
*/
|
|
155
|
-
function createDataAccessor(
|
|
156
|
-
dataStore: IfcDataStore,
|
|
157
|
-
modelId: string
|
|
158
|
-
): IFCDataAccessor {
|
|
159
|
-
// Helper to get entity info
|
|
160
|
-
const getEntityInfo = (expressId: number) => {
|
|
161
|
-
return dataStore.entities?.getName ? {
|
|
162
|
-
name: dataStore.entities.getName(expressId),
|
|
163
|
-
type: dataStore.entities.getTypeName?.(expressId),
|
|
164
|
-
globalId: dataStore.entities.getGlobalId?.(expressId),
|
|
165
|
-
} : undefined;
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
return {
|
|
169
|
-
getEntityType(expressId: number): string | undefined {
|
|
170
|
-
// Try entities table first
|
|
171
|
-
const entityType = dataStore.entities?.getTypeName?.(expressId);
|
|
172
|
-
if (entityType) return entityType;
|
|
173
|
-
|
|
174
|
-
// Fallback to entityIndex
|
|
175
|
-
const byId = dataStore.entityIndex?.byId;
|
|
176
|
-
if (byId) {
|
|
177
|
-
const entry = byId.get(expressId);
|
|
178
|
-
if (entry) {
|
|
179
|
-
return typeof entry === 'object' && 'type' in entry ? String(entry.type) : undefined;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
return undefined;
|
|
183
|
-
},
|
|
184
|
-
|
|
185
|
-
getEntityName(expressId: number): string | undefined {
|
|
186
|
-
return dataStore.entities?.getName?.(expressId);
|
|
187
|
-
},
|
|
188
|
-
|
|
189
|
-
getGlobalId(expressId: number): string | undefined {
|
|
190
|
-
return dataStore.entities?.getGlobalId?.(expressId);
|
|
191
|
-
},
|
|
192
|
-
|
|
193
|
-
getDescription(expressId: number): string | undefined {
|
|
194
|
-
return dataStore.entities?.getDescription?.(expressId);
|
|
195
|
-
},
|
|
196
|
-
|
|
197
|
-
getObjectType(expressId: number): string | undefined {
|
|
198
|
-
return dataStore.entities?.getObjectType?.(expressId);
|
|
199
|
-
},
|
|
200
|
-
|
|
201
|
-
getEntitiesByType(typeName: string): number[] {
|
|
202
|
-
const byType = dataStore.entityIndex?.byType;
|
|
203
|
-
if (byType) {
|
|
204
|
-
const ids = byType.get(typeName.toUpperCase());
|
|
205
|
-
if (ids) return Array.from(ids);
|
|
206
|
-
}
|
|
207
|
-
return [];
|
|
208
|
-
},
|
|
209
|
-
|
|
210
|
-
getAllEntityIds(): number[] {
|
|
211
|
-
const byId = dataStore.entityIndex?.byId;
|
|
212
|
-
if (byId) {
|
|
213
|
-
return Array.from(byId.keys());
|
|
214
|
-
}
|
|
215
|
-
return [];
|
|
216
|
-
},
|
|
217
|
-
|
|
218
|
-
getPropertyValue(
|
|
219
|
-
expressId: number,
|
|
220
|
-
propertySetName: string,
|
|
221
|
-
propertyName: string
|
|
222
|
-
): PropertyValueResult | undefined {
|
|
223
|
-
const propertiesStore = dataStore.properties;
|
|
224
|
-
if (!propertiesStore) return undefined;
|
|
225
|
-
|
|
226
|
-
// Get property sets for this entity using getForEntity (returns PropertySet[])
|
|
227
|
-
const psets = propertiesStore.getForEntity?.(expressId);
|
|
228
|
-
if (!psets) return undefined;
|
|
229
|
-
|
|
230
|
-
for (const pset of psets) {
|
|
231
|
-
if (pset.name.toLowerCase() === propertySetName.toLowerCase()) {
|
|
232
|
-
const props = pset.properties || [];
|
|
233
|
-
for (const prop of props) {
|
|
234
|
-
if (prop.name.toLowerCase() === propertyName.toLowerCase()) {
|
|
235
|
-
// Convert value: ensure it's a primitive type (not array)
|
|
236
|
-
let value: string | number | boolean | null = null;
|
|
237
|
-
if (Array.isArray(prop.value)) {
|
|
238
|
-
// For arrays, convert to string representation
|
|
239
|
-
value = JSON.stringify(prop.value);
|
|
240
|
-
} else {
|
|
241
|
-
value = prop.value as string | number | boolean | null;
|
|
242
|
-
}
|
|
243
|
-
return {
|
|
244
|
-
value,
|
|
245
|
-
dataType: String(prop.type || 'IFCLABEL'),
|
|
246
|
-
propertySetName: pset.name,
|
|
247
|
-
propertyName: prop.name,
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
return undefined;
|
|
254
|
-
},
|
|
255
|
-
|
|
256
|
-
getPropertySets(expressId: number): PropertySetInfo[] {
|
|
257
|
-
const propertiesStore = dataStore.properties;
|
|
258
|
-
if (!propertiesStore) return [];
|
|
259
|
-
|
|
260
|
-
// Use getForEntity (returns PropertySet[])
|
|
261
|
-
const psets = propertiesStore.getForEntity?.(expressId);
|
|
262
|
-
if (!psets) return [];
|
|
263
|
-
|
|
264
|
-
return psets.map((pset) => ({
|
|
265
|
-
name: pset.name,
|
|
266
|
-
properties: (pset.properties || []).map((prop) => {
|
|
267
|
-
// Convert value: ensure it's a primitive type (not array)
|
|
268
|
-
let value: string | number | boolean | null = null;
|
|
269
|
-
if (Array.isArray(prop.value)) {
|
|
270
|
-
value = JSON.stringify(prop.value);
|
|
271
|
-
} else {
|
|
272
|
-
value = prop.value as string | number | boolean | null;
|
|
273
|
-
}
|
|
274
|
-
return {
|
|
275
|
-
name: prop.name,
|
|
276
|
-
value,
|
|
277
|
-
dataType: String(prop.type || 'IFCLABEL'),
|
|
278
|
-
};
|
|
279
|
-
}),
|
|
280
|
-
}));
|
|
281
|
-
},
|
|
282
|
-
|
|
283
|
-
getClassifications(expressId: number): ClassificationInfo[] {
|
|
284
|
-
// Classifications might be stored separately or in properties
|
|
285
|
-
// This is a placeholder - implement based on actual data structure
|
|
286
|
-
const classifications: ClassificationInfo[] = [];
|
|
287
|
-
|
|
288
|
-
// Check if there's a classifications accessor
|
|
289
|
-
const classStore = (dataStore as { classifications?: { getForEntity?: (id: number) => ClassificationInfo[] } }).classifications;
|
|
290
|
-
if (classStore?.getForEntity) {
|
|
291
|
-
return classStore.getForEntity(expressId);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
return classifications;
|
|
295
|
-
},
|
|
296
|
-
|
|
297
|
-
getMaterials(expressId: number): MaterialInfo[] {
|
|
298
|
-
// Materials might be stored separately or in relationships
|
|
299
|
-
const materials: MaterialInfo[] = [];
|
|
300
|
-
|
|
301
|
-
// Check if there's a materials accessor
|
|
302
|
-
const matStore = (dataStore as { materials?: { getForEntity?: (id: number) => MaterialInfo[] } }).materials;
|
|
303
|
-
if (matStore?.getForEntity) {
|
|
304
|
-
return matStore.getForEntity(expressId);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
return materials;
|
|
308
|
-
},
|
|
309
|
-
|
|
310
|
-
getParent(
|
|
311
|
-
expressId: number,
|
|
312
|
-
relationType: PartOfRelation
|
|
313
|
-
): ParentInfo | undefined {
|
|
314
|
-
const relationships = dataStore.relationships;
|
|
315
|
-
if (!relationships) return undefined;
|
|
316
|
-
|
|
317
|
-
// Map IDS relation type to internal relation type
|
|
318
|
-
const relationMap: Record<PartOfRelation, string> = {
|
|
319
|
-
'IfcRelAggregates': 'Aggregates',
|
|
320
|
-
'IfcRelContainedInSpatialStructure': 'ContainedInSpatialStructure',
|
|
321
|
-
'IfcRelNests': 'Nests',
|
|
322
|
-
'IfcRelVoidsElement': 'VoidsElement',
|
|
323
|
-
'IfcRelFillsElement': 'FillsElement',
|
|
324
|
-
};
|
|
325
|
-
|
|
326
|
-
const relType = relationMap[relationType];
|
|
327
|
-
if (!relType) return undefined;
|
|
328
|
-
|
|
329
|
-
// Get related entities (parent direction)
|
|
330
|
-
const getRelated = relationships.getRelated;
|
|
331
|
-
if (getRelated) {
|
|
332
|
-
const parents = getRelated(expressId, relType as never, 'inverse');
|
|
333
|
-
if (parents && parents.length > 0) {
|
|
334
|
-
const parentId = parents[0];
|
|
335
|
-
return {
|
|
336
|
-
expressId: parentId,
|
|
337
|
-
entityType: this.getEntityType(parentId) || 'Unknown',
|
|
338
|
-
predefinedType: this.getObjectType(parentId),
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
return undefined;
|
|
344
|
-
},
|
|
345
|
-
|
|
346
|
-
getAttribute(expressId: number, attributeName: string): string | undefined {
|
|
347
|
-
const lowerName = attributeName.toLowerCase();
|
|
348
|
-
|
|
349
|
-
// Map common attribute names to accessor methods
|
|
350
|
-
switch (lowerName) {
|
|
351
|
-
case 'name':
|
|
352
|
-
return this.getEntityName(expressId);
|
|
353
|
-
case 'description':
|
|
354
|
-
return this.getDescription(expressId);
|
|
355
|
-
case 'globalid':
|
|
356
|
-
return this.getGlobalId(expressId);
|
|
357
|
-
case 'objecttype':
|
|
358
|
-
case 'predefinedtype':
|
|
359
|
-
return this.getObjectType(expressId);
|
|
360
|
-
default: {
|
|
361
|
-
// Try to get from entities table if available
|
|
362
|
-
const entities = dataStore.entities as {
|
|
363
|
-
getAttribute?: (id: number, attr: string) => string | undefined;
|
|
364
|
-
};
|
|
365
|
-
if (entities?.getAttribute) {
|
|
366
|
-
return entities.getAttribute(expressId, attributeName);
|
|
367
|
-
}
|
|
368
|
-
return undefined;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
},
|
|
372
|
-
};
|
|
153
|
+
/** Export validation report to BCF with configurable options */
|
|
154
|
+
exportReportBCF: (settings: IDSBCFExportSettings) => Promise<void>;
|
|
155
|
+
/** BCF export progress state */
|
|
156
|
+
bcfExportProgress: IDSExportProgress | null;
|
|
373
157
|
}
|
|
374
158
|
|
|
375
159
|
// ============================================================================
|
|
376
160
|
// Hook Implementation
|
|
377
161
|
// ============================================================================
|
|
378
162
|
|
|
379
|
-
|
|
380
|
-
const
|
|
381
|
-
const DEFAULT_PASSED_COLOR: [number, number, number, number] = [0.2, 0.8, 0.2, 1.0];
|
|
163
|
+
/** Dark background for BCF snapshot captures */
|
|
164
|
+
const SNAPSHOT_CLEAR_COLOR: [number, number, number, number] = [0.102, 0.106, 0.149, 1];
|
|
382
165
|
|
|
383
166
|
export function useIDS(options: UseIDSOptions = {}): UseIDSResult {
|
|
384
167
|
const {
|
|
@@ -434,7 +217,7 @@ export function useIDS(options: UseIDSOptions = {}): UseIDSResult {
|
|
|
434
217
|
const geometryResult = useViewerStore((s) => s.geometryResult);
|
|
435
218
|
|
|
436
219
|
// Ref to store original colors before IDS color overrides
|
|
437
|
-
const originalColorsRef = useRef<Map<number,
|
|
220
|
+
const originalColorsRef = useRef<Map<number, ColorTuple>>(new Map());
|
|
438
221
|
|
|
439
222
|
// Ref to access geometryResult without creating callback dependencies (prevents infinite loops)
|
|
440
223
|
const geometryResultRef = useRef(geometryResult);
|
|
@@ -504,7 +287,6 @@ export function useIDS(options: UseIDSOptions = {}): UseIDSResult {
|
|
|
504
287
|
}
|
|
505
288
|
|
|
506
289
|
// Determine model ID - use '__legacy__' for legacy single-model mode
|
|
507
|
-
const isLegacyMode = ifcDataStore && models.size === 0;
|
|
508
290
|
const modelId = activeModelId || (models.size > 0 ? Array.from(models.keys())[0] : '__legacy__');
|
|
509
291
|
|
|
510
292
|
try {
|
|
@@ -635,50 +417,15 @@ export function useIDS(options: UseIDSOptions = {}): UseIDSResult {
|
|
|
635
417
|
const applyColors = useCallback(() => {
|
|
636
418
|
if (!report) return;
|
|
637
419
|
|
|
638
|
-
const colorUpdates =
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
for (const entityResult of specResult.entityResults) {
|
|
648
|
-
const model = models.get(entityResult.modelId);
|
|
649
|
-
const globalId = model
|
|
650
|
-
? entityResult.expressId + (model.idOffset ?? 0)
|
|
651
|
-
: entityResult.expressId;
|
|
652
|
-
globalIdsToUpdate.add(globalId);
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
// Capture original colors before applying overrides (only if not already captured)
|
|
657
|
-
// Use ref to avoid dependency on geometryResult which would cause infinite loops
|
|
658
|
-
const currentGeometry = geometryResultRef.current;
|
|
659
|
-
if (currentGeometry?.meshes && originalColorsRef.current.size === 0) {
|
|
660
|
-
for (const mesh of currentGeometry.meshes) {
|
|
661
|
-
if (globalIdsToUpdate.has(mesh.expressId)) {
|
|
662
|
-
originalColorsRef.current.set(mesh.expressId, [...mesh.color] as [number, number, number, number]);
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
// Process all entity results
|
|
668
|
-
for (const specResult of report.specificationResults) {
|
|
669
|
-
for (const entityResult of specResult.entityResults) {
|
|
670
|
-
const model = models.get(entityResult.modelId);
|
|
671
|
-
const globalId = model
|
|
672
|
-
? entityResult.expressId + (model.idOffset ?? 0)
|
|
673
|
-
: entityResult.expressId;
|
|
674
|
-
|
|
675
|
-
if (entityResult.passed && displayOptions.highlightPassed) {
|
|
676
|
-
colorUpdates.set(globalId, passedClr);
|
|
677
|
-
} else if (!entityResult.passed && displayOptions.highlightFailed) {
|
|
678
|
-
colorUpdates.set(globalId, failedClr);
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
}
|
|
420
|
+
const colorUpdates = buildValidationColorUpdates(
|
|
421
|
+
report,
|
|
422
|
+
models,
|
|
423
|
+
displayOptions,
|
|
424
|
+
defaultFailedColor,
|
|
425
|
+
defaultPassedColor,
|
|
426
|
+
geometryResultRef.current,
|
|
427
|
+
originalColorsRef.current
|
|
428
|
+
);
|
|
682
429
|
|
|
683
430
|
if (colorUpdates.size > 0) {
|
|
684
431
|
updateMeshColors(colorUpdates);
|
|
@@ -686,18 +433,9 @@ export function useIDS(options: UseIDSOptions = {}): UseIDSResult {
|
|
|
686
433
|
}, [report, models, displayOptions, defaultFailedColor, defaultPassedColor, updateMeshColors]);
|
|
687
434
|
|
|
688
435
|
const clearColors = useCallback(() => {
|
|
689
|
-
|
|
690
|
-
if (
|
|
691
|
-
return;
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
// Create a new map with the original colors to restore
|
|
695
|
-
const colorUpdates = new Map<number, [number, number, number, number]>(originalColorsRef.current);
|
|
696
|
-
|
|
697
|
-
if (colorUpdates.size > 0) {
|
|
436
|
+
const colorUpdates = buildRestoreColorUpdates(originalColorsRef.current);
|
|
437
|
+
if (colorUpdates && colorUpdates.size > 0) {
|
|
698
438
|
updateMeshColors(colorUpdates);
|
|
699
|
-
// Clear the stored original colors after restoring
|
|
700
|
-
originalColorsRef.current.clear();
|
|
701
439
|
}
|
|
702
440
|
}, [updateMeshColors]);
|
|
703
441
|
|
|
@@ -820,46 +558,7 @@ export function useIDS(options: UseIDSOptions = {}): UseIDSResult {
|
|
|
820
558
|
console.warn('[IDS] No report to export');
|
|
821
559
|
return;
|
|
822
560
|
}
|
|
823
|
-
|
|
824
|
-
const exportData = {
|
|
825
|
-
document: report.document,
|
|
826
|
-
modelInfo: report.modelInfo,
|
|
827
|
-
timestamp: report.timestamp.toISOString(),
|
|
828
|
-
summary: report.summary,
|
|
829
|
-
specificationResults: report.specificationResults.map(spec => ({
|
|
830
|
-
specification: spec.specification,
|
|
831
|
-
status: spec.status,
|
|
832
|
-
applicableCount: spec.applicableCount,
|
|
833
|
-
passedCount: spec.passedCount,
|
|
834
|
-
failedCount: spec.failedCount,
|
|
835
|
-
passRate: spec.passRate,
|
|
836
|
-
entityResults: spec.entityResults.map(entity => ({
|
|
837
|
-
expressId: entity.expressId,
|
|
838
|
-
modelId: entity.modelId,
|
|
839
|
-
entityType: entity.entityType,
|
|
840
|
-
entityName: entity.entityName,
|
|
841
|
-
globalId: entity.globalId,
|
|
842
|
-
passed: entity.passed,
|
|
843
|
-
requirementResults: entity.requirementResults.map(req => ({
|
|
844
|
-
requirement: req.requirement,
|
|
845
|
-
status: req.status,
|
|
846
|
-
facetType: req.facetType,
|
|
847
|
-
checkedDescription: req.checkedDescription,
|
|
848
|
-
failureReason: req.failureReason,
|
|
849
|
-
actualValue: req.actualValue,
|
|
850
|
-
expectedValue: req.expectedValue,
|
|
851
|
-
})),
|
|
852
|
-
})),
|
|
853
|
-
})),
|
|
854
|
-
};
|
|
855
|
-
|
|
856
|
-
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
|
|
857
|
-
const url = URL.createObjectURL(blob);
|
|
858
|
-
const a = globalThis.document.createElement('a');
|
|
859
|
-
a.href = url;
|
|
860
|
-
a.download = `ids-report-${new Date().toISOString().split('T')[0]}.json`;
|
|
861
|
-
a.click();
|
|
862
|
-
URL.revokeObjectURL(url);
|
|
561
|
+
downloadReportJSON(report);
|
|
863
562
|
}, [report]);
|
|
864
563
|
|
|
865
564
|
const exportReportHTML = useCallback(() => {
|
|
@@ -867,142 +566,248 @@ export function useIDS(options: UseIDSOptions = {}): UseIDSResult {
|
|
|
867
566
|
console.warn('[IDS] No report to export');
|
|
868
567
|
return;
|
|
869
568
|
}
|
|
569
|
+
downloadReportHTML(report, locale);
|
|
570
|
+
}, [report, locale]);
|
|
870
571
|
|
|
871
|
-
// HTML escape helper to prevent XSS
|
|
872
|
-
const escapeHtml = (str: string | undefined | null): string => {
|
|
873
|
-
if (str == null) return '';
|
|
874
|
-
return String(str)
|
|
875
|
-
.replace(/&/g, '&')
|
|
876
|
-
.replace(/</g, '<')
|
|
877
|
-
.replace(/>/g, '>')
|
|
878
|
-
.replace(/"/g, '"')
|
|
879
|
-
.replace(/'/g, ''');
|
|
880
|
-
};
|
|
881
572
|
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
573
|
+
// BCF export progress state
|
|
574
|
+
const [bcfExportProgress, setBcfExportProgress] = useState<IDSExportProgress | null>(null);
|
|
575
|
+
|
|
576
|
+
// BCF store actions for 'load into panel'
|
|
577
|
+
const setBcfProject = useViewerStore((s) => s.setBcfProject);
|
|
578
|
+
const setBcfPanelVisible = useViewerStore((s) => s.setBcfPanelVisible);
|
|
579
|
+
const bcfAuthor = useViewerStore((s) => s.bcfAuthor);
|
|
580
|
+
|
|
581
|
+
const exportReportBCF = useCallback(async (settings: IDSBCFExportSettings) => {
|
|
582
|
+
if (!report) {
|
|
583
|
+
console.warn('[IDS] No report to export');
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
try {
|
|
588
|
+
const {
|
|
589
|
+
topicGrouping,
|
|
590
|
+
includePassingEntities,
|
|
591
|
+
includeCamera,
|
|
592
|
+
includeSnapshots,
|
|
593
|
+
loadIntoBcfPanel,
|
|
594
|
+
} = settings;
|
|
595
|
+
|
|
596
|
+
// Phase 1: Collect entity bounds (needed for both camera and snapshots)
|
|
597
|
+
let entityBounds: Map<string, EntityBoundsInput> | undefined;
|
|
598
|
+
|
|
599
|
+
if (includeCamera || includeSnapshots) {
|
|
600
|
+
setBcfExportProgress({ phase: 'building', current: 0, total: 1, message: 'Computing entity bounds...' });
|
|
601
|
+
|
|
602
|
+
entityBounds = new Map();
|
|
603
|
+
const geomResult = geometryResultRef.current;
|
|
604
|
+
|
|
605
|
+
// Collect geometry from all models
|
|
606
|
+
const allMeshData: Array<{ meshes: unknown[]; idOffset: number; modelId: string }> = [];
|
|
607
|
+
for (const [modelId, model] of models.entries()) {
|
|
608
|
+
if (model.geometryResult?.meshes) {
|
|
609
|
+
allMeshData.push({
|
|
610
|
+
meshes: model.geometryResult.meshes,
|
|
611
|
+
idOffset: model.idOffset ?? 0,
|
|
612
|
+
modelId,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Also include legacy single-model geometry
|
|
618
|
+
if (geomResult?.meshes && allMeshData.length === 0) {
|
|
619
|
+
allMeshData.push({
|
|
620
|
+
meshes: geomResult.meshes,
|
|
621
|
+
idOffset: 0,
|
|
622
|
+
modelId: 'default',
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Compute bounds for each entity that appears in the report
|
|
627
|
+
for (const specResult of report.specificationResults) {
|
|
628
|
+
for (const entity of specResult.entityResults) {
|
|
629
|
+
if (entity.passed && !includePassingEntities) continue;
|
|
630
|
+
const boundsKey = `${entity.modelId}:${entity.expressId}`;
|
|
631
|
+
if (entityBounds.has(boundsKey)) continue;
|
|
632
|
+
|
|
633
|
+
// Find matching model geometry
|
|
634
|
+
for (const modelData of allMeshData) {
|
|
635
|
+
if (modelData.modelId === entity.modelId || allMeshData.length === 1) {
|
|
636
|
+
const globalExpressId = entity.expressId + modelData.idOffset;
|
|
637
|
+
const bounds = getEntityBounds(
|
|
638
|
+
modelData.meshes as Parameters<typeof getEntityBounds>[0],
|
|
639
|
+
globalExpressId,
|
|
640
|
+
);
|
|
641
|
+
if (bounds) {
|
|
642
|
+
entityBounds.set(boundsKey, bounds);
|
|
643
|
+
}
|
|
644
|
+
break;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Phase 2: Batch snapshots if requested
|
|
652
|
+
let entitySnapshots: Map<string, string> | undefined;
|
|
653
|
+
|
|
654
|
+
if (includeSnapshots) {
|
|
655
|
+
entitySnapshots = new Map();
|
|
656
|
+
|
|
657
|
+
// Get renderer for direct rendering control (no selection highlight)
|
|
658
|
+
const renderer = getGlobalRenderer();
|
|
659
|
+
if (!renderer) {
|
|
660
|
+
console.warn('[IDS] No renderer available for snapshot capture');
|
|
661
|
+
} else {
|
|
662
|
+
const camera = renderer.getCamera();
|
|
663
|
+
|
|
664
|
+
// Collect all unique entities that need snapshots (Set-based O(1) dedup)
|
|
665
|
+
const seenKeys = new Set<string>();
|
|
666
|
+
const entitiesToSnapshot: Array<{ modelId: string; expressId: number; boundsKey: string }> = [];
|
|
667
|
+
for (const specResult of report.specificationResults) {
|
|
668
|
+
for (const entity of specResult.entityResults) {
|
|
669
|
+
if (entity.passed && !includePassingEntities) continue;
|
|
670
|
+
const boundsKey = `${entity.modelId}:${entity.expressId}`;
|
|
671
|
+
if (!seenKeys.has(boundsKey)) {
|
|
672
|
+
seenKeys.add(boundsKey);
|
|
673
|
+
entitiesToSnapshot.push({
|
|
674
|
+
modelId: entity.modelId,
|
|
675
|
+
expressId: entity.expressId,
|
|
676
|
+
boundsKey,
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const total = entitiesToSnapshot.length;
|
|
683
|
+
|
|
684
|
+
// Save current viewer state to restore after snapshot batch
|
|
685
|
+
const storeState = useViewerStore.getState();
|
|
686
|
+
const savedSelection = storeState.selectedEntityId;
|
|
687
|
+
const savedIsolation = storeState.isolatedEntities;
|
|
688
|
+
const savedHidden = storeState.hiddenEntities;
|
|
689
|
+
|
|
690
|
+
for (let i = 0; i < total; i++) {
|
|
691
|
+
const entity = entitiesToSnapshot[i];
|
|
692
|
+
setBcfExportProgress({
|
|
693
|
+
phase: 'snapshots',
|
|
694
|
+
current: i + 1,
|
|
695
|
+
total,
|
|
696
|
+
message: `Capturing snapshot ${i + 1}/${total}...`,
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
// Get the entity's bounds for framing
|
|
700
|
+
const bounds = entityBounds?.get(entity.boundsKey);
|
|
701
|
+
if (!bounds) continue;
|
|
702
|
+
|
|
703
|
+
// Find the global expressId for isolation (direct Map lookup)
|
|
704
|
+
const model = models.get(entity.modelId);
|
|
705
|
+
const globalExpressId = entity.expressId + (model?.idOffset ?? 0);
|
|
706
|
+
|
|
707
|
+
// Frame the entity bounds directly via camera (properly centers the object)
|
|
708
|
+
// duration=1 (not 0) because the animator skips updates when duration===0,
|
|
709
|
+
// causing the camera to never move. 1ms is effectively instant.
|
|
710
|
+
await camera.frameBounds(bounds.min, bounds.max, 1);
|
|
711
|
+
|
|
712
|
+
// Render with: entity isolated, NO selection highlight (no cyan), IDS colors intact
|
|
713
|
+
const isolationSet = new Set([globalExpressId]);
|
|
714
|
+
renderer.render({
|
|
715
|
+
isolatedIds: isolationSet,
|
|
716
|
+
selectedId: null, // No cyan selection highlight
|
|
717
|
+
clearColor: SNAPSHOT_CLEAR_COLOR,
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
// Wait for GPU commands to complete
|
|
721
|
+
const device = renderer.getGPUDevice();
|
|
722
|
+
if (device) {
|
|
723
|
+
await device.queue.onSubmittedWorkDone();
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Wait for the browser compositor to present the frame to the canvas.
|
|
727
|
+
// Without this, toDataURL() reads a stale canvas — only the last snapshot
|
|
728
|
+
// would show the entity because previous frames haven't been composited yet.
|
|
729
|
+
await new Promise<void>(resolve => requestAnimationFrame(() => resolve()));
|
|
730
|
+
|
|
731
|
+
// Capture the now-presented frame
|
|
732
|
+
const dataUrl = await renderer.captureScreenshot();
|
|
733
|
+
if (dataUrl) {
|
|
734
|
+
entitySnapshots.set(entity.boundsKey, dataUrl);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Restore viewer state — set store back to saved state directly
|
|
739
|
+
useViewerStore.setState({
|
|
740
|
+
selectedEntityId: savedSelection,
|
|
741
|
+
isolatedEntities: savedIsolation,
|
|
742
|
+
hiddenEntities: savedHidden,
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
// Re-render with restored state (original clearColor restored by omitting it)
|
|
746
|
+
renderer.render({
|
|
747
|
+
hiddenIds: savedHidden,
|
|
748
|
+
isolatedIds: savedIsolation,
|
|
749
|
+
selectedId: savedSelection,
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Phase 3: Build BCF project
|
|
755
|
+
setBcfExportProgress({ phase: 'writing', current: 0, total: 1, message: 'Building BCF project...' });
|
|
756
|
+
|
|
757
|
+
const exportOptions: IDSBCFExportOptions = {
|
|
758
|
+
author: bcfAuthor || report.document.info.author || 'ids-validator@ifc-lite',
|
|
759
|
+
projectName: `IDS Report - ${report.document.info.title}`,
|
|
760
|
+
topicGrouping,
|
|
761
|
+
includePassingEntities,
|
|
762
|
+
entityBounds,
|
|
763
|
+
entitySnapshots,
|
|
886
764
|
};
|
|
887
765
|
|
|
888
|
-
const
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
.summary-item .value { font-size: 24px; font-weight: bold; }
|
|
902
|
-
.summary-item .label { color: #6b7280; font-size: 14px; }
|
|
903
|
-
.pass { color: #22c55e; }
|
|
904
|
-
.fail { color: #ef4444; }
|
|
905
|
-
.progress-bar { height: 8px; background: #e5e7eb; border-radius: 4px; overflow: hidden; }
|
|
906
|
-
.progress-fill { height: 100%; transition: width 0.3s; }
|
|
907
|
-
.spec-card { border: 1px solid #e5e7eb; border-radius: 8px; margin-bottom: 12px; }
|
|
908
|
-
.spec-header { padding: 16px; cursor: pointer; }
|
|
909
|
-
.spec-header:hover { background: #f9fafb; }
|
|
910
|
-
.entity-list { border-top: 1px solid #e5e7eb; max-height: 400px; overflow-y: auto; }
|
|
911
|
-
.entity-row { padding: 12px 16px; border-bottom: 1px solid #f3f4f6; }
|
|
912
|
-
.entity-row:last-child { border-bottom: none; }
|
|
913
|
-
.requirement { font-size: 13px; padding: 4px 0; color: #6b7280; }
|
|
914
|
-
.failure-reason { color: #ef4444; font-size: 12px; margin-top: 2px; }
|
|
915
|
-
table { width: 100%; border-collapse: collapse; }
|
|
916
|
-
th, td { padding: 8px 12px; text-align: left; border-bottom: 1px solid #e5e7eb; }
|
|
917
|
-
th { background: #f9fafb; font-weight: 600; }
|
|
918
|
-
</style>
|
|
919
|
-
</head>
|
|
920
|
-
<body>
|
|
921
|
-
<div class="card">
|
|
922
|
-
<h1>${escapeHtml(report.document.info.title)}</h1>
|
|
923
|
-
${report.document.info.description ? `<p>${escapeHtml(report.document.info.description)}</p>` : ''}
|
|
924
|
-
<p><strong>Model:</strong> ${escapeHtml(report.modelInfo.modelId)} | <strong>Schema:</strong> ${escapeHtml(report.modelInfo.schemaVersion)} | <strong>Date:</strong> ${escapeHtml(report.timestamp.toLocaleString())}</p>
|
|
925
|
-
</div>
|
|
926
|
-
|
|
927
|
-
<div class="card">
|
|
928
|
-
<h2>Summary</h2>
|
|
929
|
-
<div class="summary">
|
|
930
|
-
<div class="summary-item">
|
|
931
|
-
<div class="value">${report.summary.totalSpecifications}</div>
|
|
932
|
-
<div class="label">Specifications</div>
|
|
933
|
-
</div>
|
|
934
|
-
<div class="summary-item">
|
|
935
|
-
<div class="value pass">${report.summary.passedSpecifications}</div>
|
|
936
|
-
<div class="label">Passed</div>
|
|
937
|
-
</div>
|
|
938
|
-
<div class="summary-item">
|
|
939
|
-
<div class="value fail">${report.summary.failedSpecifications}</div>
|
|
940
|
-
<div class="label">Failed</div>
|
|
941
|
-
</div>
|
|
942
|
-
<div class="summary-item">
|
|
943
|
-
<div class="value">${report.summary.totalEntitiesChecked}</div>
|
|
944
|
-
<div class="label">Entities Checked</div>
|
|
945
|
-
</div>
|
|
946
|
-
<div class="summary-item">
|
|
947
|
-
<div class="value">${report.summary.overallPassRate}%</div>
|
|
948
|
-
<div class="label">Pass Rate</div>
|
|
949
|
-
</div>
|
|
950
|
-
</div>
|
|
951
|
-
</div>
|
|
952
|
-
|
|
953
|
-
<div class="card">
|
|
954
|
-
<h2>Specifications</h2>
|
|
955
|
-
${report.specificationResults.map(spec => `
|
|
956
|
-
<div class="spec-card">
|
|
957
|
-
<div class="spec-header">
|
|
958
|
-
<h3 style="${statusClass(spec.status)}">${spec.status === 'pass' ? '✓' : '✗'} ${escapeHtml(spec.specification.name)}</h3>
|
|
959
|
-
${spec.specification.description ? `<p style="margin: 8px 0; color: #6b7280;">${escapeHtml(spec.specification.description)}</p>` : ''}
|
|
960
|
-
<div style="display: flex; gap: 16px; font-size: 14px; color: #6b7280;">
|
|
961
|
-
<span>${spec.applicableCount} entities</span>
|
|
962
|
-
<span class="pass">${spec.passedCount} passed</span>
|
|
963
|
-
<span class="fail">${spec.failedCount} failed</span>
|
|
964
|
-
</div>
|
|
965
|
-
<div class="progress-bar" style="margin-top: 8px;">
|
|
966
|
-
<div class="progress-fill" style="width: ${spec.passRate}%; background: ${spec.passRate >= 80 ? '#22c55e' : spec.passRate >= 50 ? '#eab308' : '#ef4444'};"></div>
|
|
967
|
-
</div>
|
|
968
|
-
</div>
|
|
969
|
-
${spec.entityResults.length > 0 ? `
|
|
970
|
-
<div class="entity-list">
|
|
971
|
-
${spec.entityResults.slice(0, 50).map(entity => `
|
|
972
|
-
<div class="entity-row">
|
|
973
|
-
<div style="${statusClass(entity.passed ? 'pass' : 'fail')}">
|
|
974
|
-
${entity.passed ? '✓' : '✗'} <strong>${escapeHtml(entity.entityName) || '#' + entity.expressId}</strong>
|
|
975
|
-
<span style="color: #6b7280; font-size: 13px;"> - ${escapeHtml(entity.entityType)}${entity.globalId ? ' · ' + escapeHtml(entity.globalId) : ''}</span>
|
|
976
|
-
</div>
|
|
977
|
-
${entity.requirementResults.filter(r => r.status === 'fail').map(req => `
|
|
978
|
-
<div class="requirement">
|
|
979
|
-
${escapeHtml(req.checkedDescription)}
|
|
980
|
-
${req.failureReason ? `<div class="failure-reason">${escapeHtml(req.failureReason)}</div>` : ''}
|
|
981
|
-
</div>
|
|
982
|
-
`).join('')}
|
|
983
|
-
</div>
|
|
984
|
-
`).join('')}
|
|
985
|
-
${spec.entityResults.length > 50 ? `<div class="entity-row" style="text-align: center; color: #6b7280;">... and ${spec.entityResults.length - 50} more entities</div>` : ''}
|
|
986
|
-
</div>
|
|
987
|
-
` : ''}
|
|
988
|
-
</div>
|
|
989
|
-
`).join('')}
|
|
990
|
-
</div>
|
|
991
|
-
|
|
992
|
-
<footer style="text-align: center; color: #6b7280; padding: 20px;">
|
|
993
|
-
Generated by IFC-Lite IDS Validator
|
|
994
|
-
</footer>
|
|
995
|
-
</body>
|
|
996
|
-
</html>`;
|
|
997
|
-
|
|
998
|
-
const blob = new Blob([html], { type: 'text/html' });
|
|
766
|
+
const bcfProject = createBCFFromIDSReport(
|
|
767
|
+
{
|
|
768
|
+
title: report.document.info.title,
|
|
769
|
+
description: report.document.info.description,
|
|
770
|
+
specificationResults: report.specificationResults,
|
|
771
|
+
},
|
|
772
|
+
exportOptions,
|
|
773
|
+
);
|
|
774
|
+
|
|
775
|
+
// Phase 4: Write BCF and download
|
|
776
|
+
setBcfExportProgress({ phase: 'writing', current: 1, total: 2, message: 'Writing BCF file...' });
|
|
777
|
+
|
|
778
|
+
const blob = await writeBCF(bcfProject);
|
|
999
779
|
const url = URL.createObjectURL(blob);
|
|
1000
780
|
const a = globalThis.document.createElement('a');
|
|
1001
781
|
a.href = url;
|
|
1002
|
-
a.download = `ids-report-${new Date().toISOString().split('T')[0]}.
|
|
782
|
+
a.download = `ids-report-${new Date().toISOString().split('T')[0]}.bcf`;
|
|
1003
783
|
a.click();
|
|
1004
784
|
URL.revokeObjectURL(url);
|
|
1005
|
-
|
|
785
|
+
|
|
786
|
+
// Phase 5: Load into BCF panel if requested
|
|
787
|
+
if (loadIntoBcfPanel) {
|
|
788
|
+
setBcfProject(bcfProject);
|
|
789
|
+
setBcfPanelVisible(true);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
setBcfExportProgress({ phase: 'done', current: 1, total: 1, message: 'Export complete!' });
|
|
793
|
+
|
|
794
|
+
// Clear progress after a delay
|
|
795
|
+
setTimeout(() => setBcfExportProgress(null), 2000);
|
|
796
|
+
|
|
797
|
+
} catch (err) {
|
|
798
|
+
const message = err instanceof Error ? err.message : 'BCF export failed';
|
|
799
|
+
setIdsError(message);
|
|
800
|
+
console.error('[IDS] BCF export error:', err);
|
|
801
|
+
setBcfExportProgress(null);
|
|
802
|
+
}
|
|
803
|
+
}, [
|
|
804
|
+
report,
|
|
805
|
+
models,
|
|
806
|
+
bcfAuthor,
|
|
807
|
+
setIdsError,
|
|
808
|
+
setBcfProject,
|
|
809
|
+
setBcfPanelVisible,
|
|
810
|
+
]);
|
|
1006
811
|
|
|
1007
812
|
// ============================================================================
|
|
1008
813
|
// Return
|
|
@@ -1061,5 +866,7 @@ export function useIDS(options: UseIDSOptions = {}): UseIDSResult {
|
|
|
1061
866
|
// Export actions
|
|
1062
867
|
exportReportJSON,
|
|
1063
868
|
exportReportHTML,
|
|
869
|
+
exportReportBCF,
|
|
870
|
+
bcfExportProgress,
|
|
1064
871
|
};
|
|
1065
872
|
}
|