@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,112 @@
|
|
|
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
|
+
* KMZ Exporter — packages a GLB model with a KML file into a KMZ archive
|
|
7
|
+
* so Google Earth can display the 3D model at its correct geolocation.
|
|
8
|
+
*
|
|
9
|
+
* KMZ is just a ZIP archive containing:
|
|
10
|
+
* doc.kml — KML document with a <Model> positioned at lat/lon/alt
|
|
11
|
+
* model.glb — the 3D model in glTF binary format
|
|
12
|
+
*
|
|
13
|
+
* The KML <Model> element uses:
|
|
14
|
+
* <Location> — latitude, longitude, altitude from reprojected georef
|
|
15
|
+
* <Orientation> — heading derived from the angle-to-grid-north
|
|
16
|
+
* <Scale> — uniform scale (1:1)
|
|
17
|
+
* <Link> — relative path to the GLB inside the archive
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { zipSync } from 'fflate';
|
|
21
|
+
import type { LatLon } from './reproject';
|
|
22
|
+
|
|
23
|
+
export interface KmzOptions {
|
|
24
|
+
/** WGS84 coordinates of the model origin */
|
|
25
|
+
latLon: LatLon;
|
|
26
|
+
/** Orthogonal height (elevation) in metres */
|
|
27
|
+
altitude: number;
|
|
28
|
+
/** Heading in degrees clockwise from north (0 = north, 90 = east) */
|
|
29
|
+
heading: number;
|
|
30
|
+
/** GLB model binary data */
|
|
31
|
+
glb: Uint8Array;
|
|
32
|
+
/** Display name for the placemark */
|
|
33
|
+
name?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Convert the IFC angle-to-grid-north (counterclockwise from east) into
|
|
38
|
+
* a KML heading (clockwise from north).
|
|
39
|
+
*
|
|
40
|
+
* IFC convention: atan2(XAxisOrdinate, XAxisAbscissa) gives the CCW angle
|
|
41
|
+
* from the map east axis to the local X axis.
|
|
42
|
+
*
|
|
43
|
+
* KML convention: heading is CW from north (0=N, 90=E, 180=S, 270=W).
|
|
44
|
+
*/
|
|
45
|
+
export function ifcAngleToKmlHeading(
|
|
46
|
+
xAxisAbscissa?: number,
|
|
47
|
+
xAxisOrdinate?: number,
|
|
48
|
+
): number {
|
|
49
|
+
if (xAxisAbscissa === undefined || xAxisOrdinate === undefined) return 0;
|
|
50
|
+
const angleFromEastCcw = Math.atan2(xAxisOrdinate, xAxisAbscissa) * (180 / Math.PI);
|
|
51
|
+
// Convert: heading = 90 - angle (CCW from east → CW from north)
|
|
52
|
+
const heading = 90 - angleFromEastCcw;
|
|
53
|
+
// Normalize to [0, 360)
|
|
54
|
+
return ((heading % 360) + 360) % 360;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function buildKml(opts: KmzOptions): string {
|
|
58
|
+
const name = escapeXml(opts.name ?? 'IFC Model');
|
|
59
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
60
|
+
<kml xmlns="http://www.opengis.net/kml/2.2">
|
|
61
|
+
<Document>
|
|
62
|
+
<name>${name}</name>
|
|
63
|
+
<Placemark>
|
|
64
|
+
<name>${name}</name>
|
|
65
|
+
<Model id="model">
|
|
66
|
+
<altitudeMode>relativeToGround</altitudeMode>
|
|
67
|
+
<Location>
|
|
68
|
+
<longitude>${opts.latLon.lon}</longitude>
|
|
69
|
+
<latitude>${opts.latLon.lat}</latitude>
|
|
70
|
+
<altitude>${opts.altitude}</altitude>
|
|
71
|
+
</Location>
|
|
72
|
+
<Orientation>
|
|
73
|
+
<heading>${opts.heading}</heading>
|
|
74
|
+
<tilt>0</tilt>
|
|
75
|
+
<roll>0</roll>
|
|
76
|
+
</Orientation>
|
|
77
|
+
<Scale>
|
|
78
|
+
<x>1</x>
|
|
79
|
+
<y>1</y>
|
|
80
|
+
<z>1</z>
|
|
81
|
+
</Scale>
|
|
82
|
+
<Link>
|
|
83
|
+
<href>model.glb</href>
|
|
84
|
+
</Link>
|
|
85
|
+
</Model>
|
|
86
|
+
</Placemark>
|
|
87
|
+
</Document>
|
|
88
|
+
</kml>`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function escapeXml(s: string): string {
|
|
92
|
+
return s
|
|
93
|
+
.replace(/&/g, '&')
|
|
94
|
+
.replace(/</g, '<')
|
|
95
|
+
.replace(/>/g, '>')
|
|
96
|
+
.replace(/"/g, '"')
|
|
97
|
+
.replace(/'/g, ''');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Build a KMZ file (ZIP archive) containing doc.kml + model.glb.
|
|
102
|
+
* Returns the KMZ as a Uint8Array ready for download.
|
|
103
|
+
*/
|
|
104
|
+
export function buildKmz(opts: KmzOptions): Uint8Array {
|
|
105
|
+
const kml = buildKml(opts);
|
|
106
|
+
const kmlBytes = new TextEncoder().encode(kml);
|
|
107
|
+
|
|
108
|
+
return zipSync({
|
|
109
|
+
'doc.kml': kmlBytes,
|
|
110
|
+
'model.glb': opts.glb,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
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
|
+
* Coordinate reprojection utilities.
|
|
7
|
+
*
|
|
8
|
+
* Converts projected coordinates (e.g. UTM eastings/northings) from an
|
|
9
|
+
* IfcMapConversion + IfcProjectedCRS pair into WGS84 longitude/latitude
|
|
10
|
+
* so they can be displayed on a web map.
|
|
11
|
+
*
|
|
12
|
+
* proj4 definitions are resolved from:
|
|
13
|
+
* 1. The bundled EPSG index (@ifc-lite/data) — covers all 7000+ codes
|
|
14
|
+
* 2. Programmatically constructed (UTM zones, well-known codes)
|
|
15
|
+
* 3. Fetched from epsg.io at runtime as last resort
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import proj4 from 'proj4';
|
|
19
|
+
import type { MapConversion, ProjectedCRS } from '@ifc-lite/parser';
|
|
20
|
+
import type { CoordinateInfo } from '@ifc-lite/geometry';
|
|
21
|
+
import { lookupProj4 } from '@ifc-lite/data';
|
|
22
|
+
|
|
23
|
+
export interface LatLon {
|
|
24
|
+
lat: number;
|
|
25
|
+
lon: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Cache resolved projection definitions (from any source).
|
|
29
|
+
const projDefCache = new Map<string, string | null>();
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Extract EPSG numeric code from a CRS name like "EPSG:32632" or "EPSG 2056".
|
|
33
|
+
*/
|
|
34
|
+
function extractEpsgCode(crs: ProjectedCRS): string | null {
|
|
35
|
+
const match = crs.name?.match(/EPSG[:\s]*(\d+)/i);
|
|
36
|
+
return match ? match[1] : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Build a proj4 definition string for a UTM zone.
|
|
41
|
+
*/
|
|
42
|
+
function utmProj4String(zone: string): string | null {
|
|
43
|
+
const match = zone.match(/^(\d{1,2})([NS])$/i);
|
|
44
|
+
if (!match) return null;
|
|
45
|
+
const zoneNum = parseInt(match[1], 10);
|
|
46
|
+
const isNorth = match[2].toUpperCase() === 'N';
|
|
47
|
+
if (zoneNum < 1 || zoneNum > 60) return null;
|
|
48
|
+
return `+proj=utm +zone=${zoneNum}${isNorth ? '' : ' +south'} +datum=WGS84 +units=m +no_defs`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Well-known +towgs84 approximations for datums that normally use grid files.
|
|
53
|
+
* These are accurate to ~1-5m, which is sufficient for map display.
|
|
54
|
+
* Grid files (like OSTN15_NTv2_OSGBtoETRS.gsb) cannot run in the browser.
|
|
55
|
+
*/
|
|
56
|
+
const DATUM_TOWGS84: Record<string, string> = {
|
|
57
|
+
'airy': '+towgs84=446.448,-125.157,542.06,0.15,0.247,0.842,-20.489', // OSGB36 (UK)
|
|
58
|
+
'clrk66': '+towgs84=-8,160,176,0,0,0,0', // NAD27 (approx)
|
|
59
|
+
'GRS80': '+towgs84=0,0,0,0,0,0,0', // GRS80-based (NAD83≈WGS84)
|
|
60
|
+
'bessel': '+towgs84=598.1,73.7,418.2,0.202,0.045,-2.455,6.7', // DHDN (Germany)
|
|
61
|
+
'intl': '+towgs84=-87,-98,-121,0,0,0,0', // NZGD49 (NZ)
|
|
62
|
+
'aust_SA': '+towgs84=-134,-48,149,0,0,0,0', // AGD84 (Australia)
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Strip +nadgrids=... from a proj4 string and add a +towgs84 approximation
|
|
67
|
+
* based on the ellipsoid. Grid files cannot be loaded in the browser.
|
|
68
|
+
*/
|
|
69
|
+
function sanitizeProj4(def: string): string {
|
|
70
|
+
if (!def.includes('+nadgrids') || def.includes('+nadgrids=@null')) return def;
|
|
71
|
+
|
|
72
|
+
// Extract the ellipsoid to find the right towgs84 approximation
|
|
73
|
+
const ellpsMatch = def.match(/\+ellps=(\S+)/);
|
|
74
|
+
const ellps = ellpsMatch?.[1] ?? '';
|
|
75
|
+
const towgs84 = DATUM_TOWGS84[ellps] ?? '+towgs84=0,0,0,0,0,0,0';
|
|
76
|
+
|
|
77
|
+
// Remove +nadgrids=... and add +towgs84
|
|
78
|
+
return def.replace(/\+nadgrids=\S+/g, '').replace(/\s+/g, ' ').trim() + ' ' + towgs84;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Fetch a proj4 definition string from epsg.io (last-resort fallback).
|
|
83
|
+
*/
|
|
84
|
+
async function fetchProj4Def(epsgCode: string): Promise<string | null> {
|
|
85
|
+
try {
|
|
86
|
+
const resp = await fetch(`https://epsg.io/${epsgCode}.proj4`);
|
|
87
|
+
if (!resp.ok) return null;
|
|
88
|
+
const text = (await resp.text()).trim();
|
|
89
|
+
if (!text || text.startsWith('<') || text.startsWith('{') || !text.includes('+')) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return text;
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Resolve a proj4 definition for the given ProjectedCRS.
|
|
100
|
+
*
|
|
101
|
+
* Resolution order:
|
|
102
|
+
* 1. Cache hit
|
|
103
|
+
* 2. Bundled EPSG index (7000+ codes with proj4 strings)
|
|
104
|
+
* 3. UTM zone heuristic (from CRS metadata)
|
|
105
|
+
* 4. Fetch from epsg.io (network fallback)
|
|
106
|
+
*/
|
|
107
|
+
export async function resolveProjection(crs: ProjectedCRS): Promise<string | null> {
|
|
108
|
+
const code = extractEpsgCode(crs);
|
|
109
|
+
|
|
110
|
+
// 1. Check cache
|
|
111
|
+
if (code && projDefCache.has(code)) {
|
|
112
|
+
return projDefCache.get(code) ?? null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 2. Bundled EPSG index (primary source — all 7000+ codes)
|
|
116
|
+
if (code) {
|
|
117
|
+
try {
|
|
118
|
+
const bundled = await lookupProj4(code);
|
|
119
|
+
if (bundled) {
|
|
120
|
+
const sanitized = sanitizeProj4(bundled);
|
|
121
|
+
projDefCache.set(code, sanitized);
|
|
122
|
+
return sanitized;
|
|
123
|
+
}
|
|
124
|
+
} catch {
|
|
125
|
+
// EPSG index not loaded yet, continue to fallbacks
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 3. UTM zone heuristic
|
|
130
|
+
if (crs.mapZone) {
|
|
131
|
+
const def = utmProj4String(crs.mapZone);
|
|
132
|
+
if (def) {
|
|
133
|
+
if (code) projDefCache.set(code, def);
|
|
134
|
+
return def;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const name = crs.name?.toUpperCase() ?? '';
|
|
138
|
+
const utmMatch = name.match(/UTM\s+ZONE\s+(\d{1,2}[NS])/i)
|
|
139
|
+
?? crs.description?.match(/UTM\s+zone\s+(\d{1,2}[NS])/i);
|
|
140
|
+
if (utmMatch) {
|
|
141
|
+
const def = utmProj4String(utmMatch[1]);
|
|
142
|
+
if (def) {
|
|
143
|
+
if (code) projDefCache.set(code, def);
|
|
144
|
+
return def;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 4. Network fallback — fetch from epsg.io
|
|
149
|
+
if (code) {
|
|
150
|
+
const raw = await fetchProj4Def(code);
|
|
151
|
+
const fetched = raw ? sanitizeProj4(raw) : null;
|
|
152
|
+
projDefCache.set(code, fetched);
|
|
153
|
+
return fetched;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Compute the model center in the projected CRS (easting, northing).
|
|
161
|
+
*
|
|
162
|
+
* The coordinate pipeline is:
|
|
163
|
+
* 1. WASM extracts IFC positions (Z-up) and may apply RTC offset (wasmRtcOffset, Z-up)
|
|
164
|
+
* 2. Mesh collector converts Z-up → Y-up: viewerX = ifcX, viewerY = ifcZ, viewerZ = -ifcY
|
|
165
|
+
* 3. CoordinateHandler may apply originShift (Y-up)
|
|
166
|
+
*
|
|
167
|
+
* To recover IFC world coordinates (Z-up) from the viewer bounds:
|
|
168
|
+
* world_yup = bounds_center + originShift + wasmRtcOffset_as_yup
|
|
169
|
+
* ifc_x = world_yup.x, ifc_y = -world_yup.z, ifc_z = world_yup.y
|
|
170
|
+
*
|
|
171
|
+
* Then the projected CRS coordinates are:
|
|
172
|
+
* easting = mapConversion.eastings + scale * (cos*ifc_x - sin*ifc_y)
|
|
173
|
+
* northing = mapConversion.northings + scale * (sin*ifc_x + cos*ifc_y)
|
|
174
|
+
*/
|
|
175
|
+
function computeProjectedCenter(
|
|
176
|
+
conversion: MapConversion,
|
|
177
|
+
coordinateInfo?: CoordinateInfo,
|
|
178
|
+
): { easting: number; northing: number } {
|
|
179
|
+
let ifcX = 0;
|
|
180
|
+
let ifcY = 0;
|
|
181
|
+
|
|
182
|
+
if (coordinateInfo) {
|
|
183
|
+
const bounds = coordinateInfo.originalBounds;
|
|
184
|
+
const shift = coordinateInfo.originShift;
|
|
185
|
+
const rtc = coordinateInfo.wasmRtcOffset;
|
|
186
|
+
|
|
187
|
+
// Convert WASM RTC offset from IFC Z-up to viewer Y-up
|
|
188
|
+
const rtcYup = rtc
|
|
189
|
+
? { x: rtc.x, y: rtc.z, z: -rtc.y }
|
|
190
|
+
: { x: 0, y: 0, z: 0 };
|
|
191
|
+
|
|
192
|
+
// Bounds center in viewer Y-up (scene-local)
|
|
193
|
+
const cx = (bounds.min.x + bounds.max.x) / 2;
|
|
194
|
+
const cz = (bounds.min.z + bounds.max.z) / 2;
|
|
195
|
+
|
|
196
|
+
// World Y-up = scene_local + originShift + wasmRtcOffset_yup
|
|
197
|
+
const worldYupX = cx + shift.x + rtcYup.x;
|
|
198
|
+
const worldYupZ = cz + shift.z + rtcYup.z;
|
|
199
|
+
|
|
200
|
+
// Convert Y-up to IFC Z-up: ifc_x = viewer_x, ifc_y = -viewer_z
|
|
201
|
+
ifcX = worldYupX;
|
|
202
|
+
ifcY = -worldYupZ;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Apply MapConversion rotation + scale + offset
|
|
206
|
+
const scale = conversion.scale ?? 1.0;
|
|
207
|
+
const abscissa = conversion.xAxisAbscissa ?? 1.0;
|
|
208
|
+
const ordinate = conversion.xAxisOrdinate ?? 0.0;
|
|
209
|
+
|
|
210
|
+
const easting = conversion.eastings + scale * (abscissa * ifcX - ordinate * ifcY);
|
|
211
|
+
const northing = conversion.northings + scale * (ordinate * ifcX + abscissa * ifcY);
|
|
212
|
+
|
|
213
|
+
return { easting, northing };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Reproject the model center from the projected CRS to WGS84 lat/lon.
|
|
218
|
+
*
|
|
219
|
+
* Uses the model's actual geometry bounds + RTC offset to determine where
|
|
220
|
+
* the model sits in the projected coordinate system, then reprojects to WGS84.
|
|
221
|
+
*
|
|
222
|
+
* @param conversion IfcMapConversion (offset, rotation, scale)
|
|
223
|
+
* @param crs IfcProjectedCRS (EPSG code)
|
|
224
|
+
* @param coordinateInfo Geometry coordinate info with bounds and RTC offset
|
|
225
|
+
*/
|
|
226
|
+
export async function reprojectToLatLon(
|
|
227
|
+
conversion: MapConversion,
|
|
228
|
+
crs: ProjectedCRS,
|
|
229
|
+
coordinateInfo?: CoordinateInfo,
|
|
230
|
+
): Promise<LatLon | null> {
|
|
231
|
+
const projDef = await resolveProjection(crs);
|
|
232
|
+
if (!projDef) return null;
|
|
233
|
+
|
|
234
|
+
const { easting, northing } = computeProjectedCenter(conversion, coordinateInfo);
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
const [lon, lat] = proj4(projDef, 'WGS84', [easting, northing]);
|
|
238
|
+
if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null;
|
|
239
|
+
if (lat < -90 || lat > 90 || lon < -180 || lon > 180) return null;
|
|
240
|
+
return { lat, lon };
|
|
241
|
+
} catch {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
}
|
package/src/lib/lens/adapter.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
extractClassificationsOnDemand,
|
|
19
19
|
extractMaterialsOnDemand,
|
|
20
20
|
} from '@ifc-lite/parser';
|
|
21
|
+
import { toGlobalIdFromModels } from '@/store/globalId';
|
|
21
22
|
import type { FederatedModel } from '@/store/types';
|
|
22
23
|
|
|
23
24
|
interface ModelEntry {
|
|
@@ -80,12 +81,13 @@ export function createLensDataProvider(
|
|
|
80
81
|
},
|
|
81
82
|
|
|
82
83
|
forEachEntity(callback: (globalId: number, modelId: string) => void): void {
|
|
84
|
+
const models = new Map(entries.map((entry) => [entry.id, { idOffset: entry.idOffset }]));
|
|
83
85
|
for (const entry of entries) {
|
|
84
86
|
const entities = entry.ifcDataStore.entities;
|
|
85
87
|
if (!entities) continue;
|
|
86
88
|
for (let i = 0; i < entities.count; i++) {
|
|
87
89
|
const expressId = entities.expressId[i];
|
|
88
|
-
callback(
|
|
90
|
+
callback(toGlobalIdFromModels(models, entry.id, expressId), entry.id);
|
|
89
91
|
}
|
|
90
92
|
}
|
|
91
93
|
},
|
package/src/main.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import ReactDOM from 'react-dom/client';
|
|
|
11
11
|
import { ClerkProvider } from '@clerk/clerk-react';
|
|
12
12
|
import { App } from './App';
|
|
13
13
|
import './index.css';
|
|
14
|
+
import 'maplibre-gl/dist/maplibre-gl.css';
|
|
14
15
|
|
|
15
16
|
const clerkPublishableKey = (import.meta.env.VITE_CLERK_PUBLISHABLE_KEY as string | undefined)?.trim();
|
|
16
17
|
|
|
@@ -119,6 +119,13 @@ function escapeCsv(value: string, sep: string): string {
|
|
|
119
119
|
* on the same LocalBackend, providing full export support for both
|
|
120
120
|
* direct dispatch calls and SDK namespace usage.
|
|
121
121
|
*/
|
|
122
|
+
function toBlobPart(content: string | Uint8Array): BlobPart {
|
|
123
|
+
if (typeof content === 'string') return content;
|
|
124
|
+
const bytes = new Uint8Array(content.byteLength);
|
|
125
|
+
bytes.set(content);
|
|
126
|
+
return bytes;
|
|
127
|
+
}
|
|
128
|
+
|
|
122
129
|
export function createExportAdapter(store: StoreApi): ExportBackendMethods {
|
|
123
130
|
/** Resolve entity data via the query subsystem */
|
|
124
131
|
function getEntityData(ref: EntityRef): EntityData | null {
|
|
@@ -354,6 +361,11 @@ export function createExportAdapter(store: StoreApi): ExportBackendMethods {
|
|
|
354
361
|
model.ifcDataStore,
|
|
355
362
|
options.includeMutations === false ? undefined : getMutationViewForModel(store, modelId) ?? undefined,
|
|
356
363
|
);
|
|
364
|
+
// Include georeferencing mutations if present
|
|
365
|
+
const georefMutations = options.includeMutations !== false
|
|
366
|
+
? state.georefMutations?.get(modelId) ?? undefined
|
|
367
|
+
: undefined;
|
|
368
|
+
|
|
357
369
|
const exportOptions: StepExportOptions = {
|
|
358
370
|
schema: options.schema ?? model.ifcDataStore.schemaVersion,
|
|
359
371
|
includeGeometry: true,
|
|
@@ -364,6 +376,7 @@ export function createExportAdapter(store: StoreApi): ExportBackendMethods {
|
|
|
364
376
|
visibleOnly,
|
|
365
377
|
hiddenEntityIds,
|
|
366
378
|
isolatedEntityIds,
|
|
379
|
+
georefMutations,
|
|
367
380
|
};
|
|
368
381
|
|
|
369
382
|
return exporter.export(exportOptions).content;
|
|
@@ -381,7 +394,7 @@ function triggerDownload(content: string | Uint8Array, filename: string, mimeTyp
|
|
|
381
394
|
if (typeof document === 'undefined') {
|
|
382
395
|
throw new Error('download() requires a browser environment (document is unavailable)');
|
|
383
396
|
}
|
|
384
|
-
const blob = new Blob([content], { type: mimeType });
|
|
397
|
+
const blob = new Blob([toBlobPart(content)], { type: mimeType });
|
|
385
398
|
const url = URL.createObjectURL(blob);
|
|
386
399
|
const a = document.createElement('a');
|
|
387
400
|
a.href = url;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import type { EntityRef, SectionPlane, CameraState, ViewerBackendMethods } from '@ifc-lite/sdk';
|
|
6
6
|
import type { StoreApi } from './types.js';
|
|
7
7
|
import { getModelForRef } from './model-compat.js';
|
|
8
|
+
import { toGlobalIdForRef } from '../../store/globalId.js';
|
|
8
9
|
|
|
9
10
|
const AXIS_TO_STORE: Record<string, 'down' | 'front' | 'side'> = {
|
|
10
11
|
x: 'side',
|
|
@@ -25,11 +26,8 @@ export function createViewerAdapter(store: StoreApi): ViewerBackendMethods {
|
|
|
25
26
|
const existing = state.pendingColorUpdates;
|
|
26
27
|
const colorMap = existing ? new Map(existing) : new Map<number, [number, number, number, number]>();
|
|
27
28
|
for (const ref of refs) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const globalId = ref.expressId + model.idOffset;
|
|
31
|
-
colorMap.set(globalId, color);
|
|
32
|
-
}
|
|
29
|
+
if (!getModelForRef(state, ref.modelId)) continue;
|
|
30
|
+
colorMap.set(toGlobalIdForRef(state.models, ref), color);
|
|
33
31
|
}
|
|
34
32
|
state.setPendingColorUpdates(colorMap);
|
|
35
33
|
return undefined;
|
|
@@ -41,10 +39,8 @@ export function createViewerAdapter(store: StoreApi): ViewerBackendMethods {
|
|
|
41
39
|
const batchMap = new Map<number, [number, number, number, number]>();
|
|
42
40
|
for (const batch of batches) {
|
|
43
41
|
for (const ref of batch.refs) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
batchMap.set(ref.expressId + model.idOffset, batch.color);
|
|
47
|
-
}
|
|
42
|
+
if (!getModelForRef(state, ref.modelId)) continue;
|
|
43
|
+
batchMap.set(toGlobalIdForRef(state.models, ref), batch.color);
|
|
48
44
|
}
|
|
49
45
|
}
|
|
50
46
|
state.setPendingColorUpdates(batchMap);
|
|
@@ -6,6 +6,7 @@ import type { EntityRef, VisibilityBackendMethods } from '@ifc-lite/sdk';
|
|
|
6
6
|
import type { StoreApi } from './types.js';
|
|
7
7
|
import { getModelForRef, type ModelLike } from './model-compat.js';
|
|
8
8
|
import { collectSpatialSubtreeElementsWithIfcSpace } from '../../store/basketVisibleSet.js';
|
|
9
|
+
import { toGlobalIdForRef, toGlobalIdFromModels } from '../../store/globalId.js';
|
|
9
10
|
import { isSpaceLikeSpatialTypeName, isSpatialStructureTypeName, type SpatialNode } from '@ifc-lite/data';
|
|
10
11
|
|
|
11
12
|
function findDescendantNode(root: SpatialNode, expressId: number): SpatialNode | null {
|
|
@@ -49,10 +50,8 @@ export function createVisibilityAdapter(store: StoreApi): VisibilityBackendMetho
|
|
|
49
50
|
// hiddenEntities set (global IDs), not hiddenEntitiesByModel.
|
|
50
51
|
const globalIds: number[] = [];
|
|
51
52
|
for (const ref of refs) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
globalIds.push(ref.expressId + model.idOffset);
|
|
55
|
-
}
|
|
53
|
+
if (!getModelForRef(state, ref.modelId)) continue;
|
|
54
|
+
globalIds.push(toGlobalIdForRef(state.models, ref));
|
|
56
55
|
}
|
|
57
56
|
if (globalIds.length > 0) {
|
|
58
57
|
state.hideEntities(globalIds);
|
|
@@ -63,10 +62,8 @@ export function createVisibilityAdapter(store: StoreApi): VisibilityBackendMetho
|
|
|
63
62
|
const state = store.getState();
|
|
64
63
|
const globalIds: number[] = [];
|
|
65
64
|
for (const ref of refs) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
globalIds.push(ref.expressId + model.idOffset);
|
|
69
|
-
}
|
|
65
|
+
if (!getModelForRef(state, ref.modelId)) continue;
|
|
66
|
+
globalIds.push(toGlobalIdForRef(state.models, ref));
|
|
70
67
|
}
|
|
71
68
|
if (globalIds.length > 0) {
|
|
72
69
|
state.showEntities(globalIds);
|
|
@@ -81,7 +78,7 @@ export function createVisibilityAdapter(store: StoreApi): VisibilityBackendMetho
|
|
|
81
78
|
if (model) {
|
|
82
79
|
const expanded = expandSpatialRef(ref, model);
|
|
83
80
|
for (const id of expanded) {
|
|
84
|
-
globalIds.push(
|
|
81
|
+
globalIds.push(toGlobalIdFromModels(state.models, ref.modelId, id));
|
|
85
82
|
}
|
|
86
83
|
}
|
|
87
84
|
}
|
|
@@ -14,6 +14,7 @@ import type { IfcDataStore } from '@ifc-lite/parser';
|
|
|
14
14
|
import type { EntityRef } from './types.js';
|
|
15
15
|
import { entityRefToString, stringToEntityRef } from './types.js';
|
|
16
16
|
import { useViewerStore } from './index.js';
|
|
17
|
+
import { toGlobalIdFromModels } from './globalId.js';
|
|
17
18
|
|
|
18
19
|
type ViewerStateSnapshot = ReturnType<typeof useViewerStore.getState>;
|
|
19
20
|
|
|
@@ -159,9 +160,7 @@ function expandRefToElements(state: ViewerStateSnapshot, ref: EntityRef): Entity
|
|
|
159
160
|
}
|
|
160
161
|
|
|
161
162
|
function toGlobalId(modelId: string, expressId: number, state: ViewerStateSnapshot): number {
|
|
162
|
-
|
|
163
|
-
const model = state.models.get(modelId);
|
|
164
|
-
return expressId + (model?.idOffset ?? 0);
|
|
163
|
+
return toGlobalIdFromModels(state.models, modelId, expressId);
|
|
165
164
|
}
|
|
166
165
|
|
|
167
166
|
function globalIdToRef(state: ViewerStateSnapshot, globalId: number): EntityRef | null {
|
|
@@ -318,7 +317,7 @@ function computeStoreyIsolation(state: ViewerStateSnapshot): Set<number> | null
|
|
|
318
317
|
const storeyElementIds = collectIfcBuildingStoreyElementsWithIfcSpace(hierarchy, localStoreyId);
|
|
319
318
|
if (!storeyElementIds) continue;
|
|
320
319
|
for (const localId of storeyElementIds) {
|
|
321
|
-
ids.add(
|
|
320
|
+
ids.add(toGlobalIdFromModels(state.models, model.id, localId));
|
|
322
321
|
}
|
|
323
322
|
}
|
|
324
323
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
import type { EntityRef, FederatedModel } from './types.js';
|
|
6
|
+
|
|
7
|
+
type ForwardModelMapLike = ReadonlyMap<string, { idOffset?: number }>;
|
|
8
|
+
type ReverseModelMapLike = ReadonlyMap<string, Pick<FederatedModel, 'idOffset' | 'maxExpressId'>>;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Convert a local expressId to the renderer/global ID space.
|
|
12
|
+
*
|
|
13
|
+
* This is the viewer-level single source of truth for modelId + expressId →
|
|
14
|
+
* globalId conversion outside Zustand hooks. It preserves single-model legacy
|
|
15
|
+
* behavior by falling back to the original expressId when no federated model
|
|
16
|
+
* entry exists.
|
|
17
|
+
*/
|
|
18
|
+
export function toGlobalIdFromModels(
|
|
19
|
+
models: ForwardModelMapLike,
|
|
20
|
+
modelId: string,
|
|
21
|
+
expressId: number,
|
|
22
|
+
): number {
|
|
23
|
+
if (modelId === 'legacy' || modelId === 'default' || modelId === '__legacy__') {
|
|
24
|
+
return expressId;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const model = models.get(modelId);
|
|
28
|
+
if (!model) {
|
|
29
|
+
return expressId;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return expressId + (model.idOffset ?? 0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolve a renderer/global ID back to the source model and local expressId.
|
|
37
|
+
*
|
|
38
|
+
* This mirrors toGlobalIdFromModels and preserves legacy single-model behavior.
|
|
39
|
+
*/
|
|
40
|
+
export function fromGlobalIdFromModels(
|
|
41
|
+
models: ReverseModelMapLike,
|
|
42
|
+
globalId: number,
|
|
43
|
+
): EntityRef | undefined {
|
|
44
|
+
if (models.size <= 1) {
|
|
45
|
+
const firstModelId = models.keys().next().value;
|
|
46
|
+
if (firstModelId) {
|
|
47
|
+
return {
|
|
48
|
+
modelId: firstModelId,
|
|
49
|
+
expressId: globalId,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
modelId: 'legacy',
|
|
54
|
+
expressId: globalId,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const [modelId, model] of models.entries()) {
|
|
59
|
+
const localExpressId = globalId - model.idOffset;
|
|
60
|
+
if (localExpressId > 0 && localExpressId <= model.maxExpressId) {
|
|
61
|
+
return {
|
|
62
|
+
modelId,
|
|
63
|
+
expressId: localExpressId,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Convert an EntityRef to the renderer/global ID space.
|
|
73
|
+
*/
|
|
74
|
+
export function toGlobalIdForRef(
|
|
75
|
+
models: ForwardModelMapLike,
|
|
76
|
+
ref: EntityRef,
|
|
77
|
+
): number {
|
|
78
|
+
return toGlobalIdFromModels(models, ref.modelId, ref.expressId);
|
|
79
|
+
}
|
package/src/store/index.ts
CHANGED
|
@@ -48,6 +48,7 @@ export { entityRefToString, stringToEntityRef, entityRefEquals, isIfcxDataStore
|
|
|
48
48
|
|
|
49
49
|
// Re-export single source of truth for globalId → EntityRef resolution
|
|
50
50
|
export { resolveEntityRef } from './resolveEntityRef.js';
|
|
51
|
+
export { fromGlobalIdFromModels, toGlobalIdFromModels, toGlobalIdForRef } from './globalId.js';
|
|
51
52
|
|
|
52
53
|
// Re-export Drawing2D types
|
|
53
54
|
export type { Drawing2DState, Drawing2DStatus, Annotation2DTool, PolygonArea2DResult, TextAnnotation2D, CloudAnnotation2D, SelectedAnnotation2D } from './slices/drawing2DSlice.js';
|