@ifc-lite/viewer 1.21.0 → 1.22.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 +57 -50
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +10 -0
- package/dist/assets/arrow-fie-E7fe.js +20 -0
- package/dist/assets/ascii-points-source-bTjLVmUX.js +1 -0
- package/dist/assets/{basketViewActivator-Bzw51jhm.js → basketViewActivator-EHAhHlwN.js} +12 -13
- package/dist/assets/bcf-Bhx-K17f.js +281 -0
- package/dist/assets/{browser-C5TFR7sH.js → browser-CVf8ATeW.js} +6 -6
- package/dist/assets/cesium-B4ZIU9jS.js +17742 -0
- package/dist/assets/decode-worker-CYqSjk1n.js +172 -0
- package/dist/assets/e57-source-CQHxE8n3.js +1 -0
- package/dist/assets/emscripten-module.browser-DcFZLAUx.js +1 -0
- package/dist/assets/exporters-KTio0Tdm.js +5723 -0
- package/dist/assets/geometry-controller.worker-Cm2P_EJr.js +7 -0
- package/dist/assets/geometry.worker-DchLBqZ8.js +1 -0
- package/dist/assets/{ids-B7AXEv7h.js → ids-CS7VCFin.js} +5 -5
- package/dist/assets/ifc-lite-C6wEhXa6.js +7 -0
- package/dist/assets/{ifc-lite_bg-DlKs5-yM.wasm → ifc-lite_bg-CSeT3fNI.wasm} +0 -0
- package/dist/assets/{ifc-lite_bg-PqmRe3Ph.wasm → ifc-lite_bg-ns4cSnX2.wasm} +0 -0
- package/dist/assets/{index-DVNSvEMh.js → index-8k9h-ANq.js} +60997 -59926
- package/dist/assets/index-BZC2YaOP.css +1 -0
- package/dist/assets/index-HqAIQkr6.js +22 -0
- package/dist/assets/inline-worker-BpBzlmd6.js +1 -0
- package/dist/assets/las-BW6LIc_j.js +1 -0
- package/dist/assets/las-source-C_IGrgRq.js +1 -0
- package/dist/assets/laz-source-jj3xI5Y4.js +125 -0
- package/dist/assets/maplibre-gl-C4LXKM6c.js +808 -0
- package/dist/assets/{native-bridge-BiD01jI9.js → native-bridge-DNrEhx2R.js} +5 -8
- package/dist/assets/{parser.worker-Bnbrl6gy.js → parser.worker-BcjkIo89.js} +2 -2
- package/dist/assets/pcd-source-Ck0UnVDn.js +3 -0
- package/dist/assets/ply-source-C8jjyzxE.js +4 -0
- package/dist/assets/{exporters-u0sz2Upj.js → sandbox-BSn5MyEJ.js} +11745 -7412
- package/dist/assets/{server-client-DP8fMPY9.js → server-client-D-kU2XAF.js} +4 -4
- package/dist/assets/{three-CDRZThFA.js → three-DwNDHx9-.js} +163 -171
- package/dist/assets/wasm-bridge-Cha08LdC.js +1 -0
- package/dist/assets/{workerHelpers-CBbWSJmd.js → workerHelpers-pUUnk9Wc.js} +1 -1
- package/dist/assets/zip-BJqVbRkU.js +2 -0
- package/dist/index.html +10 -12
- package/package.json +11 -11
- package/src/components/mcp/PlaygroundChat.tsx +90 -52
- package/src/components/viewer/CesiumOverlay.tsx +150 -91
- package/src/components/viewer/CesiumPlacementEditor.tsx +1009 -0
- package/src/components/viewer/ChatPanel.tsx +76 -93
- package/src/components/viewer/EntityContextMenu.tsx +68 -10
- package/src/components/viewer/MainToolbar.tsx +33 -3
- package/src/components/viewer/ViewportContainer.tsx +70 -16
- package/src/components/viewer/ViewportOverlays.tsx +2 -98
- package/src/components/viewer/chat/ByokKeyModal.tsx +338 -0
- package/src/components/viewer/chat/ByokStreamingPill.tsx +62 -0
- package/src/components/viewer/chat/ByokTrustDiagram.tsx +192 -0
- package/src/components/viewer/properties/GeoreferencingPanel.tsx +49 -52
- package/src/components/viewer/properties/ModelMetadataPanel.tsx +55 -44
- package/src/components/viewer/selectionHandlers.ts +7 -1
- package/src/lib/geo/cesium-bridge.ts +86 -50
- package/src/lib/geo/cesium-placement.test.ts +244 -0
- package/src/lib/geo/cesium-placement.ts +231 -0
- package/src/lib/geo/effective-georef.test.ts +74 -1
- package/src/lib/geo/effective-georef.ts +40 -93
- package/src/lib/geo/geo-scale.ts +104 -0
- package/src/lib/geo/reproject.test.ts +130 -0
- package/src/lib/geo/reproject.ts +37 -12
- package/src/lib/geo/terrain-elevation.ts +198 -89
- package/src/lib/lens/adapter.ts +52 -6
- package/src/lib/llm/clipboard-detect.test.ts +150 -0
- package/src/lib/llm/clipboard-detect.ts +90 -0
- package/src/lib/llm/models.ts +28 -0
- package/src/lib/llm/stream-direct.ts +16 -4
- package/src/lib/llm/types.ts +8 -0
- package/src/services/playground-model.ts +55 -0
- package/src/store/index.ts +4 -5
- package/src/store/slices/cesiumSlice.ts +100 -19
- package/src/store.ts +3 -0
- package/dist/assets/arrow-CZ5kQ26f.js +0 -20
- package/dist/assets/bcf-4K724hw0.js +0 -281
- package/dist/assets/cesium-DUOzBlqv.js +0 -17817
- package/dist/assets/decode-worker-t2EGKAxO.js +0 -1708
- package/dist/assets/emscripten-module.browser-CY5t0Vfq.js +0 -1
- package/dist/assets/geometry-controller.worker-NH8pZmrU.js +0 -7
- package/dist/assets/geometry.worker-Bp4rW_R1.js +0 -1
- package/dist/assets/ifc-lite-DfZHk36-.js +0 -7
- package/dist/assets/index-CSWgTe1s.css +0 -1
- package/dist/assets/index-XwKzDuw6.js +0 -22
- package/dist/assets/maplibre-gl-CGLcoNXc.js +0 -811
- package/dist/assets/sandbox-DPD1ROr0.js +0 -9700
- package/dist/assets/wasm-bridge-CErti6zX.js +0 -1
- package/dist/assets/zip-DBEtpeu6.js +0 -12
- package/src/components/viewer/CesiumSettingsDialog.tsx +0 -100
|
@@ -0,0 +1,192 @@
|
|
|
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
|
+
* Visual proof of where the API key (and chat content) goes when a BYOK model
|
|
7
|
+
* is in use. Two stacked paths:
|
|
8
|
+
* row 1 (active) browser ────► api.provider.com
|
|
9
|
+
* row 2 (blocked) browser ─► our server ─► api.provider.com (struck out)
|
|
10
|
+
*
|
|
11
|
+
* The shape of the diagram is the same for every provider — only the API host
|
|
12
|
+
* label rotates. Renders crisply in light and dark mode via Tailwind utility
|
|
13
|
+
* classes.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
interface ByokTrustDiagramProps {
|
|
17
|
+
apiHost: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function ByokTrustDiagram({ apiHost }: ByokTrustDiagramProps) {
|
|
21
|
+
return (
|
|
22
|
+
<svg
|
|
23
|
+
viewBox="0 0 520 200"
|
|
24
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
25
|
+
role="img"
|
|
26
|
+
aria-label={`Diagram: requests go directly from your browser to ${apiHost}, not via our server.`}
|
|
27
|
+
className="w-full h-auto"
|
|
28
|
+
>
|
|
29
|
+
<defs>
|
|
30
|
+
<marker
|
|
31
|
+
id="byok-arrow-active"
|
|
32
|
+
viewBox="0 0 10 10"
|
|
33
|
+
refX="9"
|
|
34
|
+
refY="5"
|
|
35
|
+
markerUnits="strokeWidth"
|
|
36
|
+
markerWidth="8"
|
|
37
|
+
markerHeight="8"
|
|
38
|
+
orient="auto"
|
|
39
|
+
>
|
|
40
|
+
<path d="M 0 0 L 10 5 L 0 10 z" className="fill-emerald-500" />
|
|
41
|
+
</marker>
|
|
42
|
+
<marker
|
|
43
|
+
id="byok-arrow-blocked"
|
|
44
|
+
viewBox="0 0 10 10"
|
|
45
|
+
refX="9"
|
|
46
|
+
refY="5"
|
|
47
|
+
markerUnits="strokeWidth"
|
|
48
|
+
markerWidth="8"
|
|
49
|
+
markerHeight="8"
|
|
50
|
+
orient="auto"
|
|
51
|
+
>
|
|
52
|
+
<path d="M 0 0 L 10 5 L 0 10 z" className="fill-muted-foreground" />
|
|
53
|
+
</marker>
|
|
54
|
+
</defs>
|
|
55
|
+
|
|
56
|
+
{/* Row 1 — the path that's actually used */}
|
|
57
|
+
<g transform="translate(0, 18)">
|
|
58
|
+
<text x="0" y="-4" className="text-[10px] uppercase tracking-wider fill-emerald-600 dark:fill-emerald-400 font-semibold">
|
|
59
|
+
✓ How your requests actually flow
|
|
60
|
+
</text>
|
|
61
|
+
|
|
62
|
+
{/* Browser box */}
|
|
63
|
+
<rect
|
|
64
|
+
x="2"
|
|
65
|
+
y="6"
|
|
66
|
+
width="120"
|
|
67
|
+
height="48"
|
|
68
|
+
rx="8"
|
|
69
|
+
className="fill-background stroke-emerald-500"
|
|
70
|
+
strokeWidth="1.75"
|
|
71
|
+
/>
|
|
72
|
+
<text x="62" y="35" textAnchor="middle" className="text-[12px] fill-foreground font-medium">
|
|
73
|
+
Your browser
|
|
74
|
+
</text>
|
|
75
|
+
|
|
76
|
+
{/* Arrow */}
|
|
77
|
+
<line
|
|
78
|
+
x1="124"
|
|
79
|
+
y1="30"
|
|
80
|
+
x2="346"
|
|
81
|
+
y2="30"
|
|
82
|
+
className="stroke-emerald-500"
|
|
83
|
+
strokeWidth="2"
|
|
84
|
+
markerEnd="url(#byok-arrow-active)"
|
|
85
|
+
/>
|
|
86
|
+
<text x="235" y="22" textAnchor="middle" className="text-[10px] fill-emerald-600 dark:fill-emerald-400 font-mono">
|
|
87
|
+
HTTPS · direct
|
|
88
|
+
</text>
|
|
89
|
+
|
|
90
|
+
{/* Provider API box (highlighted) */}
|
|
91
|
+
<rect
|
|
92
|
+
x="350"
|
|
93
|
+
y="6"
|
|
94
|
+
width="168"
|
|
95
|
+
height="48"
|
|
96
|
+
rx="8"
|
|
97
|
+
className="fill-emerald-500/10 stroke-emerald-500"
|
|
98
|
+
strokeWidth="1.75"
|
|
99
|
+
/>
|
|
100
|
+
<text x="434" y="35" textAnchor="middle" className="text-[12px] fill-foreground font-mono">
|
|
101
|
+
{apiHost}
|
|
102
|
+
</text>
|
|
103
|
+
</g>
|
|
104
|
+
|
|
105
|
+
{/* Row 2 — what we are NOT doing */}
|
|
106
|
+
<g transform="translate(0, 116)" opacity="0.55">
|
|
107
|
+
<text x="0" y="-4" className="text-[10px] uppercase tracking-wider fill-destructive font-semibold" opacity="1">
|
|
108
|
+
✗ What we never do
|
|
109
|
+
</text>
|
|
110
|
+
|
|
111
|
+
{/* Browser box (muted) */}
|
|
112
|
+
<rect
|
|
113
|
+
x="2"
|
|
114
|
+
y="6"
|
|
115
|
+
width="100"
|
|
116
|
+
height="44"
|
|
117
|
+
rx="6"
|
|
118
|
+
className="fill-background stroke-muted-foreground"
|
|
119
|
+
strokeWidth="1"
|
|
120
|
+
strokeDasharray="3 3"
|
|
121
|
+
/>
|
|
122
|
+
<text x="52" y="33" textAnchor="middle" className="text-[11px] fill-muted-foreground">
|
|
123
|
+
Your browser
|
|
124
|
+
</text>
|
|
125
|
+
|
|
126
|
+
{/* Arrow 1 (muted, dashed) */}
|
|
127
|
+
<line
|
|
128
|
+
x1="104"
|
|
129
|
+
y1="28"
|
|
130
|
+
x2="186"
|
|
131
|
+
y2="28"
|
|
132
|
+
className="stroke-muted-foreground"
|
|
133
|
+
strokeWidth="1.25"
|
|
134
|
+
strokeDasharray="3 3"
|
|
135
|
+
markerEnd="url(#byok-arrow-blocked)"
|
|
136
|
+
/>
|
|
137
|
+
|
|
138
|
+
{/* "Our server" box — struck through */}
|
|
139
|
+
<rect
|
|
140
|
+
x="190"
|
|
141
|
+
y="6"
|
|
142
|
+
width="120"
|
|
143
|
+
height="44"
|
|
144
|
+
rx="6"
|
|
145
|
+
className="fill-background stroke-muted-foreground"
|
|
146
|
+
strokeWidth="1"
|
|
147
|
+
strokeDasharray="3 3"
|
|
148
|
+
/>
|
|
149
|
+
<text x="250" y="33" textAnchor="middle" className="text-[11px] fill-muted-foreground">
|
|
150
|
+
our server
|
|
151
|
+
</text>
|
|
152
|
+
{/* Strike-through across the "our server" box */}
|
|
153
|
+
<line
|
|
154
|
+
x1="184"
|
|
155
|
+
y1="44"
|
|
156
|
+
x2="316"
|
|
157
|
+
y2="12"
|
|
158
|
+
className="stroke-destructive"
|
|
159
|
+
strokeWidth="2.5"
|
|
160
|
+
opacity="0.85"
|
|
161
|
+
/>
|
|
162
|
+
|
|
163
|
+
{/* Arrow 2 (muted, dashed) */}
|
|
164
|
+
<line
|
|
165
|
+
x1="312"
|
|
166
|
+
y1="28"
|
|
167
|
+
x2="394"
|
|
168
|
+
y2="28"
|
|
169
|
+
className="stroke-muted-foreground"
|
|
170
|
+
strokeWidth="1.25"
|
|
171
|
+
strokeDasharray="3 3"
|
|
172
|
+
markerEnd="url(#byok-arrow-blocked)"
|
|
173
|
+
/>
|
|
174
|
+
|
|
175
|
+
{/* Provider API box (muted) */}
|
|
176
|
+
<rect
|
|
177
|
+
x="398"
|
|
178
|
+
y="6"
|
|
179
|
+
width="120"
|
|
180
|
+
height="44"
|
|
181
|
+
rx="6"
|
|
182
|
+
className="fill-background stroke-muted-foreground"
|
|
183
|
+
strokeWidth="1"
|
|
184
|
+
strokeDasharray="3 3"
|
|
185
|
+
/>
|
|
186
|
+
<text x="458" y="33" textAnchor="middle" className="text-[11px] fill-muted-foreground font-mono">
|
|
187
|
+
{apiHost}
|
|
188
|
+
</text>
|
|
189
|
+
</g>
|
|
190
|
+
</svg>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
@@ -16,8 +16,13 @@ import { useViewerStore } from '@/store';
|
|
|
16
16
|
import type { CoordinateInfo, GeometryResult } from '@ifc-lite/geometry';
|
|
17
17
|
import { EpsgLookupDialog, type EpsgResult } from './EpsgLookupDialog';
|
|
18
18
|
import { LocationMap, type PickedPosition } from './LocationMap';
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
19
|
+
import { computeOrthogonalHeightForBaseAltitude } from '@/lib/geo/cesium-placement';
|
|
20
|
+
import {
|
|
21
|
+
detectScaleUnitMismatch,
|
|
22
|
+
mergeMapConversion,
|
|
23
|
+
mergeProjectedCRS,
|
|
24
|
+
supportsStandardGeoreferencing,
|
|
25
|
+
} from '@/lib/geo/effective-georef';
|
|
21
26
|
import { useIfc } from '@/hooks/useIfc';
|
|
22
27
|
import { toast } from '@/components/ui/toast';
|
|
23
28
|
|
|
@@ -335,9 +340,8 @@ export function GeoreferencingPanel({ georef, modelId, enableEditing, schemaVers
|
|
|
335
340
|
const setGeorefField = useViewerStore(s => s.setGeorefField);
|
|
336
341
|
const setGeorefFields = useViewerStore(s => s.setGeorefFields);
|
|
337
342
|
const cesiumEnabled = useViewerStore(s => s.cesiumEnabled);
|
|
338
|
-
const terrainClamp = useViewerStore(s => s.cesiumTerrainClamp);
|
|
339
|
-
const setCesiumTerrainClamp = useViewerStore(s => s.setCesiumTerrainClamp);
|
|
340
343
|
const cesiumTerrainHeight = useViewerStore(s => s.cesiumTerrainHeight);
|
|
344
|
+
const cesiumTerrainSource = useViewerStore(s => s.cesiumTerrainSource);
|
|
341
345
|
const cesiumSourceModelId = useViewerStore(s => s.cesiumSourceModelId);
|
|
342
346
|
const models = useViewerStore(s => s.models);
|
|
343
347
|
const loading = useViewerStore(s => s.loading);
|
|
@@ -351,7 +355,8 @@ export function GeoreferencingPanel({ georef, modelId, enableEditing, schemaVers
|
|
|
351
355
|
useViewerStore(s => s.mutationVersion);
|
|
352
356
|
|
|
353
357
|
const mutations = modelId ? georefMutations?.get(modelId) : undefined;
|
|
354
|
-
const
|
|
358
|
+
const isLegacySiteGeoreference = georef?.source === 'siteLocation';
|
|
359
|
+
const canUseStandardGeoreferencing = supportsStandardGeoreferencing(schemaVersion, georef);
|
|
355
360
|
|
|
356
361
|
const mergedCRS = useMemo((): ProjectedCRS | undefined => {
|
|
357
362
|
return mergeProjectedCRS(georef?.projectedCRS, mutations?.projectedCRS, lengthUnitScale ?? 1);
|
|
@@ -382,13 +387,6 @@ export function GeoreferencingPanel({ georef, modelId, enableEditing, schemaVers
|
|
|
382
387
|
return 'm';
|
|
383
388
|
}, [mergedCRS?.mapUnit]);
|
|
384
389
|
|
|
385
|
-
// Convert meters to map units (Cesium always returns meters)
|
|
386
|
-
const metersToMapUnit = useCallback((meters: number): number => {
|
|
387
|
-
if (mapUnitSuffix === 'ftUS') return meters / 0.3048006096;
|
|
388
|
-
if (mapUnitSuffix === 'ft') return meters / 0.3048;
|
|
389
|
-
return meters; // already meters
|
|
390
|
-
}, [mapUnitSuffix]);
|
|
391
|
-
|
|
392
390
|
/**
|
|
393
391
|
* Given a target world altitude (metres) for the model's ground floor
|
|
394
392
|
* (the storey nearest elevation 0, falling back to bounds.min.y when
|
|
@@ -400,14 +398,14 @@ export function GeoreferencingPanel({ georef, modelId, enableEditing, schemaVers
|
|
|
400
398
|
* world position as toggling the clamp.
|
|
401
399
|
*/
|
|
402
400
|
const oHeightForBaseAltitude = useCallback((targetBaseAltitude: number): number => {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
}, [coordinateInfo,
|
|
401
|
+
return computeOrthogonalHeightForBaseAltitude({
|
|
402
|
+
coordinateInfo,
|
|
403
|
+
projectedCRS: mergedCRS,
|
|
404
|
+
lengthUnitScale: lengthUnitScale ?? 1,
|
|
405
|
+
storeyElevations,
|
|
406
|
+
targetBaseAltitude,
|
|
407
|
+
});
|
|
408
|
+
}, [coordinateInfo, mergedCRS, lengthUnitScale, storeyElevations]);
|
|
411
409
|
|
|
412
410
|
const isMutated = useCallback((entity: 'projectedCRS' | 'mapConversion', field: string): boolean => {
|
|
413
411
|
if (!mutations) return false;
|
|
@@ -467,14 +465,8 @@ export function GeoreferencingPanel({ georef, modelId, enableEditing, schemaVers
|
|
|
467
465
|
? mergedCRS?.[field as keyof ProjectedCRS]
|
|
468
466
|
: mergedConversion?.[field as keyof MapConversion];
|
|
469
467
|
setGeorefField(modelId, entity, field, value, oldValue as string | number | undefined);
|
|
470
|
-
// Editing OrthogonalHeight implies "I want this exact altitude" — auto
|
|
471
|
-
// -release the terrain clamp so the new value actually takes effect
|
|
472
|
-
// (with clamp on, placement is locked to terrain regardless of oHeight).
|
|
473
|
-
if (entity === 'mapConversion' && field === 'orthogonalHeight' && terrainClamp) {
|
|
474
|
-
setCesiumTerrainClamp(false);
|
|
475
|
-
}
|
|
476
468
|
requestAlignmentReload();
|
|
477
|
-
}, [modelId, setGeorefField, mergedCRS, mergedConversion, requestAlignmentReload
|
|
469
|
+
}, [modelId, setGeorefField, mergedCRS, mergedConversion, requestAlignmentReload]);
|
|
478
470
|
|
|
479
471
|
// Handle angle edit: compute and set both XAxisAbscissa and XAxisOrdinate
|
|
480
472
|
const handleAngleChange = useCallback((abscissa: number, ordinate: number) => {
|
|
@@ -557,18 +549,7 @@ export function GeoreferencingPanel({ georef, modelId, enableEditing, schemaVers
|
|
|
557
549
|
}, [modelId, setGeorefFields, mergedCRS, mergedConversion, mutations, initializeMapConversionDefaults, requestAlignmentReload]);
|
|
558
550
|
|
|
559
551
|
const hasData = mergedCRS || mergedConversion;
|
|
560
|
-
const editable = enableEditing && !!modelId &&
|
|
561
|
-
|
|
562
|
-
if (enableEditing && !supportsStandardGeoreferencing) {
|
|
563
|
-
return (
|
|
564
|
-
<div className="px-2 py-1.5 flex items-center gap-2">
|
|
565
|
-
<Globe className="h-3 w-3 text-zinc-400" />
|
|
566
|
-
<span className="text-[10px] text-zinc-500 dark:text-zinc-400">
|
|
567
|
-
Georeferencing editing requires IFC4 or newer. IFC2X3 does not support IfcProjectedCRS or IfcMapConversion.
|
|
568
|
-
</span>
|
|
569
|
-
</div>
|
|
570
|
-
);
|
|
571
|
-
}
|
|
552
|
+
const editable = enableEditing && !!modelId && canUseStandardGeoreferencing;
|
|
572
553
|
|
|
573
554
|
// When no georef data exists, show "Add Georeferencing" in edit mode
|
|
574
555
|
if (!hasData && !georef?.hasGeoreference) {
|
|
@@ -616,6 +597,21 @@ export function GeoreferencingPanel({ georef, modelId, enableEditing, schemaVers
|
|
|
616
597
|
</div>
|
|
617
598
|
</div>
|
|
618
599
|
)}
|
|
600
|
+
{/* Only flag the legacy-site / unsupported-schema state when there is
|
|
601
|
+
actually nothing extractable to show. If we have a projectedCRS or
|
|
602
|
+
mapConversion (even partially), the data sections below speak for
|
|
603
|
+
themselves — the schema notice is just noise that contradicts the
|
|
604
|
+
live data the properties panel already renders. */}
|
|
605
|
+
{!canUseStandardGeoreferencing && !mergedCRS && !mergedConversion && (
|
|
606
|
+
<div className="px-3 py-1.5 flex items-center gap-2 border-b border-zinc-100 dark:border-zinc-900">
|
|
607
|
+
<Globe className="h-3 w-3 text-zinc-400 shrink-0" />
|
|
608
|
+
<span className="text-[10px] text-zinc-500 dark:text-zinc-400">
|
|
609
|
+
{isLegacySiteGeoreference
|
|
610
|
+
? 'Showing legacy IfcSite geolocation from IFC2X3. This view is read-only.'
|
|
611
|
+
: 'Georeferencing editing requires IFC4 or newer. IFC2X3 does not support IfcProjectedCRS or IfcMapConversion.'}
|
|
612
|
+
</span>
|
|
613
|
+
</div>
|
|
614
|
+
)}
|
|
619
615
|
{/* CRS summary — always visible */}
|
|
620
616
|
<div className="px-2 py-1.5 flex items-center gap-2">
|
|
621
617
|
<Globe className="h-3 w-3 text-teal-500 shrink-0" />
|
|
@@ -751,28 +747,25 @@ export function GeoreferencingPanel({ georef, modelId, enableEditing, schemaVers
|
|
|
751
747
|
</div>
|
|
752
748
|
)}
|
|
753
749
|
|
|
754
|
-
{/*
|
|
750
|
+
{/* Sampled surface height — only when Cesium overlay is active */}
|
|
755
751
|
{cesiumEnabled && isActiveCesiumModel && mergedConversion && (
|
|
756
752
|
<div className="px-3 py-1.5 border-t border-zinc-100 dark:border-zinc-900 space-y-1">
|
|
757
753
|
<div className="flex items-center gap-2">
|
|
758
754
|
<Mountain className="h-3 w-3 text-teal-500 shrink-0" />
|
|
759
|
-
<
|
|
760
|
-
<input
|
|
761
|
-
type="checkbox"
|
|
762
|
-
checked={terrainClamp}
|
|
763
|
-
onChange={(e) => setCesiumTerrainClamp(e.target.checked)}
|
|
764
|
-
className="accent-teal-500 h-3 w-3"
|
|
765
|
-
/>
|
|
766
|
-
<span className="text-[10px] text-zinc-600 dark:text-zinc-400">Clamp to terrain</span>
|
|
767
|
-
</label>
|
|
755
|
+
<span className="text-[10px] text-zinc-600 dark:text-zinc-400 flex-1">Visible surface height</span>
|
|
768
756
|
{cesiumTerrainHeight !== null ? (
|
|
769
|
-
<span className="text-[9px] font-mono text-teal-500">
|
|
757
|
+
<span className="text-[9px] font-mono text-teal-500" title={cesiumTerrainSource ?? undefined}>
|
|
770
758
|
{cesiumTerrainHeight.toFixed(1)} m
|
|
771
759
|
</span>
|
|
772
760
|
) : (
|
|
773
761
|
<span className="text-[9px] font-mono text-zinc-400">querying...</span>
|
|
774
762
|
)}
|
|
775
763
|
</div>
|
|
764
|
+
{cesiumTerrainSource && (
|
|
765
|
+
<div className="ml-5 text-[9px] text-zinc-500 dark:text-zinc-400">
|
|
766
|
+
sampled via {cesiumTerrainSource}
|
|
767
|
+
</div>
|
|
768
|
+
)}
|
|
776
769
|
{cesiumTerrainHeight !== null && editable && modelId && (
|
|
777
770
|
<div className="flex items-center gap-1 ml-5">
|
|
778
771
|
<button
|
|
@@ -780,7 +773,7 @@ export function GeoreferencingPanel({ georef, modelId, enableEditing, schemaVers
|
|
|
780
773
|
className="text-[9px] text-teal-500 hover:text-teal-700 dark:hover:text-teal-300 transition-colors flex items-center gap-0.5"
|
|
781
774
|
>
|
|
782
775
|
<Mountain className="h-2.5 w-2.5" />
|
|
783
|
-
Set OrthogonalHeight to {cesiumTerrainHeight.toFixed(1)} m
|
|
776
|
+
Set OrthogonalHeight to sampled terrain height ({cesiumTerrainHeight.toFixed(1)} m)
|
|
784
777
|
</button>
|
|
785
778
|
</div>
|
|
786
779
|
)}
|
|
@@ -809,6 +802,7 @@ function TerrainHeightButton({ modelId, editable, onApply }: {
|
|
|
809
802
|
}) {
|
|
810
803
|
const cesiumEnabled = useViewerStore(s => s.cesiumEnabled);
|
|
811
804
|
const terrainHeight = useViewerStore(s => s.cesiumTerrainHeight);
|
|
805
|
+
const terrainSource = useViewerStore(s => s.cesiumTerrainSource);
|
|
812
806
|
const sourceModelId = useViewerStore(s => s.cesiumSourceModelId);
|
|
813
807
|
|
|
814
808
|
// Only show when this panel's model is the active Cesium model
|
|
@@ -828,7 +822,10 @@ function TerrainHeightButton({ modelId, editable, onApply }: {
|
|
|
828
822
|
<span>{terrainHeight.toFixed(1)} m</span>
|
|
829
823
|
</button>
|
|
830
824
|
</TooltipTrigger>
|
|
831
|
-
<TooltipContent>
|
|
825
|
+
<TooltipContent>
|
|
826
|
+
Set OrthogonalHeight to sampled terrain height ({terrainHeight.toFixed(1)} m
|
|
827
|
+
{terrainSource ? ` via ${terrainSource}` : ''})
|
|
828
|
+
</TooltipContent>
|
|
832
829
|
</Tooltip>
|
|
833
830
|
);
|
|
834
831
|
}
|
|
@@ -124,7 +124,12 @@ export function ModelMetadataPanel({ model }: { model: FederatedModel }) {
|
|
|
124
124
|
</div>
|
|
125
125
|
</div>
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
{/* `min-h-0` is required: without it `flex-1` falls back to
|
|
128
|
+
min-height:auto and the ScrollArea grows past the panel's
|
|
129
|
+
height instead of constraining the inner viewport, so the map
|
|
130
|
+
(and any tall content underneath) overflowed past the right
|
|
131
|
+
panel's clip box. */}
|
|
132
|
+
<ScrollArea className="flex-1 min-h-0">
|
|
128
133
|
{/* File Information */}
|
|
129
134
|
<div className="border-b border-zinc-200 dark:border-zinc-800">
|
|
130
135
|
<div className="p-3 bg-zinc-50 dark:bg-zinc-900/50">
|
|
@@ -172,49 +177,11 @@ export function ModelMetadataPanel({ model }: { model: FederatedModel }) {
|
|
|
172
177
|
</div>
|
|
173
178
|
)}
|
|
174
179
|
|
|
175
|
-
{/*
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
</h4>
|
|
181
|
-
</div>
|
|
182
|
-
<div className="divide-y divide-zinc-100 dark:divide-zinc-900">
|
|
183
|
-
<div className="flex items-center gap-3 px-3 py-2">
|
|
184
|
-
<Database className="h-3.5 w-3.5 text-zinc-400 shrink-0" />
|
|
185
|
-
<span className="text-xs text-zinc-500">Total Entities</span>
|
|
186
|
-
<span className="text-xs font-mono text-zinc-900 dark:text-zinc-100 ml-auto">
|
|
187
|
-
{dataStore?.entityCount?.toLocaleString() ?? 'N/A'}
|
|
188
|
-
</span>
|
|
189
|
-
</div>
|
|
190
|
-
<div className="flex items-center gap-3 px-3 py-2">
|
|
191
|
-
<Layers className="h-3.5 w-3.5 text-zinc-400 shrink-0" />
|
|
192
|
-
<span className="text-xs text-zinc-500">Building Storeys</span>
|
|
193
|
-
<span className="text-xs font-mono text-zinc-900 dark:text-zinc-100 ml-auto">
|
|
194
|
-
{stats.storeys}
|
|
195
|
-
</span>
|
|
196
|
-
</div>
|
|
197
|
-
<div className="flex items-center gap-3 px-3 py-2">
|
|
198
|
-
<Building2 className="h-3.5 w-3.5 text-zinc-400 shrink-0" />
|
|
199
|
-
<span className="text-xs text-zinc-500">Elements with Geometry</span>
|
|
200
|
-
<span className="text-xs font-mono text-zinc-900 dark:text-zinc-100 ml-auto">
|
|
201
|
-
{stats.elementsWithGeometry.toLocaleString()}
|
|
202
|
-
</span>
|
|
203
|
-
</div>
|
|
204
|
-
<div className="flex items-center gap-3 px-3 py-2">
|
|
205
|
-
<Hash className="h-3.5 w-3.5 text-zinc-400 shrink-0" />
|
|
206
|
-
<span className="text-xs text-zinc-500">Max Express ID</span>
|
|
207
|
-
<span className="text-xs font-mono text-zinc-900 dark:text-zinc-100 ml-auto">
|
|
208
|
-
{model.maxExpressId.toLocaleString()}
|
|
209
|
-
</span>
|
|
210
|
-
</div>
|
|
211
|
-
</div>
|
|
212
|
-
</div>
|
|
213
|
-
|
|
214
|
-
{/* Georeferencing */}
|
|
215
|
-
<GeoreferencingPanel georef={georef} modelId={model.id} enableEditing schemaVersion={model.schemaVersion} coordinateInfo={model.geometryResult?.coordinateInfo} geometryResult={model.geometryResult} lengthUnitScale={unitInfo?.scale} />
|
|
216
|
-
|
|
217
|
-
{/* IfcProject Data */}
|
|
180
|
+
{/* IfcProject Data — placed near the top so the model's name,
|
|
181
|
+
description, and project-level psets are the first thing users
|
|
182
|
+
see after file info. Previously the section was at the bottom
|
|
183
|
+
of the panel (below the map), which buried critical project
|
|
184
|
+
identity below scrollable georeferencing content. */}
|
|
218
185
|
{projectData && (
|
|
219
186
|
<div className="border-b border-zinc-200 dark:border-zinc-800">
|
|
220
187
|
<div className="p-3 bg-zinc-50 dark:bg-zinc-900/50">
|
|
@@ -262,6 +229,50 @@ export function ModelMetadataPanel({ model }: { model: FederatedModel }) {
|
|
|
262
229
|
)}
|
|
263
230
|
</div>
|
|
264
231
|
)}
|
|
232
|
+
|
|
233
|
+
{/* Entity Statistics */}
|
|
234
|
+
<div className="border-b border-zinc-200 dark:border-zinc-800">
|
|
235
|
+
<div className="p-3 bg-zinc-50 dark:bg-zinc-900/50">
|
|
236
|
+
<h4 className="font-bold text-xs uppercase tracking-wide text-zinc-700 dark:text-zinc-300">
|
|
237
|
+
Statistics
|
|
238
|
+
</h4>
|
|
239
|
+
</div>
|
|
240
|
+
<div className="divide-y divide-zinc-100 dark:divide-zinc-900">
|
|
241
|
+
<div className="flex items-center gap-3 px-3 py-2">
|
|
242
|
+
<Database className="h-3.5 w-3.5 text-zinc-400 shrink-0" />
|
|
243
|
+
<span className="text-xs text-zinc-500">Total Entities</span>
|
|
244
|
+
<span className="text-xs font-mono text-zinc-900 dark:text-zinc-100 ml-auto">
|
|
245
|
+
{dataStore?.entityCount?.toLocaleString() ?? 'N/A'}
|
|
246
|
+
</span>
|
|
247
|
+
</div>
|
|
248
|
+
<div className="flex items-center gap-3 px-3 py-2">
|
|
249
|
+
<Layers className="h-3.5 w-3.5 text-zinc-400 shrink-0" />
|
|
250
|
+
<span className="text-xs text-zinc-500">Building Storeys</span>
|
|
251
|
+
<span className="text-xs font-mono text-zinc-900 dark:text-zinc-100 ml-auto">
|
|
252
|
+
{stats.storeys}
|
|
253
|
+
</span>
|
|
254
|
+
</div>
|
|
255
|
+
<div className="flex items-center gap-3 px-3 py-2">
|
|
256
|
+
<Building2 className="h-3.5 w-3.5 text-zinc-400 shrink-0" />
|
|
257
|
+
<span className="text-xs text-zinc-500">Elements with Geometry</span>
|
|
258
|
+
<span className="text-xs font-mono text-zinc-900 dark:text-zinc-100 ml-auto">
|
|
259
|
+
{stats.elementsWithGeometry.toLocaleString()}
|
|
260
|
+
</span>
|
|
261
|
+
</div>
|
|
262
|
+
<div className="flex items-center gap-3 px-3 py-2">
|
|
263
|
+
<Hash className="h-3.5 w-3.5 text-zinc-400 shrink-0" />
|
|
264
|
+
<span className="text-xs text-zinc-500">Max Express ID</span>
|
|
265
|
+
<span className="text-xs font-mono text-zinc-900 dark:text-zinc-100 ml-auto">
|
|
266
|
+
{model.maxExpressId.toLocaleString()}
|
|
267
|
+
</span>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
{/* Georeferencing — kept at the bottom because it embeds a
|
|
273
|
+
tall location map; placing it earlier would push the
|
|
274
|
+
statistics + project metadata below the fold. */}
|
|
275
|
+
<GeoreferencingPanel georef={georef} modelId={model.id} enableEditing schemaVersion={model.schemaVersion} coordinateInfo={model.geometryResult?.coordinateInfo} geometryResult={model.geometryResult} lengthUnitScale={unitInfo?.scale} />
|
|
265
276
|
</ScrollArea>
|
|
266
277
|
</div>
|
|
267
278
|
);
|
|
@@ -564,7 +564,13 @@ export function commitAddElementSlabPolygon(): void {
|
|
|
564
564
|
*/
|
|
565
565
|
export async function handleContextMenu(ctx: MouseHandlerContext, e: MouseEvent): Promise<void> {
|
|
566
566
|
e.preventDefault();
|
|
567
|
-
const { canvas, renderer } = ctx;
|
|
567
|
+
const { canvas, renderer, mouseState } = ctx;
|
|
568
|
+
// Right-drag is the pan gesture (see useMouseControls). Some browsers
|
|
569
|
+
// still fire `contextmenu` after a tiny right-drag — skip when the
|
|
570
|
+
// user actually moved, so panning never accidentally pops the menu.
|
|
571
|
+
if (mouseState.didDrag) {
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
568
574
|
const rect = canvas.getBoundingClientRect();
|
|
569
575
|
const x = e.clientX - rect.left;
|
|
570
576
|
const y = e.clientY - rect.top;
|