FERS 1.0.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
ScaleManager.tsx
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2025-present FERS Contributors (see AUTHORS.md).
3
4import { useThree, useFrame } from '@react-three/fiber';
5import { type MapControls as MapControlsImpl } from 'three-stdlib';
6import * as THREE from 'three';
7import React from 'react';
8
9interface ScaleManagerProps {
10 controlsRef: React.RefObject<MapControlsImpl | null>;
11 labelRef: React.RefObject<HTMLDivElement | null>;
12 barRef: React.RefObject<HTMLDivElement | null>;
13}
14
15/**
16 * ScaleManager is a logic-only component that lives inside the Canvas.
17 * It calculates the screen-to-world ratio and updates the external DOM elements
18 * directly to ensure high performance without React renders.
19 */
20export function ScaleManager({
21 controlsRef,
22 labelRef,
23 barRef,
24}: ScaleManagerProps) {
25 const { camera, gl } = useThree();
26
27 // Target pixel width for the scale bar
28 const targetWidthPx = 140;
29
30 useFrame(() => {
31 if (
32 !controlsRef.current ||
33 !labelRef.current ||
34 !barRef.current ||
35 !(camera instanceof THREE.PerspectiveCamera)
36 ) {
37 return;
38 }
39
40 const controls = controlsRef.current;
41
42 // 1. Calculate distance from camera to the orbit target
43 const distance = camera.position.distanceTo(controls.target);
44
45 // 2. Calculate visible height at that distance (Vertical FOV)
46 const vFOV = (camera.fov * Math.PI) / 180;
47 const visibleHeightAtTarget = 2 * Math.tan(vFOV / 2) * distance;
48
49 // 3. Calculate meters per pixel
50 const canvasHeight = gl.domElement.clientHeight;
51 const unitsPerPixel = visibleHeightAtTarget / canvasHeight;
52
53 // 4. Calculate raw world units for our target pixel width
54 const rawUnits = unitsPerPixel * targetWidthPx;
55
56 // 5. Snap to "nice" numbers (1, 2, 5, 10...)
57 const magnitude = Math.pow(10, Math.floor(Math.log10(rawUnits)));
58 const residual = rawUnits / magnitude;
59
60 let niceStep = 1;
61 if (residual > 5) niceStep = 10;
62 else if (residual > 2) niceStep = 5;
63 else if (residual > 1) niceStep = 2;
64
65 const niceUnits = niceStep * magnitude;
66
67 // 6. Calculate exact pixel width for this nice unit amount
68 const finalPixelWidth = niceUnits / unitsPerPixel;
69
70 // 7. Format Label
71 let labelText = '';
72 if (niceUnits >= 1000) {
73 labelText = `${(niceUnits / 1000).toFixed(0)} km`;
74 } else {
75 labelText = `${niceUnits.toFixed(0)} m`;
76 }
77
78 // 8. Direct DOM updates
79 if (barRef.current.style.width !== `${finalPixelWidth}px`) {
80 barRef.current.style.width = `${finalPixelWidth}px`;
81 }
82 if (labelRef.current.innerText !== labelText) {
83 labelRef.current.innerText = labelText;
84 }
85 });
86
87 return null;
88}