G1000: two-way sim sync, more PFD/MFD fidelity, authentic dialogs
Sync (FlyWithLua companions in plugins/ + server/fmssync.js): - FMS flight-plan two-way sync (App <-> in-sim FMS) via fms-sync.lua - G1000 UI-state publish (page/range/inset) via ui-sync.lua + CDI source, baro, map-range follow - Terrain awareness: elevation grid probe (terrain-probe.lua) -> red/yellow MFD overlay vs aircraft altitude PFD: - AFCS mode annunciation bar from autopilot _status datarefs - CDI source GPS/VLOC colouring, BRG1/BRG2 pointers + DME windows, marker beacons - magenta speed/altitude trend vectors, selected-altitude alerting - time-based (frame-rate-independent) smoothing for attitude/heading/tapes MFD: - nav data bar (DTK/ETE/active leg), airways overlay from earth_awy.dat, compass rose anchored to the ownship Dialogs (NEAREST/FLIGHTPLAN/DIRECT-TO/PROCEDURES): - flat, square, embedded G1000 look (no shadow/rounded/transparency) - compact lower-right placement, no close X (softkey toggles), single window - NEAREST 2-line entries (ILS/VFR, COM freq, runway length), PROC action menu Service worker: network-first HTML so reloads pick up new builds (cache v2). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { num } from '../api/useXplane.js';
|
||||
import { num, systemAlerts } from '../api/useXplane.js';
|
||||
|
||||
// The physical GDU bezel of X-Plane's "XPLANE 1000" (its G1000 clone):
|
||||
// title bar, knob columns, the 12 softkeys along the bottom — and, on the MFD,
|
||||
@@ -26,25 +26,30 @@ const PFD_MENU = {
|
||||
// page; TOPO/TERRAIN/OSM switch the base map; BACK returns. (OSM is our tuned
|
||||
// extra layer in an otherwise-empty slot.)
|
||||
const MFD_MENU = {
|
||||
root: ['SYSTEM', 'MAP', '', '', '', '', '', '', '', 'DCLTR', '', ''],
|
||||
root: ['ENGINE', 'MAP', '', '', '', '', '', '', '', 'DCLTR', '', ''],
|
||||
mapopt: ['TRAFFIC', 'PROFILE', 'TOPO', 'TERRAIN', 'AIRWAYS', '', 'NEXRAD', 'OSM', '', '', 'BACK', ''],
|
||||
system: ['DEC FUEL', 'INC FUEL', 'RST FUEL', '', '', '', '', '', '', '', 'BACK', ''],
|
||||
engine: ['DEC FUEL', 'INC FUEL', 'RST FUEL', '', '', '', '', '', '', '', 'BACK', ''],
|
||||
};
|
||||
|
||||
// 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, knobMode = 'arrows', children }) {
|
||||
export default function Bezel({ variant = 'pfd', xp, svt3d, onToggleSvt, inset, onSetInset, insetMode, onInsetMode, nrst, onToggleNrst, tmr, onToggleTmr, dme, onToggleDme, alerts, onToggleAlerts, onDirect, onProc, onFpl, onFms, 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
|
||||
const [squawk, setSquawk] = useState(''); // XPDR code being typed
|
||||
const [obs, setObs] = useState(false); // OBS (suspend) mode
|
||||
|
||||
const menu = variant === 'mfd' ? MFD_MENU : PFD_MENU;
|
||||
const keys = menu[page] || menu.root;
|
||||
let keys = menu[page] || menu.root;
|
||||
// OBS appears in the PFD root only when a flight-plan leg is active (like the real unit)
|
||||
const hasLeg = (xp?.flightPlan?.waypoints?.length || 0) >= 2;
|
||||
if (variant !== 'mfd' && page === 'root' && hasLeg) { keys = keys.slice(); keys[4] = 'OBS'; }
|
||||
const setBase = (b) => onMapMode && onMapMode((m) => ({ ...m, base: m.base === b ? 'dark' : b }));
|
||||
const xpdrMode = num(xp?.values?.xpdrMode);
|
||||
const setMode = (m) => xp && xp.setDataref('xpdrMode', m);
|
||||
const hasAlerts = systemAlerts(xp?.values).length > 0; // lights the CAUTION key
|
||||
|
||||
const typeDigit = (d) => {
|
||||
const next = (squawk + d).slice(-4);
|
||||
@@ -59,12 +64,13 @@ export default function Bezel({ variant = 'pfd', xp, svt3d, onToggleSvt, inset,
|
||||
fire(`softkey${i + 1}`); // mirror to the in-sim G1000
|
||||
if (variant === 'mfd') {
|
||||
if (label === 'MAP') setPage('mapopt');
|
||||
else if (label === 'SYSTEM') setPage('system');
|
||||
else if (label === 'ENGINE') setPage('engine');
|
||||
else if (label === 'BACK') setPage('root');
|
||||
else if (label === 'TOPO') setBase('topo');
|
||||
else if (label === 'TERRAIN') setBase('terrain');
|
||||
else if (label === 'OSM') setBase('osm');
|
||||
else if (label === 'DCLTR') onMapMode && onMapMode((m) => ({ ...m, dcltr: m.dcltr ? 0 : 1 }));
|
||||
else if (label === 'AIRWAYS') onMapMode && onMapMode((m) => ({ ...m, airways: !m.airways }));
|
||||
} else {
|
||||
if (label === 'PFD') setPage('pfd');
|
||||
else if (label === 'BACK') setPage(page === 'xpdrcode' ? 'xpdr' : 'root');
|
||||
@@ -79,6 +85,9 @@ export default function Bezel({ variant = 'pfd', xp, svt3d, onToggleSvt, inset,
|
||||
else if (label === 'TERRAIN') onInsetMode && onInsetMode((m) => ({ ...m, base: 'terrain' }));
|
||||
else if (label === 'NRST') onToggleNrst && onToggleNrst();
|
||||
else if (label === 'TMR/REF') onToggleTmr && onToggleTmr();
|
||||
else if (label === 'DME') onToggleDme && onToggleDme();
|
||||
else if (label === 'OBS') setObs((v) => !v); // suspend / OBS mode (also fires the sim softkey above)
|
||||
else if (label === 'CAUTION') onToggleAlerts && onToggleAlerts();
|
||||
else if (label === 'XPDR') setPage('xpdr');
|
||||
else if (label === 'STBY') setMode(1);
|
||||
else if (label === 'ON') setMode(2);
|
||||
@@ -94,8 +103,9 @@ export default function Bezel({ variant = 'pfd', xp, svt3d, onToggleSvt, inset,
|
||||
const isOn = (label) => {
|
||||
if (variant === 'mfd') return (label === 'TOPO' && mapMode?.base === 'topo')
|
||||
|| (label === 'TERRAIN' && mapMode?.base === 'terrain') || (label === 'OSM' && mapMode?.base === 'osm')
|
||||
|| (label === 'DCLTR' && mapMode?.dcltr > 0);
|
||||
|| (label === 'DCLTR' && mapMode?.dcltr > 0) || (label === 'AIRWAYS' && mapMode?.airways);
|
||||
return (label === 'SYN TERR' && svt3d) || (label === 'INSET' && inset) || (label === 'NRST' && nrst) || (label === 'TMR/REF' && tmr)
|
||||
|| (label === 'DME' && dme) || (label === 'OBS' && obs) || (label === 'CAUTION' && (alerts || hasAlerts))
|
||||
|| (label === 'STBY' && xpdrMode === 1) || (label === 'ON' && xpdrMode === 2) || (label === 'ALT' && xpdrMode === 3)
|
||||
|| (page === 'inset' && label === 'TOPO' && insetMode?.base === 'topo')
|
||||
|| (page === 'inset' && label === 'TERRAIN' && insetMode?.base === 'terrain')
|
||||
@@ -106,7 +116,8 @@ export default function Bezel({ variant = 'pfd', xp, svt3d, onToggleSvt, inset,
|
||||
<div className="bezel">
|
||||
<div className="bezel-knobs left">
|
||||
<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" />
|
||||
outer={['nav_outer_up', 'nav_outer_down']} inner={['nav_inner_up', 'nav_inner_down']} push="nav12"
|
||||
swap={() => xp && xp.command('nav1Swap')} />
|
||||
<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} />}
|
||||
@@ -116,36 +127,43 @@ export default function Bezel({ variant = 'pfd', xp, svt3d, onToggleSvt, inset,
|
||||
|
||||
<div className="bezel-core">
|
||||
<div className="bezel-title">XPLANE 1000</div>
|
||||
<div className="bezel-screen">{children}</div>
|
||||
{page === 'xpdrcode' && (
|
||||
<div className="squawk-entry">SQUAWK <b>{squawk.padEnd(4, '_')}</b></div>
|
||||
)}
|
||||
<div className="bezel-screen">
|
||||
<div className="screen-content">{children}</div>
|
||||
{page === 'xpdrcode' && (
|
||||
<div className="squawk-entry">SQUAWK <b>{squawk.padEnd(4, '_')}</b></div>
|
||||
)}
|
||||
{/* softkey LABELS on the display (lowest line), like the real G1000 */}
|
||||
<div className="sk-labels">
|
||||
{keys.map((s, i) => (
|
||||
<span key={i} className={`skl ${s ? '' : 'empty'} ${s === 'CAUTION' ? 'caution' : ''} ${isOn(s) ? 'on' : ''}`}>{s}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* physical bezel keys — blank, aligned under the on-screen labels */}
|
||||
<div className="softkeys">
|
||||
{keys.map((s, i) => (
|
||||
<button
|
||||
key={i}
|
||||
disabled={!s}
|
||||
onClick={() => onSoftkey(i, s)}
|
||||
className={`softkey ${s ? '' : 'empty'} ${s === 'CAUTION' ? 'caution' : ''} ${isOn(s) ? 'on' : ''}`}
|
||||
>{s}</button>
|
||||
<button key={i} disabled={!s} onClick={() => onSoftkey(i, s)}
|
||||
className={`softkey ${s ? '' : 'empty'}`} aria-label={s || undefined} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bezel-knobs right">
|
||||
<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" />
|
||||
outer={['com_outer_up', 'com_outer_down']} inner={['com_inner_up', 'com_inner_down']} push="com12"
|
||||
swap={() => xp && xp.command('com1Swap')} emerg />
|
||||
<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} mode={knobMode}
|
||||
outer={['range_up', 'range_down']} push="pan_push" pan />
|
||||
<div className="bezel-grid">
|
||||
<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="fpl" onClick={onFpl}>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} mode={knobMode}
|
||||
outer={['fms_outer_up', 'fms_outer_down']} inner={['fms_inner_up', 'fms_inner_down']} push="cursor" />
|
||||
outer={['fms_outer_up', 'fms_outer_down']} inner={['fms_inner_up', 'fms_inner_down']} push="cursor"
|
||||
onTurn={onFms} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -186,12 +204,14 @@ 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, mode = 'arrows' }) {
|
||||
function Knob({ label, sub, outer, inner, push, big, joy, pan, fire, mode = 'arrows', swap, emerg, onTurn }) {
|
||||
// turn the outer ring: fire the sim command AND notify (e.g. cycle MFD page)
|
||||
const outerStep = (dir) => { if (!outer) return; fire(dir > 0 ? outer[0] : outer[1]); if (onTurn) onTurn(dir); };
|
||||
const onWheel = (e) => {
|
||||
if (!outer) return;
|
||||
e.preventDefault();
|
||||
const set = (e.shiftKey && inner) ? inner : outer;
|
||||
fire(e.deltaY < 0 ? set[0] : set[1]);
|
||||
if (e.shiftKey && inner) fire(e.deltaY < 0 ? inner[0] : inner[1]);
|
||||
else outerStep(e.deltaY < 0 ? 1 : -1);
|
||||
};
|
||||
const zoneClick = (e) => {
|
||||
const r = e.currentTarget.getBoundingClientRect();
|
||||
@@ -199,19 +219,21 @@ function Knob({ label, sub, outer, inner, push, big, joy, pan, fire, mode = 'arr
|
||||
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]); }
|
||||
if (Math.abs(dy) >= Math.abs(dx)) outerStep(dy < 0 ? 1 : -1);
|
||||
else if (inner) fire(dx > 0 ? inner[0] : inner[1]);
|
||||
else if (outer) fire(dx > 0 ? outer[0] : outer[1]);
|
||||
else outerStep(dx > 0 ? 1 : -1);
|
||||
};
|
||||
const zones = mode === 'zones';
|
||||
return (
|
||||
<div className={`knob-wrap ${big ? 'big' : ''}`}>
|
||||
{swap && <button className="knob-swap" onClick={swap} title="Aktiv ↔ Standby">⇆</button>}
|
||||
{emerg && <span className="knob-emerg">EMERG</span>}
|
||||
<span className="knob-lbl">{label}</span>
|
||||
<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>}
|
||||
{!zones && outer && <button className="knob-arrow left" onClick={() => outerStep(-1)}>‹</button>}
|
||||
<button
|
||||
className={`knob outer ${joy ? 'joy' : ''}`}
|
||||
onWheel={onWheel}
|
||||
@@ -219,9 +241,15 @@ function Knob({ label, sub, outer, inner, push, big, joy, pan, fire, mode = 'arr
|
||||
title={zones ? `${outer ? 'oben/unten' : ''}${inner ? ' · links/rechts (fein)' : ''}${push ? ' · Mitte: PUSH' : ''}` : (push ? 'PUSH' : '')}
|
||||
>
|
||||
<span className="knob inner" />
|
||||
{joy && <div className="joy-cross">+</div>}
|
||||
{joy && (<>
|
||||
<span className="rng-ring" />
|
||||
<span className="rng-arc l">↶</span>
|
||||
<span className="rng-arc r">↷</span>
|
||||
<span className="rng-sign m">–</span>
|
||||
<span className="rng-sign p">+</span>
|
||||
</>)}
|
||||
</button>
|
||||
{!zones && outer && <button className="knob-arrow right" onClick={() => fire(outer[0])}>›</button>}
|
||||
{!zones && outer && <button className="knob-arrow right" onClick={() => outerStep(1)}>›</button>}
|
||||
{!zones && inner && <button className="knob-arrow bottom" onClick={() => fire(inner[1])}>˅</button>}
|
||||
</div>
|
||||
{pan && (
|
||||
|
||||
Reference in New Issue
Block a user