@ifc-lite/viewer 1.6.0 → 1.7.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 (95) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/dist/assets/{Arrow.dom-BjDQoB2M.js → Arrow.dom-BGPQieQQ.js} +1 -1
  3. package/dist/assets/ifc-lite_bg-DyIN_nBM.wasm +0 -0
  4. package/dist/assets/{index-YBtrHPu3.js → index-dgdgiQ9p.js} +40212 -30008
  5. package/dist/assets/index-yTqs8kgX.css +1 -0
  6. package/dist/assets/{native-bridge-CULtTDX3.js → native-bridge-DD0SNyQ5.js} +1 -1
  7. package/dist/assets/{wasm-bridge-CjL-lSak.js → wasm-bridge-D54YMO7X.js} +1 -1
  8. package/dist/index.html +2 -2
  9. package/package.json +18 -15
  10. package/src/components/viewer/BCFPanel.tsx +7 -789
  11. package/src/components/viewer/Drawing2DCanvas.tsx +1048 -0
  12. package/src/components/viewer/DrawingSettingsPanel.tsx +3 -3
  13. package/src/components/viewer/HierarchyPanel.tsx +110 -842
  14. package/src/components/viewer/IDSExportDialog.tsx +281 -0
  15. package/src/components/viewer/IDSPanel.tsx +126 -17
  16. package/src/components/viewer/KeyboardShortcutsDialog.tsx +9 -0
  17. package/src/components/viewer/LensPanel.tsx +603 -0
  18. package/src/components/viewer/MainToolbar.tsx +188 -21
  19. package/src/components/viewer/PropertiesPanel.tsx +171 -663
  20. package/src/components/viewer/PropertyEditor.tsx +866 -77
  21. package/src/components/viewer/Section2DPanel.tsx +76 -2648
  22. package/src/components/viewer/ToolOverlays.tsx +3 -1097
  23. package/src/components/viewer/ViewerLayout.tsx +132 -45
  24. package/src/components/viewer/Viewport.tsx +237 -1659
  25. package/src/components/viewer/ViewportContainer.tsx +11 -3
  26. package/src/components/viewer/bcf/BCFCreateTopicForm.tsx +134 -0
  27. package/src/components/viewer/bcf/BCFTopicDetail.tsx +388 -0
  28. package/src/components/viewer/bcf/BCFTopicList.tsx +239 -0
  29. package/src/components/viewer/bcf/bcfHelpers.tsx +109 -0
  30. package/src/components/viewer/hierarchy/HierarchyNode.tsx +328 -0
  31. package/src/components/viewer/hierarchy/treeDataBuilder.ts +464 -0
  32. package/src/components/viewer/hierarchy/types.ts +54 -0
  33. package/src/components/viewer/hierarchy/useHierarchyTree.ts +280 -0
  34. package/src/components/viewer/lists/ListBuilder.tsx +486 -0
  35. package/src/components/viewer/lists/ListPanel.tsx +540 -0
  36. package/src/components/viewer/lists/ListResultsTable.tsx +193 -0
  37. package/src/components/viewer/properties/ClassificationCard.tsx +70 -0
  38. package/src/components/viewer/properties/CoordinateDisplay.tsx +49 -0
  39. package/src/components/viewer/properties/DocumentCard.tsx +89 -0
  40. package/src/components/viewer/properties/MaterialCard.tsx +201 -0
  41. package/src/components/viewer/properties/ModelMetadataPanel.tsx +335 -0
  42. package/src/components/viewer/properties/PropertySetCard.tsx +132 -0
  43. package/src/components/viewer/properties/QuantitySetCard.tsx +79 -0
  44. package/src/components/viewer/properties/RelationshipsCard.tsx +100 -0
  45. package/src/components/viewer/properties/encodingUtils.ts +29 -0
  46. package/src/components/viewer/tools/MeasurePanel.tsx +218 -0
  47. package/src/components/viewer/tools/MeasurementVisuals.tsx +644 -0
  48. package/src/components/viewer/tools/SectionPanel.tsx +183 -0
  49. package/src/components/viewer/tools/SectionVisualization.tsx +78 -0
  50. package/src/components/viewer/tools/formatDistance.ts +18 -0
  51. package/src/components/viewer/tools/sectionConstants.ts +14 -0
  52. package/src/components/viewer/useAnimationLoop.ts +166 -0
  53. package/src/components/viewer/useGeometryStreaming.ts +398 -0
  54. package/src/components/viewer/useKeyboardControls.ts +221 -0
  55. package/src/components/viewer/useMouseControls.ts +1009 -0
  56. package/src/components/viewer/useRenderUpdates.ts +165 -0
  57. package/src/components/viewer/useTouchControls.ts +245 -0
  58. package/src/hooks/ids/idsColorSystem.ts +125 -0
  59. package/src/hooks/ids/idsDataAccessor.ts +237 -0
  60. package/src/hooks/ids/idsExportService.ts +444 -0
  61. package/src/hooks/useBCF.ts +7 -0
  62. package/src/hooks/useDrawingExport.ts +627 -0
  63. package/src/hooks/useDrawingGeneration.ts +627 -0
  64. package/src/hooks/useFloorplanView.ts +108 -0
  65. package/src/hooks/useIDS.ts +270 -463
  66. package/src/hooks/useIfc.ts +26 -1628
  67. package/src/hooks/useIfcFederation.ts +803 -0
  68. package/src/hooks/useIfcLoader.ts +508 -0
  69. package/src/hooks/useIfcServer.ts +465 -0
  70. package/src/hooks/useKeyboardShortcuts.ts +1 -1
  71. package/src/hooks/useLens.ts +129 -0
  72. package/src/hooks/useMeasure2D.ts +365 -0
  73. package/src/hooks/useViewControls.ts +218 -0
  74. package/src/lib/ifc4-pset-definitions.test.ts +161 -0
  75. package/src/lib/ifc4-pset-definitions.ts +621 -0
  76. package/src/lib/ifc4-qto-definitions.ts +315 -0
  77. package/src/lib/lens/adapter.ts +138 -0
  78. package/src/lib/lens/index.ts +5 -0
  79. package/src/lib/lists/adapter.ts +69 -0
  80. package/src/lib/lists/index.ts +28 -0
  81. package/src/lib/lists/persistence.ts +64 -0
  82. package/src/services/fs-cache.ts +1 -1
  83. package/src/services/tauri-modules.d.ts +25 -0
  84. package/src/store/index.ts +38 -2
  85. package/src/store/slices/cameraSlice.ts +14 -1
  86. package/src/store/slices/dataSlice.ts +14 -1
  87. package/src/store/slices/lensSlice.ts +184 -0
  88. package/src/store/slices/listSlice.ts +74 -0
  89. package/src/store/slices/pinboardSlice.ts +114 -0
  90. package/src/store/types.ts +5 -0
  91. package/src/utils/ifcConfig.ts +16 -3
  92. package/src/utils/serverDataModel.ts +64 -101
  93. package/src/vite-env.d.ts +3 -0
  94. package/dist/assets/ifc-lite_bg-C6kblxf9.wasm +0 -0
  95. package/dist/assets/index-v3mcCUPN.css +0 -1
@@ -0,0 +1,165 @@
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
+ * Render updates hook for the 3D viewport
7
+ * Handles visibility/selection/section/hover state re-render effects
8
+ */
9
+
10
+ import { useEffect, type MutableRefObject } from 'react';
11
+ import type { Renderer, CutPolygon2D, DrawingLine2D } from '@ifc-lite/renderer';
12
+ import type { CoordinateInfo } from '@ifc-lite/geometry';
13
+ import type { Drawing2D } from '@ifc-lite/drawing-2d';
14
+ import type { SectionPlane } from '@/store';
15
+ import { getThemeClearColor } from '../../utils/viewportUtils.js';
16
+
17
+ export interface UseRenderUpdatesParams {
18
+ rendererRef: MutableRefObject<Renderer | null>;
19
+ isInitialized: boolean;
20
+
21
+ // Theme
22
+ theme: string;
23
+ clearColorRef: MutableRefObject<[number, number, number, number]>;
24
+
25
+ // Visibility/selection state (reactive values, not refs)
26
+ hiddenEntities: Set<number>;
27
+ isolatedEntities: Set<number> | null;
28
+ selectedEntityId: number | null;
29
+ selectedEntityIds: Set<number> | undefined;
30
+ selectedModelIndex: number | undefined;
31
+ activeTool: string;
32
+ sectionPlane: SectionPlane;
33
+ sectionRange: { min: number; max: number } | null;
34
+ coordinateInfo?: CoordinateInfo;
35
+
36
+ // Refs for theme re-render
37
+ hiddenEntitiesRef: MutableRefObject<Set<number>>;
38
+ isolatedEntitiesRef: MutableRefObject<Set<number> | null>;
39
+ selectedEntityIdRef: MutableRefObject<number | null>;
40
+ selectedModelIndexRef: MutableRefObject<number | undefined>;
41
+ selectedEntityIdsRef: MutableRefObject<Set<number> | undefined>;
42
+ sectionPlaneRef: MutableRefObject<SectionPlane>;
43
+ sectionRangeRef: MutableRefObject<{ min: number; max: number } | null>;
44
+ activeToolRef: MutableRefObject<string>;
45
+
46
+ // Drawing 2D
47
+ drawing2D: Drawing2D | null;
48
+ show3DOverlay: boolean;
49
+ }
50
+
51
+ export function useRenderUpdates(params: UseRenderUpdatesParams): void {
52
+ const {
53
+ rendererRef,
54
+ isInitialized,
55
+ theme,
56
+ clearColorRef,
57
+ hiddenEntities,
58
+ isolatedEntities,
59
+ selectedEntityId,
60
+ selectedEntityIds,
61
+ selectedModelIndex,
62
+ activeTool,
63
+ sectionPlane,
64
+ sectionRange,
65
+ coordinateInfo,
66
+ hiddenEntitiesRef,
67
+ isolatedEntitiesRef,
68
+ selectedEntityIdRef,
69
+ selectedModelIndexRef,
70
+ selectedEntityIdsRef,
71
+ sectionPlaneRef,
72
+ sectionRangeRef,
73
+ activeToolRef,
74
+ drawing2D,
75
+ show3DOverlay,
76
+ } = params;
77
+
78
+ // Theme-aware clear color update
79
+ useEffect(() => {
80
+ // Update clear color when theme changes
81
+ clearColorRef.current = getThemeClearColor(theme as 'light' | 'dark');
82
+ // Re-render with new clear color
83
+ const renderer = rendererRef.current;
84
+ if (renderer && isInitialized) {
85
+ renderer.render({
86
+ hiddenIds: hiddenEntitiesRef.current,
87
+ isolatedIds: isolatedEntitiesRef.current,
88
+ selectedId: selectedEntityIdRef.current,
89
+ selectedModelIndex: selectedModelIndexRef.current,
90
+ clearColor: clearColorRef.current,
91
+ });
92
+ }
93
+ }, [theme, isInitialized]);
94
+
95
+ // 2D section overlay: upload drawing data to renderer when available
96
+ useEffect(() => {
97
+ const renderer = rendererRef.current;
98
+ if (!renderer || !isInitialized) return;
99
+
100
+ // Only show overlay when section tool is active, we have a drawing, AND 3D overlay is enabled
101
+ if (activeTool === 'section' && drawing2D && drawing2D.cutPolygons.length > 0 && show3DOverlay) {
102
+ // Convert Drawing2D format to renderer format
103
+ const polygons: CutPolygon2D[] = drawing2D.cutPolygons.map((cp) => ({
104
+ polygon: cp.polygon,
105
+ ifcType: cp.ifcType,
106
+ expressId: cp.entityId, // DrawingPolygon uses entityId
107
+ }));
108
+
109
+ // No hatching lines for 3D overlay (too dense)
110
+ const lines: DrawingLine2D[] = [];
111
+
112
+ // Upload to renderer - will be drawn on the section plane
113
+ // Pass sectionRange to match exactly what render() uses for section plane position
114
+ renderer.uploadSection2DOverlay(
115
+ polygons,
116
+ lines,
117
+ sectionPlane.axis,
118
+ sectionPlane.position,
119
+ sectionRangeRef.current ?? undefined, // Same range as section plane
120
+ sectionPlane.flipped
121
+ );
122
+ } else {
123
+ // Clear overlay when not in section mode, no drawing, or overlay disabled
124
+ renderer.clearSection2DOverlay();
125
+ }
126
+
127
+ // Re-render to show/hide overlay
128
+ renderer.render({
129
+ hiddenIds: hiddenEntitiesRef.current,
130
+ isolatedIds: isolatedEntitiesRef.current,
131
+ selectedId: selectedEntityIdRef.current,
132
+ selectedIds: selectedEntityIdsRef.current,
133
+ selectedModelIndex: selectedModelIndexRef.current,
134
+ clearColor: clearColorRef.current,
135
+ sectionPlane: activeTool === 'section' ? {
136
+ ...sectionPlane,
137
+ min: sectionRangeRef.current?.min,
138
+ max: sectionRangeRef.current?.max,
139
+ } : undefined,
140
+ });
141
+ }, [drawing2D, activeTool, sectionPlane, isInitialized, coordinateInfo, show3DOverlay]);
142
+
143
+ // Re-render when visibility, selection, or section plane changes
144
+ useEffect(() => {
145
+ const renderer = rendererRef.current;
146
+ if (!renderer || !isInitialized) return;
147
+
148
+ renderer.render({
149
+ hiddenIds: hiddenEntities,
150
+ isolatedIds: isolatedEntities,
151
+ selectedId: selectedEntityId,
152
+ selectedIds: selectedEntityIds,
153
+ selectedModelIndex,
154
+ clearColor: clearColorRef.current,
155
+ sectionPlane: activeTool === 'section' ? {
156
+ ...sectionPlane,
157
+ min: sectionRange?.min,
158
+ max: sectionRange?.max,
159
+ } : undefined,
160
+ buildingRotation: coordinateInfo?.buildingRotation,
161
+ });
162
+ }, [hiddenEntities, isolatedEntities, selectedEntityId, selectedEntityIds, selectedModelIndex, isInitialized, sectionPlane, activeTool, sectionRange, coordinateInfo?.buildingRotation]);
163
+ }
164
+
165
+ export default useRenderUpdates;
@@ -0,0 +1,245 @@
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
+ * Touch controls hook for the 3D viewport
7
+ * Handles multi-touch gesture handling (orbit, pinch-zoom, pan, tap-to-select)
8
+ */
9
+
10
+ import { useEffect, type MutableRefObject, type RefObject } from 'react';
11
+ import type { Renderer, PickResult } from '@ifc-lite/renderer';
12
+ import type { MeshData } from '@ifc-lite/geometry';
13
+ import type { SectionPlane } from '@/store';
14
+ import { getEntityCenter } from '../../utils/viewportUtils.js';
15
+
16
+ export interface TouchState {
17
+ touches: Touch[];
18
+ lastDistance: number;
19
+ lastCenter: { x: number; y: number };
20
+ tapStartTime: number;
21
+ tapStartPos: { x: number; y: number };
22
+ didMove: boolean;
23
+ multiTouch: boolean;
24
+ }
25
+
26
+ export interface UseTouchControlsParams {
27
+ canvasRef: RefObject<HTMLCanvasElement | null>;
28
+ rendererRef: MutableRefObject<Renderer | null>;
29
+ isInitialized: boolean;
30
+ touchStateRef: MutableRefObject<TouchState>;
31
+ activeToolRef: MutableRefObject<string>;
32
+ hiddenEntitiesRef: MutableRefObject<Set<number>>;
33
+ isolatedEntitiesRef: MutableRefObject<Set<number> | null>;
34
+ selectedEntityIdRef: MutableRefObject<number | null>;
35
+ selectedModelIndexRef: MutableRefObject<number | undefined>;
36
+ clearColorRef: MutableRefObject<[number, number, number, number]>;
37
+ sectionPlaneRef: MutableRefObject<SectionPlane>;
38
+ sectionRangeRef: MutableRefObject<{ min: number; max: number } | null>;
39
+ geometryRef: MutableRefObject<MeshData[] | null>;
40
+ handlePickForSelection: (pickResult: PickResult | null) => void;
41
+ getPickOptions: () => { isStreaming: boolean; hiddenIds: Set<number>; isolatedIds: Set<number> | null };
42
+ }
43
+
44
+ export function useTouchControls(params: UseTouchControlsParams): void {
45
+ const {
46
+ canvasRef,
47
+ rendererRef,
48
+ isInitialized,
49
+ touchStateRef,
50
+ activeToolRef,
51
+ hiddenEntitiesRef,
52
+ isolatedEntitiesRef,
53
+ selectedEntityIdRef,
54
+ selectedModelIndexRef,
55
+ clearColorRef,
56
+ sectionPlaneRef,
57
+ sectionRangeRef,
58
+ geometryRef,
59
+ handlePickForSelection,
60
+ getPickOptions,
61
+ } = params;
62
+
63
+ useEffect(() => {
64
+ const canvas = canvasRef.current;
65
+ const renderer = rendererRef.current;
66
+ if (!canvas || !renderer || !isInitialized) return;
67
+
68
+ const camera = renderer.getCamera();
69
+ const touchState = touchStateRef.current;
70
+
71
+ const handleTouchStart = async (e: TouchEvent) => {
72
+ e.preventDefault();
73
+ touchState.touches = Array.from(e.touches);
74
+
75
+ // Track multi-touch to prevent false tap-select after pinch/zoom
76
+ if (touchState.touches.length > 1) {
77
+ touchState.multiTouch = true;
78
+ }
79
+
80
+ if (touchState.touches.length === 1 && !touchState.multiTouch) {
81
+ touchState.lastCenter = {
82
+ x: touchState.touches[0].clientX,
83
+ y: touchState.touches[0].clientY,
84
+ };
85
+ // Record tap start for tap-to-select detection
86
+ touchState.tapStartTime = Date.now();
87
+ touchState.tapStartPos = {
88
+ x: touchState.touches[0].clientX,
89
+ y: touchState.touches[0].clientY,
90
+ };
91
+ touchState.didMove = false;
92
+
93
+ // Set orbit pivot to what user touches (same as mouse click behavior)
94
+ const rect = canvas.getBoundingClientRect();
95
+ const x = touchState.touches[0].clientX - rect.left;
96
+ const y = touchState.touches[0].clientY - rect.top;
97
+
98
+ // Uses visibility filtering so hidden elements don't affect orbit pivot
99
+ const pickResult = await renderer.pick(x, y, getPickOptions());
100
+ if (pickResult !== null) {
101
+ const center = getEntityCenter(geometryRef.current, pickResult.expressId);
102
+ if (center) {
103
+ camera.setOrbitPivot(center);
104
+ } else {
105
+ camera.setOrbitPivot(null);
106
+ }
107
+ } else {
108
+ camera.setOrbitPivot(null);
109
+ }
110
+ } else if (touchState.touches.length === 1) {
111
+ // Single touch after multi-touch - just update center for orbit
112
+ touchState.lastCenter = {
113
+ x: touchState.touches[0].clientX,
114
+ y: touchState.touches[0].clientY,
115
+ };
116
+ } else if (touchState.touches.length === 2) {
117
+ const dx = touchState.touches[1].clientX - touchState.touches[0].clientX;
118
+ const dy = touchState.touches[1].clientY - touchState.touches[0].clientY;
119
+ touchState.lastDistance = Math.sqrt(dx * dx + dy * dy);
120
+ touchState.lastCenter = {
121
+ x: (touchState.touches[0].clientX + touchState.touches[1].clientX) / 2,
122
+ y: (touchState.touches[0].clientY + touchState.touches[1].clientY) / 2,
123
+ };
124
+ }
125
+ };
126
+
127
+ const handleTouchMove = (e: TouchEvent) => {
128
+ e.preventDefault();
129
+ touchState.touches = Array.from(e.touches);
130
+
131
+ if (touchState.touches.length === 1) {
132
+ const dx = touchState.touches[0].clientX - touchState.lastCenter.x;
133
+ const dy = touchState.touches[0].clientY - touchState.lastCenter.y;
134
+
135
+ // Mark as moved if significant movement (prevents tap-select during drag)
136
+ const totalDx = touchState.touches[0].clientX - touchState.tapStartPos.x;
137
+ const totalDy = touchState.touches[0].clientY - touchState.tapStartPos.y;
138
+ if (Math.abs(totalDx) > 10 || Math.abs(totalDy) > 10) {
139
+ touchState.didMove = true;
140
+ }
141
+
142
+ camera.orbit(dx, dy, false);
143
+ touchState.lastCenter = {
144
+ x: touchState.touches[0].clientX,
145
+ y: touchState.touches[0].clientY,
146
+ };
147
+ renderer.render({
148
+ hiddenIds: hiddenEntitiesRef.current,
149
+ isolatedIds: isolatedEntitiesRef.current,
150
+ selectedId: selectedEntityIdRef.current,
151
+ selectedModelIndex: selectedModelIndexRef.current,
152
+ clearColor: clearColorRef.current,
153
+ sectionPlane: activeToolRef.current === 'section' ? {
154
+ ...sectionPlaneRef.current,
155
+ min: sectionRangeRef.current?.min,
156
+ max: sectionRangeRef.current?.max,
157
+ } : undefined,
158
+ });
159
+ } else if (touchState.touches.length === 2) {
160
+ const dx1 = touchState.touches[1].clientX - touchState.touches[0].clientX;
161
+ const dy1 = touchState.touches[1].clientY - touchState.touches[0].clientY;
162
+ const distance = Math.sqrt(dx1 * dx1 + dy1 * dy1);
163
+
164
+ const centerX = (touchState.touches[0].clientX + touchState.touches[1].clientX) / 2;
165
+ const centerY = (touchState.touches[0].clientY + touchState.touches[1].clientY) / 2;
166
+ const panDx = centerX - touchState.lastCenter.x;
167
+ const panDy = centerY - touchState.lastCenter.y;
168
+ camera.pan(panDx, panDy, false);
169
+
170
+ const zoomDelta = distance - touchState.lastDistance;
171
+ const rect = canvas.getBoundingClientRect();
172
+ camera.zoom(zoomDelta * 10, false, centerX - rect.left, centerY - rect.top, canvas.width, canvas.height);
173
+
174
+ touchState.lastDistance = distance;
175
+ touchState.lastCenter = { x: centerX, y: centerY };
176
+ renderer.render({
177
+ hiddenIds: hiddenEntitiesRef.current,
178
+ isolatedIds: isolatedEntitiesRef.current,
179
+ selectedId: selectedEntityIdRef.current,
180
+ selectedModelIndex: selectedModelIndexRef.current,
181
+ clearColor: clearColorRef.current,
182
+ sectionPlane: activeToolRef.current === 'section' ? {
183
+ ...sectionPlaneRef.current,
184
+ min: sectionRangeRef.current?.min,
185
+ max: sectionRangeRef.current?.max,
186
+ } : undefined,
187
+ });
188
+ }
189
+ };
190
+
191
+ const handleTouchEnd = async (e: TouchEvent) => {
192
+ e.preventDefault();
193
+ const previousTouchCount = touchState.touches.length;
194
+ const wasMultiTouch = touchState.multiTouch;
195
+ touchState.touches = Array.from(e.touches);
196
+
197
+ if (touchState.touches.length === 0) {
198
+ camera.stopInertia();
199
+ camera.setOrbitPivot(null);
200
+
201
+ // Tap-to-select: detect quick tap without significant movement
202
+ const tapDuration = Date.now() - touchState.tapStartTime;
203
+ const tool = activeToolRef.current;
204
+
205
+ // Only select if:
206
+ // - Was a single-finger touch (not after multi-touch gesture)
207
+ // - Tap was quick (< 300ms)
208
+ // - Didn't move significantly
209
+ // - Tool supports selection (not orbit/pan/walk/measure)
210
+ if (
211
+ previousTouchCount === 1 &&
212
+ !wasMultiTouch &&
213
+ tapDuration < 300 &&
214
+ !touchState.didMove &&
215
+ tool !== 'orbit' &&
216
+ tool !== 'pan' &&
217
+ tool !== 'walk' &&
218
+ tool !== 'measure'
219
+ ) {
220
+ const rect = canvas.getBoundingClientRect();
221
+ const x = touchState.tapStartPos.x - rect.left;
222
+ const y = touchState.tapStartPos.y - rect.top;
223
+
224
+ const pickResult = await renderer.pick(x, y, getPickOptions());
225
+ handlePickForSelection(pickResult);
226
+ }
227
+
228
+ // Reset multi-touch flag when all touches end
229
+ touchState.multiTouch = false;
230
+ }
231
+ };
232
+
233
+ canvas.addEventListener('touchstart', handleTouchStart);
234
+ canvas.addEventListener('touchmove', handleTouchMove);
235
+ canvas.addEventListener('touchend', handleTouchEnd);
236
+
237
+ return () => {
238
+ canvas.removeEventListener('touchstart', handleTouchStart);
239
+ canvas.removeEventListener('touchmove', handleTouchMove);
240
+ canvas.removeEventListener('touchend', handleTouchEnd);
241
+ };
242
+ }, [isInitialized]);
243
+ }
244
+
245
+ export default useTouchControls;
@@ -0,0 +1,125 @@
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
+ * IDS Color System
7
+ *
8
+ * Pure functions that apply and clear validation result color overrides
9
+ * on renderer meshes. No React dependencies.
10
+ */
11
+
12
+ import type { IDSValidationReport } from '@ifc-lite/ids';
13
+ import type { GeometryResult } from '@ifc-lite/geometry';
14
+
15
+ /** RGBA color tuple in 0-1 range */
16
+ export type ColorTuple = [number, number, number, number];
17
+
18
+ /** Stable default color constants */
19
+ export const DEFAULT_FAILED_COLOR: ColorTuple = [0.9, 0.2, 0.2, 1.0];
20
+ export const DEFAULT_PASSED_COLOR: ColorTuple = [0.2, 0.8, 0.2, 1.0];
21
+
22
+ /** Display options controlling which entities get color overrides */
23
+ export interface ColorDisplayOptions {
24
+ highlightFailed: boolean;
25
+ highlightPassed: boolean;
26
+ failedColor: ColorTuple;
27
+ passedColor: ColorTuple;
28
+ }
29
+
30
+ /** Model info for resolving express IDs to global IDs */
31
+ export interface ColorModelInfo {
32
+ idOffset?: number;
33
+ }
34
+
35
+ /**
36
+ * Build a map of color overrides from validation results.
37
+ *
38
+ * Also captures original colors from geometry (only if originalColors is empty)
39
+ * so they can be restored later via `buildRestoreColorUpdates`.
40
+ *
41
+ * @param report - The IDS validation report
42
+ * @param models - Map of model ID to model info (for ID offset resolution)
43
+ * @param displayOptions - Controls which highlights are active and their colors
44
+ * @param defaultFailedColor - Fallback failed color
45
+ * @param defaultPassedColor - Fallback passed color
46
+ * @param geometryResult - Current geometry for capturing original colors (may be null)
47
+ * @param originalColors - Mutable map to store original colors into (only populated if empty)
48
+ * @returns Map of globalId to color tuple for updateMeshColors
49
+ */
50
+ export function buildValidationColorUpdates(
51
+ report: IDSValidationReport,
52
+ models: ReadonlyMap<string, ColorModelInfo>,
53
+ displayOptions: ColorDisplayOptions,
54
+ defaultFailedColor: ColorTuple,
55
+ defaultPassedColor: ColorTuple,
56
+ geometryResult: GeometryResult | null | undefined,
57
+ originalColors: Map<number, ColorTuple>
58
+ ): Map<number, ColorTuple> {
59
+ const colorUpdates = new Map<number, ColorTuple>();
60
+
61
+ // Get color options
62
+ const failedClr = displayOptions.failedColor ?? defaultFailedColor;
63
+ const passedClr = displayOptions.passedColor ?? defaultPassedColor;
64
+
65
+ // Build a set of globalIds we'll be updating
66
+ const globalIdsToUpdate = new Set<number>();
67
+ for (const specResult of report.specificationResults) {
68
+ for (const entityResult of specResult.entityResults) {
69
+ const model = models.get(entityResult.modelId);
70
+ const globalId = model
71
+ ? entityResult.expressId + (model.idOffset ?? 0)
72
+ : entityResult.expressId;
73
+ globalIdsToUpdate.add(globalId);
74
+ }
75
+ }
76
+
77
+ // Capture original colors before applying overrides (only if not already captured)
78
+ if (geometryResult?.meshes && originalColors.size === 0) {
79
+ for (const mesh of geometryResult.meshes) {
80
+ if (globalIdsToUpdate.has(mesh.expressId)) {
81
+ originalColors.set(mesh.expressId, [...mesh.color] as ColorTuple);
82
+ }
83
+ }
84
+ }
85
+
86
+ // Process all entity results
87
+ for (const specResult of report.specificationResults) {
88
+ for (const entityResult of specResult.entityResults) {
89
+ const model = models.get(entityResult.modelId);
90
+ const globalId = model
91
+ ? entityResult.expressId + (model.idOffset ?? 0)
92
+ : entityResult.expressId;
93
+
94
+ if (entityResult.passed && displayOptions.highlightPassed) {
95
+ colorUpdates.set(globalId, passedClr);
96
+ } else if (!entityResult.passed && displayOptions.highlightFailed) {
97
+ colorUpdates.set(globalId, failedClr);
98
+ }
99
+ }
100
+ }
101
+
102
+ return colorUpdates;
103
+ }
104
+
105
+ /**
106
+ * Build a map of color updates to restore original colors.
107
+ *
108
+ * @param originalColors - Map of globalId to original color (will be cleared after building)
109
+ * @returns Map of globalId to original color tuple for updateMeshColors, or null if nothing to restore
110
+ */
111
+ export function buildRestoreColorUpdates(
112
+ originalColors: Map<number, ColorTuple>
113
+ ): Map<number, ColorTuple> | null {
114
+ if (originalColors.size === 0) {
115
+ return null;
116
+ }
117
+
118
+ // Create a new map with the original colors to restore
119
+ const colorUpdates = new Map<number, ColorTuple>(originalColors);
120
+
121
+ // Clear the stored original colors after building restore map
122
+ originalColors.clear();
123
+
124
+ return colorUpdates;
125
+ }