1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2025-present FERS Contributors (see AUTHORS.md).
4import { useThree, useFrame } from '@react-three/fiber';
5import { type MapControls as MapControlsImpl } from 'three-stdlib';
6import * as THREE from 'three';
7import React from 'react';
9interface ScaleManagerProps {
10 controlsRef: React.RefObject<MapControlsImpl | null>;
11 labelRef: React.RefObject<HTMLDivElement | null>;
12 barRef: React.RefObject<HTMLDivElement | null>;
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.
20export function ScaleManager({
24}: ScaleManagerProps) {
25 const { camera, gl } = useThree();
27 // Target pixel width for the scale bar
28 const targetWidthPx = 140;
32 !controlsRef.current ||
35 !(camera instanceof THREE.PerspectiveCamera)
40 const controls = controlsRef.current;
42 // 1. Calculate distance from camera to the orbit target
43 const distance = camera.position.distanceTo(controls.target);
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;
49 // 3. Calculate meters per pixel
50 const canvasHeight = gl.domElement.clientHeight;
51 const unitsPerPixel = visibleHeightAtTarget / canvasHeight;
53 // 4. Calculate raw world units for our target pixel width
54 const rawUnits = unitsPerPixel * targetWidthPx;
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;
61 if (residual > 5) niceStep = 10;
62 else if (residual > 2) niceStep = 5;
63 else if (residual > 1) niceStep = 2;
65 const niceUnits = niceStep * magnitude;
67 // 6. Calculate exact pixel width for this nice unit amount
68 const finalPixelWidth = niceUnits / unitsPerPixel;
72 if (niceUnits >= 1000) {
73 labelText = `${(niceUnits / 1000).toFixed(0)} km`;
75 labelText = `${niceUnits.toFixed(0)} m`;
78 // 8. Direct DOM updates
79 if (barRef.current.style.width !== `${finalPixelWidth}px`) {
80 barRef.current.style.width = `${finalPixelWidth}px`;
82 if (labelRef.current.innerText !== labelText) {
83 labelRef.current.innerText = labelText;