@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
|
@@ -386,6 +386,9 @@ export function useIfcServer() {
|
|
|
386
386
|
|
|
387
387
|
buildSpatialIndexGuarded(allMeshes, dataStore, setIfcDataStore);
|
|
388
388
|
} catch (err) {
|
|
389
|
+
if (!isStale?.()) {
|
|
390
|
+
console.warn('[useIfc] Server data model fetch/decode failed; geometry shown without properties:', err);
|
|
391
|
+
}
|
|
389
392
|
}
|
|
390
393
|
})(); // End of async data model fetch block - runs in background, doesn't block
|
|
391
394
|
|
package/src/hooks/useLens.ts
CHANGED
|
@@ -56,6 +56,7 @@ export function useLens() {
|
|
|
56
56
|
useViewerStore.getState().setLensRuleCounts(new Map());
|
|
57
57
|
useViewerStore.getState().setLensRuleEntityIds(new Map());
|
|
58
58
|
useViewerStore.getState().setLensAutoColorLegend([]);
|
|
59
|
+
useViewerStore.getState().setLensAppliedColors(null);
|
|
59
60
|
|
|
60
61
|
// Send empty map to signal "clear overlays" to useGeometryStreaming
|
|
61
62
|
useViewerStore.getState().setPendingColorUpdates(new Map());
|
|
@@ -101,7 +102,10 @@ export function useLens() {
|
|
|
101
102
|
useViewerStore.getState().setLensAutoColorLegend([]);
|
|
102
103
|
}
|
|
103
104
|
|
|
104
|
-
// Apply colors via overlay system — original batches are never modified
|
|
105
|
+
// Apply colors via overlay system — original batches are never modified.
|
|
106
|
+
// Remember the exact overlay so the compare overlay can restore it on
|
|
107
|
+
// teardown instead of blanking the channel the lens still owns.
|
|
108
|
+
useViewerStore.getState().setLensAppliedColors(colorMap.size > 0 ? colorMap : null);
|
|
105
109
|
if (colorMap.size > 0) {
|
|
106
110
|
useViewerStore.getState().setPendingColorUpdates(colorMap);
|
|
107
111
|
}
|
|
@@ -248,6 +248,11 @@ async function parseAnnotations(
|
|
|
248
248
|
const processor = new GeometryProcessor();
|
|
249
249
|
try {
|
|
250
250
|
await processor.init();
|
|
251
|
+
// SymbolicRepresentationCollection and each getPolyline/getCircle/getText/
|
|
252
|
+
// getFill item are wasm-bindgen handles owning WASM memory — free them
|
|
253
|
+
// deterministically (AGENTS.md §7). Leaking them to GC lets the
|
|
254
|
+
// FinalizationRegistry free them later against an already-grown/reused
|
|
255
|
+
// shared dlmalloc heap, corrupting the allocator free-list.
|
|
251
256
|
const collection = processor.parseSymbolicRepresentations(source);
|
|
252
257
|
if (debugEnabled()) {
|
|
253
258
|
console.log(
|
|
@@ -257,7 +262,9 @@ async function parseAnnotations(
|
|
|
257
262
|
: 'null',
|
|
258
263
|
);
|
|
259
264
|
}
|
|
260
|
-
if (!collection
|
|
265
|
+
if (!collection) return result;
|
|
266
|
+
try {
|
|
267
|
+
if (collection.isEmpty) return result;
|
|
261
268
|
|
|
262
269
|
// Resolve a bucket by elevation rather than by storey id.
|
|
263
270
|
//
|
|
@@ -313,34 +320,44 @@ async function parseAnnotations(
|
|
|
313
320
|
for (let i = 0; i < collection.polylineCount; i++) {
|
|
314
321
|
const poly = collection.getPolyline(i);
|
|
315
322
|
if (!poly) continue;
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
323
|
+
try {
|
|
324
|
+
if (poly.ifcType !== 'IfcAnnotation' && poly.ifcType !== 'IfcGridAxis') continue;
|
|
325
|
+
const bucket = ensureBucket(poly.expressId, poly.worldY, poly.ifcType);
|
|
326
|
+
const looseTarget = poly.ifcType === 'IfcGridAxis' ? result.gridLoose : result.loose;
|
|
327
|
+
const out = bucket ? bucket.lines : looseTarget;
|
|
328
|
+
// poly.points is consumed synchronously here (not stored), so no copy needed.
|
|
329
|
+
polylineToSegments(poly.points, poly.pointCount, poly.isClosed, out);
|
|
330
|
+
} finally {
|
|
331
|
+
poly.free();
|
|
332
|
+
}
|
|
321
333
|
}
|
|
322
334
|
|
|
323
335
|
for (let i = 0; i < collection.circleCount; i++) {
|
|
324
336
|
const circle = collection.getCircle(i);
|
|
325
337
|
if (!circle) continue;
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
338
|
+
try {
|
|
339
|
+
if (circle.ifcType !== 'IfcAnnotation' && circle.ifcType !== 'IfcGridAxis') continue;
|
|
340
|
+
const bucket = ensureBucket(circle.expressId, circle.worldY, circle.ifcType);
|
|
341
|
+
const looseTarget = circle.ifcType === 'IfcGridAxis' ? result.gridLoose : result.loose;
|
|
342
|
+
const out = bucket ? bucket.lines : looseTarget;
|
|
343
|
+
circleToSegments(
|
|
344
|
+
circle.centerX,
|
|
345
|
+
circle.centerY,
|
|
346
|
+
circle.radius,
|
|
347
|
+
circle.startAngle,
|
|
348
|
+
circle.endAngle,
|
|
349
|
+
circle.isFullCircle,
|
|
350
|
+
out,
|
|
351
|
+
);
|
|
352
|
+
} finally {
|
|
353
|
+
circle.free();
|
|
354
|
+
}
|
|
339
355
|
}
|
|
340
356
|
|
|
341
357
|
for (let i = 0; i < collection.textCount; i++) {
|
|
342
358
|
const text = collection.getText(i);
|
|
343
359
|
if (!text) continue;
|
|
360
|
+
try {
|
|
344
361
|
if (text.ifcType !== 'IfcAnnotation' && text.ifcType !== 'IfcGridAxis') continue;
|
|
345
362
|
// Skip empty literals so the renderer doesn't waste an instance slot.
|
|
346
363
|
// Decode STEP escapes — `\X2\NNNN\X0\` (UTF-16 hex code units) and
|
|
@@ -398,30 +415,45 @@ async function parseAnnotations(
|
|
|
398
415
|
};
|
|
399
416
|
(bucket ? bucket.texts : looseTextTarget).push(t2d);
|
|
400
417
|
}
|
|
418
|
+
} finally {
|
|
419
|
+
text.free();
|
|
420
|
+
}
|
|
401
421
|
}
|
|
402
422
|
|
|
403
423
|
for (let i = 0; i < collection.fillCount; i++) {
|
|
404
424
|
const fill = collection.getFill(i);
|
|
405
425
|
if (!fill) continue;
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
426
|
+
try {
|
|
427
|
+
if (fill.ifcType !== 'IfcAnnotation' && fill.ifcType !== 'IfcGridAxis') continue;
|
|
428
|
+
// fill.points / fill.holesOffsets are getter results that may be views
|
|
429
|
+
// into WASM memory; they're STORED into f2d (outlive this iteration),
|
|
430
|
+
// so copy them before the handle is freed below. Element types match
|
|
431
|
+
// the AnnotationFill2D fields (Float32Array / Uint32Array).
|
|
432
|
+
const points = new Float32Array(fill.points);
|
|
433
|
+
if (points.length < 6) continue; // <3 vertices = no polygon
|
|
434
|
+
const holesOffsets = new Uint32Array(fill.holesOffsets);
|
|
435
|
+
const f2d: AnnotationFill2D = {
|
|
436
|
+
points,
|
|
437
|
+
holesOffsets,
|
|
438
|
+
color: [fill.fillR, fill.fillG, fill.fillB, fill.fillA],
|
|
439
|
+
hatching: fill.hasHatching
|
|
440
|
+
? {
|
|
441
|
+
spacing: fill.hatchSpacing,
|
|
442
|
+
angle: fill.hatchAngle,
|
|
443
|
+
angleSecondary: Number.isNaN(fill.hatchAngleSecondary) ? null : fill.hatchAngleSecondary,
|
|
444
|
+
lineWidth: fill.hatchLineWidth,
|
|
445
|
+
}
|
|
446
|
+
: undefined,
|
|
447
|
+
};
|
|
448
|
+
const bucket = ensureBucket(fill.expressId, fill.worldY, fill.ifcType);
|
|
449
|
+
const looseFillTarget = fill.ifcType === 'IfcGridAxis' ? result.gridLooseFills : result.looseFills;
|
|
450
|
+
(bucket ? bucket.fills : looseFillTarget).push(f2d);
|
|
451
|
+
} finally {
|
|
452
|
+
fill.free();
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
} finally {
|
|
456
|
+
collection.free();
|
|
425
457
|
}
|
|
426
458
|
} finally {
|
|
427
459
|
processor.dispose();
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Viewer adapter for the `@ifc-lite/diff` engine (issue #924).
|
|
7
|
+
*
|
|
8
|
+
* Turns a loaded model — its `IfcDataStore` plus the tessellated meshes from
|
|
9
|
+
* the geometry pass — into the per-entity {@link EntityFingerprint}s the
|
|
10
|
+
* store-agnostic engine matches and classifies. This is the viewer's
|
|
11
|
+
* counterpart to the CLI adapter; the canonical data fingerprint comes from
|
|
12
|
+
* `@ifc-lite/diff`'s {@link buildDataFingerprint} (the same hash the threejs
|
|
13
|
+
* compare example pioneered) and the geometry fingerprint is the RTC-invariant
|
|
14
|
+
* WASM hash riding on each `MeshData.geometryHash`.
|
|
15
|
+
*
|
|
16
|
+
* Scope: only entities that produced at least one mesh are fingerprinted —
|
|
17
|
+
* the engine needs a geometry hash to detect geometry changes, and the
|
|
18
|
+
* compare UI colours meshed elements in 3D. Data-only edits on those meshed
|
|
19
|
+
* entities are still detected via the data hash.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
buildDataFingerprint,
|
|
24
|
+
type DataFingerprintInput,
|
|
25
|
+
type EntityFingerprint,
|
|
26
|
+
} from '@ifc-lite/diff';
|
|
27
|
+
import { RelationshipType } from '@ifc-lite/data';
|
|
28
|
+
import {
|
|
29
|
+
extractAllEntityAttributes,
|
|
30
|
+
extractPropertiesOnDemand,
|
|
31
|
+
type IfcDataStore,
|
|
32
|
+
} from '@ifc-lite/parser';
|
|
33
|
+
import type { MeshData } from '@ifc-lite/geometry';
|
|
34
|
+
import { isGeometricDataName } from './geometricData.js';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Adapter handle threaded through the diff onto each {@link CompareDiffEntry}.
|
|
38
|
+
* Carries everything the compare UI needs downstream without re-deriving it:
|
|
39
|
+
* `globalId` colours the entity in the federated renderer, while `modelId` +
|
|
40
|
+
* `localId` drive selection / property lookup.
|
|
41
|
+
*/
|
|
42
|
+
export interface CompareRef {
|
|
43
|
+
/** Federation model id this entity belongs to. */
|
|
44
|
+
modelId: string;
|
|
45
|
+
/** Original (pre-offset) express id — the key for `IfcDataStore` lookups. */
|
|
46
|
+
localId: number;
|
|
47
|
+
/** Federation global id (`localId + idOffset`) — the renderer mesh id. */
|
|
48
|
+
globalId: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface BuildFingerprintsModel {
|
|
52
|
+
/** Federation model id. */
|
|
53
|
+
modelId: string;
|
|
54
|
+
/** Parsed data store (local express ids). */
|
|
55
|
+
store: IfcDataStore;
|
|
56
|
+
/** Tessellated meshes. Express ids are federation-global (`local + idOffset`). */
|
|
57
|
+
meshes: readonly MeshData[];
|
|
58
|
+
/** This model's federation id offset (0 for the anchor / single-model load). */
|
|
59
|
+
idOffset: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Build one {@link EntityFingerprint} per meshed entity in a model.
|
|
64
|
+
*
|
|
65
|
+
* Entities are de-duplicated by express id (an entity emits several
|
|
66
|
+
* submeshes); the first mesh carrying a `geometryHash` wins (all submeshes of
|
|
67
|
+
* an entity share the whole-entity hash). The fingerprint `key` is the IFC
|
|
68
|
+
* `GlobalId` so the engine matches the same element across revisions; entities
|
|
69
|
+
* without a resolvable GlobalId fall back to a per-model synthetic key so they
|
|
70
|
+
* never collide across A/B and simply read as added/deleted.
|
|
71
|
+
*/
|
|
72
|
+
export async function buildEntityFingerprints(
|
|
73
|
+
model: BuildFingerprintsModel,
|
|
74
|
+
): Promise<EntityFingerprint<CompareRef>[]> {
|
|
75
|
+
const { store, meshes, idOffset, modelId } = model;
|
|
76
|
+
|
|
77
|
+
// local express id → first geometry hash seen for it (may be undefined when
|
|
78
|
+
// hashing was disabled or the WASM build predates it — data diff still works)
|
|
79
|
+
const geometryByLocalId = new Map<number, bigint | undefined>();
|
|
80
|
+
for (const mesh of meshes) {
|
|
81
|
+
const localId = mesh.expressId - idOffset;
|
|
82
|
+
if (!geometryByLocalId.has(localId)) {
|
|
83
|
+
geometryByLocalId.set(localId, mesh.geometryHash);
|
|
84
|
+
} else if (geometryByLocalId.get(localId) === undefined && mesh.geometryHash !== undefined) {
|
|
85
|
+
geometryByLocalId.set(localId, mesh.geometryHash);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const fingerprints: EntityFingerprint<CompareRef>[] = [];
|
|
90
|
+
let processed = 0;
|
|
91
|
+
for (const [localId, geometryHash] of geometryByLocalId) {
|
|
92
|
+
const ifcType = store.entities.getTypeName(localId) || 'IfcProduct';
|
|
93
|
+
const globalId = store.entities.getGlobalId(localId);
|
|
94
|
+
const key = globalId || `missing:${modelId}:${localId}`;
|
|
95
|
+
|
|
96
|
+
fingerprints.push({
|
|
97
|
+
key,
|
|
98
|
+
ifcType,
|
|
99
|
+
dataHash: buildDataFingerprint(buildDataInput(store, localId, ifcType)),
|
|
100
|
+
geometryHash,
|
|
101
|
+
ref: { modelId, localId, globalId: localId + idOffset },
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Per-entity property extraction reparses from the source buffer, so on a
|
|
105
|
+
// large model this loop is heavy; yield to the main thread periodically so
|
|
106
|
+
// the viewport stays responsive and the "Comparing…" spinner keeps
|
|
107
|
+
// animating instead of the UI freezing (#924).
|
|
108
|
+
if (++processed % 1500 === 0) {
|
|
109
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return fingerprints;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Does this side carry at least one usable geometry hash? Compares run on
|
|
117
|
+
* models loaded outside the WASM mesh path (e.g. huge native desktop loads)
|
|
118
|
+
* produce no hashes, which would make geometry diffs silently read every
|
|
119
|
+
* element as unchanged — callers warn when this is false. */
|
|
120
|
+
export function hasGeometryHashes(side: readonly EntityFingerprint<CompareRef>[]): boolean {
|
|
121
|
+
return side.some((fingerprint) => fingerprint.geometryHash !== undefined);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Assemble the canonical {@link DataFingerprintInput} for one entity from the
|
|
126
|
+
* store's on-demand extractors. Mirrors the extraction in
|
|
127
|
+
* `examples/threejs-viewer/src/compare.ts`; `@ifc-lite/diff` does the sorting
|
|
128
|
+
* + hashing so base and head produce byte-identical hashes for an unchanged
|
|
129
|
+
* entity.
|
|
130
|
+
*/
|
|
131
|
+
function buildDataInput(
|
|
132
|
+
store: IfcDataStore,
|
|
133
|
+
localId: number,
|
|
134
|
+
ifcType: string,
|
|
135
|
+
): DataFingerprintInput {
|
|
136
|
+
const predefinedType = extractAllEntityAttributes(store, localId).find(
|
|
137
|
+
(attribute) => attribute.name === 'PredefinedType',
|
|
138
|
+
)?.value;
|
|
139
|
+
|
|
140
|
+
// Data vs geometry: placement/coordinate data (elevation, level offsets, …)
|
|
141
|
+
// is owned by the geometry hash, so strip it from the data fingerprint — a
|
|
142
|
+
// pure move must read as a geometry change only, never "data · geometry"
|
|
143
|
+
// (see geometricData.ts). Quantities (Volume/Area/Length/…) are
|
|
144
|
+
// geometry-derived measurements and are excluded wholesale for the same
|
|
145
|
+
// reason: a reshape already shows up as a geometry change.
|
|
146
|
+
const propertySets = extractPropertiesOnDemand(store, localId)
|
|
147
|
+
.filter((set) => !isGeometricDataName(set.name))
|
|
148
|
+
.map((set) => ({
|
|
149
|
+
name: set.name,
|
|
150
|
+
properties: set.properties
|
|
151
|
+
.filter((property) => !isGeometricDataName(property.name))
|
|
152
|
+
.map((property) => ({ name: property.name, value: property.value })),
|
|
153
|
+
}))
|
|
154
|
+
.filter((set) => set.properties.length > 0);
|
|
155
|
+
|
|
156
|
+
const typeAssignments = store.relationships
|
|
157
|
+
.getRelated(localId, RelationshipType.DefinesByType, 'inverse')
|
|
158
|
+
.map((typeId) => ({
|
|
159
|
+
globalId: store.entities.getGlobalId(typeId) || undefined,
|
|
160
|
+
name: store.entities.getName(typeId) || undefined,
|
|
161
|
+
type: store.entities.getTypeName(typeId) || undefined,
|
|
162
|
+
}));
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
ifcType,
|
|
166
|
+
name: store.entities.getName(localId) || undefined,
|
|
167
|
+
description: store.entities.getDescription(localId) || undefined,
|
|
168
|
+
objectType: store.entities.getObjectType(localId) || undefined,
|
|
169
|
+
predefinedType: predefinedType != null ? String(predefinedType) : undefined,
|
|
170
|
+
propertySets,
|
|
171
|
+
typeAssignments,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
import { describe, it } from 'node:test';
|
|
6
|
+
import assert from 'node:assert';
|
|
7
|
+
import { isGeometricDataName } from './geometricData.js';
|
|
8
|
+
|
|
9
|
+
describe('isGeometricDataName — data/geometry boundary', () => {
|
|
10
|
+
it('flags placement/position data that authoring tools leak into property sets', () => {
|
|
11
|
+
for (const name of [
|
|
12
|
+
'Elevation',
|
|
13
|
+
'Elevation at Bottom',
|
|
14
|
+
'Elevation at Top',
|
|
15
|
+
'Height Offset From Level',
|
|
16
|
+
'Base Offset',
|
|
17
|
+
'Top Offset',
|
|
18
|
+
'Bottom Offset',
|
|
19
|
+
'Reference Level',
|
|
20
|
+
'Z Coordinate',
|
|
21
|
+
'Z Offset',
|
|
22
|
+
'Placement',
|
|
23
|
+
'ObjectPlacement',
|
|
24
|
+
]) {
|
|
25
|
+
assert.equal(isGeometricDataName(name), true, `${name} should be geometric`);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('does NOT flag semantic data — a move must not strip real attributes/properties', () => {
|
|
30
|
+
for (const name of [
|
|
31
|
+
'Name',
|
|
32
|
+
'Description',
|
|
33
|
+
'ObjectType',
|
|
34
|
+
'FireRating',
|
|
35
|
+
'IsExternal',
|
|
36
|
+
'LoadBearing',
|
|
37
|
+
'Reference',
|
|
38
|
+
'Pset_SlabCommon',
|
|
39
|
+
'AcousticRating',
|
|
40
|
+
'Combustible',
|
|
41
|
+
'ThermalTransmittance',
|
|
42
|
+
'Status',
|
|
43
|
+
'Mark',
|
|
44
|
+
'Material',
|
|
45
|
+
// Generic terms that are just as often semantic — must NOT be excluded.
|
|
46
|
+
'Location',
|
|
47
|
+
'Position',
|
|
48
|
+
'Datum',
|
|
49
|
+
'Offset',
|
|
50
|
+
]) {
|
|
51
|
+
assert.equal(isGeometricDataName(name), false, `${name} should be semantic data`);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The single boundary between **data** and **geometry** for model compare
|
|
7
|
+
* (issue #924).
|
|
8
|
+
*
|
|
9
|
+
* A change to where/how an element sits in space — placement, coordinates,
|
|
10
|
+
* elevation, level offsets — is a *geometry* change, fully captured by the
|
|
11
|
+
* per-entity geometry hash (world-space mesh). It must NOT also flip the data
|
|
12
|
+
* fingerprint, or a pure move reads as "data · geometry" instead of geometry
|
|
13
|
+
* only. Authoring tools leak placement into property sets (Revit's "Elevation",
|
|
14
|
+
* "Height Offset From Level", "Base/Top Offset", level references), so we filter
|
|
15
|
+
* those out of the data side by name.
|
|
16
|
+
*
|
|
17
|
+
* Used by both `buildFingerprints` (so the data hash ignores them) and
|
|
18
|
+
* `describeChange` (so the "what changed" panel doesn't list them as data).
|
|
19
|
+
* Quantities (Volume/Area/Length/…) are excluded wholesale at the call site —
|
|
20
|
+
* they are geometry-derived measurements, not in scope here.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
// Deliberately narrow: only names that are *specifically* placement (elevation,
|
|
24
|
+
// (object)placement, coordinate) or a qualified level/axis offset. Generic
|
|
25
|
+
// `Location` / `Position` / `Datum` / bare `Offset` are NOT excluded — they are
|
|
26
|
+
// just as often ordinary semantic data (a custom "Location" string, a "Position"
|
|
27
|
+
// label), and wrongly dropping them from the data diff would hide real edits.
|
|
28
|
+
const GEOMETRIC_NAME =
|
|
29
|
+
/elevation|placement|coordinate|reference\s*level|(height|base|top|bottom|level|z)\s*offset/i;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* True when an attribute / property / property-set name denotes geometric
|
|
33
|
+
* placement data that belongs to the geometry diff, not the data diff.
|
|
34
|
+
*/
|
|
35
|
+
export function isGeometricDataName(name: string): boolean {
|
|
36
|
+
return GEOMETRIC_NAME.test(name);
|
|
37
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
import { describe, it } from 'node:test';
|
|
6
|
+
import assert from 'node:assert';
|
|
7
|
+
import type { DiffEntry, DiffState, ModelDiff } from '@ifc-lite/diff';
|
|
8
|
+
import { buildCompareOverlay, COMPARE_COLORS } from './overlay.js';
|
|
9
|
+
import type { CompareRef } from './buildFingerprints.js';
|
|
10
|
+
|
|
11
|
+
function ref(globalId: number): CompareRef {
|
|
12
|
+
// modelId/localId are irrelevant to the overlay (it keys on globalId).
|
|
13
|
+
return { modelId: 'm', localId: globalId, globalId };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function entry(
|
|
17
|
+
state: DiffState,
|
|
18
|
+
opts: { base?: number; head?: number; changeKinds?: ('data' | 'geometry')[] } = {},
|
|
19
|
+
): DiffEntry<CompareRef> {
|
|
20
|
+
return {
|
|
21
|
+
key: `${state}:${opts.base ?? ''}:${opts.head ?? ''}`,
|
|
22
|
+
state,
|
|
23
|
+
changeKinds: opts.changeKinds ?? [],
|
|
24
|
+
base: opts.base !== undefined ? ({ key: '', ifcType: '', dataHash: '', ref: ref(opts.base) }) : undefined,
|
|
25
|
+
head: opts.head !== undefined ? ({ key: '', ifcType: '', dataHash: '', ref: ref(opts.head) }) : undefined,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function diffOf(entries: DiffEntry<CompareRef>[]): ModelDiff<CompareRef> {
|
|
30
|
+
// buildCompareOverlay only reads `entries`; the rest satisfies the type.
|
|
31
|
+
return { scope: 'both', entries, byKey: new Map(), counts: { added: 0, modified: 0, deleted: 0, unchanged: 0 } };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('buildCompareOverlay', () => {
|
|
35
|
+
it('colours added on the head, with nothing hidden', () => {
|
|
36
|
+
const { colorOverrides, hiddenIds } = buildCompareOverlay(diffOf([entry('added', { head: 10 })]), false);
|
|
37
|
+
assert.deepStrictEqual(colorOverrides.get(10), COMPARE_COLORS.added);
|
|
38
|
+
assert.strictEqual(hiddenIds.size, 0);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('colours deleted on the base (it only exists in A)', () => {
|
|
42
|
+
const { colorOverrides, hiddenIds } = buildCompareOverlay(diffOf([entry('deleted', { base: 5 })]), false);
|
|
43
|
+
assert.deepStrictEqual(colorOverrides.get(5), COMPARE_COLORS.deleted);
|
|
44
|
+
assert.strictEqual(hiddenIds.size, 0);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('colours modified on the head and hides the base copy', () => {
|
|
48
|
+
const { colorOverrides, hiddenIds } = buildCompareOverlay(
|
|
49
|
+
diffOf([entry('modified', { base: 5, head: 1005, changeKinds: ['geometry'] })]),
|
|
50
|
+
false,
|
|
51
|
+
);
|
|
52
|
+
assert.deepStrictEqual(colorOverrides.get(1005), COMPARE_COLORS.modified);
|
|
53
|
+
assert.ok(!colorOverrides.has(5), 'base copy is not coloured');
|
|
54
|
+
assert.ok(hiddenIds.has(5), 'base copy is hidden to avoid double geometry');
|
|
55
|
+
assert.ok(!hiddenIds.has(1005), 'head copy stays visible');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('hides both copies of an unchanged element when not showing unchanged', () => {
|
|
59
|
+
const { colorOverrides, hiddenIds } = buildCompareOverlay(
|
|
60
|
+
diffOf([entry('unchanged', { base: 5, head: 1005 })]),
|
|
61
|
+
false,
|
|
62
|
+
);
|
|
63
|
+
assert.strictEqual(colorOverrides.size, 0);
|
|
64
|
+
assert.deepStrictEqual([...hiddenIds].sort((a, b) => a - b), [5, 1005]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('ghosts the head and hides the base for unchanged when showing unchanged', () => {
|
|
68
|
+
const { colorOverrides, hiddenIds } = buildCompareOverlay(
|
|
69
|
+
diffOf([entry('unchanged', { base: 5, head: 1005 })]),
|
|
70
|
+
true,
|
|
71
|
+
);
|
|
72
|
+
assert.deepStrictEqual(colorOverrides.get(1005), COMPARE_COLORS.unchanged);
|
|
73
|
+
assert.ok(hiddenIds.has(5), 'base duplicate hidden');
|
|
74
|
+
assert.ok(!hiddenIds.has(1005), 'ghosted head stays visible');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('composes a mixed diff without cross-contaminating colours/visibility', () => {
|
|
78
|
+
const { colorOverrides, hiddenIds } = buildCompareOverlay(
|
|
79
|
+
diffOf([
|
|
80
|
+
entry('added', { head: 1001 }),
|
|
81
|
+
entry('deleted', { base: 2 }),
|
|
82
|
+
entry('modified', { base: 3, head: 1003 }),
|
|
83
|
+
entry('unchanged', { base: 4, head: 1004 }),
|
|
84
|
+
]),
|
|
85
|
+
false,
|
|
86
|
+
);
|
|
87
|
+
assert.deepStrictEqual(colorOverrides.get(1001), COMPARE_COLORS.added);
|
|
88
|
+
assert.deepStrictEqual(colorOverrides.get(2), COMPARE_COLORS.deleted);
|
|
89
|
+
assert.deepStrictEqual(colorOverrides.get(1003), COMPARE_COLORS.modified);
|
|
90
|
+
// modified base + both unchanged copies are hidden; added/deleted are not.
|
|
91
|
+
assert.deepStrictEqual([...hiddenIds].sort((a, b) => a - b), [3, 4, 1004]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('skips entries that carry no usable ref', () => {
|
|
95
|
+
const { colorOverrides, hiddenIds } = buildCompareOverlay(diffOf([entry('added', {})]), false);
|
|
96
|
+
assert.strictEqual(colorOverrides.size, 0);
|
|
97
|
+
assert.strictEqual(hiddenIds.size, 0);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Pure mapping from a model-diff to the renderer's colour + visibility
|
|
7
|
+
* channels (issue #924). Kept side-effect-free so it can be unit-tested in
|
|
8
|
+
* isolation; the `useCompareOverlay` hook wires the result into the store.
|
|
9
|
+
*
|
|
10
|
+
* Both models (A = base, B = head) are loaded in the federated scene at once,
|
|
11
|
+
* so an unchanged element exists twice at the same place. We therefore drive
|
|
12
|
+
* the comparison entirely from model B and suppress B-duplicated copies in
|
|
13
|
+
* model A:
|
|
14
|
+
*
|
|
15
|
+
* - added (B only) → green on B
|
|
16
|
+
* - modified (A and B) → yellow on B, **hide** the A copy
|
|
17
|
+
* - deleted (A only) → red on A
|
|
18
|
+
* - unchanged → ghost grey on B + hide A (when "show unchanged"),
|
|
19
|
+
* otherwise hide both
|
|
20
|
+
*
|
|
21
|
+
* Colours match the threejs compare example's palette. Keys are federation
|
|
22
|
+
* **global** ids (`CompareRef.globalId`) — exactly what the renderer's
|
|
23
|
+
* `setColorOverrides` / hidden set consume.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type { ModelDiff } from '@ifc-lite/diff';
|
|
27
|
+
import type { CompareRef } from './buildFingerprints';
|
|
28
|
+
|
|
29
|
+
export type RGBA = [number, number, number, number];
|
|
30
|
+
|
|
31
|
+
/** Diff-state colour conventions. `unchanged` is a translucent ghost. */
|
|
32
|
+
export const COMPARE_COLORS = {
|
|
33
|
+
added: [0.22, 0.78, 0.44, 1] as RGBA,
|
|
34
|
+
modified: [1.0, 0.6, 0.18, 1] as RGBA,
|
|
35
|
+
deleted: [0.95, 0.3, 0.3, 1] as RGBA,
|
|
36
|
+
unchanged: [0.45, 0.52, 0.58, 0.32] as RGBA,
|
|
37
|
+
} as const;
|
|
38
|
+
|
|
39
|
+
export interface CompareOverlay {
|
|
40
|
+
/** Per global-id colour override fed to `scene.setColorOverrides`. */
|
|
41
|
+
colorOverrides: Map<number, RGBA>;
|
|
42
|
+
/** Global ids to hide (suppresses duplicated base-model geometry). */
|
|
43
|
+
hiddenIds: Set<number>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Build the colour + hidden maps for a comparison.
|
|
48
|
+
*
|
|
49
|
+
* @param diff engine output (refs carry the federation global id)
|
|
50
|
+
* @param showUnchanged draw unchanged elements ghosted (true) or hide them
|
|
51
|
+
*/
|
|
52
|
+
export function buildCompareOverlay(
|
|
53
|
+
diff: ModelDiff<CompareRef>,
|
|
54
|
+
showUnchanged: boolean,
|
|
55
|
+
): CompareOverlay {
|
|
56
|
+
const colorOverrides = new Map<number, RGBA>();
|
|
57
|
+
const hiddenIds = new Set<number>();
|
|
58
|
+
|
|
59
|
+
for (const entry of diff.entries) {
|
|
60
|
+
const baseGlobal = entry.base?.ref.globalId;
|
|
61
|
+
const headGlobal = entry.head?.ref.globalId;
|
|
62
|
+
|
|
63
|
+
switch (entry.state) {
|
|
64
|
+
case 'added':
|
|
65
|
+
if (headGlobal !== undefined) colorOverrides.set(headGlobal, COMPARE_COLORS.added);
|
|
66
|
+
break;
|
|
67
|
+
|
|
68
|
+
case 'deleted':
|
|
69
|
+
if (baseGlobal !== undefined) colorOverrides.set(baseGlobal, COMPARE_COLORS.deleted);
|
|
70
|
+
break;
|
|
71
|
+
|
|
72
|
+
case 'modified':
|
|
73
|
+
if (headGlobal !== undefined) colorOverrides.set(headGlobal, COMPARE_COLORS.modified);
|
|
74
|
+
// Hide the old (base) copy so the yellow head reads cleanly.
|
|
75
|
+
if (baseGlobal !== undefined) hiddenIds.add(baseGlobal);
|
|
76
|
+
break;
|
|
77
|
+
|
|
78
|
+
case 'unchanged':
|
|
79
|
+
if (showUnchanged) {
|
|
80
|
+
if (headGlobal !== undefined) colorOverrides.set(headGlobal, COMPARE_COLORS.unchanged);
|
|
81
|
+
if (baseGlobal !== undefined) hiddenIds.add(baseGlobal);
|
|
82
|
+
} else {
|
|
83
|
+
if (headGlobal !== undefined) hiddenIds.add(headGlobal);
|
|
84
|
+
if (baseGlobal !== undefined) hiddenIds.add(baseGlobal);
|
|
85
|
+
}
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { colorOverrides, hiddenIds };
|
|
91
|
+
}
|
|
@@ -41,7 +41,7 @@ export function shouldPreferOrthometricTerrain(
|
|
|
41
41
|
|
|
42
42
|
export interface CesiumPlacementInput {
|
|
43
43
|
coordinateInfo?: CoordinateInfo;
|
|
44
|
-
projectedCRS?: Pick<ProjectedCRS, 'verticalDatum'> | Pick<ProjectedCRS, 'mapUnitScale'
|
|
44
|
+
projectedCRS?: Pick<ProjectedCRS, 'verticalDatum'> | Pick<ProjectedCRS, 'mapUnitScale' | 'verticalDatum'>;
|
|
45
45
|
ifcOriginHeight: number;
|
|
46
46
|
terrainHeight: number | null;
|
|
47
47
|
storeyElevations?: Map<number, number>;
|
package/src/lib/geo/reproject.ts
CHANGED
|
@@ -347,8 +347,11 @@ export async function resolveProjection(crs: ProjectedCRS): Promise<string | nul
|
|
|
347
347
|
// string itself when possible. `+datum=` is rare in modern proj4 output;
|
|
348
348
|
// the typical hint is `+ellps=` which we already accept as a weak signal
|
|
349
349
|
// inside DATUM_TOWGS84 keys above.
|
|
350
|
+
// Only positive-cache a successful result, so a transient network
|
|
351
|
+
// failure (offline/CDN hiccup/timeout) can be retried later instead of
|
|
352
|
+
// permanently poisoning this EPSG code for the rest of the session.
|
|
350
353
|
const fetched = raw ? sanitizeProj4(raw, code, null) : null;
|
|
351
|
-
projDefCache.set(code, fetched);
|
|
354
|
+
if (fetched) projDefCache.set(code, fetched);
|
|
352
355
|
return fetched;
|
|
353
356
|
}
|
|
354
357
|
|