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>
This commit is contained in:
2026-06-02 13:57:50 +02:00
parent b2fab0c374
commit 9aba24978b
16 changed files with 572 additions and 77 deletions
+50
View File
@@ -0,0 +1,50 @@
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;
}