@ifc-lite/viewer 1.28.0 → 1.28.1
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 +34 -41
- package/CHANGELOG.md +10 -0
- package/dist/assets/{basketViewActivator-BNRDNuUJ.js → basketViewActivator-Ce38DhXd.js} +7 -7
- package/dist/assets/{bcf-DCwCuP7n.js → bcf-Cv_O3JfD.js} +1 -1
- package/dist/assets/{deflate-DNGgs8Ur.js → deflate-HbyMq59o.js} +1 -1
- package/dist/assets/drawing-2d-DW98umlt.js +257 -0
- package/dist/assets/{exporters-B9v81gi9.js → exporters-BuD3XRzB.js} +463 -416
- package/dist/assets/geometry.worker-TH3fCCoY.js +1 -0
- package/dist/assets/{geotiff-D-YCLS4g.js → geotiff-B2HA8Bwm.js} +10 -10
- package/dist/assets/{ids-CCpq-5d3.js → ids-DYUFMd5f.js} +4 -4
- package/dist/assets/{ifc-lite_bg-DbgS5EUA.wasm → ifc-lite_bg-BEA5DLmg.wasm} +0 -0
- package/dist/assets/index-E9wB0zWt.css +1 -0
- package/dist/assets/{index-Bgb3_Pu_.js → index-n5O1QJMM.js} +36808 -39415
- package/dist/assets/{index.es-CWfqZyyr.js → index.es-BKVIpZgL.js} +8 -8
- package/dist/assets/{jpeg-DGOAeUqU.js → jpeg-C7hjKjPX.js} +1 -1
- package/dist/assets/{jspdf.es.min-XPLU2Wkq.js → jspdf.es.min-oWlFc42Y.js} +4 -4
- package/dist/assets/{lerc-1PMSCHwX.js → lerc-BfIOGhQz.js} +1 -1
- package/dist/assets/{lzw-C65U9lNM.js → lzw-B0jRuuW5.js} +1 -1
- package/dist/assets/{native-bridge-XxXos6yI.js → native-bridge-DpB-dtEn.js} +5 -2
- package/dist/assets/{packbits-BdMWXC3m.js → packbits-DVvBTC39.js} +1 -1
- package/dist/assets/{parser.worker-Ddwo3_06.js → parser.worker-BDsWQ6rc.js} +1 -1
- package/dist/assets/{pdf-CRwaZf3s.js → pdf-dVIqI5ac.js} +9 -9
- package/dist/assets/raw-C0ZJYGmN.js +1 -0
- package/dist/assets/{sandbox-0sDo3g3m.js → sandbox-qpJlrNN0.js} +8 -8
- package/dist/assets/{server-client-cTCJ-853.js → server-client-DVZ2huNS.js} +1 -1
- package/dist/assets/{webimage-BtakWX7W.js → webimage-B394g0Tw.js} +1 -1
- package/dist/assets/{xlsx-B1YOg2QB.js → xlsx-D-oHO76J.js} +7 -7
- package/dist/assets/{zstd-CmwsbxmM.js → zstd-Bf38MwV2.js} +1 -1
- package/dist/index.html +8 -8
- package/package.json +5 -5
- package/src/App.tsx +1 -3
- package/src/components/viewer/BCFPanel.tsx +1 -16
- package/src/components/viewer/ChatPanel.tsx +11 -46
- package/src/components/viewer/HierarchyPanel.tsx +2 -176
- package/src/components/viewer/IDSPanel.tsx +1 -26
- package/src/components/viewer/MainToolbar.tsx +75 -185
- package/src/components/viewer/MobileToolbar.tsx +1 -9
- package/src/components/viewer/PropertiesPanel.tsx +28 -126
- package/src/components/viewer/ScriptPanel.tsx +8 -34
- package/src/components/viewer/Section2DPanel.tsx +32 -1
- package/src/components/viewer/ViewerLayout.tsx +0 -2
- package/src/components/viewer/ViewportContainer.tsx +24 -42
- package/src/components/viewer/ViewportOverlays.tsx +1 -4
- package/src/components/viewer/useGeometryStreaming.ts +0 -2
- package/src/hooks/ingest/federationAlign.ts +7 -0
- package/src/hooks/useDrawingGeneration.ts +211 -13
- package/src/hooks/useIfcCache.ts +94 -41
- package/src/hooks/useIfcFederation.ts +2 -3
- package/src/hooks/useIfcLoader.ts +10 -1051
- package/src/services/cacheService.ts +9 -25
- package/src/services/desktop-export.ts +2 -59
- package/src/services/file-dialog.ts +8 -142
- package/src/store/constants.ts +23 -0
- package/src/store/index.ts +3 -5
- package/src/store/slices/drawing2DSlice.ts +8 -0
- package/src/store/slices/visibilitySlice.ts +22 -1
- package/src/store/types.ts +1 -71
- package/src/utils/ifcConfig.ts +0 -12
- package/vite.config.ts +6 -3
- package/DESKTOP_CONTRACT_VERSION +0 -1
- package/dist/assets/drawing-2d-D0dDf6Lh.js +0 -257
- package/dist/assets/event-B0kAzHa-.js +0 -1
- package/dist/assets/geometry.worker-Bpa3115V.js +0 -1
- package/dist/assets/index-BtbXFKsX.css +0 -1
- package/dist/assets/raw-CJgQdyuZ.js +0 -1
- package/dist/assets/tauri-core-stub-D8Fa-u43.js +0 -1
- package/dist/assets/tauri-dialog-stub-r7Wksg7o.js +0 -1
- package/dist/assets/tauri-fs-stub-BdeRC7aK.js +0 -1
- package/src/components/viewer/DesktopEntitlementBanner.tsx +0 -74
- package/src/components/viewer/SettingsPage.tsx +0 -581
- package/src/lib/desktop/desktopEntitlementEvents.ts +0 -39
- package/src/lib/desktop-entitlement.ts +0 -43
- package/src/lib/desktop-product.ts +0 -130
- package/src/lib/platform.ts +0 -23
- package/src/services/desktop-cache.ts +0 -186
- package/src/services/desktop-harness.ts +0 -196
- package/src/services/desktop-logger.ts +0 -20
- package/src/services/desktop-native-metadata.ts +0 -230
- package/src/services/desktop-panel-actions.ts +0 -43
- package/src/services/desktop-preferences.ts +0 -44
- package/src/services/fs-cache.ts +0 -212
- package/src/services/tauri-core-stub.ts +0 -7
- package/src/services/tauri-dialog-stub.ts +0 -7
- package/src/services/tauri-fs-stub.ts +0 -7
- package/src/services/tauri-modules.d.ts +0 -50
- package/src/store/slices/desktopEntitlementSlice.ts +0 -86
- package/src/utils/desktopModelSnapshot.ts +0 -359
- package/src/utils/nativeSpatialDataStore.ts +0 -277
- package/src-tauri/Cargo.toml +0 -29
- package/src-tauri/build.rs +0 -7
- package/src-tauri/capabilities/default.json +0 -18
- package/src-tauri/icons/128x128.png +0 -0
- package/src-tauri/icons/128x128@2x.png +0 -0
- package/src-tauri/icons/32x32.png +0 -0
- package/src-tauri/icons/Square107x107Logo.png +0 -0
- package/src-tauri/icons/Square142x142Logo.png +0 -0
- package/src-tauri/icons/Square150x150Logo.png +0 -0
- package/src-tauri/icons/Square284x284Logo.png +0 -0
- package/src-tauri/icons/Square30x30Logo.png +0 -0
- package/src-tauri/icons/Square310x310Logo.png +0 -0
- package/src-tauri/icons/Square44x44Logo.png +0 -0
- package/src-tauri/icons/Square71x71Logo.png +0 -0
- package/src-tauri/icons/Square89x89Logo.png +0 -0
- package/src-tauri/icons/StoreLogo.png +0 -0
- package/src-tauri/icons/icon.icns +0 -0
- package/src-tauri/icons/icon.ico +0 -0
- package/src-tauri/icons/icon.png +0 -0
- package/src-tauri/src/lib.rs +0 -21
- package/src-tauri/src/main.rs +0 -10
- package/src-tauri/tauri.conf.json +0 -39
|
@@ -21,10 +21,34 @@ import {
|
|
|
21
21
|
type Drawing2D,
|
|
22
22
|
type DrawingLine,
|
|
23
23
|
type SectionConfig,
|
|
24
|
+
type ProfileEntry,
|
|
25
|
+
type MeshOutline2D,
|
|
24
26
|
} from '@ifc-lite/drawing-2d';
|
|
25
27
|
import { GeometryProcessor, type GeometryResult } from '@ifc-lite/geometry';
|
|
28
|
+
import * as IfcWasm from '@ifc-lite/wasm';
|
|
26
29
|
import { customPlaneCenter } from '@/store';
|
|
27
30
|
|
|
31
|
+
// The winding-robust Rust `meshOutline2d` binding (issue #979) is gitignored →
|
|
32
|
+
// CI-built, so reference it defensively: against an older wasm bundle it's
|
|
33
|
+
// undefined and projection falls back to the TS mesh silhouette. The wasm
|
|
34
|
+
// module is already initialised (the model loaded through it), so the free
|
|
35
|
+
// function can be called without a GeometryProcessor instance.
|
|
36
|
+
interface MeshOutlineHandle {
|
|
37
|
+
readonly axisMin: number;
|
|
38
|
+
readonly axisMax: number;
|
|
39
|
+
readonly contourCount: number;
|
|
40
|
+
contour(index: number): Float32Array | undefined;
|
|
41
|
+
free(): void;
|
|
42
|
+
}
|
|
43
|
+
type MeshOutline2dFn = (
|
|
44
|
+
positions: Float32Array,
|
|
45
|
+
indices: Uint32Array,
|
|
46
|
+
axis: number,
|
|
47
|
+
flipped: boolean,
|
|
48
|
+
) => MeshOutlineHandle | undefined;
|
|
49
|
+
const meshOutline2dFn = (IfcWasm as unknown as { meshOutline2d?: MeshOutline2dFn }).meshOutline2d;
|
|
50
|
+
const AXIS_CODE: Record<'x' | 'y' | 'z', number> = { x: 0, y: 1, z: 2 };
|
|
51
|
+
|
|
28
52
|
// Axis conversion from semantic (down/front/side) to geometric (x/y/z)
|
|
29
53
|
export const AXIS_MAP: Record<'down' | 'front' | 'side', 'x' | 'y' | 'z'> = {
|
|
30
54
|
down: 'y',
|
|
@@ -61,7 +85,7 @@ interface UseDrawingGenerationParams {
|
|
|
61
85
|
bitangent: [number, number, number];
|
|
62
86
|
};
|
|
63
87
|
};
|
|
64
|
-
displayOptions: { showHiddenLines: boolean; useSymbolicRepresentations: boolean; show3DOverlay: boolean; scale: number };
|
|
88
|
+
displayOptions: { showHiddenLines: boolean; useSymbolicRepresentations: boolean; show3DOverlay: boolean; scale: number; showConstructionProjection: boolean };
|
|
65
89
|
combinedHiddenIds: Set<number>;
|
|
66
90
|
combinedIsolatedIds: Set<number> | null;
|
|
67
91
|
computedIsolatedIds?: Set<number> | null;
|
|
@@ -127,6 +151,17 @@ export function useDrawingGeneration({
|
|
|
127
151
|
useSymbolic: boolean;
|
|
128
152
|
} | null>(null);
|
|
129
153
|
|
|
154
|
+
// Cache for extracted extruded-solid profiles (issue #979 construction
|
|
155
|
+
// projection). Like symbolic reps these are section-position-independent, so
|
|
156
|
+
// they're parsed once per model and reused across section moves. Every typed
|
|
157
|
+
// array is copied off the WASM heap (`.slice()`) and the WASM handles freed
|
|
158
|
+
// deterministically before caching — caching a live view would dangle once
|
|
159
|
+
// the shared dlmalloc heap grows/reuses (AGENTS.md §7).
|
|
160
|
+
const profileCacheRef = useRef<{
|
|
161
|
+
profiles: ProfileEntry[];
|
|
162
|
+
sourceId: string | null;
|
|
163
|
+
} | null>(null);
|
|
164
|
+
|
|
130
165
|
// Generate drawing when panel opens
|
|
131
166
|
const generateDrawing = useCallback(async (isRegenerate = false) => {
|
|
132
167
|
if (!geometryResult?.meshes || geometryResult.meshes.length === 0) {
|
|
@@ -152,7 +187,7 @@ export function useDrawingGeneration({
|
|
|
152
187
|
// For multi-model: create cache key from model count and visible model IDs
|
|
153
188
|
// For single-model: use source byteLength as before
|
|
154
189
|
const modelCacheKey = models.size > 0
|
|
155
|
-
? `${models.size}-${[...models.values()].filter(m => m.visible).map(m => m.id).sort().join('
|
|
190
|
+
? `${models.size}-${[...models.values()].filter(m => m.visible).map(m => m.id).sort().join('|')}`
|
|
156
191
|
: (ifcDataStore?.source ? String(ifcDataStore.source.byteLength) : null);
|
|
157
192
|
|
|
158
193
|
const useSymbolic = displayOptions.useSymbolicRepresentations && !!ifcDataStore?.source;
|
|
@@ -341,6 +376,96 @@ export function useDrawingGeneration({
|
|
|
341
376
|
}
|
|
342
377
|
}
|
|
343
378
|
|
|
379
|
+
// Construction projection is plan-only (issue #979): the cut must be the
|
|
380
|
+
// cardinal 'down' axis and not a face-picked custom plane. The UI disables
|
|
381
|
+
// the toggle off-plan, but the persisted flag can stay true when the user
|
|
382
|
+
// switches axis — so gate generation here too, otherwise front/side/custom
|
|
383
|
+
// sections keep emitting projection the user can't turn off.
|
|
384
|
+
const projectionSupported = sectionPlane.axis === 'down' && !sectionPlane.custom;
|
|
385
|
+
const projectionOn = projectionSupported && displayOptions.showConstructionProjection;
|
|
386
|
+
|
|
387
|
+
// ── Construction projection profiles (issue #979) ────────────────────────
|
|
388
|
+
// Extract extruded-area-solid profiles for the clean projection path. Only
|
|
389
|
+
// when projection is on; cached per model since they don't move with the
|
|
390
|
+
// section. Single-model (modelIndex 0) for now, mirroring the symbolic
|
|
391
|
+
// path's federation limitation.
|
|
392
|
+
let profiles: ProfileEntry[] = [];
|
|
393
|
+
if (projectionOn && ifcDataStore?.source) {
|
|
394
|
+
const pcache = profileCacheRef.current;
|
|
395
|
+
if (pcache && pcache.sourceId === modelCacheKey) {
|
|
396
|
+
profiles = pcache.profiles;
|
|
397
|
+
} else {
|
|
398
|
+
if (!isRegenerate) {
|
|
399
|
+
setDrawingProgress(10, 'Extracting profiles...');
|
|
400
|
+
}
|
|
401
|
+
try {
|
|
402
|
+
const processor = new GeometryProcessor();
|
|
403
|
+
try {
|
|
404
|
+
await processor.init();
|
|
405
|
+
// ProfileCollection + each ProfileEntryJs are WASM-bindgen handles
|
|
406
|
+
// owning WASM memory. Copy every typed array off the heap with
|
|
407
|
+
// `.slice()` and free each handle deterministically before caching
|
|
408
|
+
// (AGENTS.md §7 — leaking to GC corrupts the shared dlmalloc heap).
|
|
409
|
+
const collection = processor.extractProfiles(ifcDataStore.source, 0);
|
|
410
|
+
if (collection) {
|
|
411
|
+
try {
|
|
412
|
+
// Profiles come back in UNSHIFTED WebGL world space, but the
|
|
413
|
+
// meshes and the section position live in the render frame
|
|
414
|
+
// (issue #945 RTC / large-coordinate shift). Subtract the same
|
|
415
|
+
// shift so projection lines land on the cut geometry for
|
|
416
|
+
// georeferenced models — a no-op for small-coordinate models
|
|
417
|
+
// (AC20). The WASM mesh path subtracts the RTC offset in IFC
|
|
418
|
+
// Z-up then converts to Y-up via (x,y,z)→(x,z,−y), so the Y-up
|
|
419
|
+
// shift is (rtc.x, rtc.z, −rtc.y); the TS path instead
|
|
420
|
+
// subtracts `originShift`, already in Y-up.
|
|
421
|
+
const ci = geometryResult.coordinateInfo;
|
|
422
|
+
const rtc = ci.wasmRtcOffset;
|
|
423
|
+
const shift = rtc
|
|
424
|
+
? { x: rtc.x, y: rtc.z, z: -rtc.y }
|
|
425
|
+
: ci.originShift;
|
|
426
|
+
const len = collection.length;
|
|
427
|
+
for (let i = 0; i < len; i++) {
|
|
428
|
+
const entry = collection.get(i);
|
|
429
|
+
if (!entry) continue;
|
|
430
|
+
try {
|
|
431
|
+
const transform = entry.transform.slice();
|
|
432
|
+
transform[12] -= shift.x;
|
|
433
|
+
transform[13] -= shift.y;
|
|
434
|
+
transform[14] -= shift.z;
|
|
435
|
+
profiles.push({
|
|
436
|
+
expressId: entry.expressId,
|
|
437
|
+
ifcType: entry.ifcType,
|
|
438
|
+
outerPoints: entry.outerPoints.slice(),
|
|
439
|
+
holeCounts: entry.holeCounts.slice(),
|
|
440
|
+
holePoints: entry.holePoints.slice(),
|
|
441
|
+
transform,
|
|
442
|
+
extrusionDir: entry.extrusionDir.slice(),
|
|
443
|
+
extrusionDepth: entry.extrusionDepth,
|
|
444
|
+
modelIndex: 0,
|
|
445
|
+
});
|
|
446
|
+
} finally {
|
|
447
|
+
entry.free();
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
} finally {
|
|
451
|
+
collection.free();
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
profileCacheRef.current = { profiles, sourceId: modelCacheKey };
|
|
455
|
+
} finally {
|
|
456
|
+
processor.dispose();
|
|
457
|
+
}
|
|
458
|
+
} catch (error) {
|
|
459
|
+
// Degrade gracefully: the drawing still renders without projection.
|
|
460
|
+
console.warn('Profile extraction failed:', error);
|
|
461
|
+
profiles = [];
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
} else if (profileCacheRef.current) {
|
|
465
|
+
// Toggle off: drop the cache so a re-enable re-extracts cleanly.
|
|
466
|
+
profileCacheRef.current = null;
|
|
467
|
+
}
|
|
468
|
+
|
|
344
469
|
let generator: Drawing2DGenerator | null = null;
|
|
345
470
|
try {
|
|
346
471
|
generator = new Drawing2DGenerator();
|
|
@@ -359,6 +484,17 @@ export function useDrawingGeneration({
|
|
|
359
484
|
// Calculate max depth as half the model extent
|
|
360
485
|
const maxDepth = (axisMax - axisMin) * 0.5;
|
|
361
486
|
|
|
487
|
+
// Construction-projection bands (issue #979). Project the full model
|
|
488
|
+
// extent on each side of the cut and let the band classifier split by
|
|
489
|
+
// side (below → solid, above → dashed). Full extent makes single-storey
|
|
490
|
+
// models with an overhead roof (e.g. AC20) "just work"; multi-storey
|
|
491
|
+
// bleed is naturally scoped when the user isolates a storey (the meshes
|
|
492
|
+
// are already filtered to it below). Flip-invariant: the classifier
|
|
493
|
+
// applies the flip sign itself. Floor at 1mm so a degenerate zero-extent
|
|
494
|
+
// model (or a storey collapsed to a single slab) doesn't yield 0-width
|
|
495
|
+
// bands that cull every element sitting on the plane.
|
|
496
|
+
const fullExtent = Math.max(axisMax - axisMin, 1e-3);
|
|
497
|
+
|
|
362
498
|
// Adjust progress to account for symbolic parsing phase (0-20%)
|
|
363
499
|
const progressOffset = symbolicLines.length > 0 ? 20 : 0;
|
|
364
500
|
const progressScale = symbolicLines.length > 0 ? 0.8 : 1;
|
|
@@ -369,6 +505,8 @@ export function useDrawingGeneration({
|
|
|
369
505
|
// Create section config
|
|
370
506
|
const config: SectionConfig = createSectionConfig(axis, position, {
|
|
371
507
|
projectionDepth: maxDepth,
|
|
508
|
+
projectionBelowDepth: fullExtent,
|
|
509
|
+
projectionAboveDepth: fullExtent,
|
|
372
510
|
includeHiddenLines: displayOptions.showHiddenLines,
|
|
373
511
|
scale: displayOptions.scale,
|
|
374
512
|
});
|
|
@@ -434,13 +572,76 @@ export function useDrawingGeneration({
|
|
|
434
572
|
return;
|
|
435
573
|
}
|
|
436
574
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
575
|
+
// Construction projection (issue #979): when enabled, project geometry
|
|
576
|
+
// beyond the cut. The clean profile path handles extruded solids; the
|
|
577
|
+
// silhouette path (includeEdges) covers non-extruded geometry — roofs,
|
|
578
|
+
// stairs, site — that has no profile. The below/above band split drives
|
|
579
|
+
// solid vs dashed; hidden-line removal (below `includeHiddenLines`) is an
|
|
580
|
+
// additional occlusion pass the user controls via "show hidden lines".
|
|
581
|
+
|
|
582
|
+
// Apply the SAME hiding/isolation filters to the profiles as to the
|
|
583
|
+
// meshes, so projection respects 3D hiding and storey isolation —
|
|
584
|
+
// otherwise other storeys' profiles project through the plan and the
|
|
585
|
+
// dedup keys (built from profiles) would suppress silhouettes for
|
|
586
|
+
// entities that aren't actually drawn.
|
|
587
|
+
let projectionProfiles = profiles;
|
|
588
|
+
if (projectionOn && profiles.length > 0) {
|
|
589
|
+
if (combinedHiddenIds.size > 0) {
|
|
590
|
+
projectionProfiles = projectionProfiles.filter((p) => !combinedHiddenIds.has(p.expressId));
|
|
591
|
+
}
|
|
592
|
+
if (combinedIsolatedIds !== null) {
|
|
593
|
+
projectionProfiles = projectionProfiles.filter((p) => combinedIsolatedIds.has(p.expressId));
|
|
594
|
+
}
|
|
595
|
+
if (computedIsolatedIds !== null && computedIsolatedIds !== undefined && computedIsolatedIds.size > 0) {
|
|
596
|
+
const isolatedSet = computedIsolatedIds;
|
|
597
|
+
projectionProfiles = projectionProfiles.filter((p) => isolatedSet.has(p.expressId));
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Winding-robust outline provider for non-extruded geometry (roofs,
|
|
602
|
+
// stairs, site). Calls the Rust meshOutline2d binding per mesh; each call
|
|
603
|
+
// copies the contour data off the WASM heap and frees the handle inline.
|
|
604
|
+
// Undefined when projection is off or the binding isn't in this wasm
|
|
605
|
+
// build → the generator falls back to the TS mesh silhouette.
|
|
606
|
+
const outlineProvider =
|
|
607
|
+
projectionOn && typeof meshOutline2dFn === 'function'
|
|
608
|
+
? (mesh: { positions: Float32Array; indices: Uint32Array }, axis: 'x' | 'y' | 'z', flipped: boolean): MeshOutline2D | null => {
|
|
609
|
+
try {
|
|
610
|
+
const handle = meshOutline2dFn(mesh.positions, mesh.indices, AXIS_CODE[axis], flipped);
|
|
611
|
+
if (!handle) return null;
|
|
612
|
+
try {
|
|
613
|
+
const contours: Float32Array[] = [];
|
|
614
|
+
for (let i = 0; i < handle.contourCount; i++) {
|
|
615
|
+
const ring = handle.contour(i);
|
|
616
|
+
if (ring) contours.push(ring.slice()); // copy off the WASM heap
|
|
617
|
+
}
|
|
618
|
+
if (contours.length === 0) return null;
|
|
619
|
+
return { contours, axisMin: handle.axisMin, axisMax: handle.axisMax };
|
|
620
|
+
} finally {
|
|
621
|
+
handle.free();
|
|
622
|
+
}
|
|
623
|
+
} catch {
|
|
624
|
+
return null; // binding unavailable/failed → silhouette fallback
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
: undefined;
|
|
628
|
+
|
|
629
|
+
const result = await generator.generate(
|
|
630
|
+
meshesToProcess,
|
|
631
|
+
config,
|
|
632
|
+
{
|
|
633
|
+
// Respect the "show hidden lines" toggle: occlusion can downgrade
|
|
634
|
+
// visible (below-cut) projection lines to dashed. Overhead lines stay
|
|
635
|
+
// dashed regardless (the generator passes them through unchanged).
|
|
636
|
+
includeHiddenLines: projectionOn ? displayOptions.showHiddenLines : false,
|
|
637
|
+
includeProjection: projectionOn,
|
|
638
|
+
includeEdges: projectionOn,
|
|
639
|
+
mergeLines: true,
|
|
640
|
+
outlineProvider,
|
|
641
|
+
onProgress: progressCallback,
|
|
642
|
+
},
|
|
643
|
+
projectionOn ? projectionProfiles : undefined,
|
|
644
|
+
);
|
|
444
645
|
|
|
445
646
|
// If we have symbolic representations, create a hybrid drawing
|
|
446
647
|
if (symbolicLines.length > 0 && entitiesWithSymbols.size > 0) {
|
|
@@ -502,10 +703,7 @@ export function useDrawingGeneration({
|
|
|
502
703
|
// The kept window is `−ANNOTATION_VIEW_DEPTH ≤ signedDist ≤ 0` on
|
|
503
704
|
// the −normal side — the side BELOW a down-looking camera, where
|
|
504
705
|
// IFC dimensions live (authored at the storey's floor elevation,
|
|
505
|
-
// not at the cut height).
|
|
506
|
-
// `EdgeExtractor.filterEdgesByDepth`, which projects above the
|
|
507
|
-
// cut: annotations and projection edges are naturally on opposite
|
|
508
|
-
// sides of the cut plane. Flipped sections look at the same world
|
|
706
|
+
// not at the cut height). Flipped sections look at the same world
|
|
509
707
|
// from the opposite side, so the slab mirrors to
|
|
510
708
|
// `0 ≤ signedDist ≤ ANNOTATION_VIEW_DEPTH`.
|
|
511
709
|
//
|
package/src/hooks/useIfcCache.ts
CHANGED
|
@@ -13,11 +13,12 @@ import { useCallback } from 'react';
|
|
|
13
13
|
import {
|
|
14
14
|
BinaryCacheWriter,
|
|
15
15
|
BinaryCacheReader,
|
|
16
|
+
SchemaVersion,
|
|
16
17
|
type CachedEntityIndexColumns,
|
|
17
|
-
type
|
|
18
|
+
type CacheDataStore,
|
|
18
19
|
type GeometryData,
|
|
19
20
|
} from '@ifc-lite/cache';
|
|
20
|
-
import { SpatialHierarchyBuilder, StepTokenizer, CompactEntityIndex, CompactEntityIndexBuilder, extractLengthUnitScale, attachDataStoreAccessors, type IfcDataStore } from '@ifc-lite/parser';
|
|
21
|
+
import { SpatialHierarchyBuilder, StepTokenizer, CompactEntityIndex, CompactEntityIndexBuilder, extractLengthUnitScale, attachDataStoreAccessors, type IfcDataStore, type IfcStoreData } from '@ifc-lite/parser';
|
|
21
22
|
import { buildSpatialIndexGuarded } from '../utils/loadingUtils.js';
|
|
22
23
|
import type { MeshData } from '@ifc-lite/geometry';
|
|
23
24
|
|
|
@@ -52,6 +53,63 @@ function buildEntityIndexFromCachedColumns(columns: CachedEntityIndexColumns): I
|
|
|
52
53
|
return { byId, byType };
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Build the viewer's runtime {@link IfcDataStore} from a deserialized
|
|
58
|
+
* {@link CacheDataStore}. This is the typed cache→runtime adapter (#952): the
|
|
59
|
+
* data tables (strings/entities/properties/quantities/relationships/
|
|
60
|
+
* spatialHierarchy) are the same `@ifc-lite/data` types in both stores, so the
|
|
61
|
+
* mapping is compiler-checked — there is no `as unknown as IfcDataStore` escape
|
|
62
|
+
* hatch, and a future required store member becomes a compile error here instead
|
|
63
|
+
* of a silent runtime crash. The only field that differs is `schema` (cache) →
|
|
64
|
+
* `schemaVersion` (runtime). Lazy entity/property/quantity accessors are wired
|
|
65
|
+
* via {@link attachDataStoreAccessors}; with a `source` + `entityIndex` they
|
|
66
|
+
* read live, otherwise they fall back to the pre-built cache tables.
|
|
67
|
+
*/
|
|
68
|
+
/**
|
|
69
|
+
* Map the cache's numeric {@link SchemaVersion} enum to the runtime store's
|
|
70
|
+
* string schema union. The cache format predates IFC5 (it stores IFC2X3/IFC4/
|
|
71
|
+
* IFC4X3 only), so anything else round-trips as IFC2X3 — matching the inverse
|
|
72
|
+
* mapping the save path uses.
|
|
73
|
+
*/
|
|
74
|
+
function cacheSchemaToVersion(schema: SchemaVersion): IfcDataStore['schemaVersion'] {
|
|
75
|
+
switch (schema) {
|
|
76
|
+
case SchemaVersion.IFC4: return 'IFC4';
|
|
77
|
+
case SchemaVersion.IFC4X3: return 'IFC4X3';
|
|
78
|
+
default: return 'IFC2X3';
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function hydrateCacheStore(
|
|
83
|
+
cacheStore: CacheDataStore,
|
|
84
|
+
extras: {
|
|
85
|
+
source: Uint8Array;
|
|
86
|
+
fileSize: number;
|
|
87
|
+
entityIndex: IfcDataStore['entityIndex'];
|
|
88
|
+
onDemandPropertyMap?: Map<number, number[]>;
|
|
89
|
+
onDemandQuantityMap?: Map<number, number[]>;
|
|
90
|
+
onDemandMaterialMap?: Map<number, number>;
|
|
91
|
+
},
|
|
92
|
+
): IfcDataStore {
|
|
93
|
+
const storeData: IfcStoreData = {
|
|
94
|
+
schemaVersion: cacheSchemaToVersion(cacheStore.schema),
|
|
95
|
+
entityCount: cacheStore.entityCount,
|
|
96
|
+
fileSize: extras.fileSize,
|
|
97
|
+
parseTime: 0,
|
|
98
|
+
source: extras.source,
|
|
99
|
+
strings: cacheStore.strings,
|
|
100
|
+
entities: cacheStore.entities,
|
|
101
|
+
properties: cacheStore.properties,
|
|
102
|
+
quantities: cacheStore.quantities,
|
|
103
|
+
relationships: cacheStore.relationships,
|
|
104
|
+
entityIndex: extras.entityIndex,
|
|
105
|
+
spatialHierarchy: cacheStore.spatialHierarchy,
|
|
106
|
+
onDemandPropertyMap: extras.onDemandPropertyMap,
|
|
107
|
+
onDemandQuantityMap: extras.onDemandQuantityMap,
|
|
108
|
+
onDemandMaterialMap: extras.onDemandMaterialMap,
|
|
109
|
+
};
|
|
110
|
+
return attachDataStoreAccessors(storeData);
|
|
111
|
+
}
|
|
112
|
+
|
|
55
113
|
// ============================================================================
|
|
56
114
|
// Types
|
|
57
115
|
// ============================================================================
|
|
@@ -123,31 +181,29 @@ export function useIfcCache() {
|
|
|
123
181
|
const result = await reader.read(cacheResult.buffer);
|
|
124
182
|
const cacheReadTime = performance.now() - cacheLoadStart;
|
|
125
183
|
|
|
126
|
-
// Convert cache data store to viewer data store format.
|
|
127
|
-
// The cache reader emits a cache-shaped store; this function mutates it
|
|
128
|
-
// in place into the parser `IfcDataStore` the viewer requires (adding
|
|
129
|
-
// source, entityIndex, on-demand maps, spatialHierarchy). Cast through
|
|
130
|
-
// `unknown` to the target shape so the subsequent property writes stay
|
|
131
|
-
// type-checked against the parser types.
|
|
132
|
-
const dataStore = result.dataStore as unknown as IfcDataStore;
|
|
133
|
-
|
|
134
184
|
// Restore the source buffer — required for on-demand property extraction
|
|
135
185
|
// AND the lazy entity accessors (getEntity/getProperties/...). The web
|
|
136
|
-
// cache persists `sourceBuffer`;
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
|
|
186
|
+
// cache persists `sourceBuffer`; fall back to the freshly read file buffer
|
|
187
|
+
// when the caller provides it. Without a source the accessors return empty
|
|
188
|
+
// (and getProperties falls back to the pre-built cache tables).
|
|
189
|
+
const cacheStore = result.dataStore;
|
|
140
190
|
const sourceBuffer = cacheResult.sourceBuffer ?? fallbackSourceBuffer;
|
|
191
|
+
let source: Uint8Array = new Uint8Array(0);
|
|
192
|
+
let entityIndex: IfcDataStore['entityIndex'] = { byId: new Map(), byType: new Map() };
|
|
193
|
+
let onDemandPropertyMap: Map<number, number[]> | undefined;
|
|
194
|
+
let onDemandQuantityMap: Map<number, number[]> | undefined;
|
|
195
|
+
let onDemandMaterialMap: Map<number, number> | undefined;
|
|
196
|
+
|
|
141
197
|
if (sourceBuffer) {
|
|
142
|
-
|
|
198
|
+
source = new Uint8Array(sourceBuffer);
|
|
143
199
|
|
|
144
200
|
if (result.entityIndex) {
|
|
145
|
-
|
|
201
|
+
entityIndex = buildEntityIndexFromCachedColumns(result.entityIndex);
|
|
146
202
|
} else {
|
|
147
203
|
// Backward compatibility for v3 caches: rebuild byte offsets from the
|
|
148
204
|
// source once, then future v4 writes persist this section.
|
|
149
|
-
const tokenizer = new StepTokenizer(
|
|
150
|
-
const estimatedCount =
|
|
205
|
+
const tokenizer = new StepTokenizer(source);
|
|
206
|
+
const estimatedCount = cacheStore.entities?.count ?? 100_000;
|
|
151
207
|
const indexBuilder = new CompactEntityIndexBuilder(estimatedCount);
|
|
152
208
|
const byType = new Map<string, number[]>();
|
|
153
209
|
|
|
@@ -160,36 +216,33 @@ export function useIfcCache() {
|
|
|
160
216
|
}
|
|
161
217
|
typeList.push(ref.expressId);
|
|
162
218
|
}
|
|
163
|
-
|
|
164
|
-
dataStore.entityIndex = { byId: compactByIdIndex, byType };
|
|
219
|
+
entityIndex = { byId: indexBuilder.build(), byType };
|
|
165
220
|
}
|
|
166
221
|
|
|
167
|
-
// Rebuild on-demand maps from relationships
|
|
222
|
+
// Rebuild on-demand maps from relationships.
|
|
168
223
|
// Pass entityIndex which contains ALL entity types including IfcPropertySet/IfcElementQuantity
|
|
169
|
-
// (the entity table may not include these since they're filtered during fresh parse)
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
);
|
|
175
|
-
dataStore.onDemandPropertyMap = onDemandPropertyMap;
|
|
176
|
-
dataStore.onDemandQuantityMap = onDemandQuantityMap;
|
|
177
|
-
// Materials tab + per-material totals read onDemandMaterialMap; without
|
|
178
|
-
// this a cache hit left the Materials grouping empty (#982 follow-up).
|
|
179
|
-
dataStore.onDemandMaterialMap = onDemandMaterialMap;
|
|
180
|
-
|
|
181
|
-
// Reattach the lazy entity/property/quantity accessors. A freshly parsed
|
|
182
|
-
// store carries these (wired by attachDataStoreAccessors), but the cache
|
|
183
|
-
// format only serialises data — so a cache-restored store would be
|
|
184
|
-
// missing getEntity()/getProperties()/etc. and crash any query path
|
|
185
|
-
// (e.g. the Properties panel: "store.getEntity is not a function").
|
|
186
|
-
// Safe here: source, entityIndex and the on-demand maps are all set.
|
|
187
|
-
attachDataStoreAccessors(dataStore as IfcDataStore);
|
|
224
|
+
// (the entity table may not include these since they're filtered during fresh parse).
|
|
225
|
+
({ onDemandPropertyMap, onDemandQuantityMap, onDemandMaterialMap } = rebuildOnDemandMaps(
|
|
226
|
+
cacheStore.entities,
|
|
227
|
+
cacheStore.relationships,
|
|
228
|
+
entityIndex
|
|
229
|
+
));
|
|
188
230
|
} else {
|
|
189
231
|
console.warn('[useIfcCache] No source buffer in cache - on-demand property extraction disabled');
|
|
190
|
-
dataStore.source = new Uint8Array(0);
|
|
191
232
|
}
|
|
192
233
|
|
|
234
|
+
// Typed cache→runtime hydration (#952): builds the parser-shaped
|
|
235
|
+
// IfcDataStore with compiler-checked field mapping (no `as unknown` cast)
|
|
236
|
+
// and wires the lazy accessors via attachDataStoreAccessors.
|
|
237
|
+
const dataStore = hydrateCacheStore(cacheStore, {
|
|
238
|
+
source,
|
|
239
|
+
fileSize: sourceBuffer?.byteLength ?? 0,
|
|
240
|
+
entityIndex,
|
|
241
|
+
onDemandPropertyMap,
|
|
242
|
+
onDemandQuantityMap,
|
|
243
|
+
onDemandMaterialMap,
|
|
244
|
+
});
|
|
245
|
+
|
|
193
246
|
// Rebuild spatial hierarchy from cache data (cache doesn't serialize it)
|
|
194
247
|
// Use SpatialHierarchyBuilder to extract elevations from source buffer
|
|
195
248
|
if (!dataStore.spatialHierarchy && dataStore.entities && dataStore.relationships) {
|
|
@@ -28,7 +28,6 @@ import {
|
|
|
28
28
|
convertIfcxMeshes,
|
|
29
29
|
} from './ingest/viewerModelIngest.js';
|
|
30
30
|
import { extractModelGeoref, alignGeometryToReference, findReferenceGeorefModel } from './ingest/federationAlign.js';
|
|
31
|
-
import { type NativeFileHandle } from '../services/file-dialog.js';
|
|
32
31
|
import { toast } from '../components/ui/toast.js';
|
|
33
32
|
import { acquireFederationLoadSlot, releaseFederationLoadSlot } from './federationLoadGate.js';
|
|
34
33
|
|
|
@@ -56,7 +55,7 @@ export interface IfcxDataStore extends IfcDataStore {
|
|
|
56
55
|
export function useIfcFederation(
|
|
57
56
|
// The ONE canonical loader. Federated adds route through it (target
|
|
58
57
|
// 'federated') so model #1 and model #N share an identical pipeline.
|
|
59
|
-
loadFile: (file: File
|
|
58
|
+
loadFile: (file: File, target?: import('./useIfcLoader.js').LoadTarget) => Promise<void>,
|
|
60
59
|
) {
|
|
61
60
|
const {
|
|
62
61
|
setLoading,
|
|
@@ -103,7 +102,7 @@ export function useIfcFederation(
|
|
|
103
102
|
* Returns the model ID on success, null on failure
|
|
104
103
|
*/
|
|
105
104
|
const addModel = useCallback(async (
|
|
106
|
-
file: File
|
|
105
|
+
file: File,
|
|
107
106
|
options?: {
|
|
108
107
|
name?: string;
|
|
109
108
|
modelId?: string;
|