@ifc-lite/viewer 1.17.4 → 1.17.6
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 +16 -16
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +117 -0
- package/DESKTOP_CONTRACT_VERSION +1 -1
- package/dist/assets/{basketViewActivator-BmnNtVfZ.js → basketViewActivator-86rgogji.js} +1 -1
- package/dist/assets/drawing-2d-DoxKMqbO.js +257 -0
- package/dist/assets/{exporters-ChAtBmlj.js → exporters-CcPS9MK5.js} +2274 -2227
- package/dist/assets/{geometry.worker-BQ0rzNo-.js → geometry.worker-BFUYA08u.js} +1 -1
- package/dist/assets/ids-DQ5jY0E8.js +1 -0
- package/dist/assets/ifc-lite_bg-BINvzoCP.wasm +0 -0
- package/dist/assets/{index-Co8E2-FE.js → index-Bfms9I4A.js} +35160 -33084
- package/dist/assets/index-_bfZsDCC.css +1 -0
- package/dist/assets/{native-bridge-BRvbckFQ.js → native-bridge-DUyLCMZS.js} +104 -104
- package/dist/assets/{sandbox-DZiNLNMk.js → sandbox-C8575tul.js} +4340 -4322
- package/dist/assets/{server-client-BV8zHZ7Y.js → server-client-BuZK7OST.js} +1 -1
- package/dist/assets/{wasm-bridge-g01g7T9b.js → wasm-bridge-JsqEGDV8.js} +1 -1
- package/dist/index.html +8 -7
- package/index.html +1 -0
- package/package.json +7 -7
- package/src/App.tsx +16 -2
- package/src/components/viewer/CesiumOverlay.tsx +62 -19
- package/src/components/viewer/ChatPanel.tsx +195 -91
- package/src/components/viewer/MainToolbar.tsx +4 -3
- package/src/components/viewer/PropertiesPanel.tsx +16 -2
- package/src/components/viewer/SettingsPage.tsx +252 -101
- package/src/components/viewer/ThemeSwitch.tsx +63 -7
- package/src/components/viewer/ViewerLayout.tsx +1 -0
- package/src/components/viewer/Viewport.tsx +14 -2
- package/src/components/viewer/ViewportContainer.tsx +49 -64
- package/src/components/viewer/ViewportOverlays.tsx +5 -2
- package/src/components/viewer/bcf/BCFTopicDetail.tsx +4 -4
- package/src/components/viewer/chat/ModelSelector.tsx +90 -54
- package/src/components/viewer/properties/GeoreferencingPanel.tsx +113 -51
- package/src/components/viewer/properties/LocationMap.tsx +9 -7
- package/src/components/viewer/properties/ModelMetadataPanel.tsx +1 -1
- package/src/components/viewer/tools/SectionCapControls.tsx +237 -0
- package/src/components/viewer/tools/SectionPanel.tsx +39 -18
- package/src/components/viewer/useAnimationLoop.ts +9 -1
- package/src/components/viewer/useRenderUpdates.ts +1 -1
- package/src/hooks/ids/idsDataAccessor.ts +60 -24
- package/src/hooks/ingest/viewerModelIngest.ts +7 -2
- package/src/hooks/useIfcFederation.ts +326 -71
- package/src/hooks/useIfcLoader.ts +1 -0
- package/src/hooks/useViewControls.ts +13 -5
- package/src/index.css +484 -10
- package/src/lib/desktop-entitlement.ts +2 -4
- package/src/lib/geo/cesium-bridge.ts +15 -7
- package/src/lib/geo/effective-georef.test.ts +73 -0
- package/src/lib/geo/effective-georef.ts +111 -0
- package/src/lib/geo/reproject.ts +105 -19
- package/src/lib/llm/byok-guard.test.ts +77 -0
- package/src/lib/llm/byok-guard.ts +39 -0
- package/src/lib/llm/free-models.test.ts +0 -6
- package/src/lib/llm/models.ts +104 -42
- package/src/lib/llm/stream-client.ts +74 -110
- package/src/lib/llm/stream-direct.test.ts +130 -0
- package/src/lib/llm/stream-direct.ts +316 -0
- package/src/lib/llm/types.ts +14 -2
- package/src/main.tsx +1 -10
- package/src/services/api-keys.ts +73 -0
- package/src/store/constants.ts +20 -2
- package/src/store/index.ts +12 -5
- package/src/store/slices/cesiumSlice.ts +5 -0
- package/src/store/slices/chatSlice.test.ts +6 -76
- package/src/store/slices/chatSlice.ts +17 -58
- package/src/store/slices/sectionSlice.test.ts +87 -7
- package/src/store/slices/sectionSlice.ts +151 -5
- package/src/store/slices/uiSlice.ts +28 -5
- package/src/store/types.ts +26 -0
- package/src/utils/nativeSpatialDataStore.ts +4 -1
- package/src/utils/viewportUtils.ts +7 -2
- package/src/vite-env.d.ts +0 -4
- package/dist/assets/drawing-2d-gWfpdfYe.js +0 -257
- package/dist/assets/ids-B4jTqB1O.js +0 -1
- package/dist/assets/ifc-lite_bg-BX4E7TX8.wasm +0 -0
- package/dist/assets/index-DckuDqlv.css +0 -1
- package/src/components/viewer/UpgradePage.tsx +0 -71
- package/src/lib/desktop/ClerkDesktopEntitlementSync.tsx +0 -175
- package/src/lib/llm/ClerkChatSync.tsx +0 -74
- package/src/lib/llm/clerk-auth.ts +0 -62
|
@@ -1 +1 @@
|
|
|
1
|
-
import{I as f,a as m}from"./exporters-
|
|
1
|
+
import{I as f,a as m}from"./exporters-CcPS9MK5.js";import"./bcf-DOG9_WPX.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};
|
package/dist/index.html
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
var saved = localStorage.getItem('ifc-lite-theme');
|
|
12
12
|
var theme = saved || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
|
13
13
|
if (theme === 'dark') document.documentElement.classList.add('dark');
|
|
14
|
+
if (theme === 'colorful') document.documentElement.classList.add('colorful');
|
|
14
15
|
})();
|
|
15
16
|
</script>
|
|
16
17
|
|
|
@@ -49,19 +50,19 @@
|
|
|
49
50
|
<meta name="theme-color" content="#7aa2f7">
|
|
50
51
|
<meta name="msapplication-TileColor" content="#1a1b26">
|
|
51
52
|
<meta name="msapplication-TileImage" content="/favicon-192x192-cropped.png">
|
|
52
|
-
<script type="module" crossorigin src="/assets/index-
|
|
53
|
+
<script type="module" crossorigin src="/assets/index-Bfms9I4A.js"></script>
|
|
53
54
|
<link rel="modulepreload" crossorigin href="/assets/arrow-CZ5kQ26f.js">
|
|
54
55
|
<link rel="modulepreload" crossorigin href="/assets/cesium-DUOzBlqv.js">
|
|
55
56
|
<link rel="modulepreload" crossorigin href="/assets/zip-DBEtpeu6.js">
|
|
56
57
|
<link rel="modulepreload" crossorigin href="/assets/bcf-DOG9_WPX.js">
|
|
57
|
-
<link rel="modulepreload" crossorigin href="/assets/exporters-
|
|
58
|
-
<link rel="modulepreload" crossorigin href="/assets/drawing-2d-
|
|
58
|
+
<link rel="modulepreload" crossorigin href="/assets/exporters-CcPS9MK5.js">
|
|
59
|
+
<link rel="modulepreload" crossorigin href="/assets/drawing-2d-DoxKMqbO.js">
|
|
59
60
|
<link rel="modulepreload" crossorigin href="/assets/lens-CSASnhAL.js">
|
|
60
|
-
<link rel="modulepreload" crossorigin href="/assets/sandbox-
|
|
61
|
-
<link rel="modulepreload" crossorigin href="/assets/server-client-
|
|
62
|
-
<link rel="modulepreload" crossorigin href="/assets/ids-
|
|
61
|
+
<link rel="modulepreload" crossorigin href="/assets/sandbox-C8575tul.js">
|
|
62
|
+
<link rel="modulepreload" crossorigin href="/assets/server-client-BuZK7OST.js">
|
|
63
|
+
<link rel="modulepreload" crossorigin href="/assets/ids-DQ5jY0E8.js">
|
|
63
64
|
<link rel="stylesheet" crossorigin href="/assets/cesium-ADbP7waU.css">
|
|
64
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
65
|
+
<link rel="stylesheet" crossorigin href="/assets/index-_bfZsDCC.css">
|
|
65
66
|
</head>
|
|
66
67
|
<body>
|
|
67
68
|
<div id="root"></div>
|
package/index.html
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
var saved = localStorage.getItem('ifc-lite-theme');
|
|
12
12
|
var theme = saved || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
|
13
13
|
if (theme === 'dark') document.documentElement.classList.add('dark');
|
|
14
|
+
if (theme === 'colorful') document.documentElement.classList.add('colorful');
|
|
14
15
|
})();
|
|
15
16
|
</script>
|
|
16
17
|
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ifc-lite/viewer",
|
|
3
|
-
"version": "1.17.
|
|
3
|
+
"version": "1.17.6",
|
|
4
4
|
"description": "IFC-Lite viewer application",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"dependencies": {
|
|
7
|
-
"@
|
|
7
|
+
"@anthropic-ai/sdk": "^0.39.0",
|
|
8
8
|
"@codemirror/autocomplete": "^6.20.0",
|
|
9
9
|
"@codemirror/commands": "^6.10.2",
|
|
10
10
|
"@codemirror/lang-javascript": "^6.2.4",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"@ifc-lite/bcf": "^1.15.3",
|
|
50
50
|
"@ifc-lite/cache": "^1.14.5",
|
|
51
51
|
"@ifc-lite/data": "^1.15.2",
|
|
52
|
-
"@ifc-lite/drawing-2d": "^1.15.
|
|
52
|
+
"@ifc-lite/drawing-2d": "^1.15.3",
|
|
53
53
|
"@ifc-lite/encoding": "^1.14.6",
|
|
54
54
|
"@ifc-lite/export": "^1.17.2",
|
|
55
55
|
"@ifc-lite/geometry": "^1.16.5",
|
|
@@ -57,14 +57,14 @@
|
|
|
57
57
|
"@ifc-lite/lens": "^1.14.4",
|
|
58
58
|
"@ifc-lite/lists": "^1.14.10",
|
|
59
59
|
"@ifc-lite/mutations": "^1.14.5",
|
|
60
|
-
"@ifc-lite/parser": "^2.1.
|
|
60
|
+
"@ifc-lite/parser": "^2.1.9",
|
|
61
61
|
"@ifc-lite/query": "^1.14.6",
|
|
62
|
-
"@ifc-lite/renderer": "^1.
|
|
62
|
+
"@ifc-lite/renderer": "^1.16.0",
|
|
63
63
|
"@ifc-lite/sandbox": "^1.14.6",
|
|
64
64
|
"@ifc-lite/sdk": "^1.14.6",
|
|
65
|
-
"@ifc-lite/server-client": "^1.15.
|
|
65
|
+
"@ifc-lite/server-client": "^1.15.3",
|
|
66
66
|
"@ifc-lite/spatial": "^1.14.5",
|
|
67
|
-
"@ifc-lite/wasm": "^1.16.
|
|
67
|
+
"@ifc-lite/wasm": "^1.16.6"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
70
|
"@tailwindcss/postcss": "^4.1.18",
|
package/src/App.tsx
CHANGED
|
@@ -3,17 +3,31 @@
|
|
|
3
3
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Main application component
|
|
6
|
+
* Main application component.
|
|
7
|
+
*
|
|
8
|
+
* The /settings route is used by the desktop shell (Tauri) for account
|
|
9
|
+
* and API key management. On the web viewer it is unreachable (no links
|
|
10
|
+
* navigate there and vercel.json does not rewrite it).
|
|
7
11
|
*/
|
|
8
12
|
|
|
9
13
|
import { ViewerLayout } from './components/viewer/ViewerLayout';
|
|
14
|
+
import { SettingsPage } from './components/viewer/SettingsPage';
|
|
10
15
|
import { BimProvider } from './sdk/BimProvider';
|
|
11
16
|
import { Toaster } from './components/ui/toast';
|
|
17
|
+
import { useEffect, useState } from 'react';
|
|
12
18
|
|
|
13
19
|
export function App() {
|
|
20
|
+
const [pathname, setPathname] = useState(() => window.location.pathname);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const onRouteChange = () => setPathname(window.location.pathname);
|
|
24
|
+
window.addEventListener('popstate', onRouteChange);
|
|
25
|
+
return () => window.removeEventListener('popstate', onRouteChange);
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
14
28
|
return (
|
|
15
29
|
<BimProvider>
|
|
16
|
-
<ViewerLayout />
|
|
30
|
+
{pathname === '/settings' ? <SettingsPage /> : <ViewerLayout />}
|
|
17
31
|
<Toaster />
|
|
18
32
|
</BimProvider>
|
|
19
33
|
);
|
|
@@ -175,6 +175,7 @@ function buildModelMatrix(
|
|
|
175
175
|
const absc = mapConversion?.xAxisAbscissa ?? 1.0;
|
|
176
176
|
const ordi = mapConversion?.xAxisOrdinate ?? 0.0;
|
|
177
177
|
const bounds = coordinateInfo?.originalBounds;
|
|
178
|
+
// Viewer bounds are already in metres (geometry engine converts from IFC native unit)
|
|
178
179
|
const mvx = bounds ? (bounds.min.x + bounds.max.x) / 2 : 0;
|
|
179
180
|
const mvy = bounds ? (bounds.min.y + bounds.max.y) / 2 : 0;
|
|
180
181
|
const mvz = bounds ? (bounds.min.z + bounds.max.z) / 2 : 0;
|
|
@@ -182,7 +183,7 @@ function buildModelMatrix(
|
|
|
182
183
|
let placementHeight = bridge.modelOrigin.height;
|
|
183
184
|
if (clamp && terrainH !== null) {
|
|
184
185
|
const minY = bounds?.min.y ?? 0;
|
|
185
|
-
const bottomOffset = mvy - minY;
|
|
186
|
+
const bottomOffset = mvy - minY; // already in metres
|
|
186
187
|
placementHeight = terrainH + bottomOffset;
|
|
187
188
|
}
|
|
188
189
|
|
|
@@ -190,12 +191,16 @@ function buildModelMatrix(
|
|
|
190
191
|
bridge.modelOrigin.longitude, bridge.modelOrigin.latitude, placementHeight,
|
|
191
192
|
);
|
|
192
193
|
const enuToEcef = Cesium.Transforms.eastNorthUpToFixedFrame(origin);
|
|
194
|
+
// No lengthUnitScale here — viewer-space GLB vertices are already in metres.
|
|
193
195
|
const sa = hScale * absc, so = hScale * ordi;
|
|
196
|
+
const tx = -(sa * mvx + so * mvz);
|
|
197
|
+
const ty = -(so * mvx - sa * mvz);
|
|
198
|
+
const tz = -mvy;
|
|
194
199
|
const ifcToEnu = new Cesium.Matrix4(
|
|
195
|
-
sa,
|
|
196
|
-
so,
|
|
197
|
-
0,
|
|
198
|
-
0,
|
|
200
|
+
sa, 0, so, tx,
|
|
201
|
+
so, 0, -sa, ty,
|
|
202
|
+
0, 1, 0, tz,
|
|
203
|
+
0, 0, 0, 1,
|
|
199
204
|
);
|
|
200
205
|
return Cesium.Matrix4.multiply(enuToEcef, ifcToEnu, new Cesium.Matrix4());
|
|
201
206
|
}
|
|
@@ -205,6 +210,8 @@ export interface CesiumOverlayProps {
|
|
|
205
210
|
projectedCRS?: ProjectedCRS;
|
|
206
211
|
coordinateInfo?: CoordinateInfo;
|
|
207
212
|
geometryResult?: GeometryResult | null;
|
|
213
|
+
/** IFC project length unit → metres (e.g. 0.001 for mm models). Default 1. */
|
|
214
|
+
lengthUnitScale?: number;
|
|
208
215
|
}
|
|
209
216
|
|
|
210
217
|
export function CesiumOverlay({
|
|
@@ -212,6 +219,7 @@ export function CesiumOverlay({
|
|
|
212
219
|
projectedCRS,
|
|
213
220
|
coordinateInfo,
|
|
214
221
|
geometryResult,
|
|
222
|
+
lengthUnitScale = 1,
|
|
215
223
|
}: CesiumOverlayProps) {
|
|
216
224
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
217
225
|
const viewerRef = useRef<InstanceType<typeof import('cesium').Viewer> | null>(null);
|
|
@@ -227,6 +235,7 @@ export function CesiumOverlay({
|
|
|
227
235
|
const ionToken = useViewerStore((s) => s.cesiumIonToken);
|
|
228
236
|
const terrainEnabled = useViewerStore((s) => s.cesiumTerrainEnabled);
|
|
229
237
|
const terrainClamp = useViewerStore((s) => s.cesiumTerrainClamp);
|
|
238
|
+
const setCesiumTerrainClamp = useViewerStore((s) => s.setCesiumTerrainClamp);
|
|
230
239
|
const terrainHeight = useViewerStore((s) => s.cesiumTerrainHeight);
|
|
231
240
|
const setCesiumTerrainHeight = useViewerStore((s) => s.setCesiumTerrainHeight);
|
|
232
241
|
const setCesiumTerrainClipY = useViewerStore((s) => s.setCesiumTerrainClipY);
|
|
@@ -238,6 +247,9 @@ export function CesiumOverlay({
|
|
|
238
247
|
terrainClampRef.current = terrainClamp;
|
|
239
248
|
terrainHeightRef.current = terrainHeight;
|
|
240
249
|
|
|
250
|
+
// Track whether we've auto-clamped to terrain (only once, so user can still uncheck)
|
|
251
|
+
const autoClampedRef = useRef(false);
|
|
252
|
+
|
|
241
253
|
// Track the Cesium model (IFC geometry loaded as glTF for correct world positioning)
|
|
242
254
|
const cesiumModelRef = useRef<{ modelMatrix: any; destroy?: () => void } | null>(null);
|
|
243
255
|
const glbCacheRef = useRef<{ meshCount: number; glb: Uint8Array } | null>(null);
|
|
@@ -386,7 +398,7 @@ export function CesiumOverlay({
|
|
|
386
398
|
let cancelled = false;
|
|
387
399
|
|
|
388
400
|
(async () => {
|
|
389
|
-
const bridge = await createCesiumBridge(mapConversion, projectedCRS, coordinateInfo);
|
|
401
|
+
const bridge = await createCesiumBridge(mapConversion, projectedCRS, coordinateInfo, lengthUnitScale);
|
|
390
402
|
if (cancelled) return;
|
|
391
403
|
|
|
392
404
|
if (!bridge) {
|
|
@@ -396,6 +408,7 @@ export function CesiumOverlay({
|
|
|
396
408
|
|
|
397
409
|
const prevBridge = bridgeRef.current;
|
|
398
410
|
bridgeRef.current = bridge;
|
|
411
|
+
autoClampedRef.current = false; // reset for new bridge
|
|
399
412
|
// Bump version so terrain query effect re-runs now that bridge is ready
|
|
400
413
|
setBridgeVersion((v) => v + 1);
|
|
401
414
|
|
|
@@ -411,11 +424,22 @@ export function CesiumOverlay({
|
|
|
411
424
|
);
|
|
412
425
|
|
|
413
426
|
if (isFirstPosition) {
|
|
414
|
-
// First time:
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
427
|
+
// First time: fly to the model location.
|
|
428
|
+
// On first load terrain tiles may not be ready, so globe.getHeight
|
|
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
|
+
});
|
|
419
443
|
} else if (prevBridge) {
|
|
420
444
|
// Georef edit: just re-render, the camera sync loop will pick
|
|
421
445
|
// up the new bridge on the next frame. No dramatic fly animation.
|
|
@@ -425,7 +449,7 @@ export function CesiumOverlay({
|
|
|
425
449
|
})();
|
|
426
450
|
|
|
427
451
|
return () => { cancelled = true; };
|
|
428
|
-
}, [status, mapConversion, projectedCRS, coordinateInfo]);
|
|
452
|
+
}, [status, mapConversion, projectedCRS, coordinateInfo, lengthUnitScale]);
|
|
429
453
|
|
|
430
454
|
// ─── Effect 2b: Query terrain height when bridge is ready ───────────────
|
|
431
455
|
// Also re-queries when terrainClamp is toggled on (in case first query failed)
|
|
@@ -448,13 +472,25 @@ export function CesiumOverlay({
|
|
|
448
472
|
bridge.queryTerrainHeight(Cesium, viewer).then((h) => {
|
|
449
473
|
if (!cancelled && h !== null) {
|
|
450
474
|
setCesiumTerrainHeight(h);
|
|
451
|
-
// Compute terrain clip Y in viewer space
|
|
452
|
-
//
|
|
453
|
-
// In viewer Y-up, the bottom of the model is at modelMinY.
|
|
454
|
-
// Terrain clip Y = modelMinY + (terrainHeight - modelOriginHeight)
|
|
455
|
-
// This places the clip plane at the terrain surface relative to model geometry.
|
|
475
|
+
// Compute terrain clip Y in viewer space (both h and modelOrigin.height are metres,
|
|
476
|
+
// bounds are also in metres since the geometry engine converts during extraction)
|
|
456
477
|
const terrainClipY = modelMinY + (h - bridge.modelOrigin.height);
|
|
457
478
|
setCesiumTerrainClipY(terrainClipY);
|
|
479
|
+
|
|
480
|
+
// Auto-enable terrain clamping if the model is significantly below terrain
|
|
481
|
+
// (only once — don't override if the user has manually toggled)
|
|
482
|
+
if (!autoClampedRef.current && h > bridge.modelOrigin.height + 5) {
|
|
483
|
+
autoClampedRef.current = true;
|
|
484
|
+
setCesiumTerrainClamp(true);
|
|
485
|
+
// Fly camera to the clamped position so the model is visible
|
|
486
|
+
viewer.camera.flyTo({
|
|
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
|
+
});
|
|
493
|
+
}
|
|
458
494
|
}
|
|
459
495
|
});
|
|
460
496
|
};
|
|
@@ -516,7 +552,14 @@ export function CesiumOverlay({
|
|
|
516
552
|
let model: { modelMatrix: any; destroy?: () => void } | null = null;
|
|
517
553
|
try {
|
|
518
554
|
model = await Cesium.Model.fromGltfAsync({
|
|
519
|
-
url: glbUrl,
|
|
555
|
+
url: glbUrl,
|
|
556
|
+
modelMatrix,
|
|
557
|
+
shadows: Cesium.ShadowMode.DISABLED,
|
|
558
|
+
// The generated GLB stores viewer-space vertices and buildModelMatrix
|
|
559
|
+
// already maps viewer axes into ENU. Avoid Cesium's default glTF
|
|
560
|
+
// Y-up/Z-forward correction or the model is rotated onto its side.
|
|
561
|
+
upAxis: Cesium.Axis.Z,
|
|
562
|
+
forwardAxis: Cesium.Axis.X,
|
|
520
563
|
});
|
|
521
564
|
} finally {
|
|
522
565
|
URL.revokeObjectURL(glbUrl);
|
|
@@ -561,7 +604,7 @@ export function CesiumOverlay({
|
|
|
561
604
|
const newMatrix = buildModelMatrix(Cesium, bridge, mapConversion, coordinateInfo, terrainClamp, terrainHeight);
|
|
562
605
|
model.modelMatrix = newMatrix;
|
|
563
606
|
viewer.scene.requestRender();
|
|
564
|
-
}, [terrainClamp, terrainHeight, mapConversion, coordinateInfo]);
|
|
607
|
+
}, [terrainClamp, terrainHeight, mapConversion, coordinateInfo, lengthUnitScale]);
|
|
565
608
|
|
|
566
609
|
// ─── Effect 3: Camera sync loop ─────────────────────────────────────────
|
|
567
610
|
useEffect(() => {
|