@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
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { MouseHandlerContext } from './mouseHandlerTypes.js';
|
|
12
|
+
import { useViewerStore } from '@/store';
|
|
13
|
+
import { fromGlobalIdFromModels, toGlobalIdFromModels } from '@/store/globalId';
|
|
14
|
+
import { toast } from '@/components/ui/toast';
|
|
12
15
|
|
|
13
16
|
/**
|
|
14
17
|
* Handle click event for selection (single click and double click).
|
|
@@ -36,6 +39,63 @@ export async function handleSelectionClick(ctx: MouseHandlerContext, e: MouseEve
|
|
|
36
39
|
return; // Skip click handling for measure tool
|
|
37
40
|
}
|
|
38
41
|
|
|
42
|
+
// Add-element tool — multi-click placement (start→end for walls/beams,
|
|
43
|
+
// corner→opposite for slab rectangle, N+Enter for slab polygon, single
|
|
44
|
+
// for columns). Uses magnetic snap so points lock to vertices/edges
|
|
45
|
+
// when the cursor is near them — same UX as the measure tool.
|
|
46
|
+
if (tool === 'addElement') {
|
|
47
|
+
const currentLock = ctx.edgeLockStateRef.current;
|
|
48
|
+
const result = renderer.raycastSceneMagnetic(x, y, {
|
|
49
|
+
edge: currentLock.edge,
|
|
50
|
+
meshExpressId: currentLock.meshExpressId,
|
|
51
|
+
lockStrength: currentLock.lockStrength,
|
|
52
|
+
}, {
|
|
53
|
+
hiddenIds: ctx.hiddenEntitiesRef.current,
|
|
54
|
+
isolatedIds: ctx.isolatedEntitiesRef.current,
|
|
55
|
+
snapOptions: ctx.snapEnabledRef.current ? {
|
|
56
|
+
snapToVertices: true,
|
|
57
|
+
snapToEdges: true,
|
|
58
|
+
snapToFaces: true,
|
|
59
|
+
screenSnapRadius: 40,
|
|
60
|
+
} : {
|
|
61
|
+
snapToVertices: false,
|
|
62
|
+
snapToEdges: false,
|
|
63
|
+
snapToFaces: false,
|
|
64
|
+
screenSnapRadius: 0,
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
const point = result.snapTarget?.position
|
|
68
|
+
?? result.intersection?.point
|
|
69
|
+
?? raycastStoreyFloor(ctx, x, y);
|
|
70
|
+
if (!point) return;
|
|
71
|
+
await handleAddElementDrop(point);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Annotate tool — drop a pin at the cursor's world point.
|
|
76
|
+
// Raycasts the scene; if the click misses geometry the draft is
|
|
77
|
+
// not opened (annotations are anchored to surface points by
|
|
78
|
+
// design, not floating in space).
|
|
79
|
+
if (tool === 'annotate') {
|
|
80
|
+
const result = renderer.raycastScene(x, y, ctx.getPickOptions());
|
|
81
|
+
if (!result?.intersection) return;
|
|
82
|
+
const { intersection } = result;
|
|
83
|
+
const store = useViewerStore.getState();
|
|
84
|
+
// Federated models — resolve which model the hit globalId belongs
|
|
85
|
+
// to so the annotation carries enough context to render its
|
|
86
|
+
// popover header. Falls back to (null, expressId) when there's
|
|
87
|
+
// only the legacy single-model state.
|
|
88
|
+
const modelLookup = fromGlobalIdFromModels(store.models, intersection.expressId);
|
|
89
|
+
const modelId = modelLookup?.modelId ?? null;
|
|
90
|
+
const localExpressId = modelLookup?.expressId ?? intersection.expressId;
|
|
91
|
+
store.beginDraft(
|
|
92
|
+
{ x: intersection.point.x, y: intersection.point.y, z: intersection.point.z },
|
|
93
|
+
localExpressId ?? null,
|
|
94
|
+
modelId,
|
|
95
|
+
);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
39
99
|
const now = Date.now();
|
|
40
100
|
const timeSinceLastClick = now - ctx.lastClickTimeRef.current;
|
|
41
101
|
const clickPos = { x, y };
|
|
@@ -71,6 +131,392 @@ export async function handleSelectionClick(ctx: MouseHandlerContext, e: MouseEve
|
|
|
71
131
|
}
|
|
72
132
|
}
|
|
73
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Find the first IfcBuildingStorey entity in the active model. Used as a
|
|
136
|
+
* fallback when the user hasn't picked a target storey in the panel.
|
|
137
|
+
*/
|
|
138
|
+
function firstStoreyExpressId(modelId: string): number | null {
|
|
139
|
+
const state = useViewerStore.getState();
|
|
140
|
+
const model = state.models.get(modelId);
|
|
141
|
+
const ids = model?.ifcDataStore?.entityIndex.byType.get('IFCBUILDINGSTOREY');
|
|
142
|
+
return ids && ids.length > 0 ? ids[0] : null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Active model resolver — falls back through the same legacy chain
|
|
147
|
+
* the rest of the viewer uses when a single model is loaded.
|
|
148
|
+
*/
|
|
149
|
+
function resolveActiveModelId(): string | null {
|
|
150
|
+
const state = useViewerStore.getState();
|
|
151
|
+
if (state.activeModelId) return state.activeModelId;
|
|
152
|
+
const first = state.models.keys().next();
|
|
153
|
+
return first.done ? null : first.value;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Convert a renderer Y-up world point to IFC Z-up storey-local
|
|
158
|
+
* coordinates with Z forced to the storey floor (0). Mirrors the
|
|
159
|
+
* matrix in `packages/renderer/src/pipeline.ts`. Z is clamped so the
|
|
160
|
+
* click landing on a vertical surface doesn't lift the element above
|
|
161
|
+
* the floor — matches construction-tool placement intuition. Refine
|
|
162
|
+
* via the Raw STEP tab if needed.
|
|
163
|
+
*/
|
|
164
|
+
export function rendererPointToIfcStoreyLocal(point: { x: number; y: number; z: number }): [number, number, number] {
|
|
165
|
+
return [point.x, -point.z, 0];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Storey-floor ray-plane intersection — used as a fallback when the
|
|
170
|
+
* scene raycast misses every mesh (so the user can place new elements
|
|
171
|
+
* in empty space, not just on existing surfaces). The floor sits at
|
|
172
|
+
* renderer Y = storey elevation; if no storey is selected we use 0
|
|
173
|
+
* (the renderer's default ground plane).
|
|
174
|
+
*/
|
|
175
|
+
function raycastStoreyFloor(
|
|
176
|
+
ctx: MouseHandlerContext,
|
|
177
|
+
x: number,
|
|
178
|
+
y: number,
|
|
179
|
+
): { x: number; y: number; z: number } | null {
|
|
180
|
+
const camera = ctx.renderer.getCamera();
|
|
181
|
+
const canvas = ctx.renderer.getCanvas();
|
|
182
|
+
if (!camera || !canvas) return null;
|
|
183
|
+
const ray = camera.unprojectToRay(x, y, canvas.clientWidth, canvas.clientHeight);
|
|
184
|
+
if (!ray) return null;
|
|
185
|
+
const planeY = resolveStoreyFloorY();
|
|
186
|
+
// Looking down typically means D.y < 0; reject parallel / near-parallel
|
|
187
|
+
// cases so we don't hand back a wildly extrapolated intersection.
|
|
188
|
+
const dy = ray.direction.y;
|
|
189
|
+
if (Math.abs(dy) < 1e-6) return null;
|
|
190
|
+
const t = (planeY - ray.origin.y) / dy;
|
|
191
|
+
if (!Number.isFinite(t) || t <= 0) return null;
|
|
192
|
+
return {
|
|
193
|
+
x: ray.origin.x + ray.direction.x * t,
|
|
194
|
+
y: planeY,
|
|
195
|
+
z: ray.origin.z + ray.direction.z * t,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Resolve the renderer Y of the currently selected (or first
|
|
201
|
+
* available) storey's floor. Falls back to 0 when nothing is loaded.
|
|
202
|
+
*/
|
|
203
|
+
function resolveStoreyFloorY(): number {
|
|
204
|
+
const state = useViewerStore.getState();
|
|
205
|
+
const modelId = state.addElementModelId ?? state.activeModelId;
|
|
206
|
+
if (!modelId) return 0;
|
|
207
|
+
const model = state.models.get(modelId);
|
|
208
|
+
const ds = model?.ifcDataStore;
|
|
209
|
+
if (!ds) return 0;
|
|
210
|
+
const storeyId = state.addElementStoreyId ?? firstStoreyExpressId(modelId);
|
|
211
|
+
if (storeyId === null) return 0;
|
|
212
|
+
return ds.spatialHierarchy?.storeyElevations?.get(storeyId) ?? 0;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Update the live hover preview for the add-element tool. Runs the
|
|
217
|
+
* same magnetic raycast as the click handler and keeps `hoverPoint`
|
|
218
|
+
* in sync with whatever the next click would place. Used by the
|
|
219
|
+
* 3D-overlay preview so the user sees the in-progress edge / rectangle
|
|
220
|
+
* / polygon segment as they move the cursor.
|
|
221
|
+
*
|
|
222
|
+
* Returns true when handled so the mouse-controls hook can early-out
|
|
223
|
+
* before falling through to the generic hover-tooltip path.
|
|
224
|
+
*/
|
|
225
|
+
export function handleAddElementHover(ctx: MouseHandlerContext, x: number, y: number): boolean {
|
|
226
|
+
const { renderer } = ctx;
|
|
227
|
+
if (!ctx.measureRaycastPendingRef.current) {
|
|
228
|
+
ctx.measureRaycastPendingRef.current = true;
|
|
229
|
+
ctx.measureRaycastFrameRef.current = requestAnimationFrame(() => {
|
|
230
|
+
ctx.measureRaycastPendingRef.current = false;
|
|
231
|
+
ctx.measureRaycastFrameRef.current = null;
|
|
232
|
+
|
|
233
|
+
const currentLock = ctx.edgeLockStateRef.current;
|
|
234
|
+
const result = renderer.raycastSceneMagnetic(x, y, {
|
|
235
|
+
edge: currentLock.edge,
|
|
236
|
+
meshExpressId: currentLock.meshExpressId,
|
|
237
|
+
lockStrength: currentLock.lockStrength,
|
|
238
|
+
}, {
|
|
239
|
+
hiddenIds: ctx.hiddenEntitiesRef.current,
|
|
240
|
+
isolatedIds: ctx.isolatedEntitiesRef.current,
|
|
241
|
+
snapOptions: ctx.snapEnabledRef.current ? {
|
|
242
|
+
snapToVertices: true,
|
|
243
|
+
snapToEdges: true,
|
|
244
|
+
snapToFaces: true,
|
|
245
|
+
screenSnapRadius: 40,
|
|
246
|
+
} : {
|
|
247
|
+
snapToVertices: false,
|
|
248
|
+
snapToEdges: false,
|
|
249
|
+
snapToFaces: false,
|
|
250
|
+
screenSnapRadius: 0,
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const point = result.snapTarget?.position
|
|
255
|
+
?? result.intersection?.point
|
|
256
|
+
?? raycastStoreyFloor(ctx, x, y);
|
|
257
|
+
const store = useViewerStore.getState();
|
|
258
|
+
store.setAddElementHoverPoint(point ? { x: point.x, y: point.y, z: point.z } : null);
|
|
259
|
+
|
|
260
|
+
// Mirror measure's snap-viz behaviour so vertex/edge/face indicators
|
|
261
|
+
// appear under the cursor with the same UX shape.
|
|
262
|
+
ctx.setSnapTarget(result.snapTarget ?? null);
|
|
263
|
+
if (result.snapTarget) {
|
|
264
|
+
if (result.edgeLock.shouldRelease) {
|
|
265
|
+
ctx.clearEdgeLock();
|
|
266
|
+
} else if (result.edgeLock.shouldLock && result.edgeLock.edge) {
|
|
267
|
+
ctx.setEdgeLock(result.edgeLock.edge, result.edgeLock.meshExpressId!, result.edgeLock.edgeT);
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
ctx.clearEdgeLock();
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Resolve the active model + storey + a snap-aware world point. Surfaces
|
|
279
|
+
* the same toast errors all add-element entry points share.
|
|
280
|
+
*/
|
|
281
|
+
function resolveAddElementContext(): { modelId: string; storeyId: number } | null {
|
|
282
|
+
const state = useViewerStore.getState();
|
|
283
|
+
const modelId = state.addElementModelId ?? resolveActiveModelId();
|
|
284
|
+
if (!modelId) {
|
|
285
|
+
toast.error("Couldn't add element: no model loaded");
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
const storeyId = state.addElementStoreyId ?? firstStoreyExpressId(modelId);
|
|
289
|
+
if (storeyId === null) {
|
|
290
|
+
toast.error("Couldn't add element: model has no IfcBuildingStorey");
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
return { modelId, storeyId };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Common post-place: pick the new entity's global id, toast, clear pending. */
|
|
297
|
+
function finishAddElement(
|
|
298
|
+
result: { expressId: number } | { error: string },
|
|
299
|
+
modelId: string,
|
|
300
|
+
label: string,
|
|
301
|
+
): void {
|
|
302
|
+
const state = useViewerStore.getState();
|
|
303
|
+
if ('error' in result) {
|
|
304
|
+
toast.error(`Couldn't add ${label.toLowerCase()}: ${result.error}`);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const globalId = toGlobalIdFromModels(state.models, modelId, result.expressId);
|
|
308
|
+
state.setSelectedEntityId(globalId);
|
|
309
|
+
state.clearAddElementPending();
|
|
310
|
+
toast.success(`${label} #${result.expressId} added — undo to remove`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Handle a click landing on the scene while the addElement tool is
|
|
315
|
+
* active. Implements a per-type click state machine:
|
|
316
|
+
*
|
|
317
|
+
* - column: 1 click → place
|
|
318
|
+
* - wall / beam: 1st click → start, 2nd click → end + place
|
|
319
|
+
* - slab (rectangle): 1st click → corner, 2nd click → opposite + place
|
|
320
|
+
* - slab (polygon): N clicks accumulate; Enter / double-click closes
|
|
321
|
+
* (handled in the keyboard layer; this function only appends)
|
|
322
|
+
*/
|
|
323
|
+
async function handleAddElementDrop(point: { x: number; y: number; z: number }): Promise<void> {
|
|
324
|
+
const ctx = resolveAddElementContext();
|
|
325
|
+
if (!ctx) return;
|
|
326
|
+
const { modelId, storeyId } = ctx;
|
|
327
|
+
|
|
328
|
+
const state = useViewerStore.getState();
|
|
329
|
+
const type = state.addElementType;
|
|
330
|
+
|
|
331
|
+
// Single-click placements: column / door / window all drop on one click.
|
|
332
|
+
if (type === 'column') {
|
|
333
|
+
const ifc = rendererPointToIfcStoreyLocal(point);
|
|
334
|
+
const p = state.addElementColumnParams;
|
|
335
|
+
finishAddElement(state.addColumn(modelId, storeyId, {
|
|
336
|
+
Position: ifc, Width: p.Width, Depth: p.Depth, Height: p.Height,
|
|
337
|
+
}), modelId, 'Column');
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (type === 'door') {
|
|
341
|
+
const ifc = rendererPointToIfcStoreyLocal(point);
|
|
342
|
+
const p = state.addElementDoorParams;
|
|
343
|
+
finishAddElement(state.addDoor(modelId, storeyId, {
|
|
344
|
+
Position: ifc, Width: p.Width, Height: p.Height, FrameThickness: p.FrameThickness,
|
|
345
|
+
}), modelId, 'Door');
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (type === 'window') {
|
|
349
|
+
const ifc = rendererPointToIfcStoreyLocal(point);
|
|
350
|
+
const p = state.addElementWindowParams;
|
|
351
|
+
finishAddElement(state.addWindow(modelId, storeyId, {
|
|
352
|
+
Position: ifc, Width: p.Width, Height: p.Height, FrameThickness: p.FrameThickness,
|
|
353
|
+
}), modelId, 'Window');
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (type === 'wall' || type === 'beam' || type === 'member') {
|
|
358
|
+
const pending = state.addElementPendingPoints;
|
|
359
|
+
if (pending.length === 0) {
|
|
360
|
+
// Start point — store the renderer-frame point and wait for end.
|
|
361
|
+
state.appendAddElementPendingPoint({ x: point.x, y: point.y, z: point.z });
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
// End point — convert both points to IFC at dispatch time.
|
|
365
|
+
const startIfc = rendererPointToIfcStoreyLocal(pending[0]);
|
|
366
|
+
const endIfc = rendererPointToIfcStoreyLocal(point);
|
|
367
|
+
if (type === 'wall') {
|
|
368
|
+
const p = state.addElementWallParams;
|
|
369
|
+
finishAddElement(state.addWall(modelId, storeyId, {
|
|
370
|
+
Start: startIfc, End: endIfc, Thickness: p.Thickness, Height: p.Height,
|
|
371
|
+
}), modelId, 'Wall');
|
|
372
|
+
} else if (type === 'beam') {
|
|
373
|
+
const p = state.addElementBeamParams;
|
|
374
|
+
finishAddElement(state.addBeam(modelId, storeyId, {
|
|
375
|
+
Start: startIfc, End: endIfc, Width: p.Width, Height: p.Height,
|
|
376
|
+
}), modelId, 'Beam');
|
|
377
|
+
} else {
|
|
378
|
+
// member
|
|
379
|
+
const p = state.addElementMemberParams;
|
|
380
|
+
finishAddElement(state.addMember(modelId, storeyId, {
|
|
381
|
+
Start: startIfc, End: endIfc, Width: p.Width, Height: p.Height,
|
|
382
|
+
}), modelId, 'Member');
|
|
383
|
+
}
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (type === 'slab' || type === 'roof' || type === 'plate' || type === 'space') {
|
|
388
|
+
if (state.addElementSlabMode === 'rectangle') {
|
|
389
|
+
const pending = state.addElementPendingPoints;
|
|
390
|
+
if (pending.length === 0) {
|
|
391
|
+
state.appendAddElementPendingPoint({ x: point.x, y: point.y, z: point.z });
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const cornerIfc = rendererPointToIfcStoreyLocal(pending[0]);
|
|
395
|
+
const oppositeIfc = rendererPointToIfcStoreyLocal(point);
|
|
396
|
+
const minX = Math.min(cornerIfc[0], oppositeIfc[0]);
|
|
397
|
+
const minY = Math.min(cornerIfc[1], oppositeIfc[1]);
|
|
398
|
+
const width = Math.abs(oppositeIfc[0] - cornerIfc[0]);
|
|
399
|
+
const depth = Math.abs(oppositeIfc[1] - cornerIfc[1]);
|
|
400
|
+
if (width <= 0 || depth <= 0) {
|
|
401
|
+
toast.error(`${capitalize(type)} corners must span a non-zero rectangle`);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const position: [number, number, number] = [minX, minY, 0];
|
|
405
|
+
switch (type) {
|
|
406
|
+
case 'slab': {
|
|
407
|
+
const p = state.addElementSlabParams;
|
|
408
|
+
finishAddElement(state.addSlab(modelId, storeyId, {
|
|
409
|
+
Position: position, Width: width, Depth: depth, Thickness: p.Thickness,
|
|
410
|
+
}), modelId, 'Slab');
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
case 'roof': {
|
|
414
|
+
const p = state.addElementRoofParams;
|
|
415
|
+
finishAddElement(state.addRoof(modelId, storeyId, {
|
|
416
|
+
Position: position, Width: width, Depth: depth, Thickness: p.Thickness,
|
|
417
|
+
}), modelId, 'Roof');
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
case 'plate': {
|
|
421
|
+
const p = state.addElementPlateParams;
|
|
422
|
+
finishAddElement(state.addPlate(modelId, storeyId, {
|
|
423
|
+
Position: position, Width: width, Depth: depth, Thickness: p.Thickness,
|
|
424
|
+
}), modelId, 'Plate');
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
case 'space': {
|
|
428
|
+
const p = state.addElementSpaceParams;
|
|
429
|
+
finishAddElement(state.addSpace(modelId, storeyId, {
|
|
430
|
+
Position: position, Width: width, Depth: depth, Height: p.Height,
|
|
431
|
+
}), modelId, 'Space');
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
// Polygon mode — append; close handled by Enter.
|
|
437
|
+
state.appendAddElementPendingPoint({ x: point.x, y: point.y, z: point.z });
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function capitalize(s: string): string {
|
|
443
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** Signed 2D polygon area via the shoelace formula. */
|
|
447
|
+
function polygonArea2D(points: Array<[number, number]>): number {
|
|
448
|
+
if (points.length < 3) return 0;
|
|
449
|
+
let area = 0;
|
|
450
|
+
for (let i = 0; i < points.length; i++) {
|
|
451
|
+
const [x1, y1] = points[i];
|
|
452
|
+
const [x2, y2] = points[(i + 1) % points.length];
|
|
453
|
+
area += x1 * y2 - x2 * y1;
|
|
454
|
+
}
|
|
455
|
+
return area * 0.5;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Close an in-progress polygon for any slab-style type
|
|
460
|
+
* (slab / roof / plate / space). Triggered by Enter. Requires
|
|
461
|
+
* ≥3 points; the builder's auto-closure handles the trailing edge.
|
|
462
|
+
*/
|
|
463
|
+
export function commitAddElementSlabPolygon(): void {
|
|
464
|
+
const state = useViewerStore.getState();
|
|
465
|
+
if (state.activeTool !== 'addElement') return;
|
|
466
|
+
const type = state.addElementType;
|
|
467
|
+
const polygonable = type === 'slab' || type === 'roof' || type === 'plate' || type === 'space';
|
|
468
|
+
if (!polygonable || state.addElementSlabMode !== 'polygon') return;
|
|
469
|
+
const pending = state.addElementPendingPoints;
|
|
470
|
+
if (pending.length < 3) {
|
|
471
|
+
toast.error(`${capitalize(type)} polygon needs at least 3 points`);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const ctx = resolveAddElementContext();
|
|
475
|
+
if (!ctx) return;
|
|
476
|
+
const { modelId, storeyId } = ctx;
|
|
477
|
+
const outer = pending.map((pt) => {
|
|
478
|
+
const ifc = rendererPointToIfcStoreyLocal(pt);
|
|
479
|
+
return [ifc[0], ifc[1]] as [number, number];
|
|
480
|
+
});
|
|
481
|
+
// Reject degenerate (zero-area) polygons — repeated or collinear
|
|
482
|
+
// pending points would otherwise produce an OuterCurve that exports
|
|
483
|
+
// as an invalid slab/roof/plate/space profile.
|
|
484
|
+
if (Math.abs(polygonArea2D(outer)) < 1e-6) {
|
|
485
|
+
toast.error(`${capitalize(type)} polygon must have a non-zero area`);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
switch (type) {
|
|
489
|
+
case 'slab': {
|
|
490
|
+
const p = state.addElementSlabParams;
|
|
491
|
+
finishAddElement(state.addSlab(modelId, storeyId, {
|
|
492
|
+
Profile: 'polygon', OuterCurve: outer, Thickness: p.Thickness,
|
|
493
|
+
}), modelId, 'Slab');
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
case 'roof': {
|
|
497
|
+
const p = state.addElementRoofParams;
|
|
498
|
+
finishAddElement(state.addRoof(modelId, storeyId, {
|
|
499
|
+
Profile: 'polygon', OuterCurve: outer, Thickness: p.Thickness,
|
|
500
|
+
}), modelId, 'Roof');
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
case 'plate': {
|
|
504
|
+
const p = state.addElementPlateParams;
|
|
505
|
+
finishAddElement(state.addPlate(modelId, storeyId, {
|
|
506
|
+
Profile: 'polygon', OuterCurve: outer, Thickness: p.Thickness,
|
|
507
|
+
}), modelId, 'Plate');
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
case 'space': {
|
|
511
|
+
const p = state.addElementSpaceParams;
|
|
512
|
+
finishAddElement(state.addSpace(modelId, storeyId, {
|
|
513
|
+
Profile: 'polygon', OuterCurve: outer, Height: p.Height,
|
|
514
|
+
}), modelId, 'Space');
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
74
520
|
/**
|
|
75
521
|
* Handle context menu event (right-click).
|
|
76
522
|
* Picks the entity under the cursor and opens the context menu.
|