@ifc-lite/viewer 1.6.1 → 1.8.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 (110) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/dist/assets/{Arrow.dom-Be1tgmo6.js → Arrow.dom-CwcRxist.js} +1 -1
  3. package/dist/assets/ifc-lite_bg-DyIN_nBM.wasm +0 -0
  4. package/dist/assets/index-7WoQ-qVC.css +1 -0
  5. package/dist/assets/{index-D1Du89Pa.js → index-BSANf7-H.js} +44948 -31410
  6. package/dist/assets/{native-bridge-A6zNnTfi.js → native-bridge-5LbrYh3R.js} +1 -1
  7. package/dist/assets/{wasm-bridge-DkRhgSvE.js → wasm-bridge-CgpLtj1h.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 +1411 -0
  12. package/src/components/viewer/DrawingSettingsPanel.tsx +3 -3
  13. package/src/components/viewer/EntityContextMenu.tsx +47 -20
  14. package/src/components/viewer/ExportDialog.tsx +166 -17
  15. package/src/components/viewer/HierarchyPanel.tsx +113 -843
  16. package/src/components/viewer/IDSExportDialog.tsx +281 -0
  17. package/src/components/viewer/IDSPanel.tsx +126 -17
  18. package/src/components/viewer/KeyboardShortcutsDialog.tsx +9 -0
  19. package/src/components/viewer/LensPanel.tsx +1366 -0
  20. package/src/components/viewer/MainToolbar.tsx +237 -37
  21. package/src/components/viewer/PropertiesPanel.tsx +171 -652
  22. package/src/components/viewer/PropertyEditor.tsx +866 -77
  23. package/src/components/viewer/Section2DPanel.tsx +329 -2661
  24. package/src/components/viewer/TextAnnotationEditor.tsx +112 -0
  25. package/src/components/viewer/ToolOverlays.tsx +3 -1097
  26. package/src/components/viewer/ViewerLayout.tsx +132 -45
  27. package/src/components/viewer/Viewport.tsx +290 -1678
  28. package/src/components/viewer/ViewportContainer.tsx +13 -3
  29. package/src/components/viewer/bcf/BCFCreateTopicForm.tsx +134 -0
  30. package/src/components/viewer/bcf/BCFTopicDetail.tsx +388 -0
  31. package/src/components/viewer/bcf/BCFTopicList.tsx +239 -0
  32. package/src/components/viewer/bcf/bcfHelpers.tsx +109 -0
  33. package/src/components/viewer/hierarchy/HierarchyNode.tsx +328 -0
  34. package/src/components/viewer/hierarchy/treeDataBuilder.ts +464 -0
  35. package/src/components/viewer/hierarchy/types.ts +54 -0
  36. package/src/components/viewer/hierarchy/useHierarchyTree.ts +280 -0
  37. package/src/components/viewer/lists/ListBuilder.tsx +486 -0
  38. package/src/components/viewer/lists/ListPanel.tsx +540 -0
  39. package/src/components/viewer/lists/ListResultsTable.tsx +227 -0
  40. package/src/components/viewer/properties/ClassificationCard.tsx +70 -0
  41. package/src/components/viewer/properties/CoordinateDisplay.tsx +49 -0
  42. package/src/components/viewer/properties/DocumentCard.tsx +89 -0
  43. package/src/components/viewer/properties/MaterialCard.tsx +201 -0
  44. package/src/components/viewer/properties/ModelMetadataPanel.tsx +335 -0
  45. package/src/components/viewer/properties/PropertySetCard.tsx +132 -0
  46. package/src/components/viewer/properties/QuantitySetCard.tsx +79 -0
  47. package/src/components/viewer/properties/RelationshipsCard.tsx +100 -0
  48. package/src/components/viewer/properties/encodingUtils.ts +29 -0
  49. package/src/components/viewer/tools/MeasurePanel.tsx +218 -0
  50. package/src/components/viewer/tools/MeasurementVisuals.tsx +644 -0
  51. package/src/components/viewer/tools/SectionPanel.tsx +183 -0
  52. package/src/components/viewer/tools/SectionVisualization.tsx +78 -0
  53. package/src/components/viewer/tools/cloudPathGenerator.test.ts +118 -0
  54. package/src/components/viewer/tools/cloudPathGenerator.ts +275 -0
  55. package/src/components/viewer/tools/computePolygonArea.test.ts +165 -0
  56. package/src/components/viewer/tools/computePolygonArea.ts +72 -0
  57. package/src/components/viewer/tools/formatDistance.ts +18 -0
  58. package/src/components/viewer/tools/sectionConstants.ts +14 -0
  59. package/src/components/viewer/useAnimationLoop.ts +166 -0
  60. package/src/components/viewer/useGeometryStreaming.ts +406 -0
  61. package/src/components/viewer/useKeyboardControls.ts +221 -0
  62. package/src/components/viewer/useMouseControls.ts +1009 -0
  63. package/src/components/viewer/useRenderUpdates.ts +165 -0
  64. package/src/components/viewer/useTouchControls.ts +245 -0
  65. package/src/hooks/ids/idsColorSystem.ts +125 -0
  66. package/src/hooks/ids/idsDataAccessor.ts +237 -0
  67. package/src/hooks/ids/idsExportService.ts +444 -0
  68. package/src/hooks/useAnnotation2D.ts +551 -0
  69. package/src/hooks/useBCF.ts +7 -0
  70. package/src/hooks/useDrawingExport.ts +709 -0
  71. package/src/hooks/useDrawingGeneration.ts +627 -0
  72. package/src/hooks/useFloorplanView.ts +108 -0
  73. package/src/hooks/useIDS.ts +270 -463
  74. package/src/hooks/useIfc.ts +26 -1628
  75. package/src/hooks/useIfcFederation.ts +803 -0
  76. package/src/hooks/useIfcLoader.ts +508 -0
  77. package/src/hooks/useIfcServer.ts +465 -0
  78. package/src/hooks/useKeyboardShortcuts.ts +114 -15
  79. package/src/hooks/useLens.ts +113 -0
  80. package/src/hooks/useLensDiscovery.ts +46 -0
  81. package/src/hooks/useMeasure2D.ts +365 -0
  82. package/src/hooks/useModelSelection.ts +5 -22
  83. package/src/hooks/useViewControls.ts +218 -0
  84. package/src/index.css +7 -1
  85. package/src/lib/ifc4-pset-definitions.test.ts +161 -0
  86. package/src/lib/ifc4-pset-definitions.ts +621 -0
  87. package/src/lib/ifc4-qto-definitions.ts +315 -0
  88. package/src/lib/lens/adapter.ts +264 -0
  89. package/src/lib/lens/index.ts +5 -0
  90. package/src/lib/lists/adapter.ts +69 -0
  91. package/src/lib/lists/columnToAutoColor.ts +33 -0
  92. package/src/lib/lists/index.ts +28 -0
  93. package/src/lib/lists/persistence.ts +64 -0
  94. package/src/services/fs-cache.ts +1 -1
  95. package/src/services/tauri-modules.d.ts +25 -0
  96. package/src/store/index.ts +52 -3
  97. package/src/store/resolveEntityRef.ts +44 -0
  98. package/src/store/slices/cameraSlice.ts +14 -1
  99. package/src/store/slices/dataSlice.ts +14 -1
  100. package/src/store/slices/drawing2DSlice.ts +321 -0
  101. package/src/store/slices/lensSlice.ts +226 -0
  102. package/src/store/slices/listSlice.ts +74 -0
  103. package/src/store/slices/pinboardSlice.ts +247 -0
  104. package/src/store/types.ts +5 -0
  105. package/src/store.ts +3 -0
  106. package/src/utils/ifcConfig.ts +16 -3
  107. package/src/utils/serverDataModel.ts +64 -101
  108. package/src/vite-env.d.ts +3 -0
  109. package/dist/assets/ifc-lite_bg-C6kblxf9.wasm +0 -0
  110. 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
+ import { describe, it } from 'node:test';
6
+ import assert from 'node:assert';
7
+ import {
8
+ computePolygonArea,
9
+ computePolygonPerimeter,
10
+ computePolygonCentroid,
11
+ formatArea,
12
+ } from './computePolygonArea.js';
13
+
14
+ describe('computePolygonArea', () => {
15
+ it('returns 0 for fewer than 3 points', () => {
16
+ assert.strictEqual(computePolygonArea([]), 0);
17
+ assert.strictEqual(computePolygonArea([{ x: 0, y: 0 }]), 0);
18
+ assert.strictEqual(computePolygonArea([{ x: 0, y: 0 }, { x: 1, y: 0 }]), 0);
19
+ });
20
+
21
+ it('computes area of a unit square', () => {
22
+ const square = [
23
+ { x: 0, y: 0 },
24
+ { x: 1, y: 0 },
25
+ { x: 1, y: 1 },
26
+ { x: 0, y: 1 },
27
+ ];
28
+ assert.strictEqual(computePolygonArea(square), 1);
29
+ });
30
+
31
+ it('computes area of a right triangle', () => {
32
+ const triangle = [
33
+ { x: 0, y: 0 },
34
+ { x: 4, y: 0 },
35
+ { x: 0, y: 3 },
36
+ ];
37
+ assert.strictEqual(computePolygonArea(triangle), 6);
38
+ });
39
+
40
+ it('computes area of a rectangle', () => {
41
+ const rect = [
42
+ { x: 0, y: 0 },
43
+ { x: 5, y: 0 },
44
+ { x: 5, y: 3 },
45
+ { x: 0, y: 3 },
46
+ ];
47
+ assert.strictEqual(computePolygonArea(rect), 15);
48
+ });
49
+
50
+ it('returns positive area regardless of winding direction', () => {
51
+ // Counter-clockwise
52
+ const ccw = [
53
+ { x: 0, y: 0 },
54
+ { x: 0, y: 2 },
55
+ { x: 2, y: 2 },
56
+ { x: 2, y: 0 },
57
+ ];
58
+ assert.strictEqual(computePolygonArea(ccw), 4);
59
+ });
60
+
61
+ it('computes area of an irregular polygon', () => {
62
+ // L-shaped polygon (6 vertices)
63
+ const lShape = [
64
+ { x: 0, y: 0 },
65
+ { x: 3, y: 0 },
66
+ { x: 3, y: 1 },
67
+ { x: 1, y: 1 },
68
+ { x: 1, y: 2 },
69
+ { x: 0, y: 2 },
70
+ ];
71
+ // Area: 3*1 + 1*1 = 4
72
+ assert.strictEqual(computePolygonArea(lShape), 4);
73
+ });
74
+ });
75
+
76
+ describe('computePolygonPerimeter', () => {
77
+ it('returns 0 for fewer than 2 points', () => {
78
+ assert.strictEqual(computePolygonPerimeter([]), 0);
79
+ assert.strictEqual(computePolygonPerimeter([{ x: 0, y: 0 }]), 0);
80
+ });
81
+
82
+ it('computes perimeter of a unit square', () => {
83
+ const square = [
84
+ { x: 0, y: 0 },
85
+ { x: 1, y: 0 },
86
+ { x: 1, y: 1 },
87
+ { x: 0, y: 1 },
88
+ ];
89
+ assert.strictEqual(computePolygonPerimeter(square), 4);
90
+ });
91
+
92
+ it('computes perimeter of a 3-4-5 right triangle', () => {
93
+ const triangle = [
94
+ { x: 0, y: 0 },
95
+ { x: 4, y: 0 },
96
+ { x: 0, y: 3 },
97
+ ];
98
+ assert.strictEqual(computePolygonPerimeter(triangle), 12);
99
+ });
100
+
101
+ it('computes perimeter of a rectangle', () => {
102
+ const rect = [
103
+ { x: 0, y: 0 },
104
+ { x: 5, y: 0 },
105
+ { x: 5, y: 3 },
106
+ { x: 0, y: 3 },
107
+ ];
108
+ assert.strictEqual(computePolygonPerimeter(rect), 16);
109
+ });
110
+ });
111
+
112
+ describe('computePolygonCentroid', () => {
113
+ it('returns origin for empty polygon', () => {
114
+ const c = computePolygonCentroid([]);
115
+ assert.strictEqual(c.x, 0);
116
+ assert.strictEqual(c.y, 0);
117
+ });
118
+
119
+ it('returns the point for a single point', () => {
120
+ const c = computePolygonCentroid([{ x: 3, y: 7 }]);
121
+ assert.strictEqual(c.x, 3);
122
+ assert.strictEqual(c.y, 7);
123
+ });
124
+
125
+ it('computes centroid of a unit square at origin', () => {
126
+ const square = [
127
+ { x: 0, y: 0 },
128
+ { x: 2, y: 0 },
129
+ { x: 2, y: 2 },
130
+ { x: 0, y: 2 },
131
+ ];
132
+ const c = computePolygonCentroid(square);
133
+ assert.strictEqual(c.x, 1);
134
+ assert.strictEqual(c.y, 1);
135
+ });
136
+
137
+ it('computes centroid of a triangle', () => {
138
+ const triangle = [
139
+ { x: 0, y: 0 },
140
+ { x: 6, y: 0 },
141
+ { x: 0, y: 6 },
142
+ ];
143
+ const c = computePolygonCentroid(triangle);
144
+ assert.strictEqual(c.x, 2);
145
+ assert.strictEqual(c.y, 2);
146
+ });
147
+ });
148
+
149
+ describe('formatArea', () => {
150
+ it('formats small areas in cm²', () => {
151
+ assert.strictEqual(formatArea(0.005), '50.0 cm²');
152
+ assert.strictEqual(formatArea(0.001), '10.0 cm²');
153
+ });
154
+
155
+ it('formats medium areas in m²', () => {
156
+ assert.strictEqual(formatArea(1), '1.00 m²');
157
+ assert.strictEqual(formatArea(25.5), '25.50 m²');
158
+ assert.strictEqual(formatArea(100), '100.00 m²');
159
+ });
160
+
161
+ it('formats large areas in hectares', () => {
162
+ assert.strictEqual(formatArea(10000), '1.00 ha');
163
+ assert.strictEqual(formatArea(50000), '5.00 ha');
164
+ });
165
+ });
@@ -0,0 +1,72 @@
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
+ * Polygon area and perimeter computation utilities for 2D annotations.
7
+ * Uses the shoelace formula for area calculation.
8
+ */
9
+
10
+ interface Point2D {
11
+ x: number;
12
+ y: number;
13
+ }
14
+
15
+ /**
16
+ * Compute the signed area of a simple polygon using the shoelace formula.
17
+ * Returns absolute value (always positive).
18
+ */
19
+ export function computePolygonArea(points: Point2D[]): number {
20
+ if (points.length < 3) return 0;
21
+ let area = 0;
22
+ const n = points.length;
23
+ for (let i = 0; i < n; i++) {
24
+ const j = (i + 1) % n;
25
+ area += points[i].x * points[j].y;
26
+ area -= points[j].x * points[i].y;
27
+ }
28
+ return Math.abs(area) / 2;
29
+ }
30
+
31
+ /**
32
+ * Compute the perimeter of a closed polygon.
33
+ */
34
+ export function computePolygonPerimeter(points: Point2D[]): number {
35
+ if (points.length < 2) return 0;
36
+ let perimeter = 0;
37
+ const n = points.length;
38
+ for (let i = 0; i < n; i++) {
39
+ const j = (i + 1) % n;
40
+ const dx = points[j].x - points[i].x;
41
+ const dy = points[j].y - points[i].y;
42
+ perimeter += Math.sqrt(dx * dx + dy * dy);
43
+ }
44
+ return perimeter;
45
+ }
46
+
47
+ /**
48
+ * Compute the centroid (geometric center) of a polygon.
49
+ */
50
+ export function computePolygonCentroid(points: Point2D[]): Point2D {
51
+ if (points.length === 0) return { x: 0, y: 0 };
52
+ let cx = 0;
53
+ let cy = 0;
54
+ for (const p of points) {
55
+ cx += p.x;
56
+ cy += p.y;
57
+ }
58
+ return { x: cx / points.length, y: cy / points.length };
59
+ }
60
+
61
+ /**
62
+ * Format an area value for display with appropriate units.
63
+ */
64
+ export function formatArea(squareMeters: number): string {
65
+ if (squareMeters < 0.01) {
66
+ return `${(squareMeters * 10000).toFixed(1)} cm²`;
67
+ } else if (squareMeters < 10000) {
68
+ return `${squareMeters.toFixed(2)} m²`;
69
+ } else {
70
+ return `${(squareMeters / 10000).toFixed(2)} ha`;
71
+ }
72
+ }
@@ -0,0 +1,18 @@
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
+ * Format a distance in meters to a human-readable string with appropriate units
7
+ */
8
+ export function formatDistance(meters: number): string {
9
+ if (meters < 0.01) {
10
+ return `${(meters * 1000).toFixed(1)} mm`;
11
+ } else if (meters < 1) {
12
+ return `${(meters * 100).toFixed(1)} cm`;
13
+ } else if (meters < 1000) {
14
+ return `${meters.toFixed(3)} m`;
15
+ } else {
16
+ return `${(meters / 1000).toFixed(2)} km`;
17
+ }
18
+ }
@@ -0,0 +1,14 @@
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
+ * Shared constants for section tool components
7
+ */
8
+
9
+ // Axis display info for semantic names
10
+ export const AXIS_INFO = {
11
+ down: { label: 'Down', description: 'Horizontal cut (floor plan view)', icon: '\u2193' },
12
+ front: { label: 'Front', description: 'Vertical cut (elevation view)', icon: '\u2192' },
13
+ side: { label: 'Side', description: 'Vertical cut (side elevation)', icon: '\u2299' },
14
+ } as const;
@@ -0,0 +1,166 @@
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
+ * Animation loop hook for the 3D viewport
7
+ * Handles requestAnimationFrame loop, camera update, ViewCube sync
8
+ */
9
+
10
+ import { useEffect, type MutableRefObject, type RefObject } from 'react';
11
+ import type { Renderer } from '@ifc-lite/renderer';
12
+ import type { SectionPlane } from '@/store';
13
+
14
+ export interface UseAnimationLoopParams {
15
+ canvasRef: RefObject<HTMLCanvasElement | null>;
16
+ rendererRef: MutableRefObject<Renderer | null>;
17
+ isInitialized: boolean;
18
+ animationFrameRef: MutableRefObject<number | null>;
19
+ lastFrameTimeRef: MutableRefObject<number>;
20
+ mouseIsDraggingRef: MutableRefObject<boolean>;
21
+ activeToolRef: MutableRefObject<string>;
22
+ hiddenEntitiesRef: MutableRefObject<Set<number>>;
23
+ isolatedEntitiesRef: MutableRefObject<Set<number> | null>;
24
+ selectedEntityIdRef: MutableRefObject<number | null>;
25
+ selectedModelIndexRef: MutableRefObject<number | undefined>;
26
+ clearColorRef: MutableRefObject<[number, number, number, number]>;
27
+ sectionPlaneRef: MutableRefObject<SectionPlane>;
28
+ sectionRangeRef: MutableRefObject<{ min: number; max: number } | null>;
29
+ lastCameraStateRef: MutableRefObject<{
30
+ position: { x: number; y: number; z: number };
31
+ rotation: { azimuth: number; elevation: number };
32
+ distance: number;
33
+ canvasWidth: number;
34
+ canvasHeight: number;
35
+ } | null>;
36
+ updateCameraRotationRealtime: (rotation: { azimuth: number; elevation: number }) => void;
37
+ calculateScale: () => void;
38
+ updateMeasurementScreenCoords: (projector: (worldPos: { x: number; y: number; z: number }) => { x: number; y: number } | null) => void;
39
+ hasPendingMeasurements: () => boolean;
40
+ }
41
+
42
+ export function useAnimationLoop(params: UseAnimationLoopParams): void {
43
+ const {
44
+ canvasRef,
45
+ rendererRef,
46
+ isInitialized,
47
+ animationFrameRef,
48
+ lastFrameTimeRef,
49
+ mouseIsDraggingRef,
50
+ activeToolRef,
51
+ hiddenEntitiesRef,
52
+ isolatedEntitiesRef,
53
+ selectedEntityIdRef,
54
+ selectedModelIndexRef,
55
+ clearColorRef,
56
+ sectionPlaneRef,
57
+ sectionRangeRef,
58
+ lastCameraStateRef,
59
+ updateCameraRotationRealtime,
60
+ calculateScale,
61
+ updateMeasurementScreenCoords,
62
+ hasPendingMeasurements,
63
+ } = params;
64
+
65
+ useEffect(() => {
66
+ const renderer = rendererRef.current;
67
+ const canvas = canvasRef.current;
68
+ if (!renderer || !canvas || !isInitialized) return;
69
+
70
+ const camera = renderer.getCamera();
71
+ let aborted = false;
72
+
73
+ // Animation loop - update ViewCube in real-time
74
+ let lastRotationUpdate = 0;
75
+ let lastScaleUpdate = 0;
76
+ const animate = (currentTime: number) => {
77
+ if (aborted) return;
78
+
79
+ const deltaTime = currentTime - lastFrameTimeRef.current;
80
+ lastFrameTimeRef.current = currentTime;
81
+
82
+ const isAnimating = camera.update(deltaTime);
83
+ if (isAnimating) {
84
+ renderer.render({
85
+ hiddenIds: hiddenEntitiesRef.current,
86
+ isolatedIds: isolatedEntitiesRef.current,
87
+ selectedId: selectedEntityIdRef.current,
88
+ selectedModelIndex: selectedModelIndexRef.current,
89
+ clearColor: clearColorRef.current,
90
+ sectionPlane: activeToolRef.current === 'section' ? {
91
+ ...sectionPlaneRef.current,
92
+ min: sectionRangeRef.current?.min,
93
+ max: sectionRangeRef.current?.max,
94
+ } : undefined,
95
+ });
96
+ // Update ViewCube during camera animation (e.g., preset view transitions)
97
+ updateCameraRotationRealtime(camera.getRotation());
98
+ calculateScale();
99
+ } else if (!mouseIsDraggingRef.current && currentTime - lastRotationUpdate > 500) {
100
+ // Update camera rotation for ViewCube when not dragging (throttled to every 500ms when idle)
101
+ updateCameraRotationRealtime(camera.getRotation());
102
+ lastRotationUpdate = currentTime;
103
+ }
104
+
105
+ // Update scale bar (throttled to every 500ms - scale rarely needs frequent updates)
106
+ if (currentTime - lastScaleUpdate > 500) {
107
+ calculateScale();
108
+ lastScaleUpdate = currentTime;
109
+ }
110
+
111
+ // Update measurement screen coordinates only when:
112
+ // 1. Measure tool is active (not in other modes)
113
+ // 2. Measurements exist
114
+ // 3. Camera actually changed
115
+ // This prevents unnecessary store updates and re-renders when not measuring
116
+ if (activeToolRef.current === 'measure') {
117
+ if (hasPendingMeasurements()) {
118
+ const cameraPos = camera.getPosition();
119
+ const cameraRot = camera.getRotation();
120
+ const cameraDist = camera.getDistance();
121
+ const currentCameraState = {
122
+ position: cameraPos,
123
+ rotation: cameraRot,
124
+ distance: cameraDist,
125
+ canvasWidth: canvas.width,
126
+ canvasHeight: canvas.height,
127
+ };
128
+
129
+ // Check if camera state changed
130
+ const lastState = lastCameraStateRef.current;
131
+ const cameraChanged =
132
+ !lastState ||
133
+ lastState.position.x !== currentCameraState.position.x ||
134
+ lastState.position.y !== currentCameraState.position.y ||
135
+ lastState.position.z !== currentCameraState.position.z ||
136
+ lastState.rotation.azimuth !== currentCameraState.rotation.azimuth ||
137
+ lastState.rotation.elevation !== currentCameraState.rotation.elevation ||
138
+ lastState.distance !== currentCameraState.distance ||
139
+ lastState.canvasWidth !== currentCameraState.canvasWidth ||
140
+ lastState.canvasHeight !== currentCameraState.canvasHeight;
141
+
142
+ if (cameraChanged) {
143
+ lastCameraStateRef.current = currentCameraState;
144
+ updateMeasurementScreenCoords((worldPos) => {
145
+ return camera.projectToScreen(worldPos, canvas.width, canvas.height);
146
+ });
147
+ }
148
+ }
149
+ }
150
+
151
+ animationFrameRef.current = requestAnimationFrame(animate);
152
+ };
153
+ lastFrameTimeRef.current = performance.now();
154
+ animationFrameRef.current = requestAnimationFrame(animate);
155
+
156
+ return () => {
157
+ aborted = true;
158
+ if (animationFrameRef.current !== null) {
159
+ cancelAnimationFrame(animationFrameRef.current);
160
+ animationFrameRef.current = null;
161
+ }
162
+ };
163
+ }, [isInitialized]);
164
+ }
165
+
166
+ export default useAnimationLoop;