@ifc-lite/viewer 1.17.4 → 1.18.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 +20 -17
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +630 -0
- package/DESKTOP_CONTRACT_VERSION +1 -1
- package/dist/assets/{basketViewActivator-BmnNtVfZ.js → basketViewActivator-Cm1QEk_R.js} +1 -1
- package/dist/assets/drawing-2d-DoxKMqbO.js +257 -0
- package/dist/assets/{exporters-ChAtBmlj.js → exporters-B_OBqIyD.js} +3479 -2845
- package/dist/assets/{geometry.worker-BQ0rzNo-.js → geometry.worker-xHHy-9DV.js} +1 -1
- package/dist/assets/ids-DQ5jY0E8.js +1 -0
- package/dist/assets/ifc-lite_bg-ADjKXSms.wasm +0 -0
- package/dist/assets/{index-Co8E2-FE.js → index-BKq-M3Mk.js} +55873 -40593
- package/dist/assets/index-COnQRuqY.css +1 -0
- package/dist/assets/{native-bridge-BRvbckFQ.js → native-bridge-SHXiQwFW.js} +104 -104
- package/dist/assets/sandbox-jez21HtV.js +9627 -0
- package/dist/assets/{server-client-BV8zHZ7Y.js → server-client-ncOQVNso.js} +1 -1
- package/dist/assets/{wasm-bridge-g01g7T9b.js → wasm-bridge-DyfBSB8z.js} +1 -1
- package/dist/index.html +8 -7
- package/index.html +1 -0
- package/package.json +13 -13
- package/src/App.tsx +16 -2
- package/src/apache-arrow.d.ts +30 -0
- package/src/components/viewer/AddElementPanel.tsx +758 -0
- package/src/components/viewer/BulkPropertyEditor.tsx +7 -0
- package/src/components/viewer/CesiumOverlay.tsx +62 -19
- package/src/components/viewer/ChatPanel.tsx +259 -93
- package/src/components/viewer/CommandPalette.tsx +56 -7
- package/src/components/viewer/EntityContextMenu.tsx +168 -4
- package/src/components/viewer/ExportChangesButton.tsx +25 -5
- package/src/components/viewer/ExportDialog.tsx +19 -1
- package/src/components/viewer/MainToolbar.tsx +73 -13
- package/src/components/viewer/PropertiesPanel.tsx +237 -23
- package/src/components/viewer/SearchInline.tsx +669 -0
- package/src/components/viewer/SearchModal.filter.builder.tsx +766 -0
- package/src/components/viewer/SearchModal.filter.tsx +514 -0
- package/src/components/viewer/SearchModal.text.tsx +388 -0
- package/src/components/viewer/SearchModal.tsx +235 -0
- package/src/components/viewer/SettingsPage.tsx +252 -101
- package/src/components/viewer/ThemeSwitch.tsx +63 -7
- package/src/components/viewer/ToolOverlays.tsx +5 -0
- package/src/components/viewer/ViewerLayout.tsx +25 -4
- package/src/components/viewer/Viewport.tsx +25 -3
- package/src/components/viewer/ViewportContainer.tsx +51 -64
- package/src/components/viewer/ViewportOverlays.tsx +5 -2
- package/src/components/viewer/annotations/AnnotationDropInput.tsx +203 -0
- package/src/components/viewer/annotations/AnnotationLayer.tsx +287 -0
- package/src/components/viewer/annotations/AnnotationPin.tsx +90 -0
- package/src/components/viewer/annotations/AnnotationPopover.tsx +296 -0
- package/src/components/viewer/bcf/BCFTopicDetail.tsx +4 -4
- package/src/components/viewer/chat/ModelSelector.tsx +90 -54
- package/src/components/viewer/lists/ListPanel.tsx +14 -21
- package/src/components/viewer/properties/GeoreferencingPanel.tsx +113 -51
- package/src/components/viewer/properties/LocationMap.tsx +9 -7
- package/src/components/viewer/properties/ModelMetadataPanel.tsx +1 -1
- package/src/components/viewer/properties/RawStepCard.tsx +332 -0
- package/src/components/viewer/properties/RawStepRow.tsx +261 -0
- package/src/components/viewer/properties/ScheduleCard.tsx +224 -0
- package/src/components/viewer/properties/TaskEditCard.tsx +510 -0
- package/src/components/viewer/properties/raw-step-format.ts +193 -0
- package/src/components/viewer/schedule/AnimationSettingsPopover.tsx +542 -0
- package/src/components/viewer/schedule/GanttDependencyArrows.tsx +89 -0
- package/src/components/viewer/schedule/GanttDragTooltip.tsx +48 -0
- package/src/components/viewer/schedule/GanttEmptyState.tsx +97 -0
- package/src/components/viewer/schedule/GanttPanel.tsx +295 -0
- package/src/components/viewer/schedule/GanttTaskBar.tsx +199 -0
- package/src/components/viewer/schedule/GanttTaskTree.tsx +250 -0
- package/src/components/viewer/schedule/GanttTimeline.tsx +305 -0
- package/src/components/viewer/schedule/GanttToolbar.tsx +406 -0
- package/src/components/viewer/schedule/GenerateAdvancedPanel.tsx +147 -0
- package/src/components/viewer/schedule/GenerateScheduleDialog.tsx +392 -0
- package/src/components/viewer/schedule/HeightStrategyPanel.tsx +120 -0
- package/src/components/viewer/schedule/generate-schedule.test.ts +439 -0
- package/src/components/viewer/schedule/generate-schedule.ts +648 -0
- package/src/components/viewer/schedule/schedule-animator.test.ts +452 -0
- package/src/components/viewer/schedule/schedule-animator.ts +488 -0
- package/src/components/viewer/schedule/schedule-selection.test.ts +148 -0
- package/src/components/viewer/schedule/schedule-selection.ts +163 -0
- package/src/components/viewer/schedule/schedule-utils.ts +223 -0
- package/src/components/viewer/schedule/useConstructionSequence.ts +156 -0
- package/src/components/viewer/schedule/useGanttBarDrag.test.ts +90 -0
- package/src/components/viewer/schedule/useGanttBarDrag.ts +305 -0
- package/src/components/viewer/schedule/useGanttSelection3DHighlight.ts +152 -0
- package/src/components/viewer/schedule/useOverlayCompositor.ts +108 -0
- package/src/components/viewer/selectionHandlers.ts +446 -0
- package/src/components/viewer/tools/AddElementOverlay.tsx +540 -0
- package/src/components/viewer/tools/SectionCapControls.tsx +237 -0
- package/src/components/viewer/tools/SectionPanel.tsx +39 -18
- package/src/components/viewer/useAnimationLoop.ts +9 -1
- package/src/components/viewer/useDuplicateShortcut.ts +77 -0
- package/src/components/viewer/useMouseControls.ts +9 -1
- package/src/components/viewer/useRenderUpdates.ts +1 -1
- package/src/hooks/ids/idsDataAccessor.ts +60 -24
- package/src/hooks/ingest/viewerModelIngest.ts +7 -2
- package/src/hooks/useIfcFederation.ts +326 -71
- package/src/hooks/useIfcLoader.ts +23 -10
- package/src/hooks/useKeyboardShortcuts.ts +25 -0
- package/src/hooks/useSandbox.ts +1 -1
- package/src/hooks/useSearchIndex.ts +125 -0
- package/src/hooks/useViewControls.ts +13 -5
- package/src/index.css +550 -10
- package/src/lib/desktop-entitlement.ts +2 -4
- package/src/lib/geo/cesium-bridge.ts +15 -7
- package/src/lib/geo/effective-georef.test.ts +73 -0
- package/src/lib/geo/effective-georef.ts +111 -0
- package/src/lib/geo/reproject.ts +105 -19
- package/src/lib/llm/byok-guard.test.ts +77 -0
- package/src/lib/llm/byok-guard.ts +39 -0
- package/src/lib/llm/free-models.test.ts +0 -6
- package/src/lib/llm/models.ts +104 -42
- package/src/lib/llm/stream-client.ts +74 -110
- package/src/lib/llm/stream-direct.test.ts +130 -0
- package/src/lib/llm/stream-direct.ts +316 -0
- package/src/lib/llm/system-prompt.test.ts +14 -0
- package/src/lib/llm/system-prompt.ts +102 -1
- package/src/lib/llm/types.ts +20 -2
- package/src/lib/recent-files.ts +38 -4
- package/src/lib/scripts/templates/bim-globals.d.ts +136 -114
- package/src/lib/scripts/templates/construction-schedule.ts +223 -0
- package/src/lib/scripts/templates.ts +7 -0
- package/src/lib/search/common-ifc-types.ts +36 -0
- package/src/lib/search/filter-evaluate.test.ts +537 -0
- package/src/lib/search/filter-evaluate.ts +610 -0
- package/src/lib/search/filter-rules.test.ts +119 -0
- package/src/lib/search/filter-rules.ts +198 -0
- package/src/lib/search/filter-schema.test.ts +233 -0
- package/src/lib/search/filter-schema.ts +146 -0
- package/src/lib/search/recent-searches.test.ts +116 -0
- package/src/lib/search/recent-searches.ts +93 -0
- package/src/lib/search/result-export.test.ts +101 -0
- package/src/lib/search/result-export.ts +104 -0
- package/src/lib/search/saved-filters.test.ts +118 -0
- package/src/lib/search/saved-filters.ts +154 -0
- package/src/lib/search/tier0-scan.test.ts +196 -0
- package/src/lib/search/tier0-scan.ts +237 -0
- package/src/lib/search/tier1-index.test.ts +242 -0
- package/src/lib/search/tier1-index.ts +448 -0
- package/src/main.tsx +1 -10
- package/src/sdk/adapters/export-adapter.test.ts +434 -1
- package/src/sdk/adapters/export-adapter.ts +404 -1
- package/src/sdk/adapters/export-schedule-splice.test.ts +127 -0
- package/src/sdk/adapters/export-schedule-splice.ts +87 -0
- package/src/sdk/adapters/model-compat.ts +8 -2
- package/src/sdk/adapters/schedule-adapter.ts +73 -0
- package/src/sdk/adapters/store-adapter.ts +201 -0
- package/src/sdk/adapters/visibility-adapter.ts +3 -0
- package/src/sdk/local-backend.ts +16 -8
- package/src/services/api-keys.ts +73 -0
- package/src/services/desktop-export.ts +3 -1
- package/src/services/desktop-native-metadata.ts +41 -18
- package/src/services/file-dialog.ts +4 -1
- package/src/services/tauri-modules.d.ts +25 -0
- package/src/store/basketVisibleSet.ts +3 -0
- package/src/store/constants.ts +20 -2
- package/src/store/globalId.ts +4 -1
- package/src/store/index.ts +82 -6
- package/src/store/slices/addElementMeshes.ts +365 -0
- package/src/store/slices/addElementSlice.ts +275 -0
- package/src/store/slices/annotationsSlice.test.ts +133 -0
- package/src/store/slices/annotationsSlice.ts +251 -0
- package/src/store/slices/cesiumSlice.ts +5 -0
- package/src/store/slices/chatSlice.test.ts +6 -76
- package/src/store/slices/chatSlice.ts +17 -58
- package/src/store/slices/dataSlice.test.ts +23 -4
- package/src/store/slices/dataSlice.ts +1 -1
- package/src/store/slices/modelSlice.test.ts +67 -9
- package/src/store/slices/modelSlice.ts +39 -7
- package/src/store/slices/mutationSlice.ts +964 -3
- package/src/store/slices/overlayCompositor.test.ts +164 -0
- package/src/store/slices/overlaySlice.test.ts +93 -0
- package/src/store/slices/overlaySlice.ts +151 -0
- package/src/store/slices/pinboardSlice.test.ts +6 -1
- package/src/store/slices/playbackSlice.ts +128 -0
- package/src/store/slices/schedule-edit-helpers.test.ts +97 -0
- package/src/store/slices/schedule-edit-helpers.ts +179 -0
- package/src/store/slices/scheduleSlice.test.ts +694 -0
- package/src/store/slices/scheduleSlice.ts +1330 -0
- package/src/store/slices/searchSlice.test.ts +342 -0
- package/src/store/slices/searchSlice.ts +341 -0
- package/src/store/slices/sectionSlice.test.ts +87 -7
- package/src/store/slices/sectionSlice.ts +151 -5
- package/src/store/slices/selectionSlice.test.ts +46 -0
- package/src/store/slices/selectionSlice.ts +20 -0
- package/src/store/slices/uiSlice.ts +28 -5
- package/src/store/types.ts +26 -0
- package/src/store.ts +14 -0
- package/src/utils/nativeSpatialDataStore.ts +4 -1
- package/src/utils/viewportUtils.ts +7 -2
- package/src/vite-env.d.ts +0 -4
- package/dist/assets/drawing-2d-gWfpdfYe.js +0 -257
- package/dist/assets/ids-B4jTqB1O.js +0 -1
- package/dist/assets/ifc-lite_bg-BX4E7TX8.wasm +0 -0
- package/dist/assets/index-DckuDqlv.css +0 -1
- package/dist/assets/sandbox-DZiNLNMk.js +0 -5933
- package/src/components/viewer/UpgradePage.tsx +0 -71
- package/src/lib/desktop/ClerkDesktopEntitlementSync.tsx +0 -175
- package/src/lib/llm/ClerkChatSync.tsx +0 -74
- package/src/lib/llm/clerk-auth.ts +0 -62
|
@@ -13,8 +13,15 @@
|
|
|
13
13
|
import { useCallback } from 'react';
|
|
14
14
|
import { useShallow } from 'zustand/react/shallow';
|
|
15
15
|
import { useViewerStore, type FederatedModel, type SchemaVersion } from '../store.js';
|
|
16
|
-
import {
|
|
17
|
-
|
|
16
|
+
import {
|
|
17
|
+
detectFormat,
|
|
18
|
+
parseFederatedIfcx,
|
|
19
|
+
type IfcDataStore,
|
|
20
|
+
type FederatedIfcxParseResult,
|
|
21
|
+
type MapConversion,
|
|
22
|
+
type ProjectedCRS,
|
|
23
|
+
} from '@ifc-lite/parser';
|
|
24
|
+
import type { CoordinateInfo, MeshData } from '@ifc-lite/geometry';
|
|
18
25
|
import { IfcQuery } from '@ifc-lite/query';
|
|
19
26
|
import { buildSpatialIndexGuarded } from '../utils/loadingUtils.js';
|
|
20
27
|
import { getDynamicBatchConfig } from '../utils/ifcConfig.js';
|
|
@@ -27,6 +34,7 @@ import {
|
|
|
27
34
|
parseStepBufferViewerModel,
|
|
28
35
|
} from './ingest/viewerModelIngest.js';
|
|
29
36
|
import { readNativeFile, type NativeFileHandle } from '../services/file-dialog.js';
|
|
37
|
+
import { getEffectiveGeoreference, type GeorefMutationDataLike } from '../lib/geo/effective-georef.js';
|
|
30
38
|
|
|
31
39
|
function isNativeFileHandle(file: File | NativeFileHandle): file is NativeFileHandle {
|
|
32
40
|
return typeof (file as NativeFileHandle).path === 'string';
|
|
@@ -39,6 +47,271 @@ function toExactArrayBuffer(bytes: Uint8Array): ArrayBuffer {
|
|
|
39
47
|
return bytes.slice().buffer;
|
|
40
48
|
}
|
|
41
49
|
|
|
50
|
+
type FederatedGeometryResult = NonNullable<FederatedModel['geometryResult']>;
|
|
51
|
+
|
|
52
|
+
interface ModelGeoref {
|
|
53
|
+
mapConversion: MapConversion;
|
|
54
|
+
projectedCRS: ProjectedCRS;
|
|
55
|
+
lengthUnitScale: number;
|
|
56
|
+
coordinateInfo?: CoordinateInfo;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface AffineTransform3D {
|
|
60
|
+
m00: number;
|
|
61
|
+
m01: number;
|
|
62
|
+
m02: number;
|
|
63
|
+
tx: number;
|
|
64
|
+
m10: number;
|
|
65
|
+
m11: number;
|
|
66
|
+
m12: number;
|
|
67
|
+
ty: number;
|
|
68
|
+
m20: number;
|
|
69
|
+
m21: number;
|
|
70
|
+
m22: number;
|
|
71
|
+
tz: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getMapUnitScale(georef: ModelGeoref): number {
|
|
75
|
+
return georef.projectedCRS.mapUnitScale ?? georef.lengthUnitScale ?? 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getAxis(conversion: MapConversion): { a: number; o: number; scale: number; denom: number } {
|
|
79
|
+
const a = conversion.xAxisAbscissa ?? 1;
|
|
80
|
+
const o = conversion.xAxisOrdinate ?? 0;
|
|
81
|
+
const scale = conversion.scale ?? 1;
|
|
82
|
+
const denom = Math.max(a * a + o * o, 1e-12);
|
|
83
|
+
return { a, o, scale, denom };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function extractModelGeoref(
|
|
87
|
+
dataStore: IfcDataStore,
|
|
88
|
+
coordinateInfo?: CoordinateInfo,
|
|
89
|
+
mutations?: GeorefMutationDataLike,
|
|
90
|
+
): ModelGeoref | null {
|
|
91
|
+
const georef = getEffectiveGeoreference(dataStore, coordinateInfo, mutations);
|
|
92
|
+
if (!georef?.mapConversion || !georef.projectedCRS?.name) return null;
|
|
93
|
+
return {
|
|
94
|
+
mapConversion: georef.mapConversion,
|
|
95
|
+
projectedCRS: georef.projectedCRS,
|
|
96
|
+
lengthUnitScale: georef.lengthUnitScale,
|
|
97
|
+
coordinateInfo,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function crsKey(crs: ProjectedCRS): string {
|
|
102
|
+
return `${crs.name ?? ''}|${crs.geodeticDatum ?? ''}|${crs.mapProjection ?? ''}|${crs.mapZone ?? ''}`.toUpperCase();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function canAlignInSameProjectedCrs(a: ModelGeoref, b: ModelGeoref): boolean {
|
|
106
|
+
return crsKey(a.projectedCRS) === crsKey(b.projectedCRS);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function totalYupOffset(coordinateInfo?: CoordinateInfo): { x: number; y: number; z: number } {
|
|
110
|
+
const shift = coordinateInfo?.originShift ?? { x: 0, y: 0, z: 0 };
|
|
111
|
+
const rtc = coordinateInfo?.wasmRtcOffset;
|
|
112
|
+
const rtcYup = rtc ? { x: rtc.x, y: rtc.z, z: -rtc.y } : { x: 0, y: 0, z: 0 };
|
|
113
|
+
return {
|
|
114
|
+
x: shift.x + rtcYup.x,
|
|
115
|
+
y: shift.y + rtcYup.y,
|
|
116
|
+
z: shift.z + rtcYup.z,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function emptyBounds() {
|
|
121
|
+
return {
|
|
122
|
+
min: { x: Infinity, y: Infinity, z: Infinity },
|
|
123
|
+
max: { x: -Infinity, y: -Infinity, z: -Infinity },
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function zeroBounds() {
|
|
128
|
+
return {
|
|
129
|
+
min: { x: 0, y: 0, z: 0 },
|
|
130
|
+
max: { x: 0, y: 0, z: 0 },
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function updateBounds(bounds: ReturnType<typeof emptyBounds>, x: number, y: number, z: number): boolean {
|
|
135
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) return false;
|
|
136
|
+
bounds.min.x = Math.min(bounds.min.x, x);
|
|
137
|
+
bounds.min.y = Math.min(bounds.min.y, y);
|
|
138
|
+
bounds.min.z = Math.min(bounds.min.z, z);
|
|
139
|
+
bounds.max.x = Math.max(bounds.max.x, x);
|
|
140
|
+
bounds.max.y = Math.max(bounds.max.y, y);
|
|
141
|
+
bounds.max.z = Math.max(bounds.max.z, z);
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function buildGeorefAlignmentTransform(source: ModelGeoref, reference: ModelGeoref): AffineTransform3D | null {
|
|
146
|
+
const sourceConv = source.mapConversion;
|
|
147
|
+
const refConv = reference.mapConversion;
|
|
148
|
+
const sourceAxis = getAxis(sourceConv);
|
|
149
|
+
const refAxis = getAxis(refConv);
|
|
150
|
+
const refDenom = refAxis.scale * refAxis.denom;
|
|
151
|
+
if (Math.abs(refDenom) < 1e-12) return null;
|
|
152
|
+
|
|
153
|
+
const sourceMapUnitScale = getMapUnitScale(source);
|
|
154
|
+
const refMapUnitScale = getMapUnitScale(reference);
|
|
155
|
+
const sourceOffset = totalYupOffset(source.coordinateInfo);
|
|
156
|
+
const refOffset = totalYupOffset(reference.coordinateInfo);
|
|
157
|
+
|
|
158
|
+
const eVx = sourceAxis.scale * sourceAxis.a;
|
|
159
|
+
const eVz = sourceAxis.scale * sourceAxis.o;
|
|
160
|
+
const eC = sourceConv.eastings * sourceMapUnitScale
|
|
161
|
+
+ sourceAxis.scale * (sourceAxis.a * sourceOffset.x + sourceAxis.o * sourceOffset.z)
|
|
162
|
+
- refConv.eastings * refMapUnitScale;
|
|
163
|
+
|
|
164
|
+
const nVx = sourceAxis.scale * sourceAxis.o;
|
|
165
|
+
const nVz = -sourceAxis.scale * sourceAxis.a;
|
|
166
|
+
const nC = sourceConv.northings * sourceMapUnitScale
|
|
167
|
+
+ sourceAxis.scale * (sourceAxis.o * sourceOffset.x - sourceAxis.a * sourceOffset.z)
|
|
168
|
+
- refConv.northings * refMapUnitScale;
|
|
169
|
+
|
|
170
|
+
const hC = sourceConv.orthogonalHeight * sourceMapUnitScale
|
|
171
|
+
+ sourceOffset.y
|
|
172
|
+
- refConv.orthogonalHeight * refMapUnitScale;
|
|
173
|
+
|
|
174
|
+
const invRefDenom = 1 / refDenom;
|
|
175
|
+
const xVx = (refAxis.a * eVx + refAxis.o * nVx) * invRefDenom;
|
|
176
|
+
const xVz = (refAxis.a * eVz + refAxis.o * nVz) * invRefDenom;
|
|
177
|
+
const xC = (refAxis.a * eC + refAxis.o * nC) * invRefDenom - refOffset.x;
|
|
178
|
+
|
|
179
|
+
const yVx = (-refAxis.o * eVx + refAxis.a * nVx) * invRefDenom;
|
|
180
|
+
const yVz = (-refAxis.o * eVz + refAxis.a * nVz) * invRefDenom;
|
|
181
|
+
const yC = (-refAxis.o * eC + refAxis.a * nC) * invRefDenom;
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
m00: xVx,
|
|
185
|
+
m01: 0,
|
|
186
|
+
m02: xVz,
|
|
187
|
+
tx: xC,
|
|
188
|
+
m10: 0,
|
|
189
|
+
m11: 1,
|
|
190
|
+
m12: 0,
|
|
191
|
+
ty: hC - refOffset.y,
|
|
192
|
+
m20: -yVx,
|
|
193
|
+
m21: 0,
|
|
194
|
+
m22: -yVz,
|
|
195
|
+
tz: -yC - refOffset.z,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function isIdentityTransform(transform: AffineTransform3D): boolean {
|
|
200
|
+
const eps = 1e-7;
|
|
201
|
+
return Math.abs(transform.m00 - 1) < eps
|
|
202
|
+
&& Math.abs(transform.m01) < eps
|
|
203
|
+
&& Math.abs(transform.m02) < eps
|
|
204
|
+
&& Math.abs(transform.tx) < eps
|
|
205
|
+
&& Math.abs(transform.m10) < eps
|
|
206
|
+
&& Math.abs(transform.m11 - 1) < eps
|
|
207
|
+
&& Math.abs(transform.m12) < eps
|
|
208
|
+
&& Math.abs(transform.ty) < eps
|
|
209
|
+
&& Math.abs(transform.m20) < eps
|
|
210
|
+
&& Math.abs(transform.m21) < eps
|
|
211
|
+
&& Math.abs(transform.m22 - 1) < eps
|
|
212
|
+
&& Math.abs(transform.tz) < eps;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function applyAlignmentTransformAndUpdateBounds(
|
|
216
|
+
geometry: FederatedGeometryResult,
|
|
217
|
+
transform: AffineTransform3D,
|
|
218
|
+
referenceInfo?: CoordinateInfo,
|
|
219
|
+
): void {
|
|
220
|
+
const bounds = emptyBounds();
|
|
221
|
+
let found = false;
|
|
222
|
+
|
|
223
|
+
for (const mesh of geometry.meshes) {
|
|
224
|
+
const positions = mesh.positions;
|
|
225
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
226
|
+
const x = positions[i];
|
|
227
|
+
const y = positions[i + 1];
|
|
228
|
+
const z = positions[i + 2];
|
|
229
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const alignedX = transform.m00 * x + transform.m01 * y + transform.m02 * z + transform.tx;
|
|
234
|
+
const alignedY = transform.m10 * x + transform.m11 * y + transform.m12 * z + transform.ty;
|
|
235
|
+
const alignedZ = transform.m20 * x + transform.m21 * y + transform.m22 * z + transform.tz;
|
|
236
|
+
positions[i] = alignedX;
|
|
237
|
+
positions[i + 1] = alignedY;
|
|
238
|
+
positions[i + 2] = alignedZ;
|
|
239
|
+
found = updateBounds(bounds, alignedX, alignedY, alignedZ) || found;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Rotate normals by the transform's 3×3 linear part (translation omitted)
|
|
243
|
+
// and renormalize. CRS alignment is a rigid rotation, so the linear part
|
|
244
|
+
// itself is the correct transform for normals; degenerate results from
|
|
245
|
+
// zero-length or non-finite inputs are left in place.
|
|
246
|
+
const normals = mesh.normals;
|
|
247
|
+
if (normals && normals.length >= 3) {
|
|
248
|
+
for (let i = 0; i < normals.length; i += 3) {
|
|
249
|
+
const nx = normals[i];
|
|
250
|
+
const ny = normals[i + 1];
|
|
251
|
+
const nz = normals[i + 2];
|
|
252
|
+
if (!Number.isFinite(nx) || !Number.isFinite(ny) || !Number.isFinite(nz)) {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
const rx = transform.m00 * nx + transform.m01 * ny + transform.m02 * nz;
|
|
256
|
+
const ry = transform.m10 * nx + transform.m11 * ny + transform.m12 * nz;
|
|
257
|
+
const rz = transform.m20 * nx + transform.m21 * ny + transform.m22 * nz;
|
|
258
|
+
const len = Math.sqrt(rx * rx + ry * ry + rz * rz);
|
|
259
|
+
if (!Number.isFinite(len) || len < 1e-12) {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
normals[i] = rx / len;
|
|
263
|
+
normals[i + 1] = ry / len;
|
|
264
|
+
normals[i + 2] = rz / len;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
geometry.coordinateInfo = {
|
|
270
|
+
originShift: referenceInfo?.originShift ?? { x: 0, y: 0, z: 0 },
|
|
271
|
+
originalBounds: found ? bounds : zeroBounds(),
|
|
272
|
+
shiftedBounds: found ? bounds : zeroBounds(),
|
|
273
|
+
hasLargeCoordinates: referenceInfo?.hasLargeCoordinates ?? false,
|
|
274
|
+
wasmRtcOffset: referenceInfo?.wasmRtcOffset,
|
|
275
|
+
buildingRotation: referenceInfo?.buildingRotation,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function alignGeometryToReferenceGeoref(
|
|
280
|
+
geometry: FederatedGeometryResult,
|
|
281
|
+
source: ModelGeoref,
|
|
282
|
+
reference: ModelGeoref,
|
|
283
|
+
): boolean {
|
|
284
|
+
if (!canAlignInSameProjectedCrs(source, reference)) {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const transform = buildGeorefAlignmentTransform(source, reference);
|
|
289
|
+
if (!transform) {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (!isIdentityTransform(transform)) {
|
|
294
|
+
applyAlignmentTransformAndUpdateBounds(geometry, transform, reference.coordinateInfo);
|
|
295
|
+
}
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function findReferenceGeorefModel(): ModelGeoref | null {
|
|
300
|
+
const state = useViewerStore.getState();
|
|
301
|
+
const modelEntries = Array.from(state.models.entries()) as Array<[string, FederatedModel]>;
|
|
302
|
+
const sorted = [...modelEntries].sort(([, a], [, b]) => (a.loadedAt ?? 0) - (b.loadedAt ?? 0));
|
|
303
|
+
for (const [modelId, model] of sorted) {
|
|
304
|
+
if (!model.ifcDataStore || !model.geometryResult) continue;
|
|
305
|
+
const georef = extractModelGeoref(
|
|
306
|
+
model.ifcDataStore,
|
|
307
|
+
model.geometryResult.coordinateInfo,
|
|
308
|
+
state.georefMutations.get(modelId),
|
|
309
|
+
);
|
|
310
|
+
if (georef) return georef;
|
|
311
|
+
}
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
|
|
42
315
|
/**
|
|
43
316
|
* Extended data store type for IFCX (IFC5) files.
|
|
44
317
|
* IFCX uses schemaVersion 'IFC5' and may include federated composition metadata.
|
|
@@ -100,9 +373,15 @@ export function useIfcFederation() {
|
|
|
100
373
|
*/
|
|
101
374
|
const addModel = useCallback(async (
|
|
102
375
|
file: File | NativeFileHandle,
|
|
103
|
-
options?: {
|
|
376
|
+
options?: {
|
|
377
|
+
name?: string;
|
|
378
|
+
modelId?: string;
|
|
379
|
+
loadedAt?: number;
|
|
380
|
+
visible?: boolean;
|
|
381
|
+
collapsed?: boolean;
|
|
382
|
+
}
|
|
104
383
|
): Promise<string | null> => {
|
|
105
|
-
const modelId = crypto.randomUUID();
|
|
384
|
+
const modelId = options?.modelId ?? crypto.randomUUID();
|
|
106
385
|
const addStart = performance.now();
|
|
107
386
|
try {
|
|
108
387
|
// IMPORTANT: Before adding a new model, check if there's a legacy model
|
|
@@ -143,6 +422,7 @@ export function useIfcFederation() {
|
|
|
143
422
|
schemaVersion: 'IFC4',
|
|
144
423
|
loadedAt: Date.now() - 1000,
|
|
145
424
|
fileSize: 0,
|
|
425
|
+
sourceFile: undefined,
|
|
146
426
|
idOffset: legacyOffset,
|
|
147
427
|
maxExpressId: legacyMaxExpressId,
|
|
148
428
|
};
|
|
@@ -190,12 +470,26 @@ export function useIfcFederation() {
|
|
|
190
470
|
schemaVersion = result.schemaVersion;
|
|
191
471
|
} else {
|
|
192
472
|
setProgress({ phase: 'Starting geometry streaming', percent: 10 });
|
|
473
|
+
|
|
474
|
+
// For federated models: use the first model's RTC offset so all models
|
|
475
|
+
// share the same coordinate origin. This ensures pixel-perfect alignment
|
|
476
|
+
// without error-prone delta adjustments.
|
|
477
|
+
let sharedRtcOffset: { x: number; y: number; z: number } | undefined;
|
|
478
|
+
const existingModelsForRtc = Array.from(useViewerStore.getState().models.values()) as FederatedModel[];
|
|
479
|
+
if (existingModelsForRtc.length > 0) {
|
|
480
|
+
const sorted = [...existingModelsForRtc].sort((a, b) => (a.loadedAt ?? 0) - (b.loadedAt ?? 0));
|
|
481
|
+
sharedRtcOffset = sorted.find(
|
|
482
|
+
(model) => model.geometryResult?.coordinateInfo?.wasmRtcOffset != null,
|
|
483
|
+
)?.geometryResult?.coordinateInfo?.wasmRtcOffset;
|
|
484
|
+
}
|
|
485
|
+
|
|
193
486
|
const result = await parseStepBufferViewerModel({
|
|
194
487
|
fileName: file.name,
|
|
195
488
|
buffer,
|
|
196
489
|
fileSizeMB,
|
|
197
490
|
getDynamicBatchSize: getDynamicBatchConfig,
|
|
198
491
|
onProgress: setProgress,
|
|
492
|
+
sharedRtcOffset,
|
|
199
493
|
});
|
|
200
494
|
parsedDataStore = result.dataStore;
|
|
201
495
|
parsedGeometry = result.geometryResult;
|
|
@@ -206,6 +500,27 @@ export function useIfcFederation() {
|
|
|
206
500
|
throw new Error('Failed to parse file');
|
|
207
501
|
}
|
|
208
502
|
|
|
503
|
+
const referenceGeoref = findReferenceGeorefModel();
|
|
504
|
+
// Include any georef edits the user has already saved for this model so
|
|
505
|
+
// that a reload after editing reflects the new placement. Without this,
|
|
506
|
+
// extractModelGeoref reads only the raw parsed metadata and mutations
|
|
507
|
+
// are silently ignored.
|
|
508
|
+
const parsedGeorefMutations = useViewerStore.getState().georefMutations.get(modelId);
|
|
509
|
+
const parsedGeoref = extractModelGeoref(
|
|
510
|
+
parsedDataStore,
|
|
511
|
+
parsedGeometry.coordinateInfo,
|
|
512
|
+
parsedGeorefMutations,
|
|
513
|
+
);
|
|
514
|
+
if (referenceGeoref && parsedGeoref) {
|
|
515
|
+
setProgress({ phase: 'Aligning georeferenced model', percent: 90 });
|
|
516
|
+
const aligned = alignGeometryToReferenceGeoref(parsedGeometry, parsedGeoref, referenceGeoref);
|
|
517
|
+
if (!aligned) {
|
|
518
|
+
console.warn(
|
|
519
|
+
`[ifc-lite] Skipped georeferenced federation alignment for "${file.name}" because CRS differs from the reference model.`,
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
209
524
|
// =========================================================================
|
|
210
525
|
// FEDERATION REGISTRY: Transform expressIds to globally unique IDs
|
|
211
526
|
// This is the BULLETPROOF fix for multi-model ID collisions
|
|
@@ -229,71 +544,10 @@ export function useIfcFederation() {
|
|
|
229
544
|
}
|
|
230
545
|
|
|
231
546
|
// =========================================================================
|
|
232
|
-
// COORDINATE ALIGNMENT:
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
// RTC offset is in IFC coordinates (Z-up). After Z-up to Y-up conversion:
|
|
237
|
-
// - IFC X → WebGL X
|
|
238
|
-
// - IFC Y → WebGL -Z
|
|
239
|
-
// - IFC Z → WebGL Y (vertical)
|
|
547
|
+
// COORDINATE ALIGNMENT: All federated models use the same shared RTC offset
|
|
548
|
+
// (passed to WASM during parsing above), so no post-processing vertex
|
|
549
|
+
// adjustment is needed. All models are already in the same coordinate space.
|
|
240
550
|
// =========================================================================
|
|
241
|
-
const existingModels = Array.from(useViewerStore.getState().models.values()) as FederatedModel[];
|
|
242
|
-
if (existingModels.length > 0) {
|
|
243
|
-
const firstModel = existingModels[0];
|
|
244
|
-
const firstRtc = firstModel.geometryResult?.coordinateInfo?.wasmRtcOffset;
|
|
245
|
-
const newRtc = parsedGeometry.coordinateInfo?.wasmRtcOffset;
|
|
246
|
-
|
|
247
|
-
// If both models have RTC offsets, use RTC delta for precise alignment
|
|
248
|
-
if (firstRtc && newRtc) {
|
|
249
|
-
// Calculate what adjustment is needed to align new model with first model
|
|
250
|
-
// First model: pos = original - firstRtc
|
|
251
|
-
// New model: pos = original - newRtc
|
|
252
|
-
// To align: newPos + adjustment = firstPos (assuming same original)
|
|
253
|
-
// adjustment = firstRtc - newRtc (add back new's RTC, subtract first's RTC)
|
|
254
|
-
const adjustX = firstRtc.x - newRtc.x; // IFC X adjustment
|
|
255
|
-
const adjustY = firstRtc.y - newRtc.y; // IFC Y adjustment
|
|
256
|
-
const adjustZ = firstRtc.z - newRtc.z; // IFC Z adjustment (vertical)
|
|
257
|
-
|
|
258
|
-
// Convert to WebGL coordinates:
|
|
259
|
-
// IFC X → WebGL X (no change)
|
|
260
|
-
// IFC Y → WebGL -Z (swap and negate)
|
|
261
|
-
// IFC Z → WebGL Y (vertical)
|
|
262
|
-
const webglAdjustX = adjustX;
|
|
263
|
-
const webglAdjustY = adjustZ; // IFC Z is WebGL Y (vertical)
|
|
264
|
-
const webglAdjustZ = -adjustY; // IFC Y is WebGL -Z
|
|
265
|
-
|
|
266
|
-
const hasSignificantAdjust = Math.abs(webglAdjustX) > 0.01 ||
|
|
267
|
-
Math.abs(webglAdjustY) > 0.01 ||
|
|
268
|
-
Math.abs(webglAdjustZ) > 0.01;
|
|
269
|
-
|
|
270
|
-
if (hasSignificantAdjust) {
|
|
271
|
-
// Apply adjustment to all mesh vertices
|
|
272
|
-
// SUBTRACT adjustment: if firstRtc > newRtc, first was shifted MORE,
|
|
273
|
-
// so new model needs to be shifted in same direction (subtract more)
|
|
274
|
-
for (const mesh of parsedGeometry.meshes) {
|
|
275
|
-
const positions = mesh.positions;
|
|
276
|
-
for (let i = 0; i < positions.length; i += 3) {
|
|
277
|
-
positions[i] -= webglAdjustX;
|
|
278
|
-
positions[i + 1] -= webglAdjustY;
|
|
279
|
-
positions[i + 2] -= webglAdjustZ;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Update coordinate info bounds
|
|
284
|
-
if (parsedGeometry.coordinateInfo) {
|
|
285
|
-
parsedGeometry.coordinateInfo.shiftedBounds.min.x -= webglAdjustX;
|
|
286
|
-
parsedGeometry.coordinateInfo.shiftedBounds.max.x -= webglAdjustX;
|
|
287
|
-
parsedGeometry.coordinateInfo.shiftedBounds.min.y -= webglAdjustY;
|
|
288
|
-
parsedGeometry.coordinateInfo.shiftedBounds.max.y -= webglAdjustY;
|
|
289
|
-
parsedGeometry.coordinateInfo.shiftedBounds.min.z -= webglAdjustZ;
|
|
290
|
-
parsedGeometry.coordinateInfo.shiftedBounds.max.z -= webglAdjustZ;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
} else {
|
|
294
|
-
// No RTC info - can't align reliably. This happens with old cache entries.
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
551
|
|
|
298
552
|
// Build spatial index AFTER ID offset + RTC alignment so it stores
|
|
299
553
|
// correct globalIds and final world-space positions.
|
|
@@ -305,11 +559,12 @@ export function useIfcFederation() {
|
|
|
305
559
|
name: options?.name ?? file.name,
|
|
306
560
|
ifcDataStore: parsedDataStore,
|
|
307
561
|
geometryResult: parsedGeometry,
|
|
308
|
-
visible: true,
|
|
309
|
-
collapsed: hasModels(), // Collapse if not first model
|
|
562
|
+
visible: options?.visible ?? true,
|
|
563
|
+
collapsed: options?.collapsed ?? hasModels(), // Collapse if not first model
|
|
310
564
|
schemaVersion,
|
|
311
|
-
loadedAt: Date.now(),
|
|
565
|
+
loadedAt: options?.loadedAt ?? Date.now(),
|
|
312
566
|
fileSize: buffer.byteLength,
|
|
567
|
+
sourceFile: file,
|
|
313
568
|
idOffset,
|
|
314
569
|
maxExpressId,
|
|
315
570
|
};
|
|
@@ -231,6 +231,7 @@ export function useIfcLoader() {
|
|
|
231
231
|
schemaVersion: 'IFC4',
|
|
232
232
|
loadedAt: Date.now(),
|
|
233
233
|
fileSize,
|
|
234
|
+
sourceFile: file,
|
|
234
235
|
idOffset: 0,
|
|
235
236
|
maxExpressId: 0,
|
|
236
237
|
loadState: 'pending',
|
|
@@ -280,15 +281,23 @@ export function useIfcLoader() {
|
|
|
280
281
|
return 'IFC2X3';
|
|
281
282
|
};
|
|
282
283
|
|
|
284
|
+
// Native renderer streaming path is currently disabled — the
|
|
285
|
+
// `huge native file` block further down handles real desktop
|
|
286
|
+
// streaming. This branch is retained as a scaffold for the future
|
|
287
|
+
// always-on native renderer integration.
|
|
288
|
+
const NATIVE_RENDERER_PATH_ENABLED = false as boolean;
|
|
283
289
|
if (
|
|
290
|
+
NATIVE_RENDERER_PATH_ENABLED &&
|
|
284
291
|
isNativeFileHandle(file) &&
|
|
285
|
-
fileName.toLowerCase().endsWith('.ifc')
|
|
286
|
-
false
|
|
292
|
+
fileName.toLowerCase().endsWith('.ifc')
|
|
287
293
|
) {
|
|
294
|
+
// Re-narrow `file` for the body — TS occasionally drops the
|
|
295
|
+
// type-predicate result inside a dead branch.
|
|
296
|
+
const nativeFile: NativeFileHandle = file;
|
|
288
297
|
const harnessRequest = getActiveHarnessRequest();
|
|
289
|
-
const nativeCacheKey = computeNativeCacheKey(
|
|
290
|
-
const shouldUseNativeCache =
|
|
291
|
-
const hugeNativeMode =
|
|
298
|
+
const nativeCacheKey = computeNativeCacheKey(nativeFile);
|
|
299
|
+
const shouldUseNativeCache = nativeFile.size >= CACHE_SIZE_THRESHOLD;
|
|
300
|
+
const hugeNativeMode = nativeFile.size >= HUGE_NATIVE_FILE_THRESHOLD;
|
|
292
301
|
let firstBatchWaitMs: number | null = null;
|
|
293
302
|
let firstVisibleGeometryMs: number | null = null;
|
|
294
303
|
let modelOpenMs: number | null = null;
|
|
@@ -311,7 +320,7 @@ export function useIfcLoader() {
|
|
|
311
320
|
let nativeGeometryCacheHit = false;
|
|
312
321
|
let nativeMetadataSnapshotHit = false;
|
|
313
322
|
let nativeMetadataSource: 'snapshot' | 'ifc-parse' = 'ifc-parse';
|
|
314
|
-
let nativeMetadataStartGate
|
|
323
|
+
let nativeMetadataStartGate = 'immediate' as 'immediate' | 'afterInteractiveGeometry' | 'afterGeometryComplete';
|
|
315
324
|
let finalCoordinateInfo: CoordinateInfo | null = null;
|
|
316
325
|
|
|
317
326
|
console.log(`[useIfc] Native renderer load: ${fileName}, size: ${fileSizeMB.toFixed(2)}MB`);
|
|
@@ -726,7 +735,7 @@ export function useIfcLoader() {
|
|
|
726
735
|
let fullNativeDataStore: IfcDataStore | null = null;
|
|
727
736
|
let nativeLoadStage: 'open' | 'streamGeometry' | 'finalizeGeometry' | 'hydrateMetadata' | 'complete' = 'open';
|
|
728
737
|
let nativeMetadataSource: 'snapshot' | 'ifc-parse' = 'ifc-parse';
|
|
729
|
-
let nativeMetadataStartGate
|
|
738
|
+
let nativeMetadataStartGate = 'immediate' as 'immediate' | 'afterInteractiveGeometry' | 'afterGeometryComplete';
|
|
730
739
|
|
|
731
740
|
setGeometryResult(null);
|
|
732
741
|
|
|
@@ -1637,8 +1646,10 @@ export function useIfcLoader() {
|
|
|
1637
1646
|
}
|
|
1638
1647
|
|
|
1639
1648
|
// Try server parsing first (enabled by default for multi-core performance)
|
|
1640
|
-
// Only for IFC4 STEP files (server doesn't support IFCX)
|
|
1641
|
-
|
|
1649
|
+
// Only for IFC4 STEP files (server doesn't support IFCX). Native
|
|
1650
|
+
// file handles (Tauri) don't have an HTTP-uploadable body, so skip
|
|
1651
|
+
// the server path and fall through to the WASM loader.
|
|
1652
|
+
if (format === 'ifc' && USE_SERVER && SERVER_URL && SERVER_URL !== '' && !isNativeFileHandle(file)) {
|
|
1642
1653
|
// Pass buffer directly - server uses File object for parsing, buffer is only for size checks
|
|
1643
1654
|
const serverSuccess = await loadFromServer(file, buffer, () => loadSessionRef.current !== currentSession);
|
|
1644
1655
|
if (serverSuccess) {
|
|
@@ -1791,7 +1802,9 @@ export function useIfcLoader() {
|
|
|
1791
1802
|
if (geometryIteratorClosed || typeof geometryIterator.return !== 'function') return;
|
|
1792
1803
|
geometryIteratorClosed = true;
|
|
1793
1804
|
try {
|
|
1794
|
-
|
|
1805
|
+
// `AsyncIterator.return()` is signed as taking a value in
|
|
1806
|
+
// current TS libs; callers conventionally pass `undefined`.
|
|
1807
|
+
await geometryIterator.return(undefined);
|
|
1795
1808
|
} catch {
|
|
1796
1809
|
// Ignore iterator shutdown failures during recovery.
|
|
1797
1810
|
}
|
|
@@ -88,6 +88,10 @@ export function useKeyboardShortcuts(options: KeyboardShortcutsOptions = {}) {
|
|
|
88
88
|
e.preventDefault();
|
|
89
89
|
setActiveTool('section');
|
|
90
90
|
}
|
|
91
|
+
if (key === 'p' && !ctrl && !shift) {
|
|
92
|
+
e.preventDefault();
|
|
93
|
+
setActiveTool('annotate');
|
|
94
|
+
}
|
|
91
95
|
|
|
92
96
|
// Basket controls (automatic context source)
|
|
93
97
|
// I = Isolate from current context
|
|
@@ -150,6 +154,26 @@ export function useKeyboardShortcuts(options: KeyboardShortcutsOptions = {}) {
|
|
|
150
154
|
resetVisibilityForHomeFromStore();
|
|
151
155
|
}
|
|
152
156
|
|
|
157
|
+
// Add-element tool shortcuts — Enter commits an in-progress slab
|
|
158
|
+
// polygon; Esc clears any pending points before falling through to
|
|
159
|
+
// the global Esc handler (which exits the tool).
|
|
160
|
+
if (activeTool === 'addElement') {
|
|
161
|
+
const state = useViewerStore.getState();
|
|
162
|
+
const polygonable = ['slab', 'roof', 'plate', 'space'].includes(state.addElementType);
|
|
163
|
+
if (key === 'enter' && polygonable && state.addElementSlabMode === 'polygon') {
|
|
164
|
+
e.preventDefault();
|
|
165
|
+
// Lazy import keeps this module out of the keyboard hook's
|
|
166
|
+
// synchronous bundle (the close handler pulls in toast).
|
|
167
|
+
import('@/components/viewer/selectionHandlers').then((mod) => mod.commitAddElementSlabPolygon());
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (key === 'escape' && state.addElementPendingPoints.length > 0) {
|
|
171
|
+
e.preventDefault();
|
|
172
|
+
state.clearAddElementPending();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
153
177
|
// Measure tool shortcuts
|
|
154
178
|
if (activeTool === 'measure') {
|
|
155
179
|
// Cancel active measurement with ESC
|
|
@@ -243,6 +267,7 @@ export const KEYBOARD_SHORTCUTS = [
|
|
|
243
267
|
{ key: 'V', description: 'Select tool', category: 'Tools' },
|
|
244
268
|
{ key: 'C', description: 'Walk mode', category: 'Tools' },
|
|
245
269
|
{ key: 'M', description: 'Measure tool', category: 'Tools' },
|
|
270
|
+
{ key: 'P', description: 'Annotate tool — drop a pin with a note', category: 'Tools' },
|
|
246
271
|
{ key: 'X', description: 'Section tool', category: 'Tools' },
|
|
247
272
|
{ key: 'S', description: 'Toggle snapping (Measure tool)', category: 'Tools' },
|
|
248
273
|
{ key: 'Esc', description: 'Cancel measurement (Measure tool)', category: 'Tools' },
|
package/src/hooks/useSandbox.ts
CHANGED
|
@@ -165,7 +165,7 @@ export function useSandbox(config?: SandboxConfig) {
|
|
|
165
165
|
// Create a fresh sandbox for every execution — full isolation
|
|
166
166
|
const { createSandbox } = await import('@ifc-lite/sandbox');
|
|
167
167
|
sandbox = await createSandbox(bim, {
|
|
168
|
-
permissions: { model: true, query: true, viewer: true, mutate: true, lens: true, export: true, files: true, ...config?.permissions },
|
|
168
|
+
permissions: { model: true, query: true, viewer: true, mutate: true, store: true, lens: true, export: true, files: true, ...config?.permissions },
|
|
169
169
|
limits: { timeoutMs: 30_000, ...config?.limits },
|
|
170
170
|
});
|
|
171
171
|
activeSandboxRef.current = sandbox;
|