PFD/cockpit polish + KAP140 autopilot + UI refinements

- PFD: full-screen 2D attitude, G1000 yellow+magenta chevron symbology, rAF
  60fps horizon smoothing, translucent tapes, slimmer softkey bar, header fixes
- Collapsible macOS-dark sidebar (Inter), VFR six-pack + engine cluster + tach
- KAP140 autopilot on the analog page; GMC-710 AFCS tab
- FMS rebuilt as an X-Plane-style CDU; PWA; settings panel (knob mode)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 17:20:16 +02:00
parent ebc33a78b7
commit 354ea5d44b
10 changed files with 253 additions and 82 deletions
+32 -19
View File
@@ -34,7 +34,7 @@ const MFD_MENU = {
// autopilot_state bitfield (best-effort; tweak per aircraft)
const AP_BITS = { fd: 1 << 0, hdg: 1 << 1, vs: 1 << 4, flc: 1 << 6, nav: 1 << 8, apr: 1 << 9, vnav: 1 << 11, altHold: 1 << 14, bc: 1 << 18 };
export default function Bezel({ variant = 'pfd', xp, svt3d, onToggleSvt, inset, onSetInset, insetMode, onInsetMode, nrst, onToggleNrst, tmr, onToggleTmr, onDirect, onProc, mapMode, onMapMode, children }) {
export default function Bezel({ variant = 'pfd', xp, svt3d, onToggleSvt, inset, onSetInset, insetMode, onInsetMode, nrst, onToggleNrst, tmr, onToggleTmr, onDirect, onProc, mapMode, onMapMode, knobMode = 'arrows', children }) {
const u = variant === 'mfd' ? 'mfd' : 'pfd'; // command prefix
const fire = (suffix) => xp && xp.command(`${u}_${suffix}`);
const [page, setPage] = useState('root'); // softkey menu page
@@ -105,12 +105,12 @@ export default function Bezel({ variant = 'pfd', xp, svt3d, onToggleSvt, inset,
return (
<div className="bezel">
<div className="bezel-knobs left">
<Knob label="NAV" sub="VOL · PUSH ID" fire={fire}
<Knob label="NAV" sub="VOL · PUSH ID" fire={fire} mode={knobMode}
outer={['nav_outer_up', 'nav_outer_down']} inner={['nav_inner_up', 'nav_inner_down']} push="nav12" />
<Knob label="HDG" sub="PUSH HDG SYNC" fire={fire}
<Knob label="HDG" sub="PUSH HDG SYNC" fire={fire} mode={knobMode}
outer={['hdg_up', 'hdg_down']} push="hdg_sync" />
{variant === 'mfd' && xp && <APController xp={xp} />}
<Knob label="ALT" sub="" big fire={fire}
<Knob label="ALT" sub="" big fire={fire} mode={knobMode}
outer={['alt_outer_up', 'alt_outer_down']} inner={['alt_inner_up', 'alt_inner_down']} />
</div>
@@ -133,18 +133,18 @@ export default function Bezel({ variant = 'pfd', xp, svt3d, onToggleSvt, inset,
</div>
<div className="bezel-knobs right">
<Knob label="COM" sub="VOL · PUSH SQ" fire={fire}
<Knob label="COM" sub="VOL · PUSH SQ" fire={fire} mode={knobMode}
outer={['com_outer_up', 'com_outer_down']} inner={['com_inner_up', 'com_inner_down']} push="com12" />
<Knob label="CRS / BARO" sub="PUSH CRS CTR" fire={fire}
<Knob label="CRS / BARO" sub="PUSH CRS CTR" fire={fire} mode={knobMode}
outer={['crs_up', 'crs_down']} inner={['baro_up', 'baro_down']} push="crs_sync" />
<Knob label="RANGE" sub="PUSH PAN" joy fire={fire}
<Knob label="RANGE" sub="PUSH PAN" joy fire={fire} mode={knobMode}
outer={['range_up', 'range_down']} push="pan_push" pan />
<div className="bezel-grid">
<BtnG fire={fire} cmd="direct" onClick={onDirect}>D</BtnG><BtnG fire={fire} cmd="menu">MENU</BtnG>
<BtnG fire={fire} cmd="fpl">FPL</BtnG><BtnG fire={fire} cmd="proc" onClick={onProc}>PROC</BtnG>
<BtnG fire={fire} cmd="clr">CLR</BtnG><BtnG fire={fire} cmd="ent">ENT</BtnG>
<BtnG fire={fire} mode={knobMode} cmd="direct" onClick={onDirect}>D</BtnG><BtnG fire={fire} mode={knobMode} cmd="menu">MENU</BtnG>
<BtnG fire={fire} mode={knobMode} cmd="fpl">FPL</BtnG><BtnG fire={fire} mode={knobMode} cmd="proc" onClick={onProc}>PROC</BtnG>
<BtnG fire={fire} mode={knobMode} cmd="clr">CLR</BtnG><BtnG fire={fire} mode={knobMode} cmd="ent">ENT</BtnG>
</div>
<Knob label="FMS" sub="PUSH CRSR" big fire={fire}
<Knob label="FMS" sub="PUSH CRSR" big fire={fire} mode={knobMode}
outer={['fms_outer_up', 'fms_outer_down']} inner={['fms_inner_up', 'fms_inner_down']} push="cursor" />
</div>
</div>
@@ -186,30 +186,43 @@ function APController({ xp }) {
// the mouse wheel; the inner ring via the top/bottom arrows (˄ ˅) and shift+wheel.
// Clicking the knob centre fires the push action (PUSH …). The RANGE knob also
// pans with a directional cross.
function Knob({ label, sub, outer, inner, push, big, joy, pan, fire }) {
function Knob({ label, sub, outer, inner, push, big, joy, pan, fire, mode = 'arrows' }) {
const onWheel = (e) => {
if (!outer) return;
e.preventDefault();
const set = (e.shiftKey && inner) ? inner : outer;
fire(e.deltaY < 0 ? set[0] : set[1]);
};
const zoneClick = (e) => {
const r = e.currentTarget.getBoundingClientRect();
const dx = e.clientX - (r.left + r.width / 2);
const dy = e.clientY - (r.top + r.height / 2);
const rel = Math.hypot(dx, dy) / (r.width / 2);
if (rel < 0.42 && push) { fire(push); return; } // centre → PUSH
if (Math.abs(dy) >= Math.abs(dx)) { if (outer) fire(dy < 0 ? outer[0] : outer[1]); }
else if (inner) fire(dx > 0 ? inner[0] : inner[1]);
else if (outer) fire(dx > 0 ? outer[0] : outer[1]);
};
const zones = mode === 'zones';
return (
<div className={`knob-wrap ${big ? 'big' : ''}`}>
<span className="knob-lbl">{label}</span>
<div className="knob-cluster">
{inner && <button className="knob-arrow top" onClick={() => fire(inner[0])}>˄</button>}
{outer && <button className="knob-arrow left" onClick={() => fire(outer[1])}></button>}
<div className={`knob-cluster ${zones ? 'zones' : ''}`}>
{/* arrows mode (touch-friendly): visible ˄‹›˅ buttons. zones mode: click
the knob face itself (top/bottom = outer, left/right = inner). */}
{!zones && inner && <button className="knob-arrow top" onClick={() => fire(inner[0])}>˄</button>}
{!zones && outer && <button className="knob-arrow left" onClick={() => fire(outer[1])}></button>}
<button
className={`knob outer ${joy ? 'joy' : ''}`}
onWheel={onWheel}
onClick={() => push && fire(push)}
title={push ? 'PUSH' : ''}
onClick={zones ? zoneClick : (() => push && fire(push))}
title={zones ? `${outer ? 'oben/unten' : ''}${inner ? ' · links/rechts (fein)' : ''}${push ? ' · Mitte: PUSH' : ''}` : (push ? 'PUSH' : '')}
>
<span className="knob inner" />
{joy && <div className="joy-cross"></div>}
</button>
{outer && <button className="knob-arrow right" onClick={() => fire(outer[0])}></button>}
{inner && <button className="knob-arrow bottom" onClick={() => fire(inner[1])}>˅</button>}
{!zones && outer && <button className="knob-arrow right" onClick={() => fire(outer[0])}></button>}
{!zones && inner && <button className="knob-arrow bottom" onClick={() => fire(inner[1])}>˅</button>}
</div>
{pan && (
<div className="pan-pad">