@ifc-lite/viewer 1.26.0 → 1.28.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 +45 -38
- package/CHANGELOG.md +93 -0
- package/dist/assets/{basketViewActivator-ZpTYWE3K.js → basketViewActivator-BNRDNuUJ.js} +9 -9
- package/dist/assets/{bcf-Ctcu_Sc2.js → bcf-DCwCuP7n.js} +56 -56
- package/dist/assets/{browser-DXS29_v9.js → browser-BIoDDfBW.js} +1 -1
- package/dist/assets/{cesium-BoVuJvTC.js → cesium-CzZn5yVA.js} +319 -319
- package/dist/assets/{decode-worker-CgM1iNSK.js → decode-worker-Cjign7Zh.js} +1 -1
- package/dist/assets/deflate-DNGgs8Ur.js +1 -0
- package/dist/assets/drawing-2d-D0dDf6Lh.js +257 -0
- package/dist/assets/e57-source-2wI9jkCA.js +1 -0
- package/dist/assets/exceljs.min-DsuzKYnj.js +29 -0
- package/dist/assets/{exporters-DSq76AVM.js → exporters-B9v81gi9.js} +1861 -1524
- package/dist/assets/geometry.worker-Bpa3115V.js +1 -0
- package/dist/assets/{geotiff-A5UjhI6L.js → geotiff-D-YCLS4g.js} +10 -10
- package/dist/assets/html2canvas.esm-Ge7aVWlp.js +5 -0
- package/dist/assets/{ids-DiLcGTer.js → ids-CCpq-5d3.js} +952 -945
- package/dist/assets/ifc-lite_bg-DbgS5EUA.wasm +0 -0
- package/dist/assets/{index-BAH8IJVR.js → index-Bgb3_Pu_.js} +47682 -42474
- package/dist/assets/index-BtbXFKsX.css +1 -0
- package/dist/assets/index.es-CWfqZyyr.js +6866 -0
- package/dist/assets/{jpeg-BzSkwo5D.js → jpeg-DGOAeUqU.js} +1 -1
- package/dist/assets/jspdf.es.min-XPLU2Wkq.js +19571 -0
- package/dist/assets/jspdf.plugin.autotable-BBLUVd7n.js +2 -0
- package/dist/assets/lens-C4p1kQ0p.js +1 -0
- package/dist/assets/{lerc-Cg2Rz-D5.js → lerc-1PMSCHwX.js} +1 -1
- package/dist/assets/{lzw-BBPPLW-0.js → lzw-C65U9lNM.js} +1 -1
- package/dist/assets/{maplibre-gl-Do6O5tDc.js → maplibre-gl-BF3Z0idw.js} +1 -1
- package/dist/assets/{native-bridge-CPojOeGE.js → native-bridge-XxXos6yI.js} +2 -2
- package/dist/assets/{packbits-yLSpjW-V.js → packbits-BdMWXC3m.js} +1 -1
- package/dist/assets/{pako.esm-Cram60i4.js → pako.esm-n3Pgozwg.js} +1 -1
- package/dist/assets/parser.worker-Ddwo3_06.js +182 -0
- package/dist/assets/pdf-CRwaZf3s.js +135 -0
- package/dist/assets/raw-CJgQdyuZ.js +1 -0
- package/dist/assets/{sandbox-CsRXlgCO.js → sandbox-0sDo3g3m.js} +3037 -2554
- package/dist/assets/server-client-cTCJ-853.js +719 -0
- package/dist/assets/{webimage-YafxjjGr.js → webimage-BtakWX7W.js} +1 -1
- package/dist/assets/xlsx-B1YOg2QB.js +142 -0
- package/dist/assets/{zip-BJqVbRkU.js → zip-DFgP-l20.js} +1 -1
- package/dist/assets/{zstd-CkSLOiuu.js → zstd-CmwsbxmM.js} +1 -1
- package/dist/index.html +10 -10
- package/package.json +27 -23
- package/src/components/mcp/PlaygroundChat.tsx +1 -0
- package/src/components/mcp/data.ts +6 -0
- package/src/components/mcp/playground-dispatcher.ts +280 -0
- package/src/components/mcp/playground-files.ts +33 -1
- package/src/components/mcp/types.ts +2 -1
- package/src/components/ui/combo-input.tsx +163 -0
- package/src/components/ui/tabs.tsx +1 -1
- package/src/components/viewer/CommandPalette.tsx +6 -1
- package/src/components/viewer/ComparePanel.tsx +420 -0
- package/src/components/viewer/HierarchyPanel.tsx +46 -7
- package/src/components/viewer/MainToolbar.tsx +19 -2
- package/src/components/viewer/PropertiesPanel.tsx +84 -8
- package/src/components/viewer/SearchInline.tsx +62 -2
- package/src/components/viewer/SearchModal.filter.builder.tsx +24 -393
- package/src/components/viewer/SearchModal.filter.editors.tsx +503 -0
- package/src/components/viewer/SearchModal.filter.tsx +64 -1
- package/src/components/viewer/SearchModal.tsx +19 -6
- package/src/components/viewer/ViewerLayout.tsx +5 -0
- package/src/components/viewer/Viewport.tsx +18 -0
- package/src/components/viewer/hierarchy/HierarchyNode.tsx +3 -3
- package/src/components/viewer/hierarchy/ifc-icons.ts +9 -0
- package/src/components/viewer/hierarchy/treeDataBuilder.ts +87 -0
- package/src/components/viewer/hierarchy/types.ts +1 -0
- package/src/components/viewer/hierarchy/useHierarchyTree.ts +6 -2
- package/src/components/viewer/lists/ColumnHeaderMenu.tsx +84 -0
- package/src/components/viewer/lists/ListBuilder.tsx +789 -280
- package/src/components/viewer/lists/ListGroupingBar.tsx +72 -0
- package/src/components/viewer/lists/ListPanel.tsx +49 -5
- package/src/components/viewer/lists/ListResultsTable.tsx +270 -176
- package/src/components/viewer/lists/list-table-utils.ts +123 -0
- package/src/components/viewer/properties/MaterialTotalsPanel.tsx +283 -0
- package/src/generated/mcp-catalog.json +4 -0
- package/src/hooks/federationLoadGate.test.ts +12 -2
- package/src/hooks/federationLoadGate.ts +9 -2
- package/src/hooks/ingest/federationAlign.ts +481 -0
- package/src/hooks/ingest/viewerModelIngest.ts +3 -212
- package/src/hooks/source-key.ts +35 -0
- package/src/hooks/useAlignmentLines3D.ts +1 -26
- package/src/hooks/useCompare.ts +0 -0
- package/src/hooks/useCompareOverlay.ts +119 -0
- package/src/hooks/useDrawingGeneration.ts +23 -1
- package/src/hooks/useGridLines3D.ts +140 -0
- package/src/hooks/useIfc.ts +1 -1
- package/src/hooks/useIfcCache.ts +32 -9
- package/src/hooks/useIfcFederation.ts +42 -810
- package/src/hooks/useIfcLoader.ts +361 -488
- package/src/hooks/useIfcServer.ts +3 -0
- package/src/hooks/useLens.ts +5 -1
- package/src/hooks/useSymbolicAnnotations.ts +70 -38
- package/src/lib/compare/buildFingerprints.ts +173 -0
- package/src/lib/compare/describeChange.ts +0 -0
- package/src/lib/compare/geometricData.test.ts +54 -0
- package/src/lib/compare/geometricData.ts +37 -0
- package/src/lib/compare/overlay.test.ts +99 -0
- package/src/lib/compare/overlay.ts +91 -0
- package/src/lib/geo/cesium-placement.ts +1 -1
- package/src/lib/geo/reproject.ts +4 -1
- package/src/lib/length-unit-scale.ts +41 -0
- package/src/lib/lists/adapter.ts +136 -11
- package/src/lib/lists/export/csv.ts +47 -0
- package/src/lib/lists/export/index.ts +49 -0
- package/src/lib/lists/export/model.ts +111 -0
- package/src/lib/lists/export/pdf.ts +67 -0
- package/src/lib/lists/export/xlsx.ts +83 -0
- package/src/lib/lists/index.ts +2 -0
- package/src/lib/llm/script-edit-ops.ts +23 -0
- package/src/lib/llm/stream-client.ts +8 -1
- package/src/lib/search/filter-evaluate.test.ts +81 -0
- package/src/lib/search/filter-evaluate.ts +59 -87
- package/src/lib/search/filter-match.ts +167 -0
- package/src/lib/search/filter-rules.test.ts +25 -0
- package/src/lib/search/filter-rules.ts +75 -2
- package/src/lib/search/filter-schema.ts +0 -0
- package/src/lib/search/result-export.ts +7 -1
- package/src/lib/slab-edit.test.ts +72 -0
- package/src/lib/slab-edit.ts +159 -19
- package/src/sdk/adapters/export-adapter.ts +9 -4
- package/src/sdk/adapters/query-adapter.ts +3 -3
- package/src/store/globalId.ts +15 -13
- package/src/store/index.ts +16 -1
- package/src/store/slices/cesiumSlice.ts +8 -1
- package/src/store/slices/compareSlice.ts +96 -0
- package/src/store/slices/lensSlice.ts +8 -0
- package/src/store/slices/listSlice.ts +6 -0
- package/src/store/slices/mutationSlice.ts +14 -6
- package/src/store/slices/searchSlice.ts +29 -3
- package/src/utils/acquireFileBuffer.test.ts +12 -4
- package/src/utils/desktopModelSnapshot.ts +2 -1
- package/src/utils/loadingUtils.ts +32 -0
- package/src/utils/nativeSpatialDataStore.ts +6 -0
- package/src/utils/serverDataModel.test.ts +6 -0
- package/src/utils/serverDataModel.ts +7 -0
- package/src/utils/spatialHierarchy.test.ts +53 -1
- package/src/utils/spatialHierarchy.ts +42 -2
- package/src/vite-env.d.ts +2 -0
- package/dist/assets/deflate-Cnx0il6E.js +0 -1
- package/dist/assets/drawing-2d-C71b8Ugx.js +0 -257
- package/dist/assets/e57-source-CQHxE8n3.js +0 -1
- package/dist/assets/geometry.worker-0Q9qEa6p.js +0 -1
- package/dist/assets/ifc-lite_bg-CEZnhM2e.wasm +0 -0
- package/dist/assets/index-B9Ug2EqU.css +0 -1
- package/dist/assets/lens-PYsLu_MA.js +0 -1
- package/dist/assets/parser.worker-8md211IW.js +0 -182
- package/dist/assets/raw-BQrAgxwT.js +0 -1
- package/dist/assets/server-client-Bk4c1CPO.js +0 -626
- package/src/hooks/ingest/resolveDataStoreOrAbort.test.ts +0 -61
- package/src/hooks/ingest/resolveDataStoreOrAbort.ts +0 -28
- package/src/hooks/ingest/watchedGeometryStream.test.ts +0 -78
- package/src/hooks/ingest/watchedGeometryStream.ts +0 -76
|
@@ -0,0 +1,481 @@
|
|
|
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
|
+
* Georeferencing / federation alignment helpers.
|
|
7
|
+
*
|
|
8
|
+
* Extracted verbatim from useIfcFederation.ts so the unified model-load path
|
|
9
|
+
* (useIfcLoader's finalizeModel) can reuse them without a circular dependency.
|
|
10
|
+
* Behaviour-preserving move — do not change the georef maths or the issue-#595 /
|
|
11
|
+
* issue-#658 comments, which encode subtle alignment behaviour.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
type IfcDataStore,
|
|
16
|
+
type MapConversion,
|
|
17
|
+
type ProjectedCRS,
|
|
18
|
+
} from '@ifc-lite/parser';
|
|
19
|
+
import type { CoordinateInfo } from '@ifc-lite/geometry';
|
|
20
|
+
import { useViewerStore, type FederatedModel } from '../../store.js';
|
|
21
|
+
import { getEffectiveGeoreference, getEffectiveHorizontalScale, hasStandardGeoreferencing, type GeorefMutationDataLike } from '../../lib/geo/effective-georef.js';
|
|
22
|
+
import { resolveMapUnitToMetreScale } from '../../lib/geo/geo-scale.js';
|
|
23
|
+
import { resolveProjection } from '../../lib/geo/reproject.js';
|
|
24
|
+
import proj4 from 'proj4';
|
|
25
|
+
|
|
26
|
+
type FederatedGeometryResult = NonNullable<FederatedModel['geometryResult']>;
|
|
27
|
+
|
|
28
|
+
export interface ModelGeoref {
|
|
29
|
+
mapConversion: MapConversion;
|
|
30
|
+
projectedCRS: ProjectedCRS;
|
|
31
|
+
lengthUnitScale: number;
|
|
32
|
+
coordinateInfo?: CoordinateInfo;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface AffineTransform3D {
|
|
36
|
+
m00: number;
|
|
37
|
+
m01: number;
|
|
38
|
+
m02: number;
|
|
39
|
+
tx: number;
|
|
40
|
+
m10: number;
|
|
41
|
+
m11: number;
|
|
42
|
+
m12: number;
|
|
43
|
+
ty: number;
|
|
44
|
+
m20: number;
|
|
45
|
+
m21: number;
|
|
46
|
+
m22: number;
|
|
47
|
+
tz: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getMapUnitScale(georef: ModelGeoref): number {
|
|
51
|
+
return resolveMapUnitToMetreScale(georef.projectedCRS.mapUnitScale, georef.lengthUnitScale ?? 1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getAxis(georef: ModelGeoref): { a: number; o: number; scale: number; denom: number } {
|
|
55
|
+
const conversion = georef.mapConversion;
|
|
56
|
+
const a = conversion.xAxisAbscissa ?? 1;
|
|
57
|
+
const o = conversion.xAxisOrdinate ?? 0;
|
|
58
|
+
// Use the effective horizontal scale: viewer geometry is already in metres,
|
|
59
|
+
// so applying IfcMapConversion.Scale raw would double-scale — see issue #595.
|
|
60
|
+
const mapUnitScale = resolveMapUnitToMetreScale(georef.projectedCRS.mapUnitScale, georef.lengthUnitScale ?? 1);
|
|
61
|
+
const scale = getEffectiveHorizontalScale(conversion.scale, mapUnitScale, georef.lengthUnitScale ?? 1);
|
|
62
|
+
const denom = Math.max(a * a + o * o, 1e-12);
|
|
63
|
+
return { a, o, scale, denom };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function extractModelGeoref(
|
|
67
|
+
dataStore: IfcDataStore,
|
|
68
|
+
coordinateInfo?: CoordinateInfo,
|
|
69
|
+
mutations?: GeorefMutationDataLike,
|
|
70
|
+
): ModelGeoref | null {
|
|
71
|
+
const georef = getEffectiveGeoreference(dataStore, coordinateInfo, mutations);
|
|
72
|
+
// Only TRUE georeferencing (real IfcMapConversion + IfcProjectedCRS) may drive
|
|
73
|
+
// federation alignment. A file with no IfcMapConversion gets a synthesised
|
|
74
|
+
// `source: 'siteLocation'` georef (EPSG:4326 from IfcSite RefLatitude/Longitude/
|
|
75
|
+
// Elevation) so it can still be pinned on the location map — but those are
|
|
76
|
+
// geographic degrees plus a raw, un-unit-scaled site elevation, not a projected
|
|
77
|
+
// metric frame. buildGeorefAlignmentTransform assumes projected eastings/
|
|
78
|
+
// northings/height in metres, so feeding it site data places the second model
|
|
79
|
+
// kilometres away: the BIMcollab ARC/STR pair share a site GUID but carry
|
|
80
|
+
// RefElevation 0 vs 20000 mm, and the height term lands ARC ~20 km below STR.
|
|
81
|
+
// Such models have no real georef relationship, so leave them in their own local
|
|
82
|
+
// frames where they overlay correctly. hasStandardGeoreferencing() excludes
|
|
83
|
+
// 'siteLocation' (see effective-georef.test.ts). (Regression from #658.)
|
|
84
|
+
if (!hasStandardGeoreferencing(georef) || !georef?.mapConversion || !georef.projectedCRS?.name) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
mapConversion: georef.mapConversion,
|
|
89
|
+
projectedCRS: georef.projectedCRS,
|
|
90
|
+
lengthUnitScale: georef.lengthUnitScale,
|
|
91
|
+
coordinateInfo,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function crsKey(crs: ProjectedCRS): string {
|
|
96
|
+
return `${crs.name ?? ''}|${crs.geodeticDatum ?? ''}|${crs.mapProjection ?? ''}|${crs.mapZone ?? ''}`.toUpperCase();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function canAlignInSameProjectedCrs(a: ModelGeoref, b: ModelGeoref): boolean {
|
|
100
|
+
return crsKey(a.projectedCRS) === crsKey(b.projectedCRS);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function totalYupOffset(coordinateInfo?: CoordinateInfo): { x: number; y: number; z: number } {
|
|
104
|
+
const shift = coordinateInfo?.originShift ?? { x: 0, y: 0, z: 0 };
|
|
105
|
+
const rtc = coordinateInfo?.wasmRtcOffset;
|
|
106
|
+
const rtcYup = rtc ? { x: rtc.x, y: rtc.z, z: -rtc.y } : { x: 0, y: 0, z: 0 };
|
|
107
|
+
return {
|
|
108
|
+
x: shift.x + rtcYup.x,
|
|
109
|
+
y: shift.y + rtcYup.y,
|
|
110
|
+
z: shift.z + rtcYup.z,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function emptyBounds() {
|
|
115
|
+
return {
|
|
116
|
+
min: { x: Infinity, y: Infinity, z: Infinity },
|
|
117
|
+
max: { x: -Infinity, y: -Infinity, z: -Infinity },
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function zeroBounds() {
|
|
122
|
+
return {
|
|
123
|
+
min: { x: 0, y: 0, z: 0 },
|
|
124
|
+
max: { x: 0, y: 0, z: 0 },
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function updateBounds(bounds: ReturnType<typeof emptyBounds>, x: number, y: number, z: number): boolean {
|
|
129
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) return false;
|
|
130
|
+
bounds.min.x = Math.min(bounds.min.x, x);
|
|
131
|
+
bounds.min.y = Math.min(bounds.min.y, y);
|
|
132
|
+
bounds.min.z = Math.min(bounds.min.z, z);
|
|
133
|
+
bounds.max.x = Math.max(bounds.max.x, x);
|
|
134
|
+
bounds.max.y = Math.max(bounds.max.y, y);
|
|
135
|
+
bounds.max.z = Math.max(bounds.max.z, z);
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function buildGeorefAlignmentTransform(source: ModelGeoref, reference: ModelGeoref): AffineTransform3D | null {
|
|
140
|
+
const sourceConv = source.mapConversion;
|
|
141
|
+
const refConv = reference.mapConversion;
|
|
142
|
+
const sourceAxis = getAxis(source);
|
|
143
|
+
const refAxis = getAxis(reference);
|
|
144
|
+
const refDenom = refAxis.scale * refAxis.denom;
|
|
145
|
+
if (Math.abs(refDenom) < 1e-12) return null;
|
|
146
|
+
|
|
147
|
+
const sourceMapUnitScale = getMapUnitScale(source);
|
|
148
|
+
const refMapUnitScale = getMapUnitScale(reference);
|
|
149
|
+
const sourceOffset = totalYupOffset(source.coordinateInfo);
|
|
150
|
+
const refOffset = totalYupOffset(reference.coordinateInfo);
|
|
151
|
+
|
|
152
|
+
const eVx = sourceAxis.scale * sourceAxis.a;
|
|
153
|
+
const eVz = sourceAxis.scale * sourceAxis.o;
|
|
154
|
+
const eC = sourceConv.eastings * sourceMapUnitScale
|
|
155
|
+
+ sourceAxis.scale * (sourceAxis.a * sourceOffset.x + sourceAxis.o * sourceOffset.z)
|
|
156
|
+
- refConv.eastings * refMapUnitScale;
|
|
157
|
+
|
|
158
|
+
const nVx = sourceAxis.scale * sourceAxis.o;
|
|
159
|
+
const nVz = -sourceAxis.scale * sourceAxis.a;
|
|
160
|
+
const nC = sourceConv.northings * sourceMapUnitScale
|
|
161
|
+
+ sourceAxis.scale * (sourceAxis.o * sourceOffset.x - sourceAxis.a * sourceOffset.z)
|
|
162
|
+
- refConv.northings * refMapUnitScale;
|
|
163
|
+
|
|
164
|
+
const hC = sourceConv.orthogonalHeight * sourceMapUnitScale
|
|
165
|
+
+ sourceOffset.y
|
|
166
|
+
- refConv.orthogonalHeight * refMapUnitScale;
|
|
167
|
+
|
|
168
|
+
const invRefDenom = 1 / refDenom;
|
|
169
|
+
const xVx = (refAxis.a * eVx + refAxis.o * nVx) * invRefDenom;
|
|
170
|
+
const xVz = (refAxis.a * eVz + refAxis.o * nVz) * invRefDenom;
|
|
171
|
+
const xC = (refAxis.a * eC + refAxis.o * nC) * invRefDenom - refOffset.x;
|
|
172
|
+
|
|
173
|
+
const yVx = (-refAxis.o * eVx + refAxis.a * nVx) * invRefDenom;
|
|
174
|
+
const yVz = (-refAxis.o * eVz + refAxis.a * nVz) * invRefDenom;
|
|
175
|
+
const yC = (-refAxis.o * eC + refAxis.a * nC) * invRefDenom;
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
m00: xVx,
|
|
179
|
+
m01: 0,
|
|
180
|
+
m02: xVz,
|
|
181
|
+
tx: xC,
|
|
182
|
+
m10: 0,
|
|
183
|
+
m11: 1,
|
|
184
|
+
m12: 0,
|
|
185
|
+
ty: hC - refOffset.y,
|
|
186
|
+
m20: -yVx,
|
|
187
|
+
m21: 0,
|
|
188
|
+
m22: -yVz,
|
|
189
|
+
tz: -yC - refOffset.z,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function isIdentityTransform(transform: AffineTransform3D): boolean {
|
|
194
|
+
const eps = 1e-7;
|
|
195
|
+
return Math.abs(transform.m00 - 1) < eps
|
|
196
|
+
&& Math.abs(transform.m01) < eps
|
|
197
|
+
&& Math.abs(transform.m02) < eps
|
|
198
|
+
&& Math.abs(transform.tx) < eps
|
|
199
|
+
&& Math.abs(transform.m10) < eps
|
|
200
|
+
&& Math.abs(transform.m11 - 1) < eps
|
|
201
|
+
&& Math.abs(transform.m12) < eps
|
|
202
|
+
&& Math.abs(transform.ty) < eps
|
|
203
|
+
&& Math.abs(transform.m20) < eps
|
|
204
|
+
&& Math.abs(transform.m21) < eps
|
|
205
|
+
&& Math.abs(transform.m22 - 1) < eps
|
|
206
|
+
&& Math.abs(transform.tz) < eps;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function applyAlignmentTransformAndUpdateBounds(
|
|
210
|
+
geometry: FederatedGeometryResult,
|
|
211
|
+
transform: AffineTransform3D,
|
|
212
|
+
referenceInfo?: CoordinateInfo,
|
|
213
|
+
): void {
|
|
214
|
+
const bounds = emptyBounds();
|
|
215
|
+
let found = false;
|
|
216
|
+
|
|
217
|
+
for (const mesh of geometry.meshes) {
|
|
218
|
+
const positions = mesh.positions;
|
|
219
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
220
|
+
const x = positions[i];
|
|
221
|
+
const y = positions[i + 1];
|
|
222
|
+
const z = positions[i + 2];
|
|
223
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const alignedX = transform.m00 * x + transform.m01 * y + transform.m02 * z + transform.tx;
|
|
228
|
+
const alignedY = transform.m10 * x + transform.m11 * y + transform.m12 * z + transform.ty;
|
|
229
|
+
const alignedZ = transform.m20 * x + transform.m21 * y + transform.m22 * z + transform.tz;
|
|
230
|
+
positions[i] = alignedX;
|
|
231
|
+
positions[i + 1] = alignedY;
|
|
232
|
+
positions[i + 2] = alignedZ;
|
|
233
|
+
found = updateBounds(bounds, alignedX, alignedY, alignedZ) || found;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Rotate normals by the transform's 3×3 linear part (translation omitted)
|
|
237
|
+
// and renormalize. CRS alignment is a rigid rotation, so the linear part
|
|
238
|
+
// itself is the correct transform for normals; degenerate results from
|
|
239
|
+
// zero-length or non-finite inputs are left in place.
|
|
240
|
+
const normals = mesh.normals;
|
|
241
|
+
if (normals && normals.length >= 3) {
|
|
242
|
+
for (let i = 0; i < normals.length; i += 3) {
|
|
243
|
+
const nx = normals[i];
|
|
244
|
+
const ny = normals[i + 1];
|
|
245
|
+
const nz = normals[i + 2];
|
|
246
|
+
if (!Number.isFinite(nx) || !Number.isFinite(ny) || !Number.isFinite(nz)) {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const rx = transform.m00 * nx + transform.m01 * ny + transform.m02 * nz;
|
|
250
|
+
const ry = transform.m10 * nx + transform.m11 * ny + transform.m12 * nz;
|
|
251
|
+
const rz = transform.m20 * nx + transform.m21 * ny + transform.m22 * nz;
|
|
252
|
+
const len = Math.sqrt(rx * rx + ry * ry + rz * rz);
|
|
253
|
+
if (!Number.isFinite(len) || len < 1e-12) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
normals[i] = rx / len;
|
|
257
|
+
normals[i + 1] = ry / len;
|
|
258
|
+
normals[i + 2] = rz / len;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
geometry.coordinateInfo = {
|
|
264
|
+
originShift: referenceInfo?.originShift ?? { x: 0, y: 0, z: 0 },
|
|
265
|
+
originalBounds: found ? bounds : zeroBounds(),
|
|
266
|
+
shiftedBounds: found ? bounds : zeroBounds(),
|
|
267
|
+
hasLargeCoordinates: referenceInfo?.hasLargeCoordinates ?? false,
|
|
268
|
+
wasmRtcOffset: referenceInfo?.wasmRtcOffset,
|
|
269
|
+
buildingRotation: referenceInfo?.buildingRotation,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Reproject every vertex from a source model's georeference into the reference
|
|
275
|
+
* model's viewer-space frame using proj4 between the two projected CRSs.
|
|
276
|
+
*
|
|
277
|
+
* Used for federated loads where models declare different IfcProjectedCRSs
|
|
278
|
+
* (e.g. EPSG:28992 + EPSG:7415 mixed RD/NAP Dutch sets, or EPSG:25831 UTM +
|
|
279
|
+
* EPSG:28992 mixed). The pipeline per vertex:
|
|
280
|
+
*
|
|
281
|
+
* viewer(Yup) ──(source RTC/shift, axis swap)──▶ IFC(Zup, source)
|
|
282
|
+
* IFC(source) ──(source MapConversion)──────────▶ source projected (eS,nS,hS)
|
|
283
|
+
* projected ──(proj4: srcDef → refDef)────────▶ reference projected (eR,nR)
|
|
284
|
+
* projected ──(reference MapConversion inverse)▶ IFC(Zup, reference)
|
|
285
|
+
* IFC(ref) ──(axis swap, reference RTC/shift)─▶ viewer(Yup, reference frame)
|
|
286
|
+
*
|
|
287
|
+
* Vertical: height passes through unchanged. Browser-side proj4 has no vertical
|
|
288
|
+
* datum transforms (no NTv2/gtx grids), so cross-CRS vertical mismatches are
|
|
289
|
+
* left for the user to resolve via the per-model orthogonalHeight editor.
|
|
290
|
+
*
|
|
291
|
+
* Normals are NOT rotated. Cross-CRS rotations between projected systems in the
|
|
292
|
+
* same locality are sub-degree, and recomputing per-vertex would require a
|
|
293
|
+
* Jacobian per mesh — acceptable trade-off for now, document if it bites.
|
|
294
|
+
*/
|
|
295
|
+
async function alignGeometryAcrossCrs(
|
|
296
|
+
geometry: FederatedGeometryResult,
|
|
297
|
+
source: ModelGeoref,
|
|
298
|
+
reference: ModelGeoref,
|
|
299
|
+
): Promise<boolean> {
|
|
300
|
+
const sourceProjDef = await resolveProjection(source.projectedCRS);
|
|
301
|
+
const refProjDef = await resolveProjection(reference.projectedCRS);
|
|
302
|
+
if (!sourceProjDef || !refProjDef) return false;
|
|
303
|
+
|
|
304
|
+
const sourceMapUnitScale = getMapUnitScale(source);
|
|
305
|
+
const refMapUnitScale = getMapUnitScale(reference);
|
|
306
|
+
const sourceAxis = getAxis(source);
|
|
307
|
+
const refAxis = getAxis(reference);
|
|
308
|
+
const sourceOffset = totalYupOffset(source.coordinateInfo);
|
|
309
|
+
const refOffset = totalYupOffset(reference.coordinateInfo);
|
|
310
|
+
|
|
311
|
+
const refDenom = refAxis.scale * refAxis.denom;
|
|
312
|
+
if (Math.abs(refDenom) < 1e-12) return false;
|
|
313
|
+
const invRefDenom = 1 / refDenom;
|
|
314
|
+
|
|
315
|
+
const sourceConv = source.mapConversion;
|
|
316
|
+
const refConv = reference.mapConversion;
|
|
317
|
+
|
|
318
|
+
const bounds = emptyBounds();
|
|
319
|
+
let found = false;
|
|
320
|
+
let projFailures = 0;
|
|
321
|
+
let attempts = 0;
|
|
322
|
+
let firstProjError: unknown = null;
|
|
323
|
+
|
|
324
|
+
for (const mesh of geometry.meshes) {
|
|
325
|
+
const positions = mesh.positions;
|
|
326
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
327
|
+
const vx = positions[i];
|
|
328
|
+
const vy = positions[i + 1];
|
|
329
|
+
const vz = positions[i + 2];
|
|
330
|
+
if (!Number.isFinite(vx) || !Number.isFinite(vy) || !Number.isFinite(vz)) continue;
|
|
331
|
+
|
|
332
|
+
// viewer(Y-up, source-local) → world(Y-up) → IFC(Z-up, source)
|
|
333
|
+
const wx = vx + sourceOffset.x;
|
|
334
|
+
const wy = vy + sourceOffset.y;
|
|
335
|
+
const wz = vz + sourceOffset.z;
|
|
336
|
+
const ifcXs = wx;
|
|
337
|
+
const ifcYs = -wz;
|
|
338
|
+
const ifcZs = wy;
|
|
339
|
+
|
|
340
|
+
// IFC(source) → source projected (apply source MapConversion)
|
|
341
|
+
const eS = sourceConv.eastings * sourceMapUnitScale
|
|
342
|
+
+ sourceAxis.scale * (sourceAxis.a * ifcXs - sourceAxis.o * ifcYs);
|
|
343
|
+
const nS = sourceConv.northings * sourceMapUnitScale
|
|
344
|
+
+ sourceAxis.scale * (sourceAxis.o * ifcXs + sourceAxis.a * ifcYs);
|
|
345
|
+
const hS = sourceConv.orthogonalHeight * sourceMapUnitScale + ifcZs;
|
|
346
|
+
|
|
347
|
+
// source projected → reference projected via proj4
|
|
348
|
+
attempts += 1;
|
|
349
|
+
let eR: number;
|
|
350
|
+
let nR: number;
|
|
351
|
+
try {
|
|
352
|
+
const projected = proj4(sourceProjDef, refProjDef, [eS, nS]);
|
|
353
|
+
eR = projected[0];
|
|
354
|
+
nR = projected[1];
|
|
355
|
+
} catch (error) {
|
|
356
|
+
projFailures += 1;
|
|
357
|
+
if (firstProjError == null) firstProjError = error;
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (!Number.isFinite(eR) || !Number.isFinite(nR)) {
|
|
361
|
+
projFailures += 1;
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
// Height transformed under identity (no vertical datum hop in browser).
|
|
365
|
+
const hR = hS;
|
|
366
|
+
|
|
367
|
+
// reference projected → IFC(reference): invert reference MapConversion
|
|
368
|
+
const dE = eR - refConv.eastings * refMapUnitScale;
|
|
369
|
+
const dN = nR - refConv.northings * refMapUnitScale;
|
|
370
|
+
const ifcXr = invRefDenom * (refAxis.a * dE + refAxis.o * dN);
|
|
371
|
+
const ifcYr = invRefDenom * (-refAxis.o * dE + refAxis.a * dN);
|
|
372
|
+
const ifcZr = hR - refConv.orthogonalHeight * refMapUnitScale;
|
|
373
|
+
|
|
374
|
+
// IFC(Z-up, reference) → world(Y-up) → viewer(Y-up, reference-local)
|
|
375
|
+
const refWorldX = ifcXr;
|
|
376
|
+
const refWorldY = ifcZr;
|
|
377
|
+
const refWorldZ = -ifcYr;
|
|
378
|
+
const alignedX = refWorldX - refOffset.x;
|
|
379
|
+
const alignedY = refWorldY - refOffset.y;
|
|
380
|
+
const alignedZ = refWorldZ - refOffset.z;
|
|
381
|
+
|
|
382
|
+
positions[i] = alignedX;
|
|
383
|
+
positions[i + 1] = alignedY;
|
|
384
|
+
positions[i + 2] = alignedZ;
|
|
385
|
+
found = updateBounds(bounds, alignedX, alignedY, alignedZ) || found;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (!found) {
|
|
390
|
+
console.warn(
|
|
391
|
+
`[ifc-lite] Cross-CRS alignment failed: ${projFailures}/${attempts} `
|
|
392
|
+
+ `vertex transforms failed for ${source.projectedCRS.name} → ${reference.projectedCRS.name}; `
|
|
393
|
+
+ 'no vertices were successfully reprojected. Leaving geometry untouched.',
|
|
394
|
+
firstProjError,
|
|
395
|
+
);
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
geometry.coordinateInfo = {
|
|
400
|
+
originShift: reference.coordinateInfo?.originShift ?? { x: 0, y: 0, z: 0 },
|
|
401
|
+
originalBounds: bounds,
|
|
402
|
+
shiftedBounds: bounds,
|
|
403
|
+
hasLargeCoordinates: reference.coordinateInfo?.hasLargeCoordinates ?? false,
|
|
404
|
+
wasmRtcOffset: reference.coordinateInfo?.wasmRtcOffset,
|
|
405
|
+
buildingRotation: reference.coordinateInfo?.buildingRotation,
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
if (projFailures > 0) {
|
|
409
|
+
console.warn(
|
|
410
|
+
`[ifc-lite] Cross-CRS alignment: ${projFailures}/${attempts} vertex transforms `
|
|
411
|
+
+ `failed from ${source.projectedCRS.name} to ${reference.projectedCRS.name}. `
|
|
412
|
+
+ 'Those vertices are left at their original positions.',
|
|
413
|
+
firstProjError,
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export type FederationAlignmentStatus = 'same-crs' | 'reprojected' | 'identity' | 'failed';
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Route alignment to the right strategy based on whether the source and
|
|
423
|
+
* reference share a projected CRS. Returns a status describing how the model
|
|
424
|
+
* was placed in the federation, suitable for surfacing in the UI.
|
|
425
|
+
*/
|
|
426
|
+
export async function alignGeometryToReference(
|
|
427
|
+
geometry: FederatedGeometryResult,
|
|
428
|
+
source: ModelGeoref,
|
|
429
|
+
reference: ModelGeoref,
|
|
430
|
+
): Promise<FederationAlignmentStatus> {
|
|
431
|
+
if (canAlignInSameProjectedCrs(source, reference)) {
|
|
432
|
+
const transform = buildGeorefAlignmentTransform(source, reference);
|
|
433
|
+
if (!transform) return 'failed';
|
|
434
|
+
if (isIdentityTransform(transform)) return 'identity';
|
|
435
|
+
applyAlignmentTransformAndUpdateBounds(geometry, transform, reference.coordinateInfo);
|
|
436
|
+
return 'same-crs';
|
|
437
|
+
}
|
|
438
|
+
const ok = await alignGeometryAcrossCrs(geometry, source, reference);
|
|
439
|
+
return ok ? 'reprojected' : 'failed';
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Select the federation anchor model.
|
|
444
|
+
*
|
|
445
|
+
* Resolution order:
|
|
446
|
+
* 1. `anchorModelIdOverride` from the store, if it points to a loaded model
|
|
447
|
+
* with a valid georeference.
|
|
448
|
+
* 2. Earliest `loadedAt` model with a valid georeference (the default — gives
|
|
449
|
+
* a stable anchor across loads while letting the user override when they
|
|
450
|
+
* want a different model to drive the world frame).
|
|
451
|
+
*/
|
|
452
|
+
export function findReferenceGeorefModel(): { modelId: string; georef: ModelGeoref } | null {
|
|
453
|
+
const state = useViewerStore.getState();
|
|
454
|
+
const override = state.anchorModelIdOverride;
|
|
455
|
+
if (override) {
|
|
456
|
+
const model = state.models.get(override) as FederatedModel | undefined;
|
|
457
|
+
if (model?.ifcDataStore && model.geometryResult) {
|
|
458
|
+
const georef = extractModelGeoref(
|
|
459
|
+
model.ifcDataStore,
|
|
460
|
+
model.geometryResult.coordinateInfo,
|
|
461
|
+
state.georefMutations.get(override),
|
|
462
|
+
);
|
|
463
|
+
if (georef) return { modelId: override, georef };
|
|
464
|
+
}
|
|
465
|
+
// Fall through if the override no longer resolves — keeps loads
|
|
466
|
+
// recoverable even if the user removed the anchor they had pinned.
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const modelEntries = Array.from(state.models.entries()) as Array<[string, FederatedModel]>;
|
|
470
|
+
const sorted = [...modelEntries].sort(([, a], [, b]) => (a.loadedAt ?? 0) - (b.loadedAt ?? 0));
|
|
471
|
+
for (const [modelId, model] of sorted) {
|
|
472
|
+
if (!model.ifcDataStore || !model.geometryResult) continue;
|
|
473
|
+
const georef = extractModelGeoref(
|
|
474
|
+
model.ifcDataStore,
|
|
475
|
+
model.geometryResult.coordinateInfo,
|
|
476
|
+
state.georefMutations.get(modelId),
|
|
477
|
+
);
|
|
478
|
+
if (georef) return { modelId, georef };
|
|
479
|
+
}
|
|
480
|
+
return null;
|
|
481
|
+
}
|