diff --git a/web/src/components/SVT.jsx b/web/src/components/SVT.jsx index 0108799..c643a9b 100644 --- a/web/src/components/SVT.jsx +++ b/web/src/components/SVT.jsx @@ -63,6 +63,26 @@ function runwayGeoJSON(list) { return { type: 'FeatureCollection', features: feats }; } +// Place the synthetic horizon exactly where the PFD attitude horizon sits, so +// the 3D terrain and the 2D attitude agree. The attitude horizon is at 28% of +// the SVT box at level flight and moves PITCH_PX (9 px) per degree within the +// 706-px-tall box; the canvas is scaled 1.5× about that 28% line (to cover the +// corners when banked). We invert MapLibre's perspective to find the camera +// pitch that lands the flat horizon there. With the default vertical FOV of +// 36.87°, the focal-length/height ratio is 0.5/tan(fov/2) = 1.5, independent of +// resolution. Screen offset of the horizon above centre = f·tan(90°−pitch). +const CANVAS_SCALE = 1.5; // matches .svt-canvas CSS transform scale +const FOV_FH = 1.5; // 0.5 / tan(fov/2), fov = 36.87° (MapLibre default) +const HORIZON0 = (270 - 74) / 706; // attitude horizon as a fraction of the SVT box (≈0.2776) +const PX_PER_DEG = 9 / 706; // PITCH_PX / box height — displayed horizon travel per ° +function cameraPitchForAircraft(aircraftPitchDeg) { + const dispFrac = HORIZON0 + PX_PER_DEG * aircraftPitchDeg; // where the horizon must appear + const rawFrac = HORIZON0 + (dispFrac - HORIZON0) / CANVAS_SCALE; // undo the 1.5× canvas scale + const t = (0.5 - rawFrac) / FOV_FH; // = tan(90°−pitch) + const pitch = 90 - (Math.atan(t) * 180) / Math.PI; + return Math.max(60, Math.min(85, pitch)); +} + export default function SVT({ values }) { const elRef = useRef(null); const mapRef = useRef(null); @@ -77,9 +97,9 @@ export default function SVT({ values }) { style: STYLE, center: [num(values.lon, -122.31), num(values.lat, 47.45)], zoom: 11.5, - pitch: 72, + pitch: cameraPitchForAircraft(num(values.pitch)), bearing: num(values.heading), - maxPitch: 76, // lower max pitch = nearer horizon = less distant terrain + maxPitch: 85, // horizon placement needs ~82° at level, more when pitched up pixelRatio: 1, // don't render at 2× on retina — big perf/bandwidth win renderWorldCopies: false, maxTileCacheSize: 40, @@ -154,7 +174,7 @@ export default function SVT({ values }) { map.jumpTo({ center: [num(v.lon, -122.31), num(v.lat, 47.45)], bearing: num(v.heading), - pitch: Math.max(58, Math.min(76, 72 + num(v.pitch))), + pitch: cameraPitchForAircraft(num(v.pitch)), zoom, }); updateTerrainAwareness(num(v.altitude, 5500));