@ifc-lite/viewer 1.19.1 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +59 -44
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +488 -0
- package/dist/assets/{basketViewActivator-CA2CTcVo.js → basketViewActivator-Bzw51jhm.js} +6 -6
- package/dist/assets/decode-worker-t2EGKAxO.js +1708 -0
- package/dist/assets/drawing-2d-Bjy8YPrg.js +257 -0
- package/dist/assets/exporters-u0sz2Upj.js +259119 -0
- package/dist/assets/geometry-controller.worker-NH8pZmrU.js +7 -0
- package/dist/assets/geometry.worker-Bp4rW_R1.js +1 -0
- package/dist/assets/ids-B7AXEv7h.js +4067 -0
- package/dist/assets/ifc-lite-DfZHk36-.js +7 -0
- package/dist/assets/ifc-lite_bg-DlKs5-yM.wasm +0 -0
- package/dist/assets/ifc-lite_bg-PqmRe3Ph.wasm +0 -0
- package/dist/assets/index-CSWgTe1s.css +1 -0
- package/dist/assets/{index-D8Epw-e7.js → index-DVNSvEMh.js} +40146 -35823
- package/dist/assets/laz-perf-Cvr_Lepg.js +1 -0
- package/dist/assets/laz-perf-DnSyzVYH.wasm +0 -0
- package/dist/assets/{native-bridge-DKmx1z95.js → native-bridge-BiD01jI9.js} +1 -1
- package/dist/assets/parser.worker-Bnbrl6gy.js +182 -0
- package/dist/assets/{sandbox-tccwm5Bo.js → sandbox-DPD1ROr0.js} +4 -4
- package/dist/assets/{server-client-LoWPK1N2.js → server-client-DP8fMPY9.js} +1 -1
- package/dist/assets/{wasm-bridge-BsJGgPMs.js → wasm-bridge-CErti6zX.js} +1 -1
- package/dist/assets/workerHelpers-CBbWSJmd.js +36 -0
- package/dist/index.html +8 -8
- package/index.html +1 -1
- package/package.json +10 -10
- package/src/components/viewer/BasketPresentationDock.tsx +3 -0
- package/src/components/viewer/CesiumOverlay.tsx +165 -120
- package/src/components/viewer/DeviationPanel.tsx +172 -0
- package/src/components/viewer/HierarchyPanel.tsx +29 -3
- package/src/components/viewer/HoverTooltip.tsx +5 -0
- package/src/components/viewer/IDSAuditSummary.tsx +389 -0
- package/src/components/viewer/IDSPanel.tsx +80 -26
- package/src/components/viewer/MainToolbar.tsx +60 -7
- package/src/components/viewer/MergeLayersBanner.tsx +108 -0
- package/src/components/viewer/MobileToolbar.tsx +326 -0
- package/src/components/viewer/PointCloudClasses.tsx +111 -0
- package/src/components/viewer/PointCloudLegend.tsx +119 -0
- package/src/components/viewer/PointCloudPanel.tsx +52 -1
- package/src/components/viewer/PropertiesPanel.tsx +37 -6
- package/src/components/viewer/RectSelectionOverlay.tsx +48 -0
- package/src/components/viewer/StatusBar.tsx +14 -0
- package/src/components/viewer/ViewerLayout.tsx +288 -95
- package/src/components/viewer/Viewport.tsx +86 -18
- package/src/components/viewer/ViewportContainer.tsx +25 -11
- package/src/components/viewer/ViewportOverlays.tsx +41 -26
- package/src/components/viewer/mouseHandlerTypes.ts +22 -0
- package/src/components/viewer/properties/GeoreferencingPanel.tsx +77 -8
- package/src/components/viewer/properties/MaterialCard.tsx +2 -2
- package/src/components/viewer/selectionHandlers.ts +41 -0
- package/src/components/viewer/tools/SectionPanel.tsx +181 -24
- package/src/components/viewer/tools/SectionVisualization.tsx +384 -3
- package/src/components/viewer/useAnimationLoop.ts +22 -0
- package/src/components/viewer/useMouseControls.ts +296 -3
- package/src/components/viewer/usePointCloudSync.ts +8 -1
- package/src/components/viewer/useRenderUpdates.ts +21 -1
- package/src/components/viewer/useTouchControls.ts +100 -41
- package/src/hooks/federationLoadGate.test.ts +90 -0
- package/src/hooks/federationLoadGate.ts +127 -0
- package/src/hooks/ids/idsDataAccessor.ts +11 -259
- package/src/hooks/ingest/pointCloudIngest.ts +127 -16
- package/src/hooks/useDrawingGeneration.ts +81 -8
- package/src/hooks/useIDS.ts +90 -10
- package/src/hooks/useIfcFederation.ts +94 -16
- package/src/hooks/useIfcLoader.ts +289 -64
- package/src/hooks/useViewerSelectors.ts +10 -0
- package/src/lib/geo/cesium-bridge.ts +84 -67
- package/src/lib/geo/clamp-anchor.test.ts +80 -0
- package/src/lib/geo/clamp-anchor.ts +57 -0
- package/src/lib/geo/effective-georef.test.ts +79 -1
- package/src/lib/geo/effective-georef.ts +83 -0
- package/src/lib/geo/reproject.ts +26 -13
- package/src/lib/geo/terrain-elevation.ts +166 -0
- package/src/lib/lens/adapter.ts +1 -1
- package/src/lib/llm/context-builder.ts +1 -1
- package/src/lib/perf/memoryAccounting.test.ts +92 -0
- package/src/lib/perf/memoryAccounting.ts +235 -0
- package/src/sdk/adapters/mutation-view.ts +1 -1
- package/src/store/constants.ts +39 -2
- package/src/store/index.ts +6 -1
- package/src/store/slices/cesiumSlice.ts +1 -1
- package/src/store/slices/idsSlice.ts +24 -0
- package/src/store/slices/loadingSlice.ts +12 -0
- package/src/store/slices/pointCloudSlice.ts +72 -1
- package/src/store/slices/sectionSlice.test.ts +590 -1
- package/src/store/slices/sectionSlice.ts +344 -17
- package/src/store/slices/uiSlice.merge-layers.test.ts +217 -0
- package/src/store/slices/uiSlice.ts +60 -2
- package/src/store/types.ts +42 -0
- package/src/store.ts +13 -0
- package/src/utils/acquireFileBuffer.test.ts +231 -0
- package/src/utils/acquireFileBuffer.ts +128 -0
- package/src/utils/ifcConfig.ts +24 -0
- package/src/utils/nativeSpatialDataStore.ts +20 -2
- package/src/utils/spatialHierarchy.test.ts +116 -0
- package/src/utils/spatialHierarchy.ts +23 -0
- package/tailwind.config.js +5 -0
- package/tsconfig.json +1 -0
- package/vite.config.ts +6 -0
- package/dist/assets/decode-worker-Collf_X_.js +0 -1320
- package/dist/assets/drawing-2d-DoxKMqbO.js +0 -257
- package/dist/assets/exporters-xbXqEDlO.js +0 -81590
- package/dist/assets/geometry.worker-DQEZB2rB.js +0 -1
- package/dist/assets/ids-2WdONLlu.js +0 -2033
- package/dist/assets/ifc-lite_bg-4yUkDRD8.wasm +0 -0
- package/dist/assets/index-BXeEKqJG.css +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/index-
|
|
2
|
-
import { P as _, Q as qe, E as Ge,
|
|
1
|
+
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/index-DVNSvEMh.js","assets/cesium-DUOzBlqv.js","assets/arrow-CZ5kQ26f.js","assets/cesium-ADbP7waU.css","assets/exporters-u0sz2Upj.js","assets/bcf-4K724hw0.js","assets/zip-DBEtpeu6.js","assets/drawing-2d-Bjy8YPrg.js","assets/lens-CSASnhAL.js","assets/server-client-DP8fMPY9.js","assets/ids-B7AXEv7h.js","assets/three-CDRZThFA.js","assets/index-CSWgTe1s.css","assets/browser-C5TFR7sH.js"])))=>i.map(i=>d[i]);
|
|
2
|
+
import { P as _, Q as qe, E as Ge, q as hn, r as pn, s as vt, t as Nt, _ as Re, __tla as __tla_0 } from "./exporters-u0sz2Upj.js";
|
|
3
3
|
import { g as Q } from "./bcf-4K724hw0.js";
|
|
4
4
|
import { h as ut, B as fn } from "./lens-CSASnhAL.js";
|
|
5
5
|
let Wi, Hi, Mi, Jt, gi, _i, Ki, Ji, Vi, zi, An, Qi, Xi, qi, Ui, ji, Gi, vn, Bi, Zi, er, Ts, tr, Yi, bi, Si, nr;
|
|
@@ -1218,7 +1218,7 @@ let __tla = Promise.all([
|
|
|
1218
1218
|
t.Elevation
|
|
1219
1219
|
]
|
|
1220
1220
|
});
|
|
1221
|
-
return this.line(e, "IFCBUILDINGSTOREY", `'${s}',#${this.ownerHistoryId},'${y(i)}',${r}
|
|
1221
|
+
return this.line(e, "IFCBUILDINGSTOREY", `'${s}',#${this.ownerHistoryId},'${y(i)}',${r},$,#${a},$,$,.ELEMENT.,${o}`), this.storeyIds.push(e), this.storeyElements.set(e, []), this.storeyPlacements.set(e, a), this.entities.push({
|
|
1222
1222
|
expressId: e,
|
|
1223
1223
|
type: "IfcBuildingStorey",
|
|
1224
1224
|
Name: i
|
|
@@ -9472,7 +9472,7 @@ Attempted to suspend at:`);
|
|
|
9472
9472
|
let t = Je(await n), [e, s, { QuickJSWASMModule: i }] = await Promise.all([
|
|
9473
9473
|
t.importModuleLoader().then(Je),
|
|
9474
9474
|
t.importFFI(),
|
|
9475
|
-
Re(()=>import("./index-
|
|
9475
|
+
Re(()=>import("./index-DVNSvEMh.js").then(async (m)=>{
|
|
9476
9476
|
await m.__tla;
|
|
9477
9477
|
return m;
|
|
9478
9478
|
}).then((a)=>a.m), __vite__mapDeps([0,1,2,3,4,5,6,7,8,9,10,11,12])).then(Je)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{I as f,
|
|
1
|
+
import{I as f,u as m}from"./exporters-u0sz2Upj.js";import"./bcf-4K724hw0.js";import"./zip-DBEtpeu6.js";import"./cesium-DUOzBlqv.js";import"./arrow-CZ5kQ26f.js";class b{bridge;initialized=!1;constructor(){this.bridge=new f}async init(){this.initialized||(await this.bridge.init(),this.initialized=!0)}isInitialized(){return this.initialized}toIfcContent(e){return typeof e=="string"?e:new TextDecoder().decode(e)}async processGeometry(e){this.initialized||await this.init(),performance.now();const i=new m(this.bridge.getApi(),this.toIfcContent(e)),r=i.collectMeshes(),s=i.getBuildingRotation();performance.now();let o=0,n=0;for(const a of r)o+=a.positions.length/3,n+=a.indices.length/3;return{meshes:r,totalVertices:o,totalTriangles:n,coordinateInfo:{originShift:{x:0,y:0,z:0},originalBounds:{min:{x:0,y:0,z:0},max:{x:0,y:0,z:0}},shiftedBounds:{min:{x:0,y:0,z:0},max:{x:0,y:0,z:0}},hasLargeCoordinates:!1,buildingRotation:s}}}async processGeometryStreaming(e,i){this.initialized||await this.init();const r=performance.now(),s=new m(this.bridge.getApi(),this.toIfcContent(e));let o=0,n=0,c=0;try{for await(const t of s.collectMeshesStreaming(50)){if(t&&typeof t=="object"&&"type"in t&&t.type==="colorUpdate")continue;const l=t;o+=l.length;for(const g of l)n+=g.positions.length/3,c+=g.indices.length/3;i.onBatch?.({meshes:l,progress:{processed:o,total:o,currentType:"processing"}})}}catch(t){throw i.onError?.(t instanceof Error?t:new Error(String(t))),t}const d=performance.now()-r,h={totalMeshes:o,totalVertices:n,totalTriangles:c,parseTimeMs:d*.3,geometryTimeMs:d*.7};return i.onComplete?.(h),h}getApi(){return this.bridge.getApi()}}export{b as WasmBridge};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
let a;
|
|
2
|
+
let __tla = (async ()=>{
|
|
3
|
+
function o(r, t) {
|
|
4
|
+
return new Promise((e)=>{
|
|
5
|
+
r.addEventListener("message", function s({ data: n }) {
|
|
6
|
+
n?.type === t && (r.removeEventListener("message", s), e(n));
|
|
7
|
+
});
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
o(self, "wasm_bindgen_worker_init").then(async ({ init: r, receiver: t })=>{
|
|
11
|
+
const e = await import("./ifc-lite-DfZHk36-.js");
|
|
12
|
+
await e.default(r), postMessage({
|
|
13
|
+
type: "wasm_bindgen_worker_ready"
|
|
14
|
+
}), e.wbg_rayon_start_worker(t);
|
|
15
|
+
});
|
|
16
|
+
a = async function(r, t, e) {
|
|
17
|
+
if (e.numThreads() === 0) throw new Error("num_threads must be > 0.");
|
|
18
|
+
const s = {
|
|
19
|
+
type: "wasm_bindgen_worker_init",
|
|
20
|
+
init: {
|
|
21
|
+
module_or_path: r,
|
|
22
|
+
memory: t
|
|
23
|
+
},
|
|
24
|
+
receiver: e.receiver()
|
|
25
|
+
};
|
|
26
|
+
await Promise.all(Array.from({
|
|
27
|
+
length: e.numThreads()
|
|
28
|
+
}, async ()=>{
|
|
29
|
+
const n = new Worker(self.location.href, {
|
|
30
|
+
type: "module"
|
|
31
|
+
});
|
|
32
|
+
return n.postMessage(s), await o(n, "wasm_bindgen_worker_ready"), n;
|
|
33
|
+
})), e.build();
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
export { a as s, __tla };
|
package/dist/index.html
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
|
6
6
|
<title>IFClite Viewer</title>
|
|
7
7
|
<script>
|
|
8
8
|
// Apply theme before first paint to prevent flash of wrong theme.
|
|
@@ -50,20 +50,20 @@
|
|
|
50
50
|
<meta name="theme-color" content="#7aa2f7">
|
|
51
51
|
<meta name="msapplication-TileColor" content="#1a1b26">
|
|
52
52
|
<meta name="msapplication-TileImage" content="/favicon-192x192-cropped.png">
|
|
53
|
-
<script type="module" crossorigin src="/assets/index-
|
|
53
|
+
<script type="module" crossorigin src="/assets/index-DVNSvEMh.js"></script>
|
|
54
54
|
<link rel="modulepreload" crossorigin href="/assets/arrow-CZ5kQ26f.js">
|
|
55
55
|
<link rel="modulepreload" crossorigin href="/assets/cesium-DUOzBlqv.js">
|
|
56
56
|
<link rel="modulepreload" crossorigin href="/assets/zip-DBEtpeu6.js">
|
|
57
57
|
<link rel="modulepreload" crossorigin href="/assets/bcf-4K724hw0.js">
|
|
58
|
-
<link rel="modulepreload" crossorigin href="/assets/exporters-
|
|
58
|
+
<link rel="modulepreload" crossorigin href="/assets/exporters-u0sz2Upj.js">
|
|
59
59
|
<link rel="modulepreload" crossorigin href="/assets/lens-CSASnhAL.js">
|
|
60
|
-
<link rel="modulepreload" crossorigin href="/assets/sandbox-
|
|
61
|
-
<link rel="modulepreload" crossorigin href="/assets/drawing-2d-
|
|
62
|
-
<link rel="modulepreload" crossorigin href="/assets/server-client-
|
|
63
|
-
<link rel="modulepreload" crossorigin href="/assets/ids-
|
|
60
|
+
<link rel="modulepreload" crossorigin href="/assets/sandbox-DPD1ROr0.js">
|
|
61
|
+
<link rel="modulepreload" crossorigin href="/assets/drawing-2d-Bjy8YPrg.js">
|
|
62
|
+
<link rel="modulepreload" crossorigin href="/assets/server-client-DP8fMPY9.js">
|
|
63
|
+
<link rel="modulepreload" crossorigin href="/assets/ids-B7AXEv7h.js">
|
|
64
64
|
<link rel="modulepreload" crossorigin href="/assets/three-CDRZThFA.js">
|
|
65
65
|
<link rel="stylesheet" crossorigin href="/assets/cesium-ADbP7waU.css">
|
|
66
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
66
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CSWgTe1s.css">
|
|
67
67
|
</head>
|
|
68
68
|
<body>
|
|
69
69
|
<div id="root"></div>
|
package/index.html
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
|
6
6
|
<title>IFClite Viewer</title>
|
|
7
7
|
<script>
|
|
8
8
|
// Apply theme before first paint to prevent flash of wrong theme.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ifc-lite/viewer",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.21.0",
|
|
4
4
|
"description": "IFC-Lite viewer application",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"dependencies": {
|
|
@@ -51,25 +51,25 @@
|
|
|
51
51
|
"zustand": "^4.4.0",
|
|
52
52
|
"@ifc-lite/bcf": "^1.15.3",
|
|
53
53
|
"@ifc-lite/cache": "^1.14.5",
|
|
54
|
-
"@ifc-lite/data": "^1.
|
|
55
|
-
"@ifc-lite/drawing-2d": "^1.
|
|
54
|
+
"@ifc-lite/data": "^1.17.0",
|
|
55
|
+
"@ifc-lite/drawing-2d": "^1.16.0",
|
|
56
56
|
"@ifc-lite/encoding": "^1.14.6",
|
|
57
57
|
"@ifc-lite/export": "^1.18.0",
|
|
58
|
-
"@ifc-lite/geometry": "^1.
|
|
59
|
-
"@ifc-lite/ids": "^1.
|
|
58
|
+
"@ifc-lite/geometry": "^1.18.1",
|
|
59
|
+
"@ifc-lite/ids": "^1.15.1",
|
|
60
60
|
"@ifc-lite/lens": "^1.14.4",
|
|
61
|
-
"@ifc-lite/lists": "^1.14.
|
|
61
|
+
"@ifc-lite/lists": "^1.14.12",
|
|
62
62
|
"@ifc-lite/mcp": "^0.2.0",
|
|
63
63
|
"@ifc-lite/mutations": "^1.15.0",
|
|
64
|
-
"@ifc-lite/parser": "^2.
|
|
65
|
-
"@ifc-lite/pointcloud": "^0.
|
|
64
|
+
"@ifc-lite/parser": "^2.4.0",
|
|
65
|
+
"@ifc-lite/pointcloud": "^0.3.0",
|
|
66
66
|
"@ifc-lite/query": "^1.14.7",
|
|
67
|
-
"@ifc-lite/renderer": "^1.
|
|
67
|
+
"@ifc-lite/renderer": "^1.20.0",
|
|
68
68
|
"@ifc-lite/sandbox": "^1.15.0",
|
|
69
69
|
"@ifc-lite/sdk": "^1.15.0",
|
|
70
70
|
"@ifc-lite/server-client": "^1.15.3",
|
|
71
71
|
"@ifc-lite/spatial": "^1.14.5",
|
|
72
|
-
"@ifc-lite/wasm": "^1.16.
|
|
72
|
+
"@ifc-lite/wasm": "^1.16.10"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
75
|
"@tailwindcss/postcss": "^4.1.18",
|
|
@@ -46,6 +46,7 @@ export function BasketPresentationDock() {
|
|
|
46
46
|
const basketViews = useViewerStore((s) => s.basketViews);
|
|
47
47
|
const activeBasketViewId = useViewerStore((s) => s.activeBasketViewId);
|
|
48
48
|
const basketPresentationVisible = useViewerStore((s) => s.basketPresentationVisible);
|
|
49
|
+
const isMobile = useViewerStore((s) => s.isMobile);
|
|
49
50
|
|
|
50
51
|
const showPinboard = useViewerStore((s) => s.showPinboard);
|
|
51
52
|
const clearIsolation = useViewerStore((s) => s.clearIsolation);
|
|
@@ -165,6 +166,8 @@ export function BasketPresentationDock() {
|
|
|
165
166
|
setBasketViewTransitionMs(viewId, Math.round(seconds * 1000));
|
|
166
167
|
}, [setBasketViewTransitionMs]);
|
|
167
168
|
|
|
169
|
+
if (isMobile) return null;
|
|
170
|
+
|
|
168
171
|
if (!basketPresentationVisible) {
|
|
169
172
|
return (
|
|
170
173
|
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 z-30 pointer-events-none">
|
|
@@ -26,6 +26,8 @@ import type { MapConversion, ProjectedCRS } from '@ifc-lite/parser';
|
|
|
26
26
|
import type { CoordinateInfo, GeometryResult } from '@ifc-lite/geometry';
|
|
27
27
|
import { getGlobalRenderer } from '@/hooks/useBCF';
|
|
28
28
|
import { createCesiumBridge, type CesiumBridge } from '@/lib/geo/cesium-bridge';
|
|
29
|
+
import { findClampAnchorY } from '@/lib/geo/clamp-anchor';
|
|
30
|
+
import { getEffectiveHorizontalScale } from '@/lib/geo/effective-georef';
|
|
29
31
|
|
|
30
32
|
// Lazy-loaded Cesium module and CSS
|
|
31
33
|
let cesiumPromise: Promise<typeof import('cesium')> | null = null;
|
|
@@ -167,11 +169,16 @@ function buildModelMatrix(
|
|
|
167
169
|
Cesium: typeof import('cesium'),
|
|
168
170
|
bridge: CesiumBridge,
|
|
169
171
|
mapConversion: MapConversion | undefined,
|
|
172
|
+
projectedCRS: ProjectedCRS | undefined,
|
|
170
173
|
coordinateInfo: CoordinateInfo | undefined,
|
|
171
|
-
|
|
172
|
-
terrainH: number | null,
|
|
174
|
+
lengthUnitScale: number,
|
|
173
175
|
) {
|
|
174
|
-
|
|
176
|
+
// GLB vertices are in viewer-space metres (geometry engine converts during
|
|
177
|
+
// extraction). IfcMapConversion.Scale is defined per IFC spec relative to
|
|
178
|
+
// the project length unit, so applying it raw to metre-converted geometry
|
|
179
|
+
// double-scales the model — see issue #595. Use the effective scale.
|
|
180
|
+
const mapUnitScale = projectedCRS?.mapUnitScale ?? lengthUnitScale;
|
|
181
|
+
const hScale = getEffectiveHorizontalScale(mapConversion?.scale, mapUnitScale, lengthUnitScale);
|
|
175
182
|
const absc = mapConversion?.xAxisAbscissa ?? 1.0;
|
|
176
183
|
const ordi = mapConversion?.xAxisOrdinate ?? 0.0;
|
|
177
184
|
const bounds = coordinateInfo?.originalBounds;
|
|
@@ -180,15 +187,11 @@ function buildModelMatrix(
|
|
|
180
187
|
const mvy = bounds ? (bounds.min.y + bounds.max.y) / 2 : 0;
|
|
181
188
|
const mvz = bounds ? (bounds.min.z + bounds.max.z) / 2 : 0;
|
|
182
189
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const bottomOffset = mvy - minY; // already in metres
|
|
187
|
-
placementHeight = terrainH + bottomOffset;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
+
// bridge.modelOrigin.height is the placement altitude — Effect 2 already
|
|
191
|
+
// baked terrain clamping (when applicable) into it before constructing the
|
|
192
|
+
// bridge, so there's no per-frame clamp adjustment to make here.
|
|
190
193
|
const origin = Cesium.Cartesian3.fromDegrees(
|
|
191
|
-
bridge.modelOrigin.longitude, bridge.modelOrigin.latitude,
|
|
194
|
+
bridge.modelOrigin.longitude, bridge.modelOrigin.latitude, bridge.modelOrigin.height,
|
|
192
195
|
);
|
|
193
196
|
const enuToEcef = Cesium.Transforms.eastNorthUpToFixedFrame(origin);
|
|
194
197
|
// No lengthUnitScale here — viewer-space GLB vertices are already in metres.
|
|
@@ -212,6 +215,10 @@ export interface CesiumOverlayProps {
|
|
|
212
215
|
geometryResult?: GeometryResult | null;
|
|
213
216
|
/** IFC project length unit → metres (e.g. 0.001 for mm models). Default 1. */
|
|
214
217
|
lengthUnitScale?: number;
|
|
218
|
+
/** IfcBuildingStorey elevations (express id → metres, viewer-Y aligned).
|
|
219
|
+
* Used to clamp the model's ground-floor storey to terrain instead of
|
|
220
|
+
* the lowest geometry vertex (which can be a basement or foundation). */
|
|
221
|
+
storeyElevations?: Map<number, number>;
|
|
215
222
|
}
|
|
216
223
|
|
|
217
224
|
export function CesiumOverlay({
|
|
@@ -220,6 +227,7 @@ export function CesiumOverlay({
|
|
|
220
227
|
coordinateInfo,
|
|
221
228
|
geometryResult,
|
|
222
229
|
lengthUnitScale = 1,
|
|
230
|
+
storeyElevations,
|
|
223
231
|
}: CesiumOverlayProps) {
|
|
224
232
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
225
233
|
const viewerRef = useRef<InstanceType<typeof import('cesium').Viewer> | null>(null);
|
|
@@ -235,25 +243,23 @@ export function CesiumOverlay({
|
|
|
235
243
|
const ionToken = useViewerStore((s) => s.cesiumIonToken);
|
|
236
244
|
const terrainEnabled = useViewerStore((s) => s.cesiumTerrainEnabled);
|
|
237
245
|
const terrainClamp = useViewerStore((s) => s.cesiumTerrainClamp);
|
|
238
|
-
const setCesiumTerrainClamp = useViewerStore((s) => s.setCesiumTerrainClamp);
|
|
239
246
|
const terrainHeight = useViewerStore((s) => s.cesiumTerrainHeight);
|
|
240
247
|
const setCesiumTerrainHeight = useViewerStore((s) => s.setCesiumTerrainHeight);
|
|
241
248
|
const setCesiumTerrainClipY = useViewerStore((s) => s.setCesiumTerrainClipY);
|
|
242
249
|
const setCesiumGlbLoaded = useViewerStore((s) => s.setCesiumGlbLoaded);
|
|
243
250
|
|
|
244
|
-
// Use refs so the camera sync loop always reads the latest values
|
|
245
|
-
const terrainClampRef = useRef(terrainClamp);
|
|
246
|
-
const terrainHeightRef = useRef(terrainHeight);
|
|
247
|
-
terrainClampRef.current = terrainClamp;
|
|
248
|
-
terrainHeightRef.current = terrainHeight;
|
|
249
|
-
|
|
250
|
-
// Track whether we've auto-clamped to terrain (only once, so user can still uncheck)
|
|
251
|
-
const autoClampedRef = useRef(false);
|
|
252
|
-
|
|
253
251
|
// Track the Cesium model (IFC geometry loaded as glTF for correct world positioning)
|
|
254
252
|
const cesiumModelRef = useRef<{ modelMatrix: any; destroy?: () => void } | null>(null);
|
|
255
253
|
const glbCacheRef = useRef<{ meshCount: number; glb: Uint8Array } | null>(null);
|
|
256
254
|
|
|
255
|
+
// Last-known placement altitude (in metres) used to keep the user's WORLD
|
|
256
|
+
// camera position stable across bridge rebuilds. When the user toggles the
|
|
257
|
+
// clamp or edits OrthogonalHeight, the model placement changes and the
|
|
258
|
+
// entire viewer→ECEF frame translates with it; we offset the IFC viewer's
|
|
259
|
+
// camera Y by the inverse so the user perceives the model moving instead
|
|
260
|
+
// of the camera being dragged along.
|
|
261
|
+
const prevPlacementRef = useRef<number | null>(null);
|
|
262
|
+
|
|
257
263
|
// ─── Effect 1: Create/destroy the Cesium viewer (heavy, rare) ───────────
|
|
258
264
|
// Only depends on cesiumEnabled, ionToken, terrainEnabled, dataSource.
|
|
259
265
|
// NOT on mapConversion/projectedCRS — those are handled by Effect 2.
|
|
@@ -296,10 +302,14 @@ export function CesiumOverlay({
|
|
|
296
302
|
|
|
297
303
|
if (cancelled) { viewer.destroy(); return; }
|
|
298
304
|
|
|
299
|
-
// Disable Cesium's user input — the IFC viewer
|
|
300
|
-
//
|
|
305
|
+
// Disable Cesium's user input — the IFC viewer drives the camera,
|
|
306
|
+
// and any input Cesium intercepts (even a stray wheel/touch event
|
|
307
|
+
// past pointer-events:none) interferes with our orbit/zoom and
|
|
308
|
+
// produces "stuck to terrain" symptoms. enableInputs is the
|
|
309
|
+
// master kill-switch; the per-mode flags below are belt-and-braces.
|
|
301
310
|
const scene = viewer.scene;
|
|
302
311
|
const sscc = scene.screenSpaceCameraController;
|
|
312
|
+
sscc.enableInputs = false;
|
|
303
313
|
sscc.enableRotate = false;
|
|
304
314
|
sscc.enableTranslate = false;
|
|
305
315
|
sscc.enableZoom = false;
|
|
@@ -386,122 +396,150 @@ export function CesiumOverlay({
|
|
|
386
396
|
};
|
|
387
397
|
}, [cesiumEnabled, ionToken, terrainEnabled, dataSource]);
|
|
388
398
|
|
|
389
|
-
// ─── Effect 2:
|
|
390
|
-
//
|
|
391
|
-
//
|
|
399
|
+
// ─── Effect 2: Build the coordinate bridge with terrain-aware placement ─
|
|
400
|
+
// Precomputes the model placement (terrain-clamped if applicable) BEFORE
|
|
401
|
+
// building the bridge that the GLB and camera will share. This way the
|
|
402
|
+
// model loads into Cesium at its final altitude — no post-load shifting,
|
|
403
|
+
// no camera/model frame divergence, no compensation gymnastics.
|
|
404
|
+
//
|
|
405
|
+
// Sequence:
|
|
406
|
+
// 1. Build a tentative bridge to recover the model's WGS84 lat/lon.
|
|
407
|
+
// 2. Query terrain at that lat/lon (sync first, async with retry next).
|
|
408
|
+
// 3. Decide whether to clamp (user toggle OR model authored below terrain).
|
|
409
|
+
// 4. Rebuild the bridge with placementHeight baked into its enuToEcef
|
|
410
|
+
// origin so model matrix and camera frame share a single altitude.
|
|
411
|
+
// 5. Push terrain-derived state (height, clip Y, clamp toggle) and
|
|
412
|
+
// install the bridge.
|
|
392
413
|
useEffect(() => {
|
|
393
414
|
if (status !== 'ready' || !mapConversion || !projectedCRS) {
|
|
394
415
|
bridgeRef.current = null;
|
|
416
|
+
prevPlacementRef.current = null;
|
|
395
417
|
return;
|
|
396
418
|
}
|
|
397
419
|
|
|
398
420
|
let cancelled = false;
|
|
399
421
|
|
|
400
422
|
(async () => {
|
|
401
|
-
const
|
|
402
|
-
|
|
423
|
+
const Cesium = cesiumModule;
|
|
424
|
+
const viewer = viewerRef.current;
|
|
425
|
+
if (!Cesium || !viewer) return;
|
|
403
426
|
|
|
404
|
-
|
|
427
|
+
const tentative = await createCesiumBridge(
|
|
428
|
+
mapConversion, projectedCRS, coordinateInfo, lengthUnitScale,
|
|
429
|
+
);
|
|
430
|
+
if (cancelled) return;
|
|
431
|
+
if (!tentative) {
|
|
405
432
|
bridgeRef.current = null;
|
|
406
433
|
return;
|
|
407
434
|
}
|
|
408
435
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
//
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
if (
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
436
|
+
// Query terrain at the model's location. queryTerrainHeight tries
|
|
437
|
+
// Cesium's sync sources first (globe.getHeight, scene.sampleHeight),
|
|
438
|
+
// then Open-Meteo as a fast network fallback that works even with
|
|
439
|
+
// Google Photorealistic 3D Tiles (where there's no Cesium terrain
|
|
440
|
+
// provider for getHeight to read). Cached per-session.
|
|
441
|
+
const t0 = performance.now();
|
|
442
|
+
let terrainH: number | null = null;
|
|
443
|
+
try { terrainH = await tentative.queryTerrainHeight(Cesium, viewer); }
|
|
444
|
+
catch (err) { console.warn('[CesiumOverlay] terrain query failed:', err); }
|
|
445
|
+
if (cancelled) return;
|
|
446
|
+
const terrainMs = performance.now() - t0;
|
|
447
|
+
|
|
448
|
+
const bounds = coordinateInfo?.originalBounds;
|
|
449
|
+
const mvy = bounds ? (bounds.min.y + bounds.max.y) / 2 : 0;
|
|
450
|
+
const minY = bounds?.min.y ?? 0;
|
|
451
|
+
// Clamp anchor: viewer-Y of the storey nearest elevation 0 (typical
|
|
452
|
+
// ground floor), falling back to bounds.min.y. Without this, basements
|
|
453
|
+
// and foundations drag the model deep below terrain.
|
|
454
|
+
const clampAnchorY = findClampAnchorY(bounds, storeyElevations);
|
|
455
|
+
const anchorOffset = mvy - clampAnchorY;
|
|
456
|
+
const ifcOHeight = tentative.modelOrigin.height;
|
|
457
|
+
// Clamp is purely the user's choice. We do NOT auto-clamp on top of
|
|
458
|
+
// the user's setting — that would reactivate the toggle the moment
|
|
459
|
+
// the user disables it (since terrain is almost always above sea-level
|
|
460
|
+
// OrthogonalHeights, the auto condition would re-fire forever and the
|
|
461
|
+
// checkbox becomes un-uncheckable).
|
|
462
|
+
const placementHeight =
|
|
463
|
+
terrainClamp && terrainH !== null
|
|
464
|
+
? terrainH + anchorOffset
|
|
465
|
+
: ifcOHeight;
|
|
466
|
+
|
|
467
|
+
console.debug(
|
|
468
|
+
`[CesiumOverlay] placement decision: terrain=${terrainH?.toFixed(2) ?? 'null'}m`
|
|
469
|
+
+ ` ifcOHeight=${ifcOHeight.toFixed(2)}m anchorY=${clampAnchorY.toFixed(2)}m`
|
|
470
|
+
+ ` (minY=${minY.toFixed(2)}m, ${storeyElevations?.size ?? 0} storeys)`
|
|
471
|
+
+ ` clamp=${terrainClamp} placement=${placementHeight.toFixed(2)}m`
|
|
472
|
+
+ ` (terrain query: ${terrainMs.toFixed(0)}ms)`
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
// Build the final bridge with the placement baked in (or reuse the
|
|
476
|
+
// tentative one when the placement matches its IFC-derived origin).
|
|
477
|
+
let bridge = tentative;
|
|
478
|
+
if (Math.abs(placementHeight - ifcOHeight) > 1e-6) {
|
|
479
|
+
const final = await createCesiumBridge(
|
|
480
|
+
mapConversion, projectedCRS, coordinateInfo, lengthUnitScale,
|
|
481
|
+
placementHeight,
|
|
424
482
|
);
|
|
425
|
-
|
|
426
|
-
if (
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
// can return undefined. Use flyTo which handles terrain automatically,
|
|
430
|
-
// targeting a safe altitude above the model origin.
|
|
431
|
-
const safeHeight = Math.max(modelOrigin.height, 100);
|
|
432
|
-
viewer.camera.flyTo({
|
|
433
|
-
destination: Cesium.Cartesian3.fromDegrees(
|
|
434
|
-
modelOrigin.longitude, modelOrigin.latitude, safeHeight + 500,
|
|
435
|
-
),
|
|
436
|
-
orientation: {
|
|
437
|
-
heading: 0,
|
|
438
|
-
pitch: Cesium.Math.toRadians(-45),
|
|
439
|
-
roll: 0,
|
|
440
|
-
},
|
|
441
|
-
duration: 0, // instant
|
|
442
|
-
});
|
|
443
|
-
} else if (prevBridge) {
|
|
444
|
-
// Georef edit: just re-render, the camera sync loop will pick
|
|
445
|
-
// up the new bridge on the next frame. No dramatic fly animation.
|
|
446
|
-
viewer.scene.requestRender();
|
|
483
|
+
if (cancelled) return;
|
|
484
|
+
if (!final) {
|
|
485
|
+
bridgeRef.current = null;
|
|
486
|
+
return;
|
|
447
487
|
}
|
|
488
|
+
bridge = final;
|
|
448
489
|
}
|
|
449
|
-
})();
|
|
450
|
-
|
|
451
|
-
return () => { cancelled = true; };
|
|
452
|
-
}, [status, mapConversion, projectedCRS, coordinateInfo, lengthUnitScale]);
|
|
453
|
-
|
|
454
|
-
// ─── Effect 2b: Query terrain height when bridge is ready ───────────────
|
|
455
|
-
// Also re-queries when terrainClamp is toggled on (in case first query failed)
|
|
456
|
-
useEffect(() => {
|
|
457
|
-
if (status !== 'ready') return;
|
|
458
|
-
const bridge = bridgeRef.current;
|
|
459
|
-
const viewer = viewerRef.current;
|
|
460
|
-
const Cesium = cesiumModule;
|
|
461
|
-
if (!bridge || !viewer || !Cesium) return;
|
|
462
490
|
|
|
463
|
-
|
|
491
|
+
if (terrainH !== null) {
|
|
492
|
+
setCesiumTerrainHeight(terrainH);
|
|
493
|
+
// terrainClipY stays in viewer-space; it represents the world terrain
|
|
494
|
+
// altitude expressed in the bridge's frame so a clip plane at that Y
|
|
495
|
+
// matches the terrain surface. Use the clamp anchor (ground floor)
|
|
496
|
+
// rather than minY so the clip plane matches the user's ground level
|
|
497
|
+
// rather than the basement floor.
|
|
498
|
+
const terrainClipY = clampAnchorY + (terrainH - ifcOHeight);
|
|
499
|
+
setCesiumTerrainClipY(terrainClipY);
|
|
500
|
+
} else {
|
|
501
|
+
// Failed re-query (offline, API down) — clear stale store fields so
|
|
502
|
+
// the clip plane doesn't drift relative to the new bridge.
|
|
503
|
+
setCesiumTerrainHeight(null);
|
|
504
|
+
setCesiumTerrainClipY(null);
|
|
505
|
+
}
|
|
464
506
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
destination: Cesium.Cartesian3.fromDegrees(
|
|
488
|
-
bridge.modelOrigin.longitude, bridge.modelOrigin.latitude, h + 500,
|
|
489
|
-
),
|
|
490
|
-
orientation: { heading: 0, pitch: Cesium.Math.toRadians(-45), roll: 0 },
|
|
491
|
-
duration: 0,
|
|
492
|
-
});
|
|
507
|
+
// World-camera stability: when this rebuild changes the placement
|
|
508
|
+
// altitude (clamp toggled, OrthogonalHeight edited), shift the IFC
|
|
509
|
+
// viewer-space camera Y by the inverse delta so the user's WORLD
|
|
510
|
+
// camera ECEF position stays put. Without this, the entire frame
|
|
511
|
+
// translates with the model and edits feel like the camera is
|
|
512
|
+
// moving instead of the model — exactly what the user reported.
|
|
513
|
+
const prevPlacement = prevPlacementRef.current;
|
|
514
|
+
prevPlacementRef.current = placementHeight;
|
|
515
|
+
if (prevPlacement !== null) {
|
|
516
|
+
const dh = placementHeight - prevPlacement;
|
|
517
|
+
// 5 cm threshold — rejects float jitter from cached terrain reads
|
|
518
|
+
// re-flowing through the same effect, while a real placement edit
|
|
519
|
+
// (clamp toggle, OrthogonalHeight change) is always far larger.
|
|
520
|
+
if (Math.abs(dh) > 0.05) {
|
|
521
|
+
const renderer = getGlobalRenderer();
|
|
522
|
+
if (renderer) {
|
|
523
|
+
const cam = renderer.getCamera();
|
|
524
|
+
const pos = cam.getPosition();
|
|
525
|
+
cam.setPosition(pos.x, pos.y - dh, pos.z);
|
|
526
|
+
console.debug(
|
|
527
|
+
`[CesiumOverlay] placement Δh=${dh.toFixed(2)}m → shifted IFC camera Y by ${(-dh).toFixed(2)}m to hold world camera`,
|
|
528
|
+
);
|
|
493
529
|
}
|
|
494
530
|
}
|
|
495
|
-
}
|
|
496
|
-
};
|
|
531
|
+
}
|
|
497
532
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
const retryTimer = setTimeout(doQuery, 5000);
|
|
533
|
+
bridgeRef.current = bridge;
|
|
534
|
+
setBridgeVersion((v) => v + 1);
|
|
535
|
+
})();
|
|
502
536
|
|
|
503
|
-
return () => { cancelled = true;
|
|
504
|
-
|
|
537
|
+
return () => { cancelled = true; };
|
|
538
|
+
// terrainEnabled and ionToken intentionally omitted — Effect 1 already
|
|
539
|
+
// owns those (it destroys/recreates the viewer when they change), and
|
|
540
|
+
// listing them here would cause a redundant bridge rebuild while the
|
|
541
|
+
// viewer is being torn down.
|
|
542
|
+
}, [status, mapConversion, projectedCRS, coordinateInfo, lengthUnitScale, terrainClamp, storeyElevations]);
|
|
505
543
|
|
|
506
544
|
// ─── Effect 2c: Load GLB into Cesium (only when geometry changes) ───────
|
|
507
545
|
// This is the heavy operation — only re-runs when geometry actually changes.
|
|
@@ -545,7 +583,7 @@ export function CesiumOverlay({
|
|
|
545
583
|
if (cancelled) return;
|
|
546
584
|
|
|
547
585
|
// Build initial model matrix
|
|
548
|
-
const modelMatrix = buildModelMatrix(Cesium, bridge, mapConversion,
|
|
586
|
+
const modelMatrix = buildModelMatrix(Cesium, bridge, mapConversion, projectedCRS, coordinateInfo, lengthUnitScale);
|
|
549
587
|
|
|
550
588
|
const blob = new Blob([glbBytes as BlobPart], { type: 'model/gltf-binary' });
|
|
551
589
|
const glbUrl = URL.createObjectURL(blob);
|
|
@@ -601,10 +639,15 @@ export function CesiumOverlay({
|
|
|
601
639
|
const Cesium = cesiumModule;
|
|
602
640
|
if (!model || !bridge || !viewer || !Cesium) return;
|
|
603
641
|
|
|
604
|
-
const newMatrix = buildModelMatrix(Cesium, bridge, mapConversion,
|
|
642
|
+
const newMatrix = buildModelMatrix(Cesium, bridge, mapConversion, projectedCRS, coordinateInfo, lengthUnitScale);
|
|
605
643
|
model.modelMatrix = newMatrix;
|
|
606
644
|
viewer.scene.requestRender();
|
|
607
|
-
|
|
645
|
+
// Depend on bridgeVersion so the matrix is rebuilt with the *new* bridge
|
|
646
|
+
// after async createCesiumBridge replaces it. Placement (terrain clamp)
|
|
647
|
+
// is now baked into bridge.modelOrigin.height by Effect 2, so terrain
|
|
648
|
+
// clamp/height changes drive a bridge rebuild instead of a per-frame
|
|
649
|
+
// matrix recomputation here.
|
|
650
|
+
}, [mapConversion, projectedCRS, coordinateInfo, lengthUnitScale, bridgeVersion]);
|
|
608
651
|
|
|
609
652
|
// ─── Effect 3: Camera sync loop ─────────────────────────────────────────
|
|
610
653
|
useEffect(() => {
|
|
@@ -632,7 +675,9 @@ export function CesiumOverlay({
|
|
|
632
675
|
const camUp = camera.getUp();
|
|
633
676
|
const fov = camera.getFOV();
|
|
634
677
|
|
|
635
|
-
//
|
|
678
|
+
// bridge.modelOrigin.height already has the placement baked in (terrain
|
|
679
|
+
// clamp resolved at bridge creation by Effect 2), so the camera frame
|
|
680
|
+
// and the model matrix share the same enuToEcef origin altitude.
|
|
636
681
|
bridge.syncCamera(Cesium, viewer, camPos, camTarget, camUp, fov);
|
|
637
682
|
|
|
638
683
|
rafRef.current = requestAnimationFrame(syncCamera);
|