@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.
- package/CHANGELOG.md +78 -0
- package/dist/assets/{Arrow.dom-BjDQoB2M.js → Arrow.dom-BGPQieQQ.js} +1 -1
- package/dist/assets/ifc-lite_bg-DyIN_nBM.wasm +0 -0
- package/dist/assets/{index-YBtrHPu3.js → index-dgdgiQ9p.js} +40212 -30008
- package/dist/assets/index-yTqs8kgX.css +1 -0
- package/dist/assets/{native-bridge-CULtTDX3.js → native-bridge-DD0SNyQ5.js} +1 -1
- package/dist/assets/{wasm-bridge-CjL-lSak.js → wasm-bridge-D54YMO7X.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +18 -15
- package/src/components/viewer/BCFPanel.tsx +7 -789
- package/src/components/viewer/Drawing2DCanvas.tsx +1048 -0
- package/src/components/viewer/DrawingSettingsPanel.tsx +3 -3
- package/src/components/viewer/HierarchyPanel.tsx +110 -842
- package/src/components/viewer/IDSExportDialog.tsx +281 -0
- package/src/components/viewer/IDSPanel.tsx +126 -17
- package/src/components/viewer/KeyboardShortcutsDialog.tsx +9 -0
- package/src/components/viewer/LensPanel.tsx +603 -0
- package/src/components/viewer/MainToolbar.tsx +188 -21
- package/src/components/viewer/PropertiesPanel.tsx +171 -663
- package/src/components/viewer/PropertyEditor.tsx +866 -77
- package/src/components/viewer/Section2DPanel.tsx +76 -2648
- package/src/components/viewer/ToolOverlays.tsx +3 -1097
- package/src/components/viewer/ViewerLayout.tsx +132 -45
- package/src/components/viewer/Viewport.tsx +237 -1659
- package/src/components/viewer/ViewportContainer.tsx +11 -3
- package/src/components/viewer/bcf/BCFCreateTopicForm.tsx +134 -0
- package/src/components/viewer/bcf/BCFTopicDetail.tsx +388 -0
- package/src/components/viewer/bcf/BCFTopicList.tsx +239 -0
- package/src/components/viewer/bcf/bcfHelpers.tsx +109 -0
- package/src/components/viewer/hierarchy/HierarchyNode.tsx +328 -0
- package/src/components/viewer/hierarchy/treeDataBuilder.ts +464 -0
- package/src/components/viewer/hierarchy/types.ts +54 -0
- package/src/components/viewer/hierarchy/useHierarchyTree.ts +280 -0
- package/src/components/viewer/lists/ListBuilder.tsx +486 -0
- package/src/components/viewer/lists/ListPanel.tsx +540 -0
- package/src/components/viewer/lists/ListResultsTable.tsx +193 -0
- package/src/components/viewer/properties/ClassificationCard.tsx +70 -0
- package/src/components/viewer/properties/CoordinateDisplay.tsx +49 -0
- package/src/components/viewer/properties/DocumentCard.tsx +89 -0
- package/src/components/viewer/properties/MaterialCard.tsx +201 -0
- package/src/components/viewer/properties/ModelMetadataPanel.tsx +335 -0
- package/src/components/viewer/properties/PropertySetCard.tsx +132 -0
- package/src/components/viewer/properties/QuantitySetCard.tsx +79 -0
- package/src/components/viewer/properties/RelationshipsCard.tsx +100 -0
- package/src/components/viewer/properties/encodingUtils.ts +29 -0
- package/src/components/viewer/tools/MeasurePanel.tsx +218 -0
- package/src/components/viewer/tools/MeasurementVisuals.tsx +644 -0
- package/src/components/viewer/tools/SectionPanel.tsx +183 -0
- package/src/components/viewer/tools/SectionVisualization.tsx +78 -0
- package/src/components/viewer/tools/formatDistance.ts +18 -0
- package/src/components/viewer/tools/sectionConstants.ts +14 -0
- package/src/components/viewer/useAnimationLoop.ts +166 -0
- package/src/components/viewer/useGeometryStreaming.ts +398 -0
- package/src/components/viewer/useKeyboardControls.ts +221 -0
- package/src/components/viewer/useMouseControls.ts +1009 -0
- package/src/components/viewer/useRenderUpdates.ts +165 -0
- package/src/components/viewer/useTouchControls.ts +245 -0
- package/src/hooks/ids/idsColorSystem.ts +125 -0
- package/src/hooks/ids/idsDataAccessor.ts +237 -0
- package/src/hooks/ids/idsExportService.ts +444 -0
- package/src/hooks/useBCF.ts +7 -0
- package/src/hooks/useDrawingExport.ts +627 -0
- package/src/hooks/useDrawingGeneration.ts +627 -0
- package/src/hooks/useFloorplanView.ts +108 -0
- package/src/hooks/useIDS.ts +270 -463
- package/src/hooks/useIfc.ts +26 -1628
- package/src/hooks/useIfcFederation.ts +803 -0
- package/src/hooks/useIfcLoader.ts +508 -0
- package/src/hooks/useIfcServer.ts +465 -0
- package/src/hooks/useKeyboardShortcuts.ts +1 -1
- package/src/hooks/useLens.ts +129 -0
- package/src/hooks/useMeasure2D.ts +365 -0
- package/src/hooks/useViewControls.ts +218 -0
- package/src/lib/ifc4-pset-definitions.test.ts +161 -0
- package/src/lib/ifc4-pset-definitions.ts +621 -0
- package/src/lib/ifc4-qto-definitions.ts +315 -0
- package/src/lib/lens/adapter.ts +138 -0
- package/src/lib/lens/index.ts +5 -0
- package/src/lib/lists/adapter.ts +69 -0
- package/src/lib/lists/index.ts +28 -0
- package/src/lib/lists/persistence.ts +64 -0
- package/src/services/fs-cache.ts +1 -1
- package/src/services/tauri-modules.d.ts +25 -0
- package/src/store/index.ts +38 -2
- package/src/store/slices/cameraSlice.ts +14 -1
- package/src/store/slices/dataSlice.ts +14 -1
- package/src/store/slices/lensSlice.ts +184 -0
- package/src/store/slices/listSlice.ts +74 -0
- package/src/store/slices/pinboardSlice.ts +114 -0
- package/src/store/types.ts +5 -0
- package/src/utils/ifcConfig.ts +16 -3
- package/src/utils/serverDataModel.ts +64 -101
- package/src/vite-env.d.ts +3 -0
- package/dist/assets/ifc-lite_bg-C6kblxf9.wasm +0 -0
- 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
|
+
}
|