Files
xplane-cockpit/web/src/api/ease.js
T
karim 9aba24978b Auto-install Lua, smooth all panels, airspace overlay + launcher region picker
FlyWithLua auto-install: bridge drops fms-sync/ui-sync/terrain-probe into
X-Plane's FlyWithLua Scripts dir on startup and self-updates (content-compare).
Graceful when no X-Plane / no FlyWithLua. /api/lua/install + status in health.
Desktop app bundles the scripts and passes LUA_SRC_DIR to the sidecar.

Smoothing: shared useEased/useEasedAngle hook (api/ease.js) with render-bail on
settle. VFR steam gauges now interpolate to 60fps instead of stepping at the
~10Hz value stream. MFD ownship no longer vibrates — position/heading eased in a
single rAF loop, follow-pan without animated-panTo pile-up (pauses on range zoom).

Airspace overlay: server/airspace.js loads per-region GeoJSON, classifies
(B/C/D/TMA/CTR/MOA/Restricted/Prohibited/Danger), bbox query, and downloads
regions on demand — FAA (US, key-free) and OpenAIP (Europe, user key). New
AIRSPACE softkey draws chart-coloured boundaries (B blue, C magenta, D dashed),
non-interactive so map-clicks still drop waypoints. Launcher gains a "Lufträume"
section to pick/download regions via the running bridge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 13:57:50 +02:00

51 lines
2.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useRef, useEffect } from 'react';
// Frame-rate-independent easing of a scalar toward a moving target (alpha from
// dt + a time constant). The rAF loop runs continuously so the value keeps
// gliding between the bridge's discrete value updates — turning a stuttery
// ~1020 Hz stream into a smooth 60 fps motion. To avoid needless React work it
// only re-renders the consumer while the value is actually moving: once settled,
// the functional setState returns the same number and React bails (Object.is).
export function useEased(target, tau = 0.08) {
const [, force] = useState(0);
const cur = useRef(target), tg = useRef(target);
tg.current = target;
useEffect(() => {
let raf, last = 0;
const loop = (now) => {
const dt = last ? Math.min(0.05, (now - last) / 1000) : 0.016; last = now;
const k = 1 - Math.exp(-dt / tau);
const next = cur.current + (tg.current - cur.current) * k;
const settled = Math.abs(tg.current - next) < 0.02;
const val = settled ? tg.current : next;
if (val !== cur.current) { cur.current = val; force((n) => (n + 1) & 0xffff); }
raf = requestAnimationFrame(loop);
};
raf = requestAnimationFrame(loop);
return () => cancelAnimationFrame(raf);
}, [tau]);
return cur.current;
}
// As above but eases along the shortest arc across the 0↔360 wrap (headings).
export function useEasedAngle(target, tau = 0.08) {
const [, force] = useState(0);
const cur = useRef(target), tg = useRef(target);
tg.current = target;
useEffect(() => {
let raf, last = 0;
const loop = (now) => {
const dt = last ? Math.min(0.05, (now - last) / 1000) : 0.016; last = now;
const k = 1 - Math.exp(-dt / tau);
const d = ((tg.current - cur.current + 540) % 360) - 180; // shortest signed arc
const next = Math.abs(d) < 0.05 ? tg.current : cur.current + d * k;
const val = ((next % 360) + 360) % 360;
if (val !== cur.current) { cur.current = val; force((n) => (n + 1) & 0xffff); }
raf = requestAnimationFrame(loop);
};
raf = requestAnimationFrame(loop);
return () => cancelAnimationFrame(raf);
}, [tau]);
return cur.current;
}