@ifc-lite/viewer 1.27.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 +35 -42
- package/CHANGELOG.md +74 -0
- package/dist/assets/{basketViewActivator-B3CdrLsb.js → basketViewActivator-Ce38DhXd.js} +8 -8
- package/dist/assets/{bcf-QeHK_Aud.js → bcf-Cv_O3JfD.js} +56 -56
- package/dist/assets/{decode-worker-CgM1iNSK.js → decode-worker-Cjign7Zh.js} +1 -1
- package/dist/assets/{deflate-B-d0SYQM.js → deflate-HbyMq59o.js} +1 -1
- package/dist/assets/drawing-2d-DW98umlt.js +257 -0
- package/dist/assets/e57-source-2wI9jkCA.js +1 -0
- package/dist/assets/{exporters-B4LbZFeT.js → exporters-BuD3XRzB.js} +1309 -1153
- package/dist/assets/geometry.worker-TH3fCCoY.js +1 -0
- package/dist/assets/{geotiff-CrVtDRFq.js → geotiff-B2HA8Bwm.js} +10 -10
- package/dist/assets/{ids-DjsGFN10.js → ids-DYUFMd5f.js} +952 -945
- package/dist/assets/{ifc-lite_bg-DsYUIHm3.wasm → ifc-lite_bg-BEA5DLmg.wasm} +0 -0
- package/dist/assets/index-E9wB0zWt.css +1 -0
- package/dist/assets/{index-COYokSKc.js → index-n5O1QJMM.js} +37877 -38126
- package/dist/assets/{index.es-CY202jA3.js → index.es-BKVIpZgL.js} +9 -9
- package/dist/assets/{jpeg-D4wOkf5h.js → jpeg-C7hjKjPX.js} +1 -1
- package/dist/assets/{jspdf.es.min-DIGb9BHN.js → jspdf.es.min-oWlFc42Y.js} +4 -4
- package/dist/assets/lens-C4p1kQ0p.js +1 -0
- package/dist/assets/{lerc-DmW0_tgf.js → lerc-BfIOGhQz.js} +1 -1
- package/dist/assets/{lzw-oWetY-d6.js → lzw-B0jRuuW5.js} +1 -1
- package/dist/assets/{native-bridge-BX8_tHXE.js → native-bridge-DpB-dtEn.js} +6 -3
- package/dist/assets/{packbits-F8Nkp4NY.js → packbits-DVvBTC39.js} +1 -1
- package/dist/assets/parser.worker-BDsWQ6rc.js +182 -0
- package/dist/assets/{pdf-Dsh3HPZB.js → pdf-dVIqI5ac.js} +10 -10
- package/dist/assets/raw-C0ZJYGmN.js +1 -0
- package/dist/assets/{sandbox-BAC3a-eN.js → sandbox-qpJlrNN0.js} +2962 -2554
- package/dist/assets/server-client-DVZ2huNS.js +719 -0
- package/dist/assets/{webimage-BLV1dgmd.js → webimage-B394g0Tw.js} +1 -1
- package/dist/assets/{xlsx-Bc2HTrjC.js → xlsx-D-oHO76J.js} +8 -8
- package/dist/assets/{zstd-C_1HxVrA.js → zstd-Bf38MwV2.js} +1 -1
- package/dist/index.html +9 -9
- package/package.json +24 -23
- package/src/App.tsx +1 -3
- package/src/components/mcp/playground-dispatcher.ts +3 -0
- package/src/components/mcp/playground-files.ts +33 -1
- package/src/components/viewer/BCFPanel.tsx +1 -16
- package/src/components/viewer/ChatPanel.tsx +11 -46
- package/src/components/viewer/CommandPalette.tsx +6 -1
- package/src/components/viewer/ComparePanel.tsx +420 -0
- package/src/components/viewer/HierarchyPanel.tsx +48 -183
- package/src/components/viewer/IDSPanel.tsx +1 -26
- package/src/components/viewer/MainToolbar.tsx +94 -187
- package/src/components/viewer/MobileToolbar.tsx +1 -9
- package/src/components/viewer/PropertiesPanel.tsx +98 -127
- package/src/components/viewer/ScriptPanel.tsx +8 -34
- package/src/components/viewer/Section2DPanel.tsx +32 -1
- package/src/components/viewer/ViewerLayout.tsx +5 -2
- package/src/components/viewer/Viewport.tsx +3 -0
- package/src/components/viewer/ViewportContainer.tsx +24 -42
- package/src/components/viewer/ViewportOverlays.tsx +1 -4
- package/src/components/viewer/hierarchy/HierarchyNode.tsx +3 -3
- package/src/components/viewer/hierarchy/ifc-icons.ts +9 -0
- package/src/components/viewer/hierarchy/treeDataBuilder.ts +87 -0
- package/src/components/viewer/hierarchy/types.ts +1 -0
- package/src/components/viewer/hierarchy/useHierarchyTree.ts +6 -2
- package/src/components/viewer/properties/MaterialTotalsPanel.tsx +283 -0
- package/src/components/viewer/useGeometryStreaming.ts +0 -2
- package/src/hooks/federationLoadGate.test.ts +12 -2
- package/src/hooks/federationLoadGate.ts +9 -2
- package/src/hooks/ingest/federationAlign.ts +488 -0
- package/src/hooks/ingest/viewerModelIngest.ts +3 -212
- package/src/hooks/useCompare.ts +0 -0
- package/src/hooks/useCompareOverlay.ts +119 -0
- package/src/hooks/useDrawingGeneration.ts +234 -14
- package/src/hooks/useIfc.ts +1 -1
- package/src/hooks/useIfcCache.ts +100 -24
- package/src/hooks/useIfcFederation.ts +42 -811
- package/src/hooks/useIfcLoader.ts +349 -1517
- package/src/hooks/useIfcServer.ts +3 -0
- package/src/hooks/useLens.ts +5 -1
- package/src/hooks/useSymbolicAnnotations.ts +70 -38
- package/src/lib/compare/buildFingerprints.ts +173 -0
- package/src/lib/compare/describeChange.ts +0 -0
- package/src/lib/compare/geometricData.test.ts +54 -0
- package/src/lib/compare/geometricData.ts +37 -0
- package/src/lib/compare/overlay.test.ts +99 -0
- package/src/lib/compare/overlay.ts +91 -0
- package/src/lib/geo/cesium-placement.ts +1 -1
- package/src/lib/geo/reproject.ts +4 -1
- package/src/lib/llm/script-edit-ops.ts +23 -0
- package/src/lib/llm/stream-client.ts +8 -1
- package/src/lib/search/result-export.ts +7 -1
- package/src/sdk/adapters/export-adapter.ts +6 -1
- 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/globalId.ts +15 -13
- package/src/store/index.ts +19 -6
- package/src/store/slices/cesiumSlice.ts +8 -1
- package/src/store/slices/compareSlice.ts +96 -0
- package/src/store/slices/drawing2DSlice.ts +8 -0
- package/src/store/slices/lensSlice.ts +8 -0
- package/src/store/slices/visibilitySlice.ts +22 -1
- package/src/store/types.ts +1 -71
- package/src/utils/acquireFileBuffer.test.ts +12 -4
- package/src/utils/ifcConfig.ts +0 -12
- package/src/utils/loadingUtils.ts +32 -0
- package/src/utils/spatialHierarchy.test.ts +53 -1
- package/src/utils/spatialHierarchy.ts +42 -2
- package/src/vite-env.d.ts +2 -0
- package/vite.config.ts +6 -3
- package/DESKTOP_CONTRACT_VERSION +0 -1
- package/dist/assets/drawing-2d-C71b8Ugx.js +0 -257
- package/dist/assets/e57-source-CQHxE8n3.js +0 -1
- package/dist/assets/event-B0kAzHa-.js +0 -1
- package/dist/assets/geometry.worker-BdH-E6NB.js +0 -1
- package/dist/assets/index-ajK6D32J.css +0 -1
- package/dist/assets/lens-PYsLu_MA.js +0 -1
- package/dist/assets/parser.worker-D591Zu_-.js +0 -182
- package/dist/assets/raw-D9iw0tmc.js +0 -1
- package/dist/assets/server-client-Cjwnm7il.js +0 -706
- 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/hooks/ingest/resolveDataStoreOrAbort.test.ts +0 -61
- package/src/hooks/ingest/resolveDataStoreOrAbort.ts +0 -28
- package/src/hooks/ingest/watchedGeometryStream.test.ts +0 -78
- package/src/hooks/ingest/watchedGeometryStream.ts +0 -76
- 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 -358
- 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;
|
|
@@ -179,18 +214,29 @@ export function useDrawingGeneration({
|
|
|
179
214
|
try {
|
|
180
215
|
await processor.init();
|
|
181
216
|
|
|
217
|
+
// SymbolicRepresentationCollection and each getPolyline/getCircle
|
|
218
|
+
// item are wasm-bindgen handles owning WASM memory — free them
|
|
219
|
+
// deterministically (AGENTS.md §7). Leaking them to GC lets the
|
|
220
|
+
// FinalizationRegistry free them later against an already-grown/
|
|
221
|
+
// reused shared dlmalloc heap, corrupting the allocator free-list.
|
|
182
222
|
const symbolicCollection = processor.parseSymbolicRepresentations(ifcDataStore!.source);
|
|
183
223
|
// For single-model (legacy) mode, model index is always 0
|
|
184
224
|
// Multi-model symbolic parsing would require iterating over each model separately
|
|
185
225
|
const symbolicModelIndex = 0;
|
|
186
226
|
|
|
187
|
-
if (symbolicCollection
|
|
227
|
+
if (symbolicCollection) {
|
|
228
|
+
try {
|
|
229
|
+
if (!symbolicCollection.isEmpty) {
|
|
188
230
|
// Process polylines
|
|
189
231
|
for (let i = 0; i < symbolicCollection.polylineCount; i++) {
|
|
190
232
|
const poly = symbolicCollection.getPolyline(i);
|
|
191
233
|
if (!poly) continue;
|
|
234
|
+
try {
|
|
192
235
|
|
|
193
236
|
entitiesWithSymbols.add(poly.expressId);
|
|
237
|
+
// poly.points is consumed synchronously within this iteration
|
|
238
|
+
// (centroid sum + segment pushes read scalar values out of it);
|
|
239
|
+
// the array itself is never stored, so no copy is needed.
|
|
194
240
|
const points = poly.points;
|
|
195
241
|
const pointCount = poly.pointCount;
|
|
196
242
|
// WASM exposes `worldY` on every symbolic primitive — the
|
|
@@ -249,12 +295,16 @@ export function useDrawingGeneration({
|
|
|
249
295
|
worldZ: polyWorldZ,
|
|
250
296
|
});
|
|
251
297
|
}
|
|
298
|
+
} finally {
|
|
299
|
+
poly.free();
|
|
300
|
+
}
|
|
252
301
|
}
|
|
253
302
|
|
|
254
303
|
// Process circles/arcs
|
|
255
304
|
for (let i = 0; i < symbolicCollection.circleCount; i++) {
|
|
256
305
|
const circle = symbolicCollection.getCircle(i);
|
|
257
306
|
if (!circle) continue;
|
|
307
|
+
try {
|
|
258
308
|
|
|
259
309
|
entitiesWithSymbols.add(circle.expressId);
|
|
260
310
|
const numSegments = circle.isFullCircle ? 32 : 16;
|
|
@@ -293,6 +343,13 @@ export function useDrawingGeneration({
|
|
|
293
343
|
worldZ: circleWorldZ,
|
|
294
344
|
});
|
|
295
345
|
}
|
|
346
|
+
} finally {
|
|
347
|
+
circle.free();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
} finally {
|
|
352
|
+
symbolicCollection.free();
|
|
296
353
|
}
|
|
297
354
|
}
|
|
298
355
|
} finally {
|
|
@@ -319,6 +376,96 @@ export function useDrawingGeneration({
|
|
|
319
376
|
}
|
|
320
377
|
}
|
|
321
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
|
+
|
|
322
469
|
let generator: Drawing2DGenerator | null = null;
|
|
323
470
|
try {
|
|
324
471
|
generator = new Drawing2DGenerator();
|
|
@@ -337,6 +484,17 @@ export function useDrawingGeneration({
|
|
|
337
484
|
// Calculate max depth as half the model extent
|
|
338
485
|
const maxDepth = (axisMax - axisMin) * 0.5;
|
|
339
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
|
+
|
|
340
498
|
// Adjust progress to account for symbolic parsing phase (0-20%)
|
|
341
499
|
const progressOffset = symbolicLines.length > 0 ? 20 : 0;
|
|
342
500
|
const progressScale = symbolicLines.length > 0 ? 0.8 : 1;
|
|
@@ -347,6 +505,8 @@ export function useDrawingGeneration({
|
|
|
347
505
|
// Create section config
|
|
348
506
|
const config: SectionConfig = createSectionConfig(axis, position, {
|
|
349
507
|
projectionDepth: maxDepth,
|
|
508
|
+
projectionBelowDepth: fullExtent,
|
|
509
|
+
projectionAboveDepth: fullExtent,
|
|
350
510
|
includeHiddenLines: displayOptions.showHiddenLines,
|
|
351
511
|
scale: displayOptions.scale,
|
|
352
512
|
});
|
|
@@ -412,13 +572,76 @@ export function useDrawingGeneration({
|
|
|
412
572
|
return;
|
|
413
573
|
}
|
|
414
574
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
+
);
|
|
422
645
|
|
|
423
646
|
// If we have symbolic representations, create a hybrid drawing
|
|
424
647
|
if (symbolicLines.length > 0 && entitiesWithSymbols.size > 0) {
|
|
@@ -480,10 +703,7 @@ export function useDrawingGeneration({
|
|
|
480
703
|
// The kept window is `−ANNOTATION_VIEW_DEPTH ≤ signedDist ≤ 0` on
|
|
481
704
|
// the −normal side — the side BELOW a down-looking camera, where
|
|
482
705
|
// IFC dimensions live (authored at the storey's floor elevation,
|
|
483
|
-
// not at the cut height).
|
|
484
|
-
// `EdgeExtractor.filterEdgesByDepth`, which projects above the
|
|
485
|
-
// cut: annotations and projection edges are naturally on opposite
|
|
486
|
-
// 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
|
|
487
707
|
// from the opposite side, so the slab mirrors to
|
|
488
708
|
// `0 ≤ signedDist ≤ ANNOTATION_VIEW_DEPTH`.
|
|
489
709
|
//
|
package/src/hooks/useIfc.ts
CHANGED
|
@@ -82,7 +82,7 @@ export function useIfc() {
|
|
|
82
82
|
findModelForEntity,
|
|
83
83
|
resolveGlobalId,
|
|
84
84
|
realignFederation,
|
|
85
|
-
} = useIfcFederation();
|
|
85
|
+
} = useIfcFederation(loadFile);
|
|
86
86
|
|
|
87
87
|
// Memoize query to prevent recreation on every render
|
|
88
88
|
// For single-model backward compatibility
|
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, 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
|
// ============================================================================
|
|
@@ -109,7 +167,8 @@ export function useIfcCache() {
|
|
|
109
167
|
const loadFromCache = useCallback(async (
|
|
110
168
|
cacheResult: CacheResult,
|
|
111
169
|
fileName: string,
|
|
112
|
-
cacheKey?: string
|
|
170
|
+
cacheKey?: string,
|
|
171
|
+
fallbackSourceBuffer?: ArrayBufferLike,
|
|
113
172
|
): Promise<CacheLoadResult> => {
|
|
114
173
|
try {
|
|
115
174
|
const cacheLoadStart = performance.now();
|
|
@@ -122,20 +181,29 @@ export function useIfcCache() {
|
|
|
122
181
|
const result = await reader.read(cacheResult.buffer);
|
|
123
182
|
const cacheReadTime = performance.now() - cacheLoadStart;
|
|
124
183
|
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
184
|
+
// Restore the source buffer — required for on-demand property extraction
|
|
185
|
+
// AND the lazy entity accessors (getEntity/getProperties/...). The web
|
|
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;
|
|
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
|
+
|
|
197
|
+
if (sourceBuffer) {
|
|
198
|
+
source = new Uint8Array(sourceBuffer);
|
|
131
199
|
|
|
132
200
|
if (result.entityIndex) {
|
|
133
|
-
|
|
201
|
+
entityIndex = buildEntityIndexFromCachedColumns(result.entityIndex);
|
|
134
202
|
} else {
|
|
135
203
|
// Backward compatibility for v3 caches: rebuild byte offsets from the
|
|
136
204
|
// source once, then future v4 writes persist this section.
|
|
137
|
-
const tokenizer = new StepTokenizer(
|
|
138
|
-
const estimatedCount =
|
|
205
|
+
const tokenizer = new StepTokenizer(source);
|
|
206
|
+
const estimatedCount = cacheStore.entities?.count ?? 100_000;
|
|
139
207
|
const indexBuilder = new CompactEntityIndexBuilder(estimatedCount);
|
|
140
208
|
const byType = new Map<string, number[]>();
|
|
141
209
|
|
|
@@ -148,25 +216,33 @@ export function useIfcCache() {
|
|
|
148
216
|
}
|
|
149
217
|
typeList.push(ref.expressId);
|
|
150
218
|
}
|
|
151
|
-
|
|
152
|
-
dataStore.entityIndex = { byId: compactByIdIndex, byType };
|
|
219
|
+
entityIndex = { byId: indexBuilder.build(), byType };
|
|
153
220
|
}
|
|
154
221
|
|
|
155
|
-
// Rebuild on-demand maps from relationships
|
|
222
|
+
// Rebuild on-demand maps from relationships.
|
|
156
223
|
// Pass entityIndex which contains ALL entity types including IfcPropertySet/IfcElementQuantity
|
|
157
|
-
// (the entity table may not include these since they're filtered during fresh parse)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
);
|
|
163
|
-
dataStore.onDemandPropertyMap = onDemandPropertyMap;
|
|
164
|
-
dataStore.onDemandQuantityMap = onDemandQuantityMap;
|
|
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
|
+
));
|
|
165
230
|
} else {
|
|
166
231
|
console.warn('[useIfcCache] No source buffer in cache - on-demand property extraction disabled');
|
|
167
|
-
dataStore.source = new Uint8Array(0);
|
|
168
232
|
}
|
|
169
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
|
+
|
|
170
246
|
// Rebuild spatial hierarchy from cache data (cache doesn't serialize it)
|
|
171
247
|
// Use SpatialHierarchyBuilder to extract elevations from source buffer
|
|
172
248
|
if (!dataStore.spatialHierarchy && dataStore.entities && dataStore.relationships) {
|