@ifc-lite/viewer 1.1.6 → 1.5.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/LICENSE +373 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/Arrow.dom-B0e15b_b.js +20 -0
- package/dist/assets/arrow2-bb-jcVEo.js +2 -0
- package/dist/assets/arrow2_bg-4Y7xYo54.wasm +0 -0
- package/dist/assets/arrow2_bg-BlXl-cSQ.js +1 -0
- package/dist/assets/arrow2_bg-BoXCojjR.wasm +0 -0
- package/dist/assets/desktop-cache-oPzaWXYE.js +1 -0
- package/dist/assets/event-DIOks52T.js +1 -0
- package/dist/assets/ifc-cache-BAN4vcd4.js +1 -0
- package/dist/assets/ifc-lite_bg-C6kblxf9.wasm +0 -0
- package/dist/assets/index-Dgd6vzw_.js +65252 -0
- package/dist/assets/index-v3mcCUPN.css +1 -0
- package/dist/assets/native-bridge-Ci7NLjlZ.js +111 -0
- package/dist/assets/wasm-bridge-Dc82YpdZ.js +1 -0
- package/dist/favicon-16x16-cropped.png +0 -0
- package/dist/favicon-16x16.png +0 -0
- package/dist/favicon-192x192-cropped.png +0 -0
- package/dist/favicon-192x192.png +0 -0
- package/dist/favicon-32x32-cropped.png +0 -0
- package/dist/favicon-32x32.png +0 -0
- package/dist/favicon-48x48-cropped.png +0 -0
- package/dist/favicon-48x48.png +0 -0
- package/dist/favicon-512x512-cropped.png +0 -0
- package/dist/favicon-512x512.png +0 -0
- package/dist/favicon-64x64-cropped.png +0 -0
- package/dist/favicon-64x64.png +0 -0
- package/dist/favicon-96x96-cropped.png +0 -0
- package/dist/favicon-96x96.png +0 -0
- package/dist/favicon-square-512.png +0 -0
- package/dist/favicon.ico +0 -0
- package/dist/favicon.png +0 -0
- package/dist/favicon.svg +3 -0
- package/dist/index.html +44 -0
- package/dist/logo.png +0 -0
- package/dist/manifest.json +48 -0
- package/index.html +33 -2
- package/package.json +34 -17
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon-16x16-cropped.png +0 -0
- package/public/favicon-16x16.png +0 -0
- package/public/favicon-192x192-cropped.png +0 -0
- package/public/favicon-192x192.png +0 -0
- package/public/favicon-32x32-cropped.png +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon-48x48-cropped.png +0 -0
- package/public/favicon-48x48.png +0 -0
- package/public/favicon-512x512-cropped.png +0 -0
- package/public/favicon-512x512.png +0 -0
- package/public/favicon-64x64-cropped.png +0 -0
- package/public/favicon-64x64.png +0 -0
- package/public/favicon-96x96-cropped.png +0 -0
- package/public/favicon-96x96.png +0 -0
- package/public/favicon-square-512.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.png +0 -0
- package/public/favicon.svg +3 -0
- package/public/logo.png +0 -0
- package/public/manifest.json +48 -0
- package/src/App.tsx +2 -0
- package/src/components/ui/alert.tsx +62 -0
- package/src/components/ui/badge.tsx +39 -0
- package/src/components/ui/dialog.tsx +120 -0
- package/src/components/ui/label.tsx +27 -0
- package/src/components/ui/select.tsx +151 -0
- package/src/components/ui/switch.tsx +30 -0
- package/src/components/ui/table.tsx +120 -0
- package/src/components/ui/tabs.tsx +1 -1
- package/src/components/viewer/BCFPanel.tsx +1164 -0
- package/src/components/viewer/BulkPropertyEditor.tsx +875 -0
- package/src/components/viewer/DataConnector.tsx +840 -0
- package/src/components/viewer/DrawingSettingsPanel.tsx +536 -0
- package/src/components/viewer/EntityContextMenu.tsx +45 -17
- package/src/components/viewer/ExportChangesButton.tsx +195 -0
- package/src/components/viewer/ExportDialog.tsx +402 -0
- package/src/components/viewer/HierarchyPanel.tsx +1132 -218
- package/src/components/viewer/IDSPanel.tsx +661 -0
- package/src/components/viewer/KeyboardShortcutsDialog.tsx +245 -39
- package/src/components/viewer/MainToolbar.tsx +418 -94
- package/src/components/viewer/PropertiesPanel.tsx +1355 -91
- package/src/components/viewer/PropertyEditor.tsx +611 -0
- package/src/components/viewer/Section2DPanel.tsx +3313 -0
- package/src/components/viewer/SheetSetupPanel.tsx +502 -0
- package/src/components/viewer/StatusBar.tsx +27 -16
- package/src/components/viewer/TitleBlockEditor.tsx +437 -0
- package/src/components/viewer/ToolOverlays.tsx +935 -127
- package/src/components/viewer/ViewerLayout.tsx +40 -11
- package/src/components/viewer/Viewport.tsx +1276 -336
- package/src/components/viewer/ViewportContainer.tsx +554 -18
- package/src/components/viewer/ViewportOverlays.tsx +24 -7
- package/src/hooks/useBCF.ts +504 -0
- package/src/hooks/useIDS.ts +1065 -0
- package/src/hooks/useIfc.ts +1534 -205
- package/src/hooks/useIfcCache.ts +279 -0
- package/src/hooks/useKeyboardShortcuts.ts +50 -8
- package/src/hooks/useModelSelection.ts +61 -0
- package/src/hooks/useViewerSelectors.ts +218 -0
- package/src/hooks/useWebGPU.ts +80 -0
- package/src/index.css +265 -27
- package/src/lib/platform.ts +23 -0
- package/src/services/cacheService.ts +142 -0
- package/src/services/desktop-cache.ts +143 -0
- package/src/services/fs-cache.ts +212 -0
- package/src/services/ifc-cache.ts +14 -6
- package/src/store/constants.ts +85 -0
- package/src/store/index.ts +214 -0
- package/src/store/slices/bcfSlice.ts +372 -0
- package/src/store/slices/cameraSlice.ts +63 -0
- package/src/store/slices/dataSlice.test.ts +226 -0
- package/src/store/slices/dataSlice.ts +112 -0
- package/src/store/slices/drawing2DSlice.ts +340 -0
- package/src/store/slices/hoverSlice.ts +40 -0
- package/src/store/slices/idsSlice.ts +310 -0
- package/src/store/slices/loadingSlice.ts +33 -0
- package/src/store/slices/measurementSlice.test.ts +217 -0
- package/src/store/slices/measurementSlice.ts +293 -0
- package/src/store/slices/modelSlice.test.ts +271 -0
- package/src/store/slices/modelSlice.ts +211 -0
- package/src/store/slices/mutationSlice.ts +502 -0
- package/src/store/slices/sectionSlice.test.ts +125 -0
- package/src/store/slices/sectionSlice.ts +58 -0
- package/src/store/slices/selectionSlice.test.ts +286 -0
- package/src/store/slices/selectionSlice.ts +263 -0
- package/src/store/slices/sheetSlice.ts +565 -0
- package/src/store/slices/uiSlice.ts +58 -0
- package/src/store/slices/visibilitySlice.test.ts +304 -0
- package/src/store/slices/visibilitySlice.ts +277 -0
- package/src/store/types.test.ts +135 -0
- package/src/store/types.ts +248 -0
- package/src/store.ts +40 -515
- package/src/utils/ifcConfig.ts +82 -0
- package/src/utils/localParsingUtils.ts +287 -0
- package/src/utils/serverDataModel.ts +783 -0
- package/src/utils/spatialHierarchy.ts +283 -0
- package/src/utils/viewportUtils.ts +334 -0
- package/src/vite-env.d.ts +23 -0
- package/src/webgpu-types.d.ts +128 -0
- package/src-tauri/Cargo.toml +29 -0
- package/src-tauri/build.rs +7 -0
- package/src-tauri/capabilities/default.json +18 -0
- package/src-tauri/icons/128x128.png +0 -0
- package/src-tauri/icons/128x128@2x.png +0 -0
- package/src-tauri/icons/32x32.png +0 -0
- package/src-tauri/icons/Square107x107Logo.png +0 -0
- package/src-tauri/icons/Square142x142Logo.png +0 -0
- package/src-tauri/icons/Square150x150Logo.png +0 -0
- package/src-tauri/icons/Square284x284Logo.png +0 -0
- package/src-tauri/icons/Square30x30Logo.png +0 -0
- package/src-tauri/icons/Square310x310Logo.png +0 -0
- package/src-tauri/icons/Square44x44Logo.png +0 -0
- package/src-tauri/icons/Square71x71Logo.png +0 -0
- package/src-tauri/icons/Square89x89Logo.png +0 -0
- package/src-tauri/icons/StoreLogo.png +0 -0
- package/src-tauri/icons/icon.icns +0 -0
- package/src-tauri/icons/icon.ico +0 -0
- package/src-tauri/icons/icon.png +0 -0
- package/src-tauri/src/lib.rs +21 -0
- package/src-tauri/src/main.rs +10 -0
- package/src-tauri/tauri.conf.json +39 -0
- package/vite.config.ts +174 -26
- package/public/ifc-lite_bg.wasm +0 -0
- package/public/web-ifc.wasm +0 -0
- package/src/components/Viewport.tsx +0 -723
- package/src/components/viewer/BoxSelectionOverlay.tsx +0 -53
|
@@ -0,0 +1,293 @@
|
|
|
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
|
+
* Measurement state slice
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { StateCreator } from 'zustand';
|
|
10
|
+
import type { SnapTarget } from '@ifc-lite/renderer';
|
|
11
|
+
import type {
|
|
12
|
+
MeasurePoint,
|
|
13
|
+
Measurement,
|
|
14
|
+
ActiveMeasurement,
|
|
15
|
+
EdgeLockState,
|
|
16
|
+
SnapVisualization,
|
|
17
|
+
MeasurementConstraintEdge,
|
|
18
|
+
OrthogonalAxis,
|
|
19
|
+
} from '../types.js';
|
|
20
|
+
import { EDGE_LOCK_DEFAULTS } from '../constants.js';
|
|
21
|
+
|
|
22
|
+
// Monotonic counter to prevent ID collisions under rapid measurement creation
|
|
23
|
+
let measurementCounter = 0;
|
|
24
|
+
|
|
25
|
+
export interface MeasurementSlice {
|
|
26
|
+
// State
|
|
27
|
+
measurements: Measurement[];
|
|
28
|
+
pendingMeasurePoint: MeasurePoint | null;
|
|
29
|
+
activeMeasurement: ActiveMeasurement | null;
|
|
30
|
+
snapTarget: SnapTarget | null;
|
|
31
|
+
snapEnabled: boolean;
|
|
32
|
+
snapVisualization: SnapVisualization | null;
|
|
33
|
+
edgeLockState: EdgeLockState;
|
|
34
|
+
/** Edge constraint for perpendicular measurements (when shift is held) */
|
|
35
|
+
measurementConstraintEdge: MeasurementConstraintEdge | null;
|
|
36
|
+
|
|
37
|
+
// Legacy measurement actions
|
|
38
|
+
addMeasurePoint: (point: MeasurePoint) => void;
|
|
39
|
+
completeMeasurement: (endPoint: MeasurePoint) => void;
|
|
40
|
+
|
|
41
|
+
// Drag-based measurement actions
|
|
42
|
+
startMeasurement: (point: MeasurePoint) => void;
|
|
43
|
+
updateMeasurement: (point: MeasurePoint) => void;
|
|
44
|
+
finalizeMeasurement: () => void;
|
|
45
|
+
cancelMeasurement: () => void;
|
|
46
|
+
deleteMeasurement: (id: string) => void;
|
|
47
|
+
clearMeasurements: () => void;
|
|
48
|
+
updateMeasurementScreenCoords: (
|
|
49
|
+
projectToScreen: (worldPos: { x: number; y: number; z: number }) => { x: number; y: number } | null
|
|
50
|
+
) => void;
|
|
51
|
+
|
|
52
|
+
// Snap actions
|
|
53
|
+
setSnapTarget: (target: SnapTarget | null) => void;
|
|
54
|
+
setSnapVisualization: (viz: SnapVisualization | null) => void;
|
|
55
|
+
toggleSnap: () => void;
|
|
56
|
+
|
|
57
|
+
// Edge lock actions
|
|
58
|
+
setEdgeLock: (edge: EdgeLockState['edge'], meshExpressId: number | null, edgeT?: number) => void;
|
|
59
|
+
updateEdgeLockPosition: (edgeT: number, isCorner: boolean, cornerValence: number) => void;
|
|
60
|
+
clearEdgeLock: () => void;
|
|
61
|
+
incrementEdgeLockStrength: () => void;
|
|
62
|
+
|
|
63
|
+
// Orthogonal constraint actions (shift+drag)
|
|
64
|
+
setMeasurementConstraintEdge: (edge: MeasurementConstraintEdge | null) => void;
|
|
65
|
+
updateConstraintActiveAxis: (axis: OrthogonalAxis | null) => void;
|
|
66
|
+
clearMeasurementConstraintEdge: () => void;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const getDefaultEdgeLockState = (): EdgeLockState => ({
|
|
70
|
+
edge: null,
|
|
71
|
+
meshExpressId: null,
|
|
72
|
+
edgeT: 0,
|
|
73
|
+
lockStrength: 0,
|
|
74
|
+
isCorner: false,
|
|
75
|
+
cornerValence: 0,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export const createMeasurementSlice: StateCreator<MeasurementSlice, [], [], MeasurementSlice> = (set, get) => ({
|
|
79
|
+
// Initial state
|
|
80
|
+
measurements: [],
|
|
81
|
+
pendingMeasurePoint: null,
|
|
82
|
+
activeMeasurement: null,
|
|
83
|
+
snapTarget: null,
|
|
84
|
+
snapEnabled: true,
|
|
85
|
+
snapVisualization: null,
|
|
86
|
+
edgeLockState: getDefaultEdgeLockState(),
|
|
87
|
+
measurementConstraintEdge: null,
|
|
88
|
+
|
|
89
|
+
// Legacy measurement actions
|
|
90
|
+
addMeasurePoint: (point) => set({ pendingMeasurePoint: point }),
|
|
91
|
+
|
|
92
|
+
completeMeasurement: (endPoint) => set((state) => {
|
|
93
|
+
if (!state.pendingMeasurePoint) return {};
|
|
94
|
+
const start = state.pendingMeasurePoint;
|
|
95
|
+
const distance = Math.sqrt(
|
|
96
|
+
Math.pow(endPoint.x - start.x, 2) +
|
|
97
|
+
Math.pow(endPoint.y - start.y, 2) +
|
|
98
|
+
Math.pow(endPoint.z - start.z, 2)
|
|
99
|
+
);
|
|
100
|
+
// Use counter combined with timestamp to guarantee unique IDs
|
|
101
|
+
measurementCounter++;
|
|
102
|
+
const measurement: Measurement = {
|
|
103
|
+
id: `m-${Date.now()}-${measurementCounter}`,
|
|
104
|
+
start,
|
|
105
|
+
end: endPoint,
|
|
106
|
+
distance,
|
|
107
|
+
};
|
|
108
|
+
return {
|
|
109
|
+
measurements: [...state.measurements, measurement],
|
|
110
|
+
pendingMeasurePoint: null,
|
|
111
|
+
};
|
|
112
|
+
}),
|
|
113
|
+
|
|
114
|
+
// Drag-based measurement actions
|
|
115
|
+
startMeasurement: (point) => set({
|
|
116
|
+
activeMeasurement: {
|
|
117
|
+
start: point,
|
|
118
|
+
current: point,
|
|
119
|
+
distance: 0,
|
|
120
|
+
},
|
|
121
|
+
}),
|
|
122
|
+
|
|
123
|
+
updateMeasurement: (point) => set((state) => {
|
|
124
|
+
if (!state.activeMeasurement) return {};
|
|
125
|
+
const start = state.activeMeasurement.start;
|
|
126
|
+
const distance = Math.sqrt(
|
|
127
|
+
Math.pow(point.x - start.x, 2) +
|
|
128
|
+
Math.pow(point.y - start.y, 2) +
|
|
129
|
+
Math.pow(point.z - start.z, 2)
|
|
130
|
+
);
|
|
131
|
+
return {
|
|
132
|
+
activeMeasurement: {
|
|
133
|
+
start,
|
|
134
|
+
current: point,
|
|
135
|
+
distance,
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}),
|
|
139
|
+
|
|
140
|
+
finalizeMeasurement: () => set((state) => {
|
|
141
|
+
if (!state.activeMeasurement) return {};
|
|
142
|
+
// Use counter combined with timestamp to guarantee unique IDs
|
|
143
|
+
measurementCounter++;
|
|
144
|
+
const measurement: Measurement = {
|
|
145
|
+
id: `m-${Date.now()}-${measurementCounter}`,
|
|
146
|
+
start: state.activeMeasurement.start,
|
|
147
|
+
end: state.activeMeasurement.current,
|
|
148
|
+
distance: state.activeMeasurement.distance,
|
|
149
|
+
};
|
|
150
|
+
return {
|
|
151
|
+
measurements: [...state.measurements, measurement],
|
|
152
|
+
activeMeasurement: null,
|
|
153
|
+
snapTarget: null,
|
|
154
|
+
measurementConstraintEdge: null,
|
|
155
|
+
};
|
|
156
|
+
}),
|
|
157
|
+
|
|
158
|
+
cancelMeasurement: () => set({
|
|
159
|
+
activeMeasurement: null,
|
|
160
|
+
snapTarget: null,
|
|
161
|
+
measurementConstraintEdge: null,
|
|
162
|
+
}),
|
|
163
|
+
|
|
164
|
+
deleteMeasurement: (id) => set((state) => ({
|
|
165
|
+
measurements: state.measurements.filter((m) => m.id !== id),
|
|
166
|
+
})),
|
|
167
|
+
|
|
168
|
+
clearMeasurements: () => set({
|
|
169
|
+
measurements: [],
|
|
170
|
+
pendingMeasurePoint: null,
|
|
171
|
+
activeMeasurement: null,
|
|
172
|
+
snapTarget: null,
|
|
173
|
+
}),
|
|
174
|
+
|
|
175
|
+
updateMeasurementScreenCoords: (projectToScreen) => {
|
|
176
|
+
const state = get();
|
|
177
|
+
let hasChanges = false;
|
|
178
|
+
|
|
179
|
+
// Check completed measurements for changes
|
|
180
|
+
const updatedMeasurements = state.measurements.map((m) => {
|
|
181
|
+
const startScreen = projectToScreen(m.start);
|
|
182
|
+
const endScreen = projectToScreen(m.end);
|
|
183
|
+
|
|
184
|
+
const newStartX = startScreen?.x ?? m.start.screenX;
|
|
185
|
+
const newStartY = startScreen?.y ?? m.start.screenY;
|
|
186
|
+
const newEndX = endScreen?.x ?? m.end.screenX;
|
|
187
|
+
const newEndY = endScreen?.y ?? m.end.screenY;
|
|
188
|
+
|
|
189
|
+
if (
|
|
190
|
+
newStartX !== m.start.screenX ||
|
|
191
|
+
newStartY !== m.start.screenY ||
|
|
192
|
+
newEndX !== m.end.screenX ||
|
|
193
|
+
newEndY !== m.end.screenY
|
|
194
|
+
) {
|
|
195
|
+
hasChanges = true;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
...m,
|
|
200
|
+
start: { ...m.start, screenX: newStartX, screenY: newStartY },
|
|
201
|
+
end: { ...m.end, screenX: newEndX, screenY: newEndY },
|
|
202
|
+
};
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Check active measurement for changes
|
|
206
|
+
let updatedActiveMeasurement = state.activeMeasurement;
|
|
207
|
+
if (state.activeMeasurement) {
|
|
208
|
+
const startScreen = projectToScreen(state.activeMeasurement.start);
|
|
209
|
+
const currentScreen = projectToScreen(state.activeMeasurement.current);
|
|
210
|
+
|
|
211
|
+
const newStartX = startScreen?.x ?? state.activeMeasurement.start.screenX;
|
|
212
|
+
const newStartY = startScreen?.y ?? state.activeMeasurement.start.screenY;
|
|
213
|
+
const newCurrentX = currentScreen?.x ?? state.activeMeasurement.current.screenX;
|
|
214
|
+
const newCurrentY = currentScreen?.y ?? state.activeMeasurement.current.screenY;
|
|
215
|
+
|
|
216
|
+
if (
|
|
217
|
+
newStartX !== state.activeMeasurement.start.screenX ||
|
|
218
|
+
newStartY !== state.activeMeasurement.start.screenY ||
|
|
219
|
+
newCurrentX !== state.activeMeasurement.current.screenX ||
|
|
220
|
+
newCurrentY !== state.activeMeasurement.current.screenY
|
|
221
|
+
) {
|
|
222
|
+
hasChanges = true;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
updatedActiveMeasurement = {
|
|
226
|
+
...state.activeMeasurement,
|
|
227
|
+
start: { ...state.activeMeasurement.start, screenX: newStartX, screenY: newStartY },
|
|
228
|
+
current: { ...state.activeMeasurement.current, screenX: newCurrentX, screenY: newCurrentY },
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Early exit if nothing changed
|
|
233
|
+
if (!hasChanges) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
set({
|
|
238
|
+
measurements: updatedMeasurements,
|
|
239
|
+
activeMeasurement: updatedActiveMeasurement,
|
|
240
|
+
});
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
// Snap actions
|
|
244
|
+
setSnapTarget: (snapTarget) => set({ snapTarget }),
|
|
245
|
+
setSnapVisualization: (snapVisualization) => set({ snapVisualization }),
|
|
246
|
+
toggleSnap: () => set((state) => ({ snapEnabled: !state.snapEnabled })),
|
|
247
|
+
|
|
248
|
+
// Edge lock actions
|
|
249
|
+
setEdgeLock: (edge, meshExpressId, edgeT = EDGE_LOCK_DEFAULTS.INITIAL_T) => set({
|
|
250
|
+
edgeLockState: {
|
|
251
|
+
edge,
|
|
252
|
+
meshExpressId,
|
|
253
|
+
edgeT,
|
|
254
|
+
lockStrength: EDGE_LOCK_DEFAULTS.INITIAL_STRENGTH,
|
|
255
|
+
isCorner: false,
|
|
256
|
+
cornerValence: 0,
|
|
257
|
+
},
|
|
258
|
+
}),
|
|
259
|
+
|
|
260
|
+
updateEdgeLockPosition: (edgeT, isCorner, cornerValence) => set((state) => ({
|
|
261
|
+
edgeLockState: {
|
|
262
|
+
...state.edgeLockState,
|
|
263
|
+
edgeT,
|
|
264
|
+
isCorner,
|
|
265
|
+
cornerValence,
|
|
266
|
+
},
|
|
267
|
+
})),
|
|
268
|
+
|
|
269
|
+
clearEdgeLock: () => set({ edgeLockState: getDefaultEdgeLockState() }),
|
|
270
|
+
|
|
271
|
+
incrementEdgeLockStrength: () => set((state) => ({
|
|
272
|
+
edgeLockState: {
|
|
273
|
+
...state.edgeLockState,
|
|
274
|
+
lockStrength: Math.min(
|
|
275
|
+
state.edgeLockState.lockStrength + EDGE_LOCK_DEFAULTS.STRENGTH_INCREMENT,
|
|
276
|
+
EDGE_LOCK_DEFAULTS.MAX_STRENGTH
|
|
277
|
+
),
|
|
278
|
+
},
|
|
279
|
+
})),
|
|
280
|
+
|
|
281
|
+
// Orthogonal constraint actions
|
|
282
|
+
setMeasurementConstraintEdge: (edge) => set({ measurementConstraintEdge: edge }),
|
|
283
|
+
updateConstraintActiveAxis: (axis) => set((state) => {
|
|
284
|
+
if (!state.measurementConstraintEdge) return {};
|
|
285
|
+
return {
|
|
286
|
+
measurementConstraintEdge: {
|
|
287
|
+
...state.measurementConstraintEdge,
|
|
288
|
+
activeAxis: axis,
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
}),
|
|
292
|
+
clearMeasurementConstraintEdge: () => set({ measurementConstraintEdge: null }),
|
|
293
|
+
});
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
import { describe, it, beforeEach } from 'node:test';
|
|
6
|
+
import assert from 'node:assert';
|
|
7
|
+
import { createModelSlice, type ModelSlice } from './modelSlice.js';
|
|
8
|
+
import type { FederatedModel } from '../types.js';
|
|
9
|
+
|
|
10
|
+
// Helper to create a mock model
|
|
11
|
+
function createMockModel(id: string, name: string): FederatedModel {
|
|
12
|
+
return {
|
|
13
|
+
id,
|
|
14
|
+
name,
|
|
15
|
+
ifcDataStore: {} as any,
|
|
16
|
+
geometryResult: {} as any,
|
|
17
|
+
visible: true,
|
|
18
|
+
collapsed: false,
|
|
19
|
+
schemaVersion: 'IFC4',
|
|
20
|
+
loadedAt: Date.now(),
|
|
21
|
+
fileSize: 1024,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('ModelSlice', () => {
|
|
26
|
+
let state: ModelSlice;
|
|
27
|
+
let setState: (partial: Partial<ModelSlice> | ((state: ModelSlice) => Partial<ModelSlice>)) => void;
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
// Create a mock set function that updates state
|
|
31
|
+
setState = (partial) => {
|
|
32
|
+
if (typeof partial === 'function') {
|
|
33
|
+
const updates = partial(state);
|
|
34
|
+
state = { ...state, ...updates };
|
|
35
|
+
} else {
|
|
36
|
+
state = { ...state, ...partial };
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Create slice with mock set function
|
|
41
|
+
state = createModelSlice(setState, () => state, {} as any);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('initial state', () => {
|
|
45
|
+
it('should have empty models map', () => {
|
|
46
|
+
assert.strictEqual(state.models.size, 0);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should have null activeModelId', () => {
|
|
50
|
+
assert.strictEqual(state.activeModelId, null);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should report hasModels as false', () => {
|
|
54
|
+
assert.strictEqual(state.hasModels(), false);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('addModel', () => {
|
|
59
|
+
it('should add a model to the map', () => {
|
|
60
|
+
const model = createMockModel('model-1', 'Test Model');
|
|
61
|
+
state.addModel(model);
|
|
62
|
+
assert.strictEqual(state.models.size, 1);
|
|
63
|
+
assert.strictEqual(state.models.get('model-1')?.name, 'Test Model');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should set first model as active', () => {
|
|
67
|
+
const model = createMockModel('model-1', 'Test Model');
|
|
68
|
+
state.addModel(model);
|
|
69
|
+
assert.strictEqual(state.activeModelId, 'model-1');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should collapse existing models when adding new ones', () => {
|
|
73
|
+
const model1 = createMockModel('model-1', 'First Model');
|
|
74
|
+
const model2 = createMockModel('model-2', 'Second Model');
|
|
75
|
+
|
|
76
|
+
state.addModel(model1);
|
|
77
|
+
assert.strictEqual(state.models.get('model-1')?.collapsed, false);
|
|
78
|
+
|
|
79
|
+
state.addModel(model2);
|
|
80
|
+
// First model should now be collapsed
|
|
81
|
+
assert.strictEqual(state.models.get('model-1')?.collapsed, true);
|
|
82
|
+
// New model should not be collapsed
|
|
83
|
+
assert.strictEqual(state.models.get('model-2')?.collapsed, false);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should not change activeModelId when adding subsequent models', () => {
|
|
87
|
+
const model1 = createMockModel('model-1', 'First Model');
|
|
88
|
+
const model2 = createMockModel('model-2', 'Second Model');
|
|
89
|
+
|
|
90
|
+
state.addModel(model1);
|
|
91
|
+
state.addModel(model2);
|
|
92
|
+
|
|
93
|
+
// Active model should still be the first one
|
|
94
|
+
assert.strictEqual(state.activeModelId, 'model-1');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should report hasModels as true after adding', () => {
|
|
98
|
+
const model = createMockModel('model-1', 'Test Model');
|
|
99
|
+
state.addModel(model);
|
|
100
|
+
assert.strictEqual(state.hasModels(), true);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('removeModel', () => {
|
|
105
|
+
it('should remove a model from the map', () => {
|
|
106
|
+
const model = createMockModel('model-1', 'Test Model');
|
|
107
|
+
state.addModel(model);
|
|
108
|
+
state.removeModel('model-1');
|
|
109
|
+
assert.strictEqual(state.models.size, 0);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should update activeModelId if removed model was active', () => {
|
|
113
|
+
const model1 = createMockModel('model-1', 'First Model');
|
|
114
|
+
const model2 = createMockModel('model-2', 'Second Model');
|
|
115
|
+
|
|
116
|
+
state.addModel(model1);
|
|
117
|
+
state.addModel(model2);
|
|
118
|
+
state.setActiveModel('model-1');
|
|
119
|
+
|
|
120
|
+
state.removeModel('model-1');
|
|
121
|
+
// Active model should switch to model-2
|
|
122
|
+
assert.strictEqual(state.activeModelId, 'model-2');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should set activeModelId to null when last model removed', () => {
|
|
126
|
+
const model = createMockModel('model-1', 'Test Model');
|
|
127
|
+
state.addModel(model);
|
|
128
|
+
state.removeModel('model-1');
|
|
129
|
+
assert.strictEqual(state.activeModelId, null);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should not affect activeModelId if removed model was not active', () => {
|
|
133
|
+
const model1 = createMockModel('model-1', 'First Model');
|
|
134
|
+
const model2 = createMockModel('model-2', 'Second Model');
|
|
135
|
+
|
|
136
|
+
state.addModel(model1);
|
|
137
|
+
state.addModel(model2);
|
|
138
|
+
|
|
139
|
+
state.removeModel('model-2');
|
|
140
|
+
assert.strictEqual(state.activeModelId, 'model-1');
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('clearAllModels', () => {
|
|
145
|
+
it('should remove all models', () => {
|
|
146
|
+
state.addModel(createMockModel('model-1', 'First'));
|
|
147
|
+
state.addModel(createMockModel('model-2', 'Second'));
|
|
148
|
+
|
|
149
|
+
state.clearAllModels();
|
|
150
|
+
|
|
151
|
+
assert.strictEqual(state.models.size, 0);
|
|
152
|
+
assert.strictEqual(state.activeModelId, null);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('setActiveModel', () => {
|
|
157
|
+
it('should update activeModelId', () => {
|
|
158
|
+
const model1 = createMockModel('model-1', 'First Model');
|
|
159
|
+
const model2 = createMockModel('model-2', 'Second Model');
|
|
160
|
+
|
|
161
|
+
state.addModel(model1);
|
|
162
|
+
state.addModel(model2);
|
|
163
|
+
|
|
164
|
+
state.setActiveModel('model-2');
|
|
165
|
+
assert.strictEqual(state.activeModelId, 'model-2');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should allow setting to null', () => {
|
|
169
|
+
const model = createMockModel('model-1', 'Test Model');
|
|
170
|
+
state.addModel(model);
|
|
171
|
+
state.setActiveModel(null);
|
|
172
|
+
assert.strictEqual(state.activeModelId, null);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('setModelVisibility', () => {
|
|
177
|
+
it('should update model visibility', () => {
|
|
178
|
+
const model = createMockModel('model-1', 'Test Model');
|
|
179
|
+
state.addModel(model);
|
|
180
|
+
|
|
181
|
+
state.setModelVisibility('model-1', false);
|
|
182
|
+
assert.strictEqual(state.models.get('model-1')?.visible, false);
|
|
183
|
+
|
|
184
|
+
state.setModelVisibility('model-1', true);
|
|
185
|
+
assert.strictEqual(state.models.get('model-1')?.visible, true);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should do nothing for non-existent model', () => {
|
|
189
|
+
state.setModelVisibility('non-existent', false);
|
|
190
|
+
// Should not throw, just return empty update
|
|
191
|
+
assert.strictEqual(state.models.size, 0);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('setModelCollapsed', () => {
|
|
196
|
+
it('should update model collapsed state', () => {
|
|
197
|
+
const model = createMockModel('model-1', 'Test Model');
|
|
198
|
+
state.addModel(model);
|
|
199
|
+
|
|
200
|
+
state.setModelCollapsed('model-1', true);
|
|
201
|
+
assert.strictEqual(state.models.get('model-1')?.collapsed, true);
|
|
202
|
+
|
|
203
|
+
state.setModelCollapsed('model-1', false);
|
|
204
|
+
assert.strictEqual(state.models.get('model-1')?.collapsed, false);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe('setModelName', () => {
|
|
209
|
+
it('should update model name', () => {
|
|
210
|
+
const model = createMockModel('model-1', 'Original Name');
|
|
211
|
+
state.addModel(model);
|
|
212
|
+
|
|
213
|
+
state.setModelName('model-1', 'New Name');
|
|
214
|
+
assert.strictEqual(state.models.get('model-1')?.name, 'New Name');
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe('getModel', () => {
|
|
219
|
+
it('should return model by ID', () => {
|
|
220
|
+
const model = createMockModel('model-1', 'Test Model');
|
|
221
|
+
state.addModel(model);
|
|
222
|
+
|
|
223
|
+
const retrieved = state.getModel('model-1');
|
|
224
|
+
assert.strictEqual(retrieved?.name, 'Test Model');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should return undefined for non-existent ID', () => {
|
|
228
|
+
const retrieved = state.getModel('non-existent');
|
|
229
|
+
assert.strictEqual(retrieved, undefined);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe('getActiveModel', () => {
|
|
234
|
+
it('should return the active model', () => {
|
|
235
|
+
const model = createMockModel('model-1', 'Test Model');
|
|
236
|
+
state.addModel(model);
|
|
237
|
+
|
|
238
|
+
const active = state.getActiveModel();
|
|
239
|
+
assert.strictEqual(active?.id, 'model-1');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('should return undefined when no active model', () => {
|
|
243
|
+
const active = state.getActiveModel();
|
|
244
|
+
assert.strictEqual(active, undefined);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe('getAllVisibleModels', () => {
|
|
249
|
+
it('should return only visible models', () => {
|
|
250
|
+
state.addModel(createMockModel('model-1', 'First'));
|
|
251
|
+
state.addModel(createMockModel('model-2', 'Second'));
|
|
252
|
+
state.addModel(createMockModel('model-3', 'Third'));
|
|
253
|
+
|
|
254
|
+
state.setModelVisibility('model-2', false);
|
|
255
|
+
|
|
256
|
+
const visible = state.getAllVisibleModels();
|
|
257
|
+
assert.strictEqual(visible.length, 2);
|
|
258
|
+
assert.ok(visible.some(m => m.id === 'model-1'));
|
|
259
|
+
assert.ok(visible.some(m => m.id === 'model-3'));
|
|
260
|
+
assert.ok(!visible.some(m => m.id === 'model-2'));
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should return empty array when all models hidden', () => {
|
|
264
|
+
state.addModel(createMockModel('model-1', 'First'));
|
|
265
|
+
state.setModelVisibility('model-1', false);
|
|
266
|
+
|
|
267
|
+
const visible = state.getAllVisibleModels();
|
|
268
|
+
assert.strictEqual(visible.length, 0);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
});
|