@ifc-lite/viewer 1.1.7 → 1.6.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.
Files changed (164) hide show
  1. package/LICENSE +373 -0
  2. package/dist/apple-touch-icon.png +0 -0
  3. package/dist/assets/Arrow.dom-BjDQoB2M.js +20 -0
  4. package/dist/assets/arrow2-bb-jcVEo.js +2 -0
  5. package/dist/assets/arrow2_bg-4Y7xYo54.wasm +0 -0
  6. package/dist/assets/arrow2_bg-BlXl-cSQ.js +1 -0
  7. package/dist/assets/arrow2_bg-BoXCojjR.wasm +0 -0
  8. package/dist/assets/desktop-cache-oPzaWXYE.js +1 -0
  9. package/dist/assets/event-DIOks52T.js +1 -0
  10. package/dist/assets/ifc-cache-BAN4vcd4.js +1 -0
  11. package/dist/assets/ifc-lite_bg-C6kblxf9.wasm +0 -0
  12. package/dist/assets/index-YBtrHPu3.js +65252 -0
  13. package/dist/assets/index-v3mcCUPN.css +1 -0
  14. package/dist/assets/native-bridge-CULtTDX3.js +111 -0
  15. package/dist/assets/wasm-bridge-CjL-lSak.js +1 -0
  16. package/dist/favicon-16x16-cropped.png +0 -0
  17. package/dist/favicon-16x16.png +0 -0
  18. package/dist/favicon-192x192-cropped.png +0 -0
  19. package/dist/favicon-192x192.png +0 -0
  20. package/dist/favicon-32x32-cropped.png +0 -0
  21. package/dist/favicon-32x32.png +0 -0
  22. package/dist/favicon-48x48-cropped.png +0 -0
  23. package/dist/favicon-48x48.png +0 -0
  24. package/dist/favicon-512x512-cropped.png +0 -0
  25. package/dist/favicon-512x512.png +0 -0
  26. package/dist/favicon-64x64-cropped.png +0 -0
  27. package/dist/favicon-64x64.png +0 -0
  28. package/dist/favicon-96x96-cropped.png +0 -0
  29. package/dist/favicon-96x96.png +0 -0
  30. package/dist/favicon-square-512.png +0 -0
  31. package/dist/favicon.ico +0 -0
  32. package/dist/favicon.png +0 -0
  33. package/dist/favicon.svg +3 -0
  34. package/dist/index.html +44 -0
  35. package/dist/logo.png +0 -0
  36. package/dist/manifest.json +48 -0
  37. package/index.html +33 -2
  38. package/package.json +34 -17
  39. package/public/apple-touch-icon.png +0 -0
  40. package/public/favicon-16x16-cropped.png +0 -0
  41. package/public/favicon-16x16.png +0 -0
  42. package/public/favicon-192x192-cropped.png +0 -0
  43. package/public/favicon-192x192.png +0 -0
  44. package/public/favicon-32x32-cropped.png +0 -0
  45. package/public/favicon-32x32.png +0 -0
  46. package/public/favicon-48x48-cropped.png +0 -0
  47. package/public/favicon-48x48.png +0 -0
  48. package/public/favicon-512x512-cropped.png +0 -0
  49. package/public/favicon-512x512.png +0 -0
  50. package/public/favicon-64x64-cropped.png +0 -0
  51. package/public/favicon-64x64.png +0 -0
  52. package/public/favicon-96x96-cropped.png +0 -0
  53. package/public/favicon-96x96.png +0 -0
  54. package/public/favicon-square-512.png +0 -0
  55. package/public/favicon.ico +0 -0
  56. package/public/favicon.png +0 -0
  57. package/public/favicon.svg +3 -0
  58. package/public/logo.png +0 -0
  59. package/public/manifest.json +48 -0
  60. package/src/App.tsx +2 -0
  61. package/src/components/ui/alert.tsx +62 -0
  62. package/src/components/ui/badge.tsx +39 -0
  63. package/src/components/ui/dialog.tsx +120 -0
  64. package/src/components/ui/label.tsx +27 -0
  65. package/src/components/ui/select.tsx +151 -0
  66. package/src/components/ui/switch.tsx +30 -0
  67. package/src/components/ui/table.tsx +120 -0
  68. package/src/components/ui/tabs.tsx +1 -1
  69. package/src/components/viewer/BCFPanel.tsx +1164 -0
  70. package/src/components/viewer/BulkPropertyEditor.tsx +875 -0
  71. package/src/components/viewer/DataConnector.tsx +840 -0
  72. package/src/components/viewer/DrawingSettingsPanel.tsx +536 -0
  73. package/src/components/viewer/EntityContextMenu.tsx +45 -17
  74. package/src/components/viewer/ExportChangesButton.tsx +195 -0
  75. package/src/components/viewer/ExportDialog.tsx +402 -0
  76. package/src/components/viewer/HierarchyPanel.tsx +1132 -218
  77. package/src/components/viewer/IDSPanel.tsx +661 -0
  78. package/src/components/viewer/KeyboardShortcutsDialog.tsx +245 -39
  79. package/src/components/viewer/MainToolbar.tsx +418 -94
  80. package/src/components/viewer/PropertiesPanel.tsx +1355 -91
  81. package/src/components/viewer/PropertyEditor.tsx +611 -0
  82. package/src/components/viewer/Section2DPanel.tsx +3313 -0
  83. package/src/components/viewer/SheetSetupPanel.tsx +502 -0
  84. package/src/components/viewer/StatusBar.tsx +27 -16
  85. package/src/components/viewer/TitleBlockEditor.tsx +437 -0
  86. package/src/components/viewer/ToolOverlays.tsx +935 -127
  87. package/src/components/viewer/ViewerLayout.tsx +40 -11
  88. package/src/components/viewer/Viewport.tsx +1276 -336
  89. package/src/components/viewer/ViewportContainer.tsx +554 -18
  90. package/src/components/viewer/ViewportOverlays.tsx +24 -7
  91. package/src/hooks/useBCF.ts +504 -0
  92. package/src/hooks/useIDS.ts +1065 -0
  93. package/src/hooks/useIfc.ts +1534 -205
  94. package/src/hooks/useIfcCache.ts +279 -0
  95. package/src/hooks/useKeyboardShortcuts.ts +50 -8
  96. package/src/hooks/useModelSelection.ts +61 -0
  97. package/src/hooks/useViewerSelectors.ts +218 -0
  98. package/src/hooks/useWebGPU.ts +80 -0
  99. package/src/index.css +265 -27
  100. package/src/lib/platform.ts +23 -0
  101. package/src/services/cacheService.ts +142 -0
  102. package/src/services/desktop-cache.ts +143 -0
  103. package/src/services/fs-cache.ts +212 -0
  104. package/src/services/ifc-cache.ts +14 -6
  105. package/src/store/constants.ts +85 -0
  106. package/src/store/index.ts +214 -0
  107. package/src/store/slices/bcfSlice.ts +372 -0
  108. package/src/store/slices/cameraSlice.ts +63 -0
  109. package/src/store/slices/dataSlice.test.ts +226 -0
  110. package/src/store/slices/dataSlice.ts +112 -0
  111. package/src/store/slices/drawing2DSlice.ts +340 -0
  112. package/src/store/slices/hoverSlice.ts +40 -0
  113. package/src/store/slices/idsSlice.ts +310 -0
  114. package/src/store/slices/loadingSlice.ts +33 -0
  115. package/src/store/slices/measurementSlice.test.ts +217 -0
  116. package/src/store/slices/measurementSlice.ts +293 -0
  117. package/src/store/slices/modelSlice.test.ts +271 -0
  118. package/src/store/slices/modelSlice.ts +211 -0
  119. package/src/store/slices/mutationSlice.ts +502 -0
  120. package/src/store/slices/sectionSlice.test.ts +125 -0
  121. package/src/store/slices/sectionSlice.ts +58 -0
  122. package/src/store/slices/selectionSlice.test.ts +286 -0
  123. package/src/store/slices/selectionSlice.ts +263 -0
  124. package/src/store/slices/sheetSlice.ts +565 -0
  125. package/src/store/slices/uiSlice.ts +58 -0
  126. package/src/store/slices/visibilitySlice.test.ts +304 -0
  127. package/src/store/slices/visibilitySlice.ts +277 -0
  128. package/src/store/types.test.ts +135 -0
  129. package/src/store/types.ts +248 -0
  130. package/src/store.ts +40 -515
  131. package/src/utils/ifcConfig.ts +82 -0
  132. package/src/utils/localParsingUtils.ts +287 -0
  133. package/src/utils/serverDataModel.ts +783 -0
  134. package/src/utils/spatialHierarchy.ts +283 -0
  135. package/src/utils/viewportUtils.ts +334 -0
  136. package/src/vite-env.d.ts +23 -0
  137. package/src/webgpu-types.d.ts +128 -0
  138. package/src-tauri/Cargo.toml +29 -0
  139. package/src-tauri/build.rs +7 -0
  140. package/src-tauri/capabilities/default.json +18 -0
  141. package/src-tauri/icons/128x128.png +0 -0
  142. package/src-tauri/icons/128x128@2x.png +0 -0
  143. package/src-tauri/icons/32x32.png +0 -0
  144. package/src-tauri/icons/Square107x107Logo.png +0 -0
  145. package/src-tauri/icons/Square142x142Logo.png +0 -0
  146. package/src-tauri/icons/Square150x150Logo.png +0 -0
  147. package/src-tauri/icons/Square284x284Logo.png +0 -0
  148. package/src-tauri/icons/Square30x30Logo.png +0 -0
  149. package/src-tauri/icons/Square310x310Logo.png +0 -0
  150. package/src-tauri/icons/Square44x44Logo.png +0 -0
  151. package/src-tauri/icons/Square71x71Logo.png +0 -0
  152. package/src-tauri/icons/Square89x89Logo.png +0 -0
  153. package/src-tauri/icons/StoreLogo.png +0 -0
  154. package/src-tauri/icons/icon.icns +0 -0
  155. package/src-tauri/icons/icon.ico +0 -0
  156. package/src-tauri/icons/icon.png +0 -0
  157. package/src-tauri/src/lib.rs +21 -0
  158. package/src-tauri/src/main.rs +10 -0
  159. package/src-tauri/tauri.conf.json +39 -0
  160. package/vite.config.ts +174 -26
  161. package/public/ifc-lite_bg.wasm +0 -0
  162. package/public/web-ifc.wasm +0 -0
  163. package/src/components/Viewport.tsx +0 -723
  164. package/src/components/viewer/BoxSelectionOverlay.tsx +0 -53
@@ -0,0 +1,226 @@
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 { createDataSlice, type DataSlice } from './dataSlice.js';
8
+ import { DATA_DEFAULTS } from '../constants.js';
9
+
10
+ // Mock mesh data for testing
11
+ const createMockMesh = (expressId: number, color: [number, number, number, number] = [1, 0, 0, 1]) => ({
12
+ expressId,
13
+ positions: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]),
14
+ indices: new Uint32Array([0, 1, 2]),
15
+ normals: new Float32Array([0, 0, 1, 0, 0, 1, 0, 0, 1]),
16
+ color,
17
+ ifcType: 'IfcWall',
18
+ });
19
+
20
+ describe('DataSlice', () => {
21
+ let state: DataSlice;
22
+ let setState: (partial: Partial<DataSlice> | ((state: DataSlice) => Partial<DataSlice>)) => void;
23
+
24
+ beforeEach(() => {
25
+ setState = (partial) => {
26
+ if (typeof partial === 'function') {
27
+ const updates = partial(state);
28
+ state = { ...state, ...updates };
29
+ } else {
30
+ state = { ...state, ...partial };
31
+ }
32
+ };
33
+
34
+ state = createDataSlice(setState, () => state, {} as any);
35
+ });
36
+
37
+ describe('initial state', () => {
38
+ it('should have null ifcDataStore', () => {
39
+ assert.strictEqual(state.ifcDataStore, null);
40
+ });
41
+
42
+ it('should have null geometryResult', () => {
43
+ assert.strictEqual(state.geometryResult, null);
44
+ });
45
+
46
+ it('should have null pendingColorUpdates', () => {
47
+ assert.strictEqual(state.pendingColorUpdates, null);
48
+ });
49
+ });
50
+
51
+ describe('setIfcDataStore', () => {
52
+ it('should set the data store', () => {
53
+ const mockStore = { entityCount: 100 } as any;
54
+ state.setIfcDataStore(mockStore);
55
+ assert.strictEqual(state.ifcDataStore, mockStore);
56
+ });
57
+
58
+ it('should allow setting to null', () => {
59
+ state.setIfcDataStore({ entityCount: 100 } as any);
60
+ state.setIfcDataStore(null);
61
+ assert.strictEqual(state.ifcDataStore, null);
62
+ });
63
+ });
64
+
65
+ describe('setGeometryResult', () => {
66
+ it('should set the geometry result', () => {
67
+ const mockResult = { meshes: [], totalTriangles: 0, totalVertices: 0 } as any;
68
+ state.setGeometryResult(mockResult);
69
+ assert.strictEqual(state.geometryResult, mockResult);
70
+ });
71
+ });
72
+
73
+ describe('appendGeometryBatch', () => {
74
+ it('should create new geometry result when none exists', () => {
75
+ const meshes = [createMockMesh(1), createMockMesh(2)];
76
+ state.appendGeometryBatch(meshes as any);
77
+
78
+ assert.notStrictEqual(state.geometryResult, null);
79
+ assert.strictEqual(state.geometryResult?.meshes.length, 2);
80
+ });
81
+
82
+ it('should append meshes to existing result', () => {
83
+ const mesh1 = createMockMesh(1);
84
+ const mesh2 = createMockMesh(2);
85
+
86
+ state.appendGeometryBatch([mesh1] as any);
87
+ state.appendGeometryBatch([mesh2] as any);
88
+
89
+ assert.strictEqual(state.geometryResult?.meshes.length, 2);
90
+ });
91
+
92
+ it('should use provided coordinate info', () => {
93
+ const meshes = [createMockMesh(1)];
94
+ const coordInfo = {
95
+ originShift: { x: 10, y: 20, z: 30 },
96
+ originalBounds: { min: { x: 0, y: 0, z: 0 }, max: { x: 100, y: 100, z: 100 } },
97
+ shiftedBounds: { min: { x: -10, y: -20, z: -30 }, max: { x: 90, y: 80, z: 70 } },
98
+ hasLargeCoordinates: true,
99
+ };
100
+
101
+ state.appendGeometryBatch(meshes as any, coordInfo);
102
+
103
+ assert.deepStrictEqual(state.geometryResult?.coordinateInfo.originShift, { x: 10, y: 20, z: 30 });
104
+ assert.strictEqual(state.geometryResult?.coordinateInfo.hasLargeCoordinates, true);
105
+ });
106
+
107
+ it('should use default coordinate info when not provided', () => {
108
+ const meshes = [createMockMesh(1)];
109
+ state.appendGeometryBatch(meshes as any);
110
+
111
+ // Should have fresh copies, not shared references
112
+ assert.deepStrictEqual(state.geometryResult?.coordinateInfo.originShift, DATA_DEFAULTS.ORIGIN_SHIFT);
113
+ assert.strictEqual(state.geometryResult?.coordinateInfo.hasLargeCoordinates, DATA_DEFAULTS.HAS_LARGE_COORDINATES);
114
+ });
115
+
116
+ it('should create fresh coordinate info copies (not shared references)', () => {
117
+ const meshes = [createMockMesh(1)];
118
+ state.appendGeometryBatch(meshes as any);
119
+
120
+ // Mutate the result's coordinate info
121
+ state.geometryResult!.coordinateInfo.originShift.x = 999;
122
+
123
+ // DATA_DEFAULTS should not be affected
124
+ assert.strictEqual(DATA_DEFAULTS.ORIGIN_SHIFT.x, 0);
125
+ });
126
+ });
127
+
128
+ describe('updateMeshColors', () => {
129
+ it('should update mesh colors', () => {
130
+ const mesh = createMockMesh(1, [1, 0, 0, 1]);
131
+ state.appendGeometryBatch([mesh] as any);
132
+
133
+ const updates = new Map<number, [number, number, number, number]>();
134
+ updates.set(1, [0, 1, 0, 1]); // Change to green
135
+
136
+ state.updateMeshColors(updates);
137
+
138
+ assert.deepStrictEqual(state.geometryResult?.meshes[0].color, [0, 1, 0, 1]);
139
+ });
140
+
141
+ it('should clone the updates Map to prevent external mutation', () => {
142
+ const mesh = createMockMesh(1);
143
+ state.appendGeometryBatch([mesh] as any);
144
+
145
+ const updates = new Map<number, [number, number, number, number]>();
146
+ updates.set(1, [0, 1, 0, 1]);
147
+
148
+ state.updateMeshColors(updates);
149
+
150
+ // Mutate the original map
151
+ updates.set(1, [1, 1, 1, 1]);
152
+
153
+ // State should not be affected
154
+ assert.notStrictEqual(state.pendingColorUpdates, updates);
155
+ });
156
+
157
+ it('should not update when no geometry result', () => {
158
+ const updates = new Map<number, [number, number, number, number]>();
159
+ updates.set(1, [0, 1, 0, 1]);
160
+
161
+ state.updateMeshColors(updates);
162
+
163
+ assert.strictEqual(state.geometryResult, null);
164
+ });
165
+
166
+ it('should preserve unaffected meshes', () => {
167
+ const mesh1 = createMockMesh(1, [1, 0, 0, 1]);
168
+ const mesh2 = createMockMesh(2, [0, 0, 1, 1]);
169
+ state.appendGeometryBatch([mesh1, mesh2] as any);
170
+
171
+ const updates = new Map<number, [number, number, number, number]>();
172
+ updates.set(1, [0, 1, 0, 1]); // Only update mesh 1
173
+
174
+ state.updateMeshColors(updates);
175
+
176
+ assert.deepStrictEqual(state.geometryResult?.meshes[0].color, [0, 1, 0, 1]);
177
+ assert.deepStrictEqual(state.geometryResult?.meshes[1].color, [0, 0, 1, 1]);
178
+ });
179
+ });
180
+
181
+ describe('clearPendingColorUpdates', () => {
182
+ it('should clear pending color updates', () => {
183
+ const mesh = createMockMesh(1);
184
+ state.appendGeometryBatch([mesh] as any);
185
+
186
+ const updates = new Map<number, [number, number, number, number]>();
187
+ updates.set(1, [0, 1, 0, 1]);
188
+ state.updateMeshColors(updates);
189
+
190
+ state.clearPendingColorUpdates();
191
+
192
+ assert.strictEqual(state.pendingColorUpdates, null);
193
+ });
194
+ });
195
+
196
+ describe('updateCoordinateInfo', () => {
197
+ it('should update coordinate info', () => {
198
+ const mesh = createMockMesh(1);
199
+ state.appendGeometryBatch([mesh] as any);
200
+
201
+ const newCoordInfo = {
202
+ originShift: { x: 100, y: 200, z: 300 },
203
+ originalBounds: { min: { x: 0, y: 0, z: 0 }, max: { x: 50, y: 50, z: 50 } },
204
+ shiftedBounds: { min: { x: -100, y: -200, z: -300 }, max: { x: -50, y: -150, z: -250 } },
205
+ hasLargeCoordinates: true,
206
+ };
207
+
208
+ state.updateCoordinateInfo(newCoordInfo);
209
+
210
+ assert.deepStrictEqual(state.geometryResult?.coordinateInfo, newCoordInfo);
211
+ });
212
+
213
+ it('should not update when no geometry result', () => {
214
+ const newCoordInfo = {
215
+ originShift: { x: 100, y: 200, z: 300 },
216
+ originalBounds: { min: { x: 0, y: 0, z: 0 }, max: { x: 50, y: 50, z: 50 } },
217
+ shiftedBounds: { min: { x: -100, y: -200, z: -300 }, max: { x: -50, y: -150, z: -250 } },
218
+ hasLargeCoordinates: true,
219
+ };
220
+
221
+ state.updateCoordinateInfo(newCoordInfo);
222
+
223
+ assert.strictEqual(state.geometryResult, null);
224
+ });
225
+ });
226
+ });
@@ -0,0 +1,112 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+
5
+ /**
6
+ * Data state slice (IFC data and geometry)
7
+ */
8
+
9
+ import type { StateCreator } from 'zustand';
10
+ import type { IfcDataStore } from '@ifc-lite/parser';
11
+ import type { GeometryResult, CoordinateInfo } from '@ifc-lite/geometry';
12
+ import { DATA_DEFAULTS } from '../constants.js';
13
+
14
+ export interface DataSlice {
15
+ // State
16
+ ifcDataStore: IfcDataStore | null;
17
+ geometryResult: GeometryResult | null;
18
+ pendingColorUpdates: Map<number, [number, number, number, number]> | null;
19
+
20
+ // Actions
21
+ setIfcDataStore: (result: IfcDataStore | null) => void;
22
+ setGeometryResult: (result: GeometryResult | null) => void;
23
+ appendGeometryBatch: (meshes: GeometryResult['meshes'], coordinateInfo?: CoordinateInfo) => void;
24
+ updateMeshColors: (updates: Map<number, [number, number, number, number]>) => void;
25
+ clearPendingColorUpdates: () => void;
26
+ updateCoordinateInfo: (coordinateInfo: CoordinateInfo) => void;
27
+ }
28
+
29
+ const getDefaultCoordinateInfo = (): CoordinateInfo => ({
30
+ // Create fresh copies to avoid shared object references
31
+ originShift: { x: DATA_DEFAULTS.ORIGIN_SHIFT.x, y: DATA_DEFAULTS.ORIGIN_SHIFT.y, z: DATA_DEFAULTS.ORIGIN_SHIFT.z },
32
+ originalBounds: {
33
+ min: { x: DATA_DEFAULTS.ORIGIN_SHIFT.x, y: DATA_DEFAULTS.ORIGIN_SHIFT.y, z: DATA_DEFAULTS.ORIGIN_SHIFT.z },
34
+ max: { x: DATA_DEFAULTS.ORIGIN_SHIFT.x, y: DATA_DEFAULTS.ORIGIN_SHIFT.y, z: DATA_DEFAULTS.ORIGIN_SHIFT.z },
35
+ },
36
+ shiftedBounds: {
37
+ min: { x: DATA_DEFAULTS.ORIGIN_SHIFT.x, y: DATA_DEFAULTS.ORIGIN_SHIFT.y, z: DATA_DEFAULTS.ORIGIN_SHIFT.z },
38
+ max: { x: DATA_DEFAULTS.ORIGIN_SHIFT.x, y: DATA_DEFAULTS.ORIGIN_SHIFT.y, z: DATA_DEFAULTS.ORIGIN_SHIFT.z },
39
+ },
40
+ hasLargeCoordinates: DATA_DEFAULTS.HAS_LARGE_COORDINATES,
41
+ });
42
+
43
+ export const createDataSlice: StateCreator<DataSlice, [], [], DataSlice> = (set) => ({
44
+ // Initial state
45
+ ifcDataStore: null,
46
+ geometryResult: null,
47
+ pendingColorUpdates: null,
48
+
49
+ // Actions
50
+ setIfcDataStore: (ifcDataStore) => set({ ifcDataStore }),
51
+
52
+ setGeometryResult: (geometryResult) => set({ geometryResult }),
53
+
54
+ appendGeometryBatch: (meshes, coordinateInfo) => set((state) => {
55
+ if (!state.geometryResult) {
56
+ const totalTriangles = meshes.reduce((sum, m) => sum + (m.indices.length / 3), 0);
57
+ const totalVertices = meshes.reduce((sum, m) => sum + (m.positions.length / 3), 0);
58
+ return {
59
+ geometryResult: {
60
+ meshes,
61
+ totalTriangles,
62
+ totalVertices,
63
+ coordinateInfo: coordinateInfo || getDefaultCoordinateInfo(),
64
+ },
65
+ };
66
+ }
67
+ const allMeshes = [...state.geometryResult.meshes, ...meshes];
68
+ const totalTriangles = allMeshes.reduce((sum, m) => sum + (m.indices.length / 3), 0);
69
+ const totalVertices = allMeshes.reduce((sum, m) => sum + (m.positions.length / 3), 0);
70
+ return {
71
+ geometryResult: {
72
+ ...state.geometryResult,
73
+ meshes: allMeshes,
74
+ totalTriangles,
75
+ totalVertices,
76
+ coordinateInfo: coordinateInfo || state.geometryResult.coordinateInfo,
77
+ },
78
+ };
79
+ }),
80
+
81
+ updateMeshColors: (updates) => set((state) => {
82
+ if (!state.geometryResult) return {};
83
+ // Clone the Map to prevent external mutation of pendingColorUpdates
84
+ const clonedUpdates = new Map(updates);
85
+ const updatedMeshes = state.geometryResult.meshes.map(mesh => {
86
+ const newColor = clonedUpdates.get(mesh.expressId);
87
+ if (newColor) {
88
+ return { ...mesh, color: newColor };
89
+ }
90
+ return mesh;
91
+ });
92
+ return {
93
+ geometryResult: {
94
+ ...state.geometryResult,
95
+ meshes: updatedMeshes,
96
+ },
97
+ pendingColorUpdates: clonedUpdates,
98
+ };
99
+ }),
100
+
101
+ clearPendingColorUpdates: () => set({ pendingColorUpdates: null }),
102
+
103
+ updateCoordinateInfo: (coordinateInfo) => set((state) => {
104
+ if (!state.geometryResult) return {};
105
+ return {
106
+ geometryResult: {
107
+ ...state.geometryResult,
108
+ coordinateInfo,
109
+ },
110
+ };
111
+ }),
112
+ });
@@ -0,0 +1,340 @@
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
+ * 2D Drawing generation state slice
7
+ *
8
+ * Manages state for generating and viewing 2D architectural drawings
9
+ * (floor plans, sections, elevations) from the 3D model.
10
+ */
11
+
12
+ import type { StateCreator } from 'zustand';
13
+ import type { Drawing2D, GraphicOverrideRule, GraphicOverridePreset } from '@ifc-lite/drawing-2d';
14
+ import { BUILT_IN_PRESETS } from '@ifc-lite/drawing-2d';
15
+
16
+ export type Drawing2DStatus = 'idle' | 'generating' | 'ready' | 'error';
17
+
18
+ /** Point in 2D drawing coordinates */
19
+ export interface Point2D {
20
+ x: number;
21
+ y: number;
22
+ }
23
+
24
+ /** Measurement result */
25
+ export interface Measure2DResult {
26
+ id: string;
27
+ start: Point2D;
28
+ end: Point2D;
29
+ distance: number; // in drawing units (typically meters)
30
+ }
31
+
32
+ export interface Drawing2DState {
33
+ /** Current drawing data (null when not generated) */
34
+ drawing2D: Drawing2D | null;
35
+ /** Generation status */
36
+ drawing2DStatus: Drawing2DStatus;
37
+ /** Generation progress (0-100) */
38
+ drawing2DProgress: number;
39
+ /** Progress phase description */
40
+ drawing2DPhase: string;
41
+ /** Error message if generation failed */
42
+ drawing2DError: string | null;
43
+ /** Whether the 2D panel is visible */
44
+ drawing2DPanelVisible: boolean;
45
+ /** SVG content for export (cached) */
46
+ drawing2DSvgContent: string | null;
47
+ /** Display options */
48
+ drawing2DDisplayOptions: {
49
+ showHiddenLines: boolean;
50
+ showHatching: boolean;
51
+ showAnnotations: boolean;
52
+ show3DOverlay: boolean;
53
+ scale: number;
54
+ /** Use authored symbolic representations (Plan/Annotation) when available instead of section cut */
55
+ useSymbolicRepresentations: boolean;
56
+ };
57
+ /** Available graphic override presets */
58
+ graphicOverridePresets: GraphicOverridePreset[];
59
+ /** Currently active preset ID (null = no preset) */
60
+ activePresetId: string | null;
61
+ /** Custom user-defined override rules */
62
+ customOverrideRules: GraphicOverrideRule[];
63
+ /** Whether to apply graphic overrides */
64
+ overridesEnabled: boolean;
65
+ /** Panel visibility for override editor */
66
+ overridesPanelVisible: boolean;
67
+
68
+ // 2D Measure Tool
69
+ /** Whether measure mode is active */
70
+ measure2DMode: boolean;
71
+ /** Start point of current measurement (drawing coords) */
72
+ measure2DStart: Point2D | null;
73
+ /** Current/end point of measurement (drawing coords) */
74
+ measure2DCurrent: Point2D | null;
75
+ /** Whether shift is held for orthogonal constraint */
76
+ measure2DShiftLocked: boolean;
77
+ /** Axis locked to when shift is held ('x' | 'y' | null) */
78
+ measure2DLockedAxis: 'x' | 'y' | null;
79
+ /** Completed measurements */
80
+ measure2DResults: Measure2DResult[];
81
+ /** Current snap point (if snapping to geometry) */
82
+ measure2DSnapPoint: Point2D | null;
83
+ }
84
+
85
+ export interface Drawing2DSlice extends Drawing2DState {
86
+ // Drawing Actions
87
+ setDrawing2D: (drawing: Drawing2D | null) => void;
88
+ setDrawing2DStatus: (status: Drawing2DStatus) => void;
89
+ setDrawing2DProgress: (progress: number, phase: string) => void;
90
+ setDrawing2DError: (error: string | null) => void;
91
+ setDrawing2DPanelVisible: (visible: boolean) => void;
92
+ toggleDrawing2DPanel: () => void;
93
+ setDrawing2DSvgContent: (svg: string | null) => void;
94
+ updateDrawing2DDisplayOptions: (options: Partial<Drawing2DState['drawing2DDisplayOptions']>) => void;
95
+ clearDrawing2D: () => void;
96
+
97
+ // Graphic Override Actions
98
+ setActivePreset: (presetId: string | null) => void;
99
+ addCustomRule: (rule: GraphicOverrideRule) => void;
100
+ updateCustomRule: (ruleId: string, updates: Partial<GraphicOverrideRule>) => void;
101
+ removeCustomRule: (ruleId: string) => void;
102
+ clearCustomRules: () => void;
103
+ setOverridesEnabled: (enabled: boolean) => void;
104
+ toggleOverridesEnabled: () => void;
105
+ setOverridesPanelVisible: (visible: boolean) => void;
106
+ toggleOverridesPanel: () => void;
107
+ /** Get all active rules (preset + custom) sorted by priority */
108
+ getActiveOverrideRules: () => GraphicOverrideRule[];
109
+
110
+ // 2D Measure Actions
111
+ setMeasure2DMode: (enabled: boolean) => void;
112
+ toggleMeasure2DMode: () => void;
113
+ setMeasure2DStart: (point: Point2D | null) => void;
114
+ setMeasure2DCurrent: (point: Point2D | null) => void;
115
+ setMeasure2DShiftLocked: (locked: boolean, axis?: 'x' | 'y' | null) => void;
116
+ addMeasure2DResult: (result: Measure2DResult) => void;
117
+ removeMeasure2DResult: (id: string) => void;
118
+ clearMeasure2DResults: () => void;
119
+ setMeasure2DSnapPoint: (point: Point2D | null) => void;
120
+ /** Complete current measurement and add to results */
121
+ completeMeasure2D: () => void;
122
+ /** Cancel current measurement */
123
+ cancelMeasure2D: () => void;
124
+ }
125
+
126
+ const getDefaultDisplayOptions = (): Drawing2DState['drawing2DDisplayOptions'] => ({
127
+ showHiddenLines: true,
128
+ showHatching: true,
129
+ showAnnotations: true,
130
+ show3DOverlay: true, // Show 3D overlay by default
131
+ scale: 100, // 1:100 default
132
+ useSymbolicRepresentations: false, // Default to section cut (Body geometry)
133
+ });
134
+
135
+ const getDefaultState = (): Drawing2DState => ({
136
+ drawing2D: null,
137
+ drawing2DStatus: 'idle',
138
+ drawing2DProgress: 0,
139
+ drawing2DPhase: '',
140
+ drawing2DError: null,
141
+ drawing2DPanelVisible: false,
142
+ drawing2DSvgContent: null,
143
+ drawing2DDisplayOptions: getDefaultDisplayOptions(),
144
+ // Graphic overrides
145
+ graphicOverridePresets: BUILT_IN_PRESETS,
146
+ activePresetId: 'preset-3d-colors', // Default to IFC Materials
147
+ customOverrideRules: [],
148
+ overridesEnabled: true,
149
+ overridesPanelVisible: false,
150
+ // 2D Measure
151
+ measure2DMode: false,
152
+ measure2DStart: null,
153
+ measure2DCurrent: null,
154
+ measure2DShiftLocked: false,
155
+ measure2DLockedAxis: null,
156
+ measure2DResults: [],
157
+ measure2DSnapPoint: null,
158
+ });
159
+
160
+ export const createDrawing2DSlice: StateCreator<Drawing2DSlice, [], [], Drawing2DSlice> = (set, get) => ({
161
+ // Initial state
162
+ ...getDefaultState(),
163
+
164
+ // Drawing Actions
165
+ setDrawing2D: (drawing) => set({
166
+ drawing2D: drawing,
167
+ drawing2DStatus: drawing ? 'ready' : 'idle',
168
+ drawing2DError: null,
169
+ }),
170
+
171
+ setDrawing2DStatus: (status) => set({ drawing2DStatus: status }),
172
+
173
+ setDrawing2DProgress: (progress, phase) => set({
174
+ drawing2DProgress: progress,
175
+ drawing2DPhase: phase,
176
+ }),
177
+
178
+ setDrawing2DError: (error) => set({
179
+ drawing2DError: error,
180
+ drawing2DStatus: error ? 'error' : 'idle',
181
+ }),
182
+
183
+ setDrawing2DPanelVisible: (visible) => set({ drawing2DPanelVisible: visible }),
184
+
185
+ toggleDrawing2DPanel: () => set((state) => ({ drawing2DPanelVisible: !state.drawing2DPanelVisible })),
186
+
187
+ setDrawing2DSvgContent: (svg) => set({ drawing2DSvgContent: svg }),
188
+
189
+ updateDrawing2DDisplayOptions: (options) => set((state) => ({
190
+ drawing2DDisplayOptions: { ...state.drawing2DDisplayOptions, ...options },
191
+ })),
192
+
193
+ clearDrawing2D: () => set(getDefaultState()),
194
+
195
+ // Graphic Override Actions
196
+ setActivePreset: (presetId) => set({ activePresetId: presetId }),
197
+
198
+ addCustomRule: (rule) => set((state) => ({
199
+ customOverrideRules: [...state.customOverrideRules, rule],
200
+ })),
201
+
202
+ updateCustomRule: (ruleId, updates) => set((state) => ({
203
+ customOverrideRules: state.customOverrideRules.map((rule) =>
204
+ rule.id === ruleId ? { ...rule, ...updates } : rule
205
+ ),
206
+ })),
207
+
208
+ removeCustomRule: (ruleId) => set((state) => ({
209
+ customOverrideRules: state.customOverrideRules.filter((rule) => rule.id !== ruleId),
210
+ })),
211
+
212
+ clearCustomRules: () => set({ customOverrideRules: [] }),
213
+
214
+ setOverridesEnabled: (enabled) => set({ overridesEnabled: enabled }),
215
+
216
+ toggleOverridesEnabled: () => set((state) => ({ overridesEnabled: !state.overridesEnabled })),
217
+
218
+ setOverridesPanelVisible: (visible) => set({ overridesPanelVisible: visible }),
219
+
220
+ toggleOverridesPanel: () => set((state) => ({ overridesPanelVisible: !state.overridesPanelVisible })),
221
+
222
+ getActiveOverrideRules: () => {
223
+ const state = get();
224
+ if (!state.overridesEnabled) return [];
225
+
226
+ const presetRules: GraphicOverrideRule[] = [];
227
+
228
+ // Get rules from active preset
229
+ if (state.activePresetId) {
230
+ const preset = state.graphicOverridePresets.find((p) => p.id === state.activePresetId);
231
+ if (preset) {
232
+ presetRules.push(...preset.rules);
233
+ }
234
+ }
235
+
236
+ // Combine with custom rules and sort by priority
237
+ const allRules = [...presetRules, ...state.customOverrideRules];
238
+ return allRules
239
+ .filter((rule) => rule.enabled)
240
+ .sort((a, b) => a.priority - b.priority);
241
+ },
242
+
243
+ // 2D Measure Actions
244
+ setMeasure2DMode: (enabled) => set({
245
+ measure2DMode: enabled,
246
+ // Clear measurement state when disabling
247
+ ...(enabled ? {} : {
248
+ measure2DStart: null,
249
+ measure2DCurrent: null,
250
+ measure2DShiftLocked: false,
251
+ measure2DLockedAxis: null,
252
+ measure2DSnapPoint: null,
253
+ }),
254
+ }),
255
+
256
+ toggleMeasure2DMode: () => {
257
+ const state = get();
258
+ set({
259
+ measure2DMode: !state.measure2DMode,
260
+ // Clear measurement state when disabling
261
+ ...(!state.measure2DMode ? {} : {
262
+ measure2DStart: null,
263
+ measure2DCurrent: null,
264
+ measure2DShiftLocked: false,
265
+ measure2DLockedAxis: null,
266
+ measure2DSnapPoint: null,
267
+ }),
268
+ });
269
+ },
270
+
271
+ setMeasure2DStart: (point) => set({ measure2DStart: point }),
272
+
273
+ setMeasure2DCurrent: (point) => set({ measure2DCurrent: point }),
274
+
275
+ setMeasure2DShiftLocked: (locked, axis = null) => set({
276
+ measure2DShiftLocked: locked,
277
+ measure2DLockedAxis: locked ? axis : null,
278
+ }),
279
+
280
+ addMeasure2DResult: (result) => set((state) => ({
281
+ measure2DResults: [...state.measure2DResults, result],
282
+ })),
283
+
284
+ removeMeasure2DResult: (id) => set((state) => ({
285
+ measure2DResults: state.measure2DResults.filter((r) => r.id !== id),
286
+ })),
287
+
288
+ clearMeasure2DResults: () => set({ measure2DResults: [] }),
289
+
290
+ setMeasure2DSnapPoint: (point) => set({ measure2DSnapPoint: point }),
291
+
292
+ completeMeasure2D: () => {
293
+ const state = get();
294
+ if (state.measure2DStart && state.measure2DCurrent) {
295
+ const start = state.measure2DStart;
296
+ const end = state.measure2DCurrent;
297
+ const dx = end.x - start.x;
298
+ const dy = end.y - start.y;
299
+ const distance = Math.sqrt(dx * dx + dy * dy);
300
+
301
+ // Ignore zero-length measurements (click without drag)
302
+ const MIN_MEASUREMENT_DISTANCE = 0.001; // 1mm minimum
303
+ if (distance < MIN_MEASUREMENT_DISTANCE) {
304
+ // Reset state without saving the measurement
305
+ set({
306
+ measure2DStart: null,
307
+ measure2DCurrent: null,
308
+ measure2DShiftLocked: false,
309
+ measure2DLockedAxis: null,
310
+ measure2DSnapPoint: null,
311
+ });
312
+ return;
313
+ }
314
+
315
+ const result: Measure2DResult = {
316
+ id: `measure-${Date.now()}`,
317
+ start,
318
+ end,
319
+ distance,
320
+ };
321
+
322
+ set({
323
+ measure2DResults: [...state.measure2DResults, result],
324
+ measure2DStart: null,
325
+ measure2DCurrent: null,
326
+ measure2DShiftLocked: false,
327
+ measure2DLockedAxis: null,
328
+ measure2DSnapPoint: null,
329
+ });
330
+ }
331
+ },
332
+
333
+ cancelMeasure2D: () => set({
334
+ measure2DStart: null,
335
+ measure2DCurrent: null,
336
+ measure2DShiftLocked: false,
337
+ measure2DLockedAxis: null,
338
+ measure2DSnapPoint: null,
339
+ }),
340
+ });