@ifc-lite/viewer 1.19.0 → 1.21.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/.turbo/turbo-build.log +59 -43
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +496 -0
- package/dist/assets/basketViewActivator-Bzw51jhm.js +71 -0
- package/dist/assets/{bcf-DOG9_WPX.js → bcf-4K724hw0.js} +18 -18
- package/dist/assets/decode-worker-t2EGKAxO.js +1708 -0
- package/dist/assets/drawing-2d-Bjy8YPrg.js +257 -0
- package/dist/assets/exporters-u0sz2Upj.js +259119 -0
- package/dist/assets/geometry-controller.worker-NH8pZmrU.js +7 -0
- package/dist/assets/geometry.worker-Bp4rW_R1.js +1 -0
- package/dist/assets/ids-B7AXEv7h.js +4067 -0
- package/dist/assets/ifc-lite-DfZHk36-.js +7 -0
- package/dist/assets/ifc-lite_bg-DlKs5-yM.wasm +0 -0
- package/dist/assets/ifc-lite_bg-PqmRe3Ph.wasm +0 -0
- package/dist/assets/index-CSWgTe1s.css +1 -0
- package/dist/assets/{index-BOi3BuUI.js → index-DVNSvEMh.js} +49877 -28410
- package/dist/assets/laz-perf-Cvr_Lepg.js +1 -0
- package/dist/assets/laz-perf-DnSyzVYH.wasm +0 -0
- package/dist/assets/{native-bridge-CpBeOPQa.js → native-bridge-BiD01jI9.js} +2 -2
- package/dist/assets/parser.worker-Bnbrl6gy.js +182 -0
- package/dist/assets/{sandbox-Baez7n-t.js → sandbox-DPD1ROr0.js} +548 -530
- package/dist/assets/{server-client-BB6cMAXE.js → server-client-DP8fMPY9.js} +1 -1
- package/dist/assets/three-CDRZThFA.js +4057 -0
- package/dist/assets/{wasm-bridge-CAYCUHbE.js → wasm-bridge-CErti6zX.js} +1 -1
- package/dist/assets/workerHelpers-CBbWSJmd.js +36 -0
- package/dist/index.html +10 -9
- package/dist/samples/building-architecture.ifc +453 -0
- package/dist/samples/hello-wall.ifc +1054 -0
- package/dist/samples/infra-bridge.ifc +962 -0
- package/index.html +1 -1
- package/package.json +15 -10
- package/public/samples/building-architecture.ifc +453 -0
- package/public/samples/hello-wall.ifc +1054 -0
- package/public/samples/infra-bridge.ifc +962 -0
- package/src/App.tsx +37 -3
- package/src/components/mcp/HeroScene.tsx +876 -0
- package/src/components/mcp/McpLanding.tsx +1318 -0
- package/src/components/mcp/McpPlayground.tsx +524 -0
- package/src/components/mcp/PlaygroundChat.tsx +1097 -0
- package/src/components/mcp/PlaygroundViewer.tsx +815 -0
- package/src/components/mcp/README.md +171 -0
- package/src/components/mcp/data.ts +659 -0
- package/src/components/mcp/playground-dispatcher.ts +1649 -0
- package/src/components/mcp/playground-files.ts +107 -0
- package/src/components/mcp/playground-uploads.ts +122 -0
- package/src/components/mcp/types.ts +65 -0
- package/src/components/mcp/use-mcp-page.ts +109 -0
- package/src/components/viewer/BasketPresentationDock.tsx +3 -0
- package/src/components/viewer/CesiumOverlay.tsx +165 -120
- package/src/components/viewer/DeviationPanel.tsx +172 -0
- package/src/components/viewer/HierarchyPanel.tsx +29 -3
- package/src/components/viewer/HoverTooltip.tsx +5 -0
- package/src/components/viewer/IDSAuditSummary.tsx +389 -0
- package/src/components/viewer/IDSPanel.tsx +80 -26
- package/src/components/viewer/MainToolbar.tsx +79 -7
- package/src/components/viewer/MergeLayersBanner.tsx +108 -0
- package/src/components/viewer/MobileToolbar.tsx +326 -0
- package/src/components/viewer/PointCloudClasses.tsx +111 -0
- package/src/components/viewer/PointCloudLegend.tsx +119 -0
- package/src/components/viewer/PointCloudPanel.tsx +52 -1
- package/src/components/viewer/PropertiesPanel.tsx +37 -6
- package/src/components/viewer/RectSelectionOverlay.tsx +48 -0
- package/src/components/viewer/StatusBar.tsx +14 -0
- package/src/components/viewer/ViewerLayout.tsx +288 -95
- package/src/components/viewer/Viewport.tsx +86 -18
- package/src/components/viewer/ViewportContainer.tsx +60 -15
- package/src/components/viewer/ViewportOverlays.tsx +41 -26
- package/src/components/viewer/mouseHandlerTypes.ts +22 -0
- package/src/components/viewer/properties/GeoreferencingPanel.tsx +77 -8
- package/src/components/viewer/properties/MaterialCard.tsx +2 -2
- package/src/components/viewer/selectionHandlers.ts +41 -0
- package/src/components/viewer/tools/SectionPanel.tsx +181 -24
- package/src/components/viewer/tools/SectionVisualization.tsx +384 -3
- package/src/components/viewer/useAnimationLoop.ts +22 -0
- package/src/components/viewer/useMouseControls.ts +296 -3
- package/src/components/viewer/usePointCloudSync.ts +8 -1
- package/src/components/viewer/useRenderUpdates.ts +21 -1
- package/src/components/viewer/useTouchControls.ts +100 -41
- package/src/generated/mcp-catalog.json +82 -0
- package/src/hooks/federationLoadGate.test.ts +90 -0
- package/src/hooks/federationLoadGate.ts +127 -0
- package/src/hooks/ids/idsDataAccessor.ts +11 -259
- package/src/hooks/ingest/pointCloudIngest.ts +127 -16
- package/src/hooks/useDrawingGeneration.ts +81 -8
- package/src/hooks/useIDS.ts +90 -10
- package/src/hooks/useIfcFederation.ts +94 -16
- package/src/hooks/useIfcLoader.ts +289 -64
- package/src/hooks/useViewerSelectors.ts +10 -0
- package/src/lib/geo/cesium-bridge.ts +84 -67
- package/src/lib/geo/clamp-anchor.test.ts +80 -0
- package/src/lib/geo/clamp-anchor.ts +57 -0
- package/src/lib/geo/effective-georef.test.ts +79 -1
- package/src/lib/geo/effective-georef.ts +83 -0
- package/src/lib/geo/reproject.ts +26 -13
- package/src/lib/geo/terrain-elevation.ts +166 -0
- package/src/lib/lens/adapter.ts +1 -1
- package/src/lib/llm/context-builder.ts +1 -1
- package/src/lib/perf/memoryAccounting.test.ts +92 -0
- package/src/lib/perf/memoryAccounting.ts +235 -0
- package/src/sdk/adapters/mutation-view.ts +1 -1
- package/src/store/constants.ts +39 -2
- package/src/store/index.ts +6 -1
- package/src/store/slices/cesiumSlice.ts +1 -1
- package/src/store/slices/idsSlice.ts +24 -0
- package/src/store/slices/loadingSlice.ts +12 -0
- package/src/store/slices/pointCloudSlice.ts +72 -1
- package/src/store/slices/sectionSlice.test.ts +590 -1
- package/src/store/slices/sectionSlice.ts +344 -17
- package/src/store/slices/uiSlice.merge-layers.test.ts +217 -0
- package/src/store/slices/uiSlice.ts +60 -2
- package/src/store/types.ts +42 -0
- package/src/store.ts +13 -0
- package/src/utils/acquireFileBuffer.test.ts +231 -0
- package/src/utils/acquireFileBuffer.ts +128 -0
- package/src/utils/ifcConfig.ts +24 -0
- package/src/utils/nativeSpatialDataStore.ts +20 -2
- package/src/utils/spatialHierarchy.test.ts +116 -0
- package/src/utils/spatialHierarchy.ts +23 -0
- package/tailwind.config.js +5 -0
- package/tsconfig.json +1 -0
- package/vite.config.ts +12 -0
- package/dist/assets/basketViewActivator-RZy5c3Td.js +0 -1
- package/dist/assets/decode-worker-Collf_X_.js +0 -1320
- package/dist/assets/drawing-2d-DoxKMqbO.js +0 -257
- package/dist/assets/exporters-BraHBeoi.js +0 -81583
- package/dist/assets/geometry.worker-DQEZB2rB.js +0 -1
- package/dist/assets/ids-DQ5jY0E8.js +0 -1
- package/dist/assets/ifc-lite_bg-4yUkDRD8.wasm +0 -0
- package/dist/assets/index-0XpVr_S5.css +0 -1
package/src/hooks/useIDS.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { useCallback, useMemo, useEffect, useRef, useState } from 'react';
|
|
17
17
|
import { useViewerStore } from '@/store';
|
|
18
18
|
import type {
|
|
19
|
+
IDSAuditReport,
|
|
19
20
|
IDSDocument,
|
|
20
21
|
IDSValidationReport,
|
|
21
22
|
IDSModelInfo,
|
|
@@ -23,6 +24,8 @@ import type {
|
|
|
23
24
|
ValidationProgress,
|
|
24
25
|
} from '@ifc-lite/ids';
|
|
25
26
|
import {
|
|
27
|
+
auditIDSDocument,
|
|
28
|
+
IDSParseError,
|
|
26
29
|
parseIDS,
|
|
27
30
|
validateIDS,
|
|
28
31
|
createTranslationService,
|
|
@@ -61,6 +64,15 @@ export interface UseIDSResult {
|
|
|
61
64
|
// State
|
|
62
65
|
/** Loaded IDS document */
|
|
63
66
|
document: IDSDocument | null;
|
|
67
|
+
/**
|
|
68
|
+
* Audit report for the loaded IDS document — flags authoring issues
|
|
69
|
+
* surfaced by the document auditor (invalid IFC entities, malformed
|
|
70
|
+
* restrictions, missing required attributes, …). `null` when no
|
|
71
|
+
* document is loaded or the audit is still in flight.
|
|
72
|
+
*/
|
|
73
|
+
auditReport: IDSAuditReport | null;
|
|
74
|
+
/** True while the document auditor is running. */
|
|
75
|
+
auditing: boolean;
|
|
64
76
|
/** Validation report */
|
|
65
77
|
report: IDSValidationReport | null;
|
|
66
78
|
/** Loading state */
|
|
@@ -176,6 +188,8 @@ export function useIDS(options: UseIDSOptions = {}): UseIDSResult {
|
|
|
176
188
|
|
|
177
189
|
// IDS store state
|
|
178
190
|
const document = useViewerStore((s) => s.idsDocument);
|
|
191
|
+
const auditReport = useViewerStore((s) => s.idsAuditReport);
|
|
192
|
+
const auditing = useViewerStore((s) => s.idsAuditing);
|
|
179
193
|
const report = useViewerStore((s) => s.idsValidationReport);
|
|
180
194
|
const loading = useViewerStore((s) => s.idsLoading);
|
|
181
195
|
const progress = useViewerStore((s) => s.idsProgress);
|
|
@@ -190,6 +204,8 @@ export function useIDS(options: UseIDSOptions = {}): UseIDSResult {
|
|
|
190
204
|
// IDS store actions
|
|
191
205
|
const setIdsDocument = useViewerStore((s) => s.setIdsDocument);
|
|
192
206
|
const clearIdsDocument = useViewerStore((s) => s.clearIdsDocument);
|
|
207
|
+
const setIdsAuditReport = useViewerStore((s) => s.setIdsAuditReport);
|
|
208
|
+
const setIdsAuditing = useViewerStore((s) => s.setIdsAuditing);
|
|
193
209
|
const setIdsValidationReport = useViewerStore((s) => s.setIdsValidationReport);
|
|
194
210
|
const clearIdsValidationReport = useViewerStore((s) => s.clearIdsValidationReport);
|
|
195
211
|
const setIdsProgress = useViewerStore((s) => s.setIdsProgress);
|
|
@@ -249,22 +265,84 @@ export function useIDS(options: UseIDSOptions = {}): UseIDSResult {
|
|
|
249
265
|
// ============================================================================
|
|
250
266
|
|
|
251
267
|
const loadIDS = useCallback((xmlContent: string) => {
|
|
268
|
+
setIdsLoading(true);
|
|
269
|
+
setIdsError(null);
|
|
270
|
+
setIdsAuditing(true);
|
|
271
|
+
// Clear the previous audit/document up front so a re-load with a
|
|
272
|
+
// malformed file doesn't show stale issues from the previous one.
|
|
273
|
+
setIdsAuditReport(null);
|
|
274
|
+
|
|
275
|
+
// Try to parse synchronously so the panel switches into "document
|
|
276
|
+
// loaded" mode immediately. Capture any parse error but DON'T early-
|
|
277
|
+
// return — the auditor's permissive shim has its own parser and can
|
|
278
|
+
// still surface structured `E_PARSE_XML` / `E_XSD_*` issues even
|
|
279
|
+
// when the strict parser threw.
|
|
280
|
+
let parsed: IDSDocument | null = null;
|
|
281
|
+
let parseErrorMessage: string | null = null;
|
|
252
282
|
try {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
console.info(`[IDS] Loaded: "${doc.info.title}" (${doc.specifications.length} specifications)`);
|
|
283
|
+
parsed = parseIDS(xmlContent);
|
|
284
|
+
setIdsDocument(parsed);
|
|
285
|
+
console.info(
|
|
286
|
+
`[IDS] Loaded: "${parsed.info.title}" (${parsed.specifications.length} specifications)`
|
|
287
|
+
);
|
|
260
288
|
} catch (err) {
|
|
261
|
-
|
|
262
|
-
|
|
289
|
+
// Drop any previously-loaded document so the panel shows the
|
|
290
|
+
// empty state with the new audit, not the stale prior content.
|
|
291
|
+
setIdsDocument(null);
|
|
292
|
+
// Preserve the underlying detail (e.g. xmldom's
|
|
293
|
+
// "unexpected token at line N column M") instead of just the
|
|
294
|
+
// top-level "Invalid XML format" — that's the actionable bit.
|
|
295
|
+
if (err instanceof IDSParseError) {
|
|
296
|
+
parseErrorMessage = err.details
|
|
297
|
+
? `${err.message}: ${err.details}`
|
|
298
|
+
: err.message;
|
|
299
|
+
} else {
|
|
300
|
+
parseErrorMessage =
|
|
301
|
+
err instanceof Error ? err.message : 'Failed to parse IDS file';
|
|
302
|
+
}
|
|
263
303
|
console.error('[IDS] Parse error:', err);
|
|
264
304
|
} finally {
|
|
265
305
|
setIdsLoading(false);
|
|
266
306
|
}
|
|
267
|
-
|
|
307
|
+
|
|
308
|
+
// Always run the audit, even on parse failure. The permissive
|
|
309
|
+
// shim handles malformed XML gracefully and produces a single
|
|
310
|
+
// `E_PARSE_XML` issue plus whatever else it can salvage.
|
|
311
|
+
void auditIDSDocument(xmlContent)
|
|
312
|
+
.then((report) => {
|
|
313
|
+
setIdsAuditReport(report);
|
|
314
|
+
// If parse failed but the audit succeeded with no errors,
|
|
315
|
+
// something is internally inconsistent — keep the parse error
|
|
316
|
+
// visible. If the audit also reported errors (almost always the
|
|
317
|
+
// case on parse failure), the panel will surface those rich
|
|
318
|
+
// issues alongside / instead of the bare error string.
|
|
319
|
+
if (parseErrorMessage && report.issues.length === 0) {
|
|
320
|
+
setIdsError(parseErrorMessage);
|
|
321
|
+
} else if (parseErrorMessage) {
|
|
322
|
+
// Audit has structured issues — clear the bare-string error
|
|
323
|
+
// so the panel relies on the audit summary as the source of
|
|
324
|
+
// truth (it carries the same information in richer form).
|
|
325
|
+
setIdsError(null);
|
|
326
|
+
}
|
|
327
|
+
if (report.status === 'error') {
|
|
328
|
+
console.warn(
|
|
329
|
+
`[IDS] Audit found ${
|
|
330
|
+
report.issues.filter((i) => i.severity === 'error').length
|
|
331
|
+
} error(s) in the IDS document`
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
})
|
|
335
|
+
.catch((auditErr) => {
|
|
336
|
+
// Audit itself crashed — non-fatal but unusual. Clear the audit
|
|
337
|
+
// and fall back to whatever parse error we collected.
|
|
338
|
+
console.error('[IDS] Audit failed:', auditErr);
|
|
339
|
+
setIdsAuditReport(null);
|
|
340
|
+
if (parseErrorMessage) setIdsError(parseErrorMessage);
|
|
341
|
+
})
|
|
342
|
+
.finally(() => {
|
|
343
|
+
setIdsAuditing(false);
|
|
344
|
+
});
|
|
345
|
+
}, [setIdsDocument, setIdsLoading, setIdsError, setIdsAuditReport, setIdsAuditing]);
|
|
268
346
|
|
|
269
347
|
const loadIDSFile = useCallback(async (file: File) => {
|
|
270
348
|
try {
|
|
@@ -830,6 +908,8 @@ export function useIDS(options: UseIDSOptions = {}): UseIDSResult {
|
|
|
830
908
|
return {
|
|
831
909
|
// State
|
|
832
910
|
document,
|
|
911
|
+
auditReport,
|
|
912
|
+
auditing,
|
|
833
913
|
report,
|
|
834
914
|
loading,
|
|
835
915
|
progress,
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* Extracted from useIfc.ts for better separation of concerns
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { useCallback } from 'react';
|
|
13
|
+
import { useCallback, useRef } from 'react';
|
|
14
14
|
import { useShallow } from 'zustand/react/shallow';
|
|
15
15
|
import { useViewerStore, type FederatedModel, type SchemaVersion } from '../store.js';
|
|
16
16
|
import {
|
|
@@ -40,7 +40,9 @@ import {
|
|
|
40
40
|
} from './ingest/pointCloudIngest.js';
|
|
41
41
|
import { getGlobalRenderer } from './useBCF.js';
|
|
42
42
|
import { readNativeFile, type NativeFileHandle } from '../services/file-dialog.js';
|
|
43
|
-
import { getEffectiveGeoreference, type GeorefMutationDataLike } from '../lib/geo/effective-georef.js';
|
|
43
|
+
import { getEffectiveGeoreference, getEffectiveHorizontalScale, type GeorefMutationDataLike } from '../lib/geo/effective-georef.js';
|
|
44
|
+
import { acquireFederationLoadSlot, releaseFederationLoadSlot } from './federationLoadGate.js';
|
|
45
|
+
import { acquireFileBuffer } from '../utils/acquireFileBuffer.js';
|
|
44
46
|
|
|
45
47
|
function isNativeFileHandle(file: File | NativeFileHandle): file is NativeFileHandle {
|
|
46
48
|
return typeof (file as NativeFileHandle).path === 'string';
|
|
@@ -81,10 +83,14 @@ function getMapUnitScale(georef: ModelGeoref): number {
|
|
|
81
83
|
return georef.projectedCRS.mapUnitScale ?? georef.lengthUnitScale ?? 1;
|
|
82
84
|
}
|
|
83
85
|
|
|
84
|
-
function getAxis(
|
|
86
|
+
function getAxis(georef: ModelGeoref): { a: number; o: number; scale: number; denom: number } {
|
|
87
|
+
const conversion = georef.mapConversion;
|
|
85
88
|
const a = conversion.xAxisAbscissa ?? 1;
|
|
86
89
|
const o = conversion.xAxisOrdinate ?? 0;
|
|
87
|
-
|
|
90
|
+
// Use the effective horizontal scale: viewer geometry is already in metres,
|
|
91
|
+
// so applying IfcMapConversion.Scale raw would double-scale — see issue #595.
|
|
92
|
+
const mapUnitScale = georef.projectedCRS.mapUnitScale ?? georef.lengthUnitScale ?? 1;
|
|
93
|
+
const scale = getEffectiveHorizontalScale(conversion.scale, mapUnitScale, georef.lengthUnitScale ?? 1);
|
|
88
94
|
const denom = Math.max(a * a + o * o, 1e-12);
|
|
89
95
|
return { a, o, scale, denom };
|
|
90
96
|
}
|
|
@@ -151,8 +157,8 @@ function updateBounds(bounds: ReturnType<typeof emptyBounds>, x: number, y: numb
|
|
|
151
157
|
function buildGeorefAlignmentTransform(source: ModelGeoref, reference: ModelGeoref): AffineTransform3D | null {
|
|
152
158
|
const sourceConv = source.mapConversion;
|
|
153
159
|
const refConv = reference.mapConversion;
|
|
154
|
-
const sourceAxis = getAxis(
|
|
155
|
-
const refAxis = getAxis(
|
|
160
|
+
const sourceAxis = getAxis(source);
|
|
161
|
+
const refAxis = getAxis(reference);
|
|
156
162
|
const refDenom = refAxis.scale * refAxis.denom;
|
|
157
163
|
if (Math.abs(refDenom) < 1e-12) return null;
|
|
158
164
|
|
|
@@ -372,6 +378,13 @@ export function useIfcFederation() {
|
|
|
372
378
|
findModelForGlobalId: s.findModelForGlobalId,
|
|
373
379
|
})));
|
|
374
380
|
|
|
381
|
+
// Per-call ownership token. Each addModel() bumps this; state writes
|
|
382
|
+
// (loading/error/progress) in the catch block must compare back to
|
|
383
|
+
// their captured value before mutating, so a cancelled load A doesn't
|
|
384
|
+
// overwrite progress for a newer load B that started after A's abort.
|
|
385
|
+
// Mirrors the same pattern in useIfcLoader.ts.
|
|
386
|
+
const loadSessionRef = useRef(0);
|
|
387
|
+
|
|
375
388
|
/**
|
|
376
389
|
* Add a model to the federation (multi-model support)
|
|
377
390
|
* Uses FederationRegistry to assign unique ID offsets - BULLETPROOF against ID collisions
|
|
@@ -389,6 +402,16 @@ export function useIfcFederation() {
|
|
|
389
402
|
): Promise<string | null> => {
|
|
390
403
|
const modelId = options?.modelId ?? crypto.randomUUID();
|
|
391
404
|
const addStart = performance.now();
|
|
405
|
+
// Bump the per-call ownership token first so that any error path
|
|
406
|
+
// (including the load gate) can compare against this captured value
|
|
407
|
+
// before mutating shared loading/error/progress state.
|
|
408
|
+
const currentSession = ++loadSessionRef.current;
|
|
409
|
+
// Memory-aware load gate: if a previous federation load is still in
|
|
410
|
+
// flight on this tab and admitting this one would exceed the device
|
|
411
|
+
// memory budget, wait until headroom frees. Single-file loads never
|
|
412
|
+
// wait. See `federationLoadGate.ts` for the budget formula. (#600)
|
|
413
|
+
const fileSizeForGateMB = (typeof (file as File).size === 'number' ? (file as File).size : 0) / (1024 * 1024);
|
|
414
|
+
const gateSlot = await acquireFederationLoadSlot(fileSizeForGateMB);
|
|
392
415
|
try {
|
|
393
416
|
// IMPORTANT: Before adding a new model, check if there's a legacy model
|
|
394
417
|
// (loaded via loadFile) that's not in the Map yet. If so, migrate it first.
|
|
@@ -439,10 +462,24 @@ export function useIfcFederation() {
|
|
|
439
462
|
setError(null);
|
|
440
463
|
setProgress({ phase: 'Loading file', percent: 0 });
|
|
441
464
|
|
|
442
|
-
// Read file from disk
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
465
|
+
// Read file from disk. The browser path streams files above
|
|
466
|
+
// `STREAM_SAB_THRESHOLD` directly into a SharedArrayBuffer, eliminating
|
|
467
|
+
// the doubled peak (ArrayBuffer + SAB) of `await file.arrayBuffer()`
|
|
468
|
+
// when the geometry pipeline copies into its own SAB. The native path
|
|
469
|
+
// still reads via Tauri's Rust IPC because it bounds memory differently.
|
|
470
|
+
// (#600)
|
|
471
|
+
let buffer: ArrayBuffer;
|
|
472
|
+
if (isNativeFileHandle(file)) {
|
|
473
|
+
buffer = toExactArrayBuffer(await readNativeFile(file.path));
|
|
474
|
+
} else {
|
|
475
|
+
// The cast preserves the previous ArrayBuffer-shaped contract for
|
|
476
|
+
// every downstream consumer. When the underlying store is a SAB,
|
|
477
|
+
// downstream code only ever reads bytes via `new Uint8Array(buffer)`
|
|
478
|
+
// / `new DataView(buffer)`, both of which work on either backing
|
|
479
|
+
// store. The cast is purely type-system; runtime is identical.
|
|
480
|
+
const acquired = await acquireFileBuffer(file as File);
|
|
481
|
+
buffer = acquired.buffer as ArrayBuffer;
|
|
482
|
+
}
|
|
446
483
|
const fileSizeMB = buffer.byteLength / (1024 * 1024);
|
|
447
484
|
|
|
448
485
|
// Detect point cloud formats first — we never run them through
|
|
@@ -461,7 +498,7 @@ export function useIfcFederation() {
|
|
|
461
498
|
// depends on persisting it onto the FederatedModel record.
|
|
462
499
|
let pointCloudHandleId: number | undefined;
|
|
463
500
|
|
|
464
|
-
if (format === 'las' || format === 'laz' || format === 'ply' || format === 'pcd' || format === 'e57') {
|
|
501
|
+
if (format === 'las' || format === 'laz' || format === 'ply' || format === 'pcd' || format === 'e57' || format === 'pts' || format === 'xyz') {
|
|
465
502
|
const renderer = getGlobalRenderer();
|
|
466
503
|
if (!renderer) {
|
|
467
504
|
setError('Renderer not initialised — try again after the viewer mounts.');
|
|
@@ -482,11 +519,25 @@ export function useIfcFederation() {
|
|
|
482
519
|
onProgress: setProgress,
|
|
483
520
|
onAssetCountDelta: incCount,
|
|
484
521
|
});
|
|
522
|
+
// Expose cancellation while the stream is in-flight. Capture
|
|
523
|
+
// the canceller as a named ref so the cleanup can verify the
|
|
524
|
+
// store still points at us before clearing — a second
|
|
525
|
+
// addModel() that began before this one settles must not lose
|
|
526
|
+
// its Cancel button to our finally block.
|
|
527
|
+
const { setActiveStreamCanceller } = useViewerStore.getState();
|
|
528
|
+
const cancelStream = () => ingest.streamHandle.cancel();
|
|
529
|
+
setActiveStreamCanceller(cancelStream);
|
|
485
530
|
// ingest.done rejects on stream errors; ingestPointCloud's onError
|
|
486
531
|
// callback already calls removePointCloudAsset + incCount(-1), so
|
|
487
532
|
// the outer catch must NOT repeat that cleanup or the count goes
|
|
488
533
|
// negative when other point clouds are still loaded.
|
|
489
|
-
|
|
534
|
+
try {
|
|
535
|
+
await ingest.done;
|
|
536
|
+
} finally {
|
|
537
|
+
if (useViewerStore.getState().activeStreamCanceller === cancelStream) {
|
|
538
|
+
setActiveStreamCanceller(null);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
490
541
|
parsedDataStore = ingest.dataStore;
|
|
491
542
|
parsedGeometry = ingest.geometryResult;
|
|
492
543
|
schemaVersion = ingest.schemaVersion;
|
|
@@ -652,10 +703,31 @@ export function useIfcFederation() {
|
|
|
652
703
|
return modelId;
|
|
653
704
|
|
|
654
705
|
} catch (err) {
|
|
706
|
+
// Only mutate shared loading/error/progress state if our session
|
|
707
|
+
// is still the active one. A second addModel() that started after
|
|
708
|
+
// we were cancelled has already taken over the spinner — we must
|
|
709
|
+
// not overwrite it with our "Cancelled" state.
|
|
710
|
+
const isCurrent = loadSessionRef.current === currentSession;
|
|
711
|
+
// User-initiated cancel surfaces as an AbortError. Map it to a
|
|
712
|
+
// benign "Cancelled" state so the federated path matches the
|
|
713
|
+
// single-model loader rather than reporting a parse failure.
|
|
714
|
+
if (err instanceof DOMException && err.name === 'AbortError') {
|
|
715
|
+
console.log('[useIfc] addModel cancelled by user');
|
|
716
|
+
if (isCurrent) {
|
|
717
|
+
setError(null);
|
|
718
|
+
setProgress({ phase: 'Cancelled', percent: 0 });
|
|
719
|
+
setLoading(false);
|
|
720
|
+
}
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
655
723
|
console.error('[useIfc] addModel failed:', err);
|
|
656
|
-
|
|
657
|
-
|
|
724
|
+
if (isCurrent) {
|
|
725
|
+
setError(err instanceof Error ? err.message : 'Unknown error');
|
|
726
|
+
setLoading(false);
|
|
727
|
+
}
|
|
658
728
|
return null;
|
|
729
|
+
} finally {
|
|
730
|
+
releaseFederationLoadSlot(gateSlot);
|
|
659
731
|
}
|
|
660
732
|
}, [setLoading, setError, setProgress, setIfcDataStore, setGeometryResult, storeAddModel, hasModels, registerModelOffset]);
|
|
661
733
|
|
|
@@ -864,7 +936,10 @@ export function useIfcFederation() {
|
|
|
864
936
|
return;
|
|
865
937
|
}
|
|
866
938
|
|
|
867
|
-
// Check that all files are IFCX format and read buffers
|
|
939
|
+
// Check that all files are IFCX format and read buffers.
|
|
940
|
+
// IFCX is JSON; SAB streaming would force a SAB→scratch copy in
|
|
941
|
+
// safeUtf8Decode + retain the scratch (net worse peak than ArrayBuffer).
|
|
942
|
+
// Keep on file.arrayBuffer().
|
|
868
943
|
const buffers: Array<{ buffer: ArrayBuffer; name: string }> = [];
|
|
869
944
|
for (const file of files) {
|
|
870
945
|
const buffer = await file.arrayBuffer();
|
|
@@ -918,7 +993,10 @@ export function useIfcFederation() {
|
|
|
918
993
|
return;
|
|
919
994
|
}
|
|
920
995
|
|
|
921
|
-
// Read new overlay buffers
|
|
996
|
+
// Read new overlay buffers.
|
|
997
|
+
// IFCX is JSON; SAB streaming would force a SAB→scratch copy in
|
|
998
|
+
// safeUtf8Decode + retain the scratch (net worse peak than ArrayBuffer).
|
|
999
|
+
// Keep on file.arrayBuffer().
|
|
922
1000
|
const newBuffers: Array<{ buffer: ArrayBuffer; name: string }> = [];
|
|
923
1001
|
for (const file of files) {
|
|
924
1002
|
const buffer = await file.arrayBuffer();
|