@ifc-lite/viewer 1.17.2 → 1.17.3
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 +30 -29
- package/.turbo/turbo-typecheck.log +1 -42
- package/CHANGELOG.md +9 -0
- package/dist/assets/arrow-DJf2ErbF.js +20 -0
- package/dist/assets/basketViewActivator-aojwdomq.js +1 -0
- package/dist/assets/bcf-D5-QWGO9.js +281 -0
- package/dist/assets/{browser-BDShTXzi.js → browser-CKs-FY1P.js} +1 -1
- package/dist/assets/drawing-2d-gWfpdfYe.js +257 -0
- package/dist/assets/epsg-index.generated-BjJrt_0S.js +1 -0
- package/dist/assets/exporters-C_6J153K.js +79896 -0
- package/dist/assets/geometry.worker-Nz9_YIqh.js +1 -0
- package/dist/assets/ids-B4jTqB1O.js +1 -0
- package/dist/assets/{ifc-lite_bg-FNRmpSvM.wasm → ifc-lite_bg-eSkBTizQ.wasm} +0 -0
- package/dist/assets/index-jhBr1wbn.js +101666 -0
- package/dist/assets/index-pbE7itQS.css +1 -0
- package/dist/assets/lens-CSASnhAL.js +1 -0
- package/dist/assets/maplibre-gl-BpvwNKKy.js +811 -0
- package/dist/assets/{native-bridge-Crsb7TKz.js → native-bridge-DSIyEYXG.js} +6 -4
- package/dist/assets/{arrow2-bb-jcVEo.js → parquet-CEXmQNRO.js} +2 -2
- package/dist/assets/sandbox-B79eavQ3.js +5933 -0
- package/dist/assets/server-client-D3bUPJJc.js +626 -0
- package/dist/assets/wasm-bridge-B0J07fZZ.js +1 -0
- package/dist/assets/zip-B-jFFAGa.js +12 -0
- package/dist/index.html +11 -2
- package/package.json +24 -19
- package/src/components/viewer/ExportChangesButton.tsx +18 -3
- package/src/components/viewer/ExportDialog.tsx +16 -3
- package/src/components/viewer/HierarchyPanel.tsx +6 -6
- package/src/components/viewer/PropertiesPanel.tsx +96 -60
- package/src/components/viewer/Section2DPanel.tsx +3 -2
- package/src/components/viewer/ViewportContainer.tsx +5 -4
- package/src/components/viewer/hierarchy/treeDataBuilder.ts +2 -1
- package/src/components/viewer/properties/EpsgLookupDialog.tsx +418 -0
- package/src/components/viewer/properties/GeoreferencingPanel.tsx +591 -0
- package/src/components/viewer/properties/LocationMap.tsx +289 -0
- package/src/components/viewer/properties/ModelMetadataPanel.tsx +3 -70
- package/src/hooks/bcfIdLookup.ts +13 -11
- package/src/hooks/ids/idsColorSystem.ts +3 -8
- package/src/hooks/useIDS.ts +31 -16
- package/src/hooks/useIfcFederation.ts +2 -2
- package/src/lib/geo/kmz-exporter.ts +112 -0
- package/src/lib/geo/reproject.ts +244 -0
- package/src/lib/lens/adapter.ts +3 -1
- package/src/main.tsx +1 -0
- package/src/sdk/adapters/export-adapter.ts +14 -1
- package/src/sdk/adapters/viewer-adapter.ts +5 -9
- package/src/sdk/adapters/visibility-adapter.ts +6 -9
- package/src/store/basketVisibleSet.ts +3 -4
- package/src/store/globalId.ts +79 -0
- package/src/store/index.ts +1 -0
- package/src/store/slices/mutationSlice.ts +178 -0
- package/src/store/slices/pinboardSlice.ts +4 -8
- package/vite.config.ts +17 -0
- package/dist/assets/Arrow.dom-BhOg9lpn.js +0 -20
- package/dist/assets/arrow2_bg-BlXl-cSQ.js +0 -1
- package/dist/assets/basketViewActivator-BRG5DBmM.js +0 -1
- package/dist/assets/geometry.worker-kgiT_Qhh.js +0 -1
- package/dist/assets/index-B1Ecw4AU.js +0 -189756
- package/dist/assets/index-Ba4eoTe7.css +0 -1
- package/dist/assets/index-CrgYBjTn.js +0 -229
- package/dist/assets/module-6F3E5H7Y-tx0BadV3.js +0 -6
- package/dist/assets/wasm-bridge-mJUhb7uk.js +0 -1
|
@@ -0,0 +1,289 @@
|
|
|
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
|
+
* LocationMap — a compact MapLibre GL JS minimap that shows the model's
|
|
7
|
+
* real-world position derived from IfcMapConversion + IfcProjectedCRS.
|
|
8
|
+
*
|
|
9
|
+
* Renders as a collapsible panel below the georeferencing fields.
|
|
10
|
+
* Includes links to Google Maps, OpenStreetMap, and Google Earth (KMZ export).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { useEffect, useRef, useState, useMemo, useCallback } from 'react';
|
|
14
|
+
import { Map as MapIcon, ExternalLink, Loader2, MapPinOff, Globe2 } from 'lucide-react';
|
|
15
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
|
16
|
+
import type { MapConversion, ProjectedCRS } from '@ifc-lite/parser';
|
|
17
|
+
import type { CoordinateInfo, GeometryResult } from '@ifc-lite/geometry';
|
|
18
|
+
import { GLTFExporter } from '@ifc-lite/export';
|
|
19
|
+
import { reprojectToLatLon, type LatLon } from '@/lib/geo/reproject';
|
|
20
|
+
import { buildKmz, ifcAngleToKmlHeading } from '@/lib/geo/kmz-exporter';
|
|
21
|
+
|
|
22
|
+
// Lazy-load maplibre-gl to avoid bloating the initial bundle
|
|
23
|
+
let maplibrePromise: Promise<typeof import('maplibre-gl')> | null = null;
|
|
24
|
+
function loadMaplibre() {
|
|
25
|
+
if (!maplibrePromise) {
|
|
26
|
+
maplibrePromise = import('maplibre-gl');
|
|
27
|
+
}
|
|
28
|
+
return maplibrePromise;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface LocationMapProps {
|
|
32
|
+
mapConversion?: MapConversion;
|
|
33
|
+
projectedCRS?: ProjectedCRS;
|
|
34
|
+
/** Coordinate info from the model's GeometryResult (includes bounds and RTC offset) */
|
|
35
|
+
coordinateInfo?: CoordinateInfo;
|
|
36
|
+
/** Geometry result for KMZ export (optional — KMZ button hidden if not provided) */
|
|
37
|
+
geometryResult?: GeometryResult | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type MapState = 'idle' | 'loading' | 'ready' | 'error';
|
|
41
|
+
|
|
42
|
+
export function LocationMap({ mapConversion, projectedCRS, coordinateInfo, geometryResult }: LocationMapProps) {
|
|
43
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
44
|
+
const mapRef = useRef<InstanceType<typeof import('maplibre-gl').Map> | null>(null);
|
|
45
|
+
const markerRef = useRef<InstanceType<typeof import('maplibre-gl').Marker> | null>(null);
|
|
46
|
+
|
|
47
|
+
const [mapState, setMapState] = useState<MapState>('idle');
|
|
48
|
+
const [latLon, setLatLon] = useState<LatLon | null>(null);
|
|
49
|
+
const [error, setError] = useState<string | null>(null);
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (!mapConversion || !projectedCRS) {
|
|
53
|
+
setLatLon(null);
|
|
54
|
+
setError(null);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let cancelled = false;
|
|
59
|
+
setMapState('loading');
|
|
60
|
+
setError(null);
|
|
61
|
+
|
|
62
|
+
reprojectToLatLon(mapConversion, projectedCRS, coordinateInfo).then(result => {
|
|
63
|
+
if (cancelled) return;
|
|
64
|
+
if (result) {
|
|
65
|
+
setLatLon(result);
|
|
66
|
+
setMapState('ready');
|
|
67
|
+
} else {
|
|
68
|
+
setLatLon(null);
|
|
69
|
+
setError('Could not resolve projection — EPSG code may be unsupported');
|
|
70
|
+
setMapState('error');
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return () => { cancelled = true; };
|
|
75
|
+
}, [mapConversion, projectedCRS, coordinateInfo]);
|
|
76
|
+
|
|
77
|
+
// Initialize/update the map when we have a valid lat/lon
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (!latLon || !containerRef.current) return;
|
|
80
|
+
|
|
81
|
+
let cancelled = false;
|
|
82
|
+
|
|
83
|
+
loadMaplibre().then(maplibregl => {
|
|
84
|
+
if (cancelled || !containerRef.current) return;
|
|
85
|
+
|
|
86
|
+
// If map already exists, just fly to new position
|
|
87
|
+
if (mapRef.current) {
|
|
88
|
+
mapRef.current.flyTo({ center: [latLon.lon, latLon.lat], zoom: 15, duration: 1200 });
|
|
89
|
+
if (markerRef.current) {
|
|
90
|
+
markerRef.current.setLngLat([latLon.lon, latLon.lat]);
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Create new map
|
|
96
|
+
const map = new maplibregl.Map({
|
|
97
|
+
container: containerRef.current,
|
|
98
|
+
style: 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json',
|
|
99
|
+
center: [latLon.lon, latLon.lat],
|
|
100
|
+
zoom: 15,
|
|
101
|
+
attributionControl: false,
|
|
102
|
+
interactive: true,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
map.addControl(new maplibregl.NavigationControl({ showCompass: false }), 'top-right');
|
|
106
|
+
map.addControl(new maplibregl.AttributionControl({ compact: false }), 'bottom-right');
|
|
107
|
+
|
|
108
|
+
// Add marker at model location
|
|
109
|
+
const marker = new maplibregl.Marker({ color: '#14b8a6' })
|
|
110
|
+
.setLngLat([latLon.lon, latLon.lat])
|
|
111
|
+
.addTo(map);
|
|
112
|
+
|
|
113
|
+
mapRef.current = map;
|
|
114
|
+
markerRef.current = marker;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return () => {
|
|
118
|
+
cancelled = true;
|
|
119
|
+
};
|
|
120
|
+
}, [latLon]);
|
|
121
|
+
|
|
122
|
+
// Cleanup on unmount
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
return () => {
|
|
125
|
+
markerRef.current?.remove();
|
|
126
|
+
markerRef.current = null;
|
|
127
|
+
mapRef.current?.remove();
|
|
128
|
+
mapRef.current = null;
|
|
129
|
+
};
|
|
130
|
+
}, []);
|
|
131
|
+
|
|
132
|
+
const googleMapsUrl = useMemo(() => {
|
|
133
|
+
if (!latLon) return null;
|
|
134
|
+
return `https://www.google.com/maps?q=${latLon.lat},${latLon.lon}`;
|
|
135
|
+
}, [latLon]);
|
|
136
|
+
|
|
137
|
+
const openStreetMapUrl = useMemo(() => {
|
|
138
|
+
if (!latLon) return null;
|
|
139
|
+
return `https://www.openstreetmap.org/?mlat=${latLon.lat}&mlon=${latLon.lon}#map=17/${latLon.lat}/${latLon.lon}`;
|
|
140
|
+
}, [latLon]);
|
|
141
|
+
|
|
142
|
+
const handleExportKmz = useCallback(() => {
|
|
143
|
+
if (!latLon || !geometryResult || !mapConversion) return;
|
|
144
|
+
try {
|
|
145
|
+
const exporter = new GLTFExporter(geometryResult);
|
|
146
|
+
const glb = new Uint8Array(exporter.exportGLB({ includeMetadata: true }));
|
|
147
|
+
const heading = ifcAngleToKmlHeading(mapConversion.xAxisAbscissa, mapConversion.xAxisOrdinate);
|
|
148
|
+
const kmz = buildKmz({
|
|
149
|
+
latLon,
|
|
150
|
+
altitude: mapConversion.orthogonalHeight,
|
|
151
|
+
heading,
|
|
152
|
+
glb,
|
|
153
|
+
name: 'IFC Model',
|
|
154
|
+
});
|
|
155
|
+
const blob = new Blob([kmz], { type: 'application/vnd.google-earth.kmz' });
|
|
156
|
+
const url = URL.createObjectURL(blob);
|
|
157
|
+
const a = document.createElement('a');
|
|
158
|
+
a.href = url;
|
|
159
|
+
a.download = 'model.kmz';
|
|
160
|
+
a.click();
|
|
161
|
+
URL.revokeObjectURL(url);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
console.error('KMZ export failed:', err);
|
|
164
|
+
}
|
|
165
|
+
}, [latLon, geometryResult, mapConversion]);
|
|
166
|
+
|
|
167
|
+
const isDarkRef = useRef(false);
|
|
168
|
+
|
|
169
|
+
const handleStyleToggle = useCallback(() => {
|
|
170
|
+
if (!mapRef.current) return;
|
|
171
|
+
isDarkRef.current = !isDarkRef.current;
|
|
172
|
+
mapRef.current.setStyle(
|
|
173
|
+
isDarkRef.current
|
|
174
|
+
? 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json'
|
|
175
|
+
: 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json',
|
|
176
|
+
);
|
|
177
|
+
// Re-add marker after style fully loads
|
|
178
|
+
if (markerRef.current && mapRef.current) {
|
|
179
|
+
mapRef.current.once('style.load', () => {
|
|
180
|
+
if (markerRef.current && mapRef.current) {
|
|
181
|
+
markerRef.current.addTo(mapRef.current);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}, []);
|
|
186
|
+
|
|
187
|
+
// Nothing to show if no georeferencing data
|
|
188
|
+
if (!mapConversion || !projectedCRS) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<div className="border-t border-zinc-100 dark:border-zinc-900">
|
|
194
|
+
{/* Header */}
|
|
195
|
+
<div className="flex items-center gap-2 px-3 py-1.5">
|
|
196
|
+
<MapIcon className="h-3 w-3 text-teal-500 shrink-0" />
|
|
197
|
+
<span className="font-bold text-[11px] text-zinc-700 dark:text-zinc-300 uppercase tracking-wide flex-1">
|
|
198
|
+
Location
|
|
199
|
+
</span>
|
|
200
|
+
{latLon && (
|
|
201
|
+
<span className="text-[10px] font-mono text-teal-600/70 dark:text-teal-500/60">
|
|
202
|
+
{latLon.lat.toFixed(5)}, {latLon.lon.toFixed(5)}
|
|
203
|
+
</span>
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
{/* Map container */}
|
|
208
|
+
{mapState === 'loading' && (
|
|
209
|
+
<div className="flex items-center justify-center h-[180px] bg-zinc-50 dark:bg-zinc-900/50">
|
|
210
|
+
<Loader2 className="h-4 w-4 text-teal-500 animate-spin" />
|
|
211
|
+
<span className="text-[10px] text-zinc-400 ml-2">Resolving coordinates...</span>
|
|
212
|
+
</div>
|
|
213
|
+
)}
|
|
214
|
+
|
|
215
|
+
{mapState === 'error' && (
|
|
216
|
+
<div className="flex items-center justify-center h-[60px] bg-zinc-50 dark:bg-zinc-900/50 gap-2 px-3">
|
|
217
|
+
<MapPinOff className="h-3.5 w-3.5 text-zinc-400 shrink-0" />
|
|
218
|
+
<span className="text-[10px] text-zinc-400">{error}</span>
|
|
219
|
+
</div>
|
|
220
|
+
)}
|
|
221
|
+
|
|
222
|
+
{(mapState === 'ready' || (mapState === 'loading' && latLon)) && (
|
|
223
|
+
<>
|
|
224
|
+
<div
|
|
225
|
+
ref={containerRef}
|
|
226
|
+
className="h-[180px] w-full [&_.maplibregl-ctrl-attrib]:!text-[7px] [&_.maplibregl-ctrl-attrib]:!bg-white/40 [&_.maplibregl-ctrl-attrib]:dark:!bg-black/30 [&_.maplibregl-ctrl-attrib]:!py-0 [&_.maplibregl-ctrl-attrib]:!px-1 [&_.maplibregl-ctrl-attrib]:!shadow-none [&_.maplibregl-ctrl-attrib]:!text-zinc-400/70 [&_.maplibregl-ctrl-attrib_a]:!text-zinc-400/70 [&_.maplibregl-ctrl-attrib]:!leading-normal"
|
|
227
|
+
style={{ minHeight: 180 }}
|
|
228
|
+
/>
|
|
229
|
+
|
|
230
|
+
{/* Action links */}
|
|
231
|
+
<div className="flex items-center gap-3 px-3 py-1.5 border-t border-zinc-100 dark:border-zinc-900">
|
|
232
|
+
{googleMapsUrl && (
|
|
233
|
+
<Tooltip>
|
|
234
|
+
<TooltipTrigger asChild>
|
|
235
|
+
<a
|
|
236
|
+
href={googleMapsUrl}
|
|
237
|
+
target="_blank"
|
|
238
|
+
rel="noopener noreferrer"
|
|
239
|
+
className="flex items-center gap-1 text-[10px] text-teal-600 dark:text-teal-400 hover:text-teal-800 dark:hover:text-teal-300 transition-colors"
|
|
240
|
+
>
|
|
241
|
+
<ExternalLink className="h-2.5 w-2.5" />
|
|
242
|
+
Google Maps
|
|
243
|
+
</a>
|
|
244
|
+
</TooltipTrigger>
|
|
245
|
+
<TooltipContent>Open model location in Google Maps</TooltipContent>
|
|
246
|
+
</Tooltip>
|
|
247
|
+
)}
|
|
248
|
+
{openStreetMapUrl && (
|
|
249
|
+
<Tooltip>
|
|
250
|
+
<TooltipTrigger asChild>
|
|
251
|
+
<a
|
|
252
|
+
href={openStreetMapUrl}
|
|
253
|
+
target="_blank"
|
|
254
|
+
rel="noopener noreferrer"
|
|
255
|
+
className="flex items-center gap-1 text-[10px] text-teal-600 dark:text-teal-400 hover:text-teal-800 dark:hover:text-teal-300 transition-colors"
|
|
256
|
+
>
|
|
257
|
+
<ExternalLink className="h-2.5 w-2.5" />
|
|
258
|
+
OpenStreetMap
|
|
259
|
+
</a>
|
|
260
|
+
</TooltipTrigger>
|
|
261
|
+
<TooltipContent>Open model location in OpenStreetMap</TooltipContent>
|
|
262
|
+
</Tooltip>
|
|
263
|
+
)}
|
|
264
|
+
{geometryResult && (
|
|
265
|
+
<Tooltip>
|
|
266
|
+
<TooltipTrigger asChild>
|
|
267
|
+
<button
|
|
268
|
+
onClick={handleExportKmz}
|
|
269
|
+
className="flex items-center gap-1 text-[10px] text-teal-600 dark:text-teal-400 hover:text-teal-800 dark:hover:text-teal-300 transition-colors"
|
|
270
|
+
>
|
|
271
|
+
<Globe2 className="h-2.5 w-2.5" />
|
|
272
|
+
Google Earth
|
|
273
|
+
</button>
|
|
274
|
+
</TooltipTrigger>
|
|
275
|
+
<TooltipContent>Download KMZ to open in Google Earth with model at correct position</TooltipContent>
|
|
276
|
+
</Tooltip>
|
|
277
|
+
)}
|
|
278
|
+
<button
|
|
279
|
+
onClick={handleStyleToggle}
|
|
280
|
+
className="ml-auto text-[10px] text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-300 transition-colors"
|
|
281
|
+
>
|
|
282
|
+
Toggle style
|
|
283
|
+
</button>
|
|
284
|
+
</div>
|
|
285
|
+
</>
|
|
286
|
+
)}
|
|
287
|
+
</div>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
@@ -18,11 +18,11 @@ import {
|
|
|
18
18
|
Hash,
|
|
19
19
|
Database,
|
|
20
20
|
Building2,
|
|
21
|
-
Globe,
|
|
22
21
|
Ruler,
|
|
23
22
|
} from 'lucide-react';
|
|
24
23
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
25
24
|
import { PropertySetCard } from './PropertySetCard';
|
|
25
|
+
import { GeoreferencingPanel } from './GeoreferencingPanel';
|
|
26
26
|
import type { PropertySet } from './encodingUtils';
|
|
27
27
|
import type { FederatedModel } from '@/store/types';
|
|
28
28
|
import { extractGeoreferencingOnDemand, extractLengthUnitScale, type IfcDataStore } from '@ifc-lite/parser';
|
|
@@ -147,7 +147,7 @@ export function ModelMetadataPanel({ model }: { model: FederatedModel }) {
|
|
|
147
147
|
{formatDate(model.loadedAt)}
|
|
148
148
|
</span>
|
|
149
149
|
</div>
|
|
150
|
-
{dataStore && (
|
|
150
|
+
{dataStore && dataStore.parseTime != null && (
|
|
151
151
|
<div className="flex items-center gap-3 px-3 py-2">
|
|
152
152
|
<Clock className="h-3.5 w-3.5 text-zinc-400 shrink-0" />
|
|
153
153
|
<span className="text-xs text-zinc-500">Parse Time</span>
|
|
@@ -212,74 +212,7 @@ export function ModelMetadataPanel({ model }: { model: FederatedModel }) {
|
|
|
212
212
|
</div>
|
|
213
213
|
|
|
214
214
|
{/* Georeferencing */}
|
|
215
|
-
{georef
|
|
216
|
-
<div className="border-b border-zinc-200 dark:border-zinc-800">
|
|
217
|
-
<div className="p-3 bg-teal-50/50 dark:bg-teal-950/20">
|
|
218
|
-
<div className="flex items-center gap-2">
|
|
219
|
-
<Globe className="h-3.5 w-3.5 text-teal-600 dark:text-teal-400" />
|
|
220
|
-
<h4 className="font-bold text-xs uppercase tracking-wide text-teal-700 dark:text-teal-300">
|
|
221
|
-
Georeferencing
|
|
222
|
-
</h4>
|
|
223
|
-
</div>
|
|
224
|
-
</div>
|
|
225
|
-
<div className="divide-y divide-zinc-100 dark:divide-zinc-900">
|
|
226
|
-
{georef.projectedCRS?.name && (
|
|
227
|
-
<div className="flex items-center gap-3 px-3 py-2">
|
|
228
|
-
<span className="text-xs text-zinc-500 shrink-0">CRS</span>
|
|
229
|
-
<span className="text-xs font-mono text-teal-700 dark:text-teal-400 ml-auto truncate max-w-[65%]">
|
|
230
|
-
{georef.projectedCRS.name}
|
|
231
|
-
</span>
|
|
232
|
-
</div>
|
|
233
|
-
)}
|
|
234
|
-
{georef.projectedCRS?.geodeticDatum && (
|
|
235
|
-
<div className="flex items-center gap-3 px-3 py-2">
|
|
236
|
-
<span className="text-xs text-zinc-500 shrink-0">Geodetic Datum</span>
|
|
237
|
-
<span className="text-xs font-mono text-teal-700 dark:text-teal-400 ml-auto">
|
|
238
|
-
{georef.projectedCRS.geodeticDatum}
|
|
239
|
-
</span>
|
|
240
|
-
</div>
|
|
241
|
-
)}
|
|
242
|
-
{georef.projectedCRS?.mapProjection && (
|
|
243
|
-
<div className="flex items-center gap-3 px-3 py-2">
|
|
244
|
-
<span className="text-xs text-zinc-500 shrink-0">Projection</span>
|
|
245
|
-
<span className="text-xs font-mono text-teal-700 dark:text-teal-400 ml-auto truncate max-w-[65%]">
|
|
246
|
-
{georef.projectedCRS.mapProjection}
|
|
247
|
-
</span>
|
|
248
|
-
</div>
|
|
249
|
-
)}
|
|
250
|
-
{georef.mapConversion && (
|
|
251
|
-
<>
|
|
252
|
-
<div className="flex items-center gap-3 px-3 py-2">
|
|
253
|
-
<span className="text-xs text-zinc-500 shrink-0">Eastings</span>
|
|
254
|
-
<span className="text-xs font-mono text-teal-700 dark:text-teal-400 ml-auto tabular-nums">
|
|
255
|
-
{georef.mapConversion.eastings.toFixed(3)}
|
|
256
|
-
</span>
|
|
257
|
-
</div>
|
|
258
|
-
<div className="flex items-center gap-3 px-3 py-2">
|
|
259
|
-
<span className="text-xs text-zinc-500 shrink-0">Northings</span>
|
|
260
|
-
<span className="text-xs font-mono text-teal-700 dark:text-teal-400 ml-auto tabular-nums">
|
|
261
|
-
{georef.mapConversion.northings.toFixed(3)}
|
|
262
|
-
</span>
|
|
263
|
-
</div>
|
|
264
|
-
<div className="flex items-center gap-3 px-3 py-2">
|
|
265
|
-
<span className="text-xs text-zinc-500 shrink-0">Height</span>
|
|
266
|
-
<span className="text-xs font-mono text-teal-700 dark:text-teal-400 ml-auto tabular-nums">
|
|
267
|
-
{georef.mapConversion.orthogonalHeight.toFixed(3)}
|
|
268
|
-
</span>
|
|
269
|
-
</div>
|
|
270
|
-
{georef.mapConversion.scale != null && georef.mapConversion.scale !== 1.0 && (
|
|
271
|
-
<div className="flex items-center gap-3 px-3 py-2">
|
|
272
|
-
<span className="text-xs text-zinc-500 shrink-0">Scale</span>
|
|
273
|
-
<span className="text-xs font-mono text-teal-700 dark:text-teal-400 ml-auto">
|
|
274
|
-
{georef.mapConversion.scale}
|
|
275
|
-
</span>
|
|
276
|
-
</div>
|
|
277
|
-
)}
|
|
278
|
-
</>
|
|
279
|
-
)}
|
|
280
|
-
</div>
|
|
281
|
-
</div>
|
|
282
|
-
)}
|
|
215
|
+
<GeoreferencingPanel georef={georef} modelId={model.id} enableEditing schemaVersion={model.schemaVersion} coordinateInfo={model.geometryResult?.coordinateInfo} geometryResult={model.geometryResult} />
|
|
283
216
|
|
|
284
217
|
{/* IfcProject Data */}
|
|
285
218
|
{projectData && (
|
package/src/hooks/bcfIdLookup.ts
CHANGED
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
* accounting for multi-model federation offsets and single-model fallback.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import type { FederatedModel
|
|
12
|
+
import type { FederatedModel } from '@/store/types';
|
|
13
|
+
import type { IfcDataStore } from '@ifc-lite/parser';
|
|
14
|
+
import { fromGlobalIdFromModels, toGlobalIdFromModels } from '@/store/globalId';
|
|
13
15
|
|
|
14
16
|
export interface IdLookupResult {
|
|
15
17
|
expressId: number;
|
|
@@ -29,8 +31,10 @@ export function globalIdToExpressId(
|
|
|
29
31
|
for (const [modelId, model] of models.entries()) {
|
|
30
32
|
const localExpressId = model.ifcDataStore?.entities?.getExpressIdByGlobalId(globalIdString);
|
|
31
33
|
if (localExpressId !== undefined && localExpressId > 0) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
return {
|
|
35
|
+
expressId: toGlobalIdFromModels(models, modelId, localExpressId),
|
|
36
|
+
modelId,
|
|
37
|
+
};
|
|
34
38
|
}
|
|
35
39
|
}
|
|
36
40
|
// Single-model fallback
|
|
@@ -52,14 +56,12 @@ export function expressIdToGlobalId(
|
|
|
52
56
|
models: Map<string, FederatedModel>,
|
|
53
57
|
ifcDataStore: IfcDataStore | null | undefined,
|
|
54
58
|
): string | null {
|
|
55
|
-
// Multi-model path:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (globalIdString) return globalIdString;
|
|
62
|
-
}
|
|
59
|
+
// Multi-model path: resolve through the centralized reverse conversion helper
|
|
60
|
+
const resolved = fromGlobalIdFromModels(models, expressId);
|
|
61
|
+
if (resolved && resolved.modelId !== 'legacy') {
|
|
62
|
+
const model = models.get(resolved.modelId);
|
|
63
|
+
const globalIdString = model?.ifcDataStore?.entities?.getGlobalId(resolved.expressId);
|
|
64
|
+
if (globalIdString) return globalIdString;
|
|
63
65
|
}
|
|
64
66
|
// Single-model fallback: use legacy ifcDataStore directly
|
|
65
67
|
if (models.size === 0 && ifcDataStore?.entities) {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import type { IDSValidationReport } from '@ifc-lite/ids';
|
|
13
13
|
import type { GeometryResult } from '@ifc-lite/geometry';
|
|
14
|
+
import { toGlobalIdFromModels } from '../../store/globalId.js';
|
|
14
15
|
|
|
15
16
|
/** RGBA color tuple in 0-1 range */
|
|
16
17
|
export type ColorTuple = [number, number, number, number];
|
|
@@ -66,10 +67,7 @@ export function buildValidationColorUpdates(
|
|
|
66
67
|
const globalIdsToUpdate = new Set<number>();
|
|
67
68
|
for (const specResult of report.specificationResults) {
|
|
68
69
|
for (const entityResult of specResult.entityResults) {
|
|
69
|
-
const
|
|
70
|
-
const globalId = model
|
|
71
|
-
? entityResult.expressId + (model.idOffset ?? 0)
|
|
72
|
-
: entityResult.expressId;
|
|
70
|
+
const globalId = toGlobalIdFromModels(models, entityResult.modelId, entityResult.expressId);
|
|
73
71
|
globalIdsToUpdate.add(globalId);
|
|
74
72
|
}
|
|
75
73
|
}
|
|
@@ -86,10 +84,7 @@ export function buildValidationColorUpdates(
|
|
|
86
84
|
// Process all entity results
|
|
87
85
|
for (const specResult of report.specificationResults) {
|
|
88
86
|
for (const entityResult of specResult.entityResults) {
|
|
89
|
-
const
|
|
90
|
-
const globalId = model
|
|
91
|
-
? entityResult.expressId + (model.idOffset ?? 0)
|
|
92
|
-
: entityResult.expressId;
|
|
87
|
+
const globalId = toGlobalIdFromModels(models, entityResult.modelId, entityResult.expressId);
|
|
93
88
|
|
|
94
89
|
if (entityResult.passed && displayOptions.highlightPassed) {
|
|
95
90
|
colorUpdates.set(globalId, passedClr);
|
package/src/hooks/useIDS.ts
CHANGED
|
@@ -213,12 +213,28 @@ export function useIDS(options: UseIDSOptions = {}): UseIDSResult {
|
|
|
213
213
|
const setSelectedEntityId = useViewerStore((s) => s.setSelectedEntityId);
|
|
214
214
|
const setSelectedEntity = useViewerStore((s) => s.setSelectedEntity);
|
|
215
215
|
const setIsolatedEntities = useViewerStore((s) => s.setIsolatedEntities);
|
|
216
|
+
const toGlobalId = useViewerStore((s) => s.toGlobalId);
|
|
216
217
|
const cameraCallbacks = useViewerStore((s) => s.cameraCallbacks);
|
|
217
218
|
const geometryResult = useViewerStore((s) => s.geometryResult);
|
|
218
219
|
|
|
219
220
|
// Ref to store original colors before IDS color overrides
|
|
220
221
|
const originalColorsRef = useRef<Map<number, ColorTuple>>(new Map());
|
|
221
222
|
|
|
223
|
+
const toViewerGlobalId = useCallback((modelId: string, expressId: number): number | undefined => {
|
|
224
|
+
if (
|
|
225
|
+
modelId === '__legacy__'
|
|
226
|
+
|| modelId === 'legacy'
|
|
227
|
+
|| models.size === 0
|
|
228
|
+
|| (models.size === 1 && !models.has(modelId))
|
|
229
|
+
) {
|
|
230
|
+
return expressId;
|
|
231
|
+
}
|
|
232
|
+
if (!models.has(modelId)) {
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
return toGlobalId(modelId, expressId);
|
|
236
|
+
}, [models, toGlobalId]);
|
|
237
|
+
|
|
222
238
|
// Ref to access geometryResult without creating callback dependencies (prevents infinite loops)
|
|
223
239
|
const geometryResultRef = useRef(geometryResult);
|
|
224
240
|
geometryResultRef.current = geometryResult;
|
|
@@ -357,7 +373,7 @@ export function useIDS(options: UseIDSOptions = {}): UseIDSResult {
|
|
|
357
373
|
|
|
358
374
|
// Sync to viewer selection
|
|
359
375
|
// Handle legacy mode vs federation mode
|
|
360
|
-
const isLegacyMode = modelId === '__legacy__' || models.size === 0;
|
|
376
|
+
const isLegacyMode = modelId === '__legacy__' || modelId === 'legacy' || models.size === 0;
|
|
361
377
|
|
|
362
378
|
if (isLegacyMode) {
|
|
363
379
|
// Legacy mode: globalId equals expressId, use 'legacy' for selection
|
|
@@ -365,9 +381,9 @@ export function useIDS(options: UseIDSOptions = {}): UseIDSResult {
|
|
|
365
381
|
// Use 'legacy' as the modelId for PropertiesPanel compatibility
|
|
366
382
|
setSelectedEntity({ modelId: 'legacy', expressId });
|
|
367
383
|
} else {
|
|
368
|
-
// Federation mode:
|
|
369
|
-
const
|
|
370
|
-
|
|
384
|
+
// Federation mode: use the store helper so ID resolution stays centralized.
|
|
385
|
+
const globalId = toViewerGlobalId(modelId, expressId);
|
|
386
|
+
if (globalId == null) return;
|
|
371
387
|
setSelectedEntityId(globalId);
|
|
372
388
|
setSelectedEntity({ modelId, expressId });
|
|
373
389
|
}
|
|
@@ -378,7 +394,7 @@ export function useIDS(options: UseIDSOptions = {}): UseIDSResult {
|
|
|
378
394
|
cameraCallbacks.frameSelection?.();
|
|
379
395
|
}, 50);
|
|
380
396
|
}
|
|
381
|
-
}, [setIdsActiveEntity, setSelectedEntityId, setSelectedEntity, models, cameraCallbacks]);
|
|
397
|
+
}, [setIdsActiveEntity, setSelectedEntityId, setSelectedEntity, models, cameraCallbacks, toViewerGlobalId]);
|
|
382
398
|
|
|
383
399
|
const clearEntitySelection = useCallback(() => {
|
|
384
400
|
setIdsActiveEntity(null);
|
|
@@ -462,15 +478,14 @@ export function useIDS(options: UseIDSOptions = {}): UseIDSResult {
|
|
|
462
478
|
const modelId = key.substring(0, lastColonIndex);
|
|
463
479
|
const expressIdStr = key.substring(lastColonIndex + 1);
|
|
464
480
|
const expressId = parseInt(expressIdStr, 10);
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
failedIds.add(globalId);
|
|
481
|
+
const globalId = toViewerGlobalId(modelId, expressId);
|
|
482
|
+
if (globalId != null) failedIds.add(globalId);
|
|
468
483
|
}
|
|
469
484
|
|
|
470
485
|
if (failedIds.size > 0) {
|
|
471
486
|
setIsolatedEntities(failedIds);
|
|
472
487
|
}
|
|
473
|
-
}, [idsFailedEntityIds,
|
|
488
|
+
}, [idsFailedEntityIds, setIsolatedEntities, toViewerGlobalId]);
|
|
474
489
|
|
|
475
490
|
const isolatePassed = useCallback(() => {
|
|
476
491
|
const passedIds = new Set<number>();
|
|
@@ -480,15 +495,14 @@ export function useIDS(options: UseIDSOptions = {}): UseIDSResult {
|
|
|
480
495
|
const modelId = key.substring(0, lastColonIndex);
|
|
481
496
|
const expressIdStr = key.substring(lastColonIndex + 1);
|
|
482
497
|
const expressId = parseInt(expressIdStr, 10);
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
passedIds.add(globalId);
|
|
498
|
+
const globalId = toViewerGlobalId(modelId, expressId);
|
|
499
|
+
if (globalId != null) passedIds.add(globalId);
|
|
486
500
|
}
|
|
487
501
|
|
|
488
502
|
if (passedIds.size > 0) {
|
|
489
503
|
setIsolatedEntities(passedIds);
|
|
490
504
|
}
|
|
491
|
-
}, [idsPassedEntityIds,
|
|
505
|
+
}, [idsPassedEntityIds, setIsolatedEntities, toViewerGlobalId]);
|
|
492
506
|
|
|
493
507
|
const clearIsolation = useCallback(() => {
|
|
494
508
|
setIsolatedEntities(null);
|
|
@@ -632,7 +646,8 @@ export function useIDS(options: UseIDSOptions = {}): UseIDSResult {
|
|
|
632
646
|
// Find matching model geometry
|
|
633
647
|
for (const modelData of allMeshData) {
|
|
634
648
|
if (modelData.modelId === entity.modelId || allMeshData.length === 1) {
|
|
635
|
-
const globalExpressId = entity.
|
|
649
|
+
const globalExpressId = toViewerGlobalId(entity.modelId, entity.expressId);
|
|
650
|
+
if (globalExpressId == null) break;
|
|
636
651
|
const bounds = getEntityBounds(
|
|
637
652
|
modelData.meshes as Parameters<typeof getEntityBounds>[0],
|
|
638
653
|
globalExpressId,
|
|
@@ -700,8 +715,8 @@ export function useIDS(options: UseIDSOptions = {}): UseIDSResult {
|
|
|
700
715
|
if (!bounds) continue;
|
|
701
716
|
|
|
702
717
|
// Find the global expressId for isolation (direct Map lookup)
|
|
703
|
-
const
|
|
704
|
-
|
|
718
|
+
const globalExpressId = toViewerGlobalId(entity.modelId, entity.expressId);
|
|
719
|
+
if (globalExpressId == null) continue;
|
|
705
720
|
|
|
706
721
|
// Frame the entity bounds directly via camera (properly centers the object)
|
|
707
722
|
// duration=1 (not 0) because the animator skips updates when duration===0,
|
|
@@ -38,7 +38,7 @@ export interface IfcxDataStore extends IfcDataStore {
|
|
|
38
38
|
/** Original buffers for re-composition when adding overlays */
|
|
39
39
|
_federatedBuffers?: Array<{ buffer: ArrayBuffer; name: string }>;
|
|
40
40
|
/** Composition statistics */
|
|
41
|
-
_compositionStats?: {
|
|
41
|
+
_compositionStats?: { layersUsed: number; inheritanceResolutions: number; crossLayerReferences: number };
|
|
42
42
|
/** Layer info for display */
|
|
43
43
|
_layerInfo?: Array<{ id: string; name: string; meshCount: number }>;
|
|
44
44
|
}
|
|
@@ -630,7 +630,7 @@ export function useIfcFederation() {
|
|
|
630
630
|
name: b.name,
|
|
631
631
|
})),
|
|
632
632
|
_compositionStats: result.compositionStats,
|
|
633
|
-
} as IfcxDataStore;
|
|
633
|
+
} as unknown as IfcxDataStore;
|
|
634
634
|
|
|
635
635
|
// IfcxDataStore extends IfcDataStore (with schemaVersion: 'IFC5'), so this is safe
|
|
636
636
|
setIfcDataStore(dataStore);
|