Manual audit B/C/D: VNAV control, Direct-To descent, NRST actions
D — NRST page: each nearest entry can now load its tower/CTAF into COM1 standby (→COM) or a VOR into NAV1 standby (→NAV), and fly Direct-To it (D→). Nearest now takes xp; com/nav standby datarefs made writable. C — Direct-To with VNAV descent: the DTO dialog's ALT (MSL/AGL) and OFFSET fields are now editable; entering an altitude makes the target a designated VNAV fix (alt+dsgn) and arms VNAV, so the descent profile + PFD chevrons compute. B — VNAV control: shared vnav config (enabled/fpa/offsetNm) threaded to PFD + FplPage. The CURRENT VNV PROFILE panel gains ENBL/CNCL VNV, FPA ±, along-track ATK ± and VNV-D→ keys; the profile + PFD chevrons honour the chosen FPA/offset and hide when cancelled. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+9
-5
@@ -70,6 +70,10 @@ export default function App() {
|
||||
const fpl = win === 'fpl', dto = win === 'dto', proc = win === 'proc';
|
||||
// MFD map mode (base layer + overlays), switched via the Map-Opt softkeys.
|
||||
const [mapMode, setMapMode] = useState({ base: 'topo' });
|
||||
// VNAV profile control (FPL VNAV keys + Direct-To descent): enabled gates the
|
||||
// profile/chevrons, fpa is the descent angle (°), offsetNm levels off that far
|
||||
// before the waypoint. See FplPage CURRENT VNV PROFILE + PFD chevrons.
|
||||
const [vnavCfg, setVnavCfg] = useState({ enabled: true, fpa: 3, offsetNm: 0 });
|
||||
// Altimeter barometric units (false = inHg, true = hectopascal) — PFD ALT UNIT softkey.
|
||||
const [baroHpa, setBaroHpa] = useState(false);
|
||||
// Barometric minimums (set in TMR/REF) — shown on the PFD altimeter as BARO MIN.
|
||||
@@ -93,11 +97,11 @@ export default function App() {
|
||||
// the display's lower-right (like the real unit), not over the whole app.
|
||||
const dialogs = (
|
||||
<>
|
||||
{dto && <DirectTo xp={xp} onClose={() => setWin(null)} />}
|
||||
{dto && <DirectTo xp={xp} onClose={() => setWin(null)} vnav={vnavCfg} onVnav={setVnavCfg} />}
|
||||
{proc && <Proc xp={xp} onClose={() => setWin(null)} />}
|
||||
{fpl && (
|
||||
<div className="gwin-backdrop" onClick={() => setWin(null)}>
|
||||
<div onClick={(e) => e.stopPropagation()}><FplPage xp={xp} onClose={() => setWin(null)} /></div>
|
||||
<div onClick={(e) => e.stopPropagation()}><FplPage xp={xp} onClose={() => setWin(null)} vnav={vnavCfg} onVnav={setVnavCfg} /></div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -140,16 +144,16 @@ export default function App() {
|
||||
tmr={tmr} onToggleTmr={() => toggleWin('tmr')} dme={dme} onToggleDme={() => toggleWin('dme')}
|
||||
alerts={alerts} onToggleAlerts={() => toggleWin('alerts')} onProc={() => toggleWin('proc')} onFpl={() => toggleWin('fpl')} onClr={() => setWin(null)}
|
||||
altHpa={baroHpa} onAltUnit={setBaroHpa} obs={obs} onObs={() => setObs((v) => !v)}>
|
||||
<PFD values={xp.values} command={xp.command} connected={xp.xpConnected} svt={svt3d} inset={inset} insetMode={insetMode} nrst={nrst} onCloseNrst={() => setWin(null)}
|
||||
<PFD xp={xp} values={xp.values} command={xp.command} connected={xp.xpConnected} svt={svt3d} inset={inset} insetMode={insetMode} nrst={nrst} onCloseNrst={() => setWin(null)}
|
||||
tmr={tmr} onCloseTmr={() => setWin(null)} dme={dme} onCloseDme={() => setWin(null)}
|
||||
alerts={alerts} onCloseAlerts={() => setWin(null)} baroHpa={baroHpa} obs={obs}
|
||||
minimums={minimums} onMinimums={setMinimums} flightPlan={xp.flightPlan} fp={xp.fp} />
|
||||
minimums={minimums} onMinimums={setMinimums} flightPlan={xp.flightPlan} fp={xp.fp} vnav={vnavCfg} />
|
||||
{dialogs}
|
||||
</Bezel>
|
||||
)}
|
||||
{tab === 'mfd' && (
|
||||
<Bezel variant="mfd" xp={xp} knobMode={knobMode} mapMode={mapMode} onMapMode={setMapMode} onDirect={() => toggleWin('dto')} onProc={() => toggleWin('proc')} onFms={cycleMfd} onFpl={() => setMfdPage('fpl')} onClr={() => setWin(null)}>
|
||||
<MFD values={xp.values} flightPlan={xp.flightPlan} fp={xp.fp} mapMode={mapMode} page={mfdPage} onCycle={cycleMfd} xp={xp} />
|
||||
<MFD values={xp.values} flightPlan={xp.flightPlan} fp={xp.fp} mapMode={mapMode} page={mfdPage} onCycle={cycleMfd} xp={xp} vnav={vnavCfg} onVnav={setVnavCfg} />
|
||||
{dialogs}
|
||||
</Bezel>
|
||||
)}
|
||||
|
||||
@@ -17,11 +17,14 @@ function distBrg(la1, lo1, la2, lo2) {
|
||||
return { dist, brg };
|
||||
}
|
||||
|
||||
export default function DirectTo({ xp, onClose }) {
|
||||
export default function DirectTo({ xp, onClose, vnav, onVnav }) {
|
||||
const { values, fp, command } = xp;
|
||||
const cfg = vnav || { enabled: true, fpa: 3, offsetNm: 0 };
|
||||
const [entry, setEntry] = useState('');
|
||||
const [hits, setHits] = useState([]);
|
||||
const [sel, setSel] = useState(null); // chosen { id, lat, lon, type }
|
||||
const [altFt, setAltFt] = useState(''); // optional VNAV target altitude
|
||||
const [agl, setAgl] = useState(false); // MSL vs AGL reference (for airports)
|
||||
const inputRef = useRef(null);
|
||||
|
||||
useEffect(() => { inputRef.current?.focus(); }, []);
|
||||
@@ -40,11 +43,17 @@ export default function DirectTo({ xp, onClose }) {
|
||||
|
||||
const activate = () => {
|
||||
if (!sel) return;
|
||||
// Optional VNAV descent: a target altitude makes the Direct-To waypoint a
|
||||
// designated VNAV fix, so the CURRENT VNV PROFILE + PFD chevrons compute the
|
||||
// descent (FPA/offset from the shared VNAV config). AGL adds field elevation.
|
||||
const a = parseInt(altFt, 10);
|
||||
const tgtAlt = isFinite(a) && a > 0 ? (agl ? a + (num(sel.elev) || 0) : a) : null;
|
||||
fp.set({ name: 'ACTIVE', waypoints: [
|
||||
{ id: 'PPOS', lat, lon, type: 'USR' },
|
||||
{ id: sel.id, lat: sel.lat, lon: sel.lon, type: sel.type || 'WPT' },
|
||||
{ id: sel.id, lat: sel.lat, lon: sel.lon, type: sel.type || 'WPT', ...(tgtAlt ? { alt: tgtAlt, dsgn: true } : {}) },
|
||||
] });
|
||||
command('direct'); // mirror to the in-sim G1000
|
||||
if (tgtAlt && onVnav) onVnav((c) => ({ ...c, enabled: true })); // arm VNAV for the descent
|
||||
onClose();
|
||||
};
|
||||
|
||||
@@ -74,11 +83,23 @@ export default function DirectTo({ xp, onClose }) {
|
||||
</div>
|
||||
)}
|
||||
<div className="dto-grid">
|
||||
<b>ALT</b><span>_____FT</span><b>OFFSET</b><span>+0NM</span>
|
||||
<b>ALT</b>
|
||||
<span className="dto-altedit">
|
||||
<input className="dto-alt" value={altFt} inputMode="numeric"
|
||||
onChange={(e) => setAltFt(e.target.value.replace(/[^0-9]/g, '').slice(0, 5))}
|
||||
placeholder="_____" />FT
|
||||
<button className="dto-unit" onClick={() => setAgl((g) => !g)}>{agl ? 'AGL' : 'MSL'}</button>
|
||||
</span>
|
||||
<b>OFFSET</b>
|
||||
<span className="dto-off">
|
||||
<button onClick={() => onVnav && onVnav((c) => ({ ...c, offsetNm: Math.max(0, (c.offsetNm || 0) - 1) }))}>−</button>
|
||||
{cfg.offsetNm || 0}NM
|
||||
<button onClick={() => onVnav && onVnav((c) => ({ ...c, offsetNm: Math.min(20, (c.offsetNm || 0) + 1) }))}>+</button>
|
||||
</span>
|
||||
<b>BRG</b><span>{preview ? `${String(Math.round(preview.brg)).padStart(3, '0')}°` : '___°'}</span>
|
||||
<b>FPA</b><span>{(cfg.fpa || 3).toFixed(1)}°</span>
|
||||
<b>DIS</b><span>{preview ? `${preview.dist.toFixed(1)}NM` : '__._NM'}</span>
|
||||
<b>CRS</b><span>{preview ? `${String(Math.round(preview.brg)).padStart(3, '0')}°` : '___°'}</span>
|
||||
<span /><span />
|
||||
</div>
|
||||
<div className="dto-foot">
|
||||
<button className="dto-act" disabled={!sel} onClick={activate}>ACTIVATE</button>
|
||||
|
||||
@@ -18,7 +18,8 @@ function brng(a, b) {
|
||||
}
|
||||
const fmtHrs = (h) => { const m = Math.round(h * 60); return `${Math.floor(m / 60)}:${String(m % 60).padStart(2, '0')}`; };
|
||||
|
||||
export default function FplPage({ xp, full = false, onClose }) {
|
||||
export default function FplPage({ xp, full = false, onClose, vnav: vnavCfg, onVnav }) {
|
||||
const cfg = vnavCfg || { enabled: true, fpa: 3, offsetNm: 0 };
|
||||
const { flightPlan, fp, values, exportMsg } = xp;
|
||||
const wps = flightPlan.waypoints || [];
|
||||
const active = Math.max(1, Math.min(wps.length - 1, flightPlan.activeLeg ?? 1));
|
||||
@@ -69,22 +70,24 @@ export default function FplPage({ xp, full = false, onClose }) {
|
||||
// from the path, time-to-top-of-descent.
|
||||
const alt = num(values.altitude);
|
||||
let vnav = null;
|
||||
if (gs > 40) {
|
||||
if (cfg.enabled && gs > 40) {
|
||||
const tan = Math.tan((cfg.fpa * Math.PI) / 180);
|
||||
const off = Math.max(0, cfg.offsetNm || 0);
|
||||
let c = 0, pl = num(values.lat), po = num(values.lon);
|
||||
for (let i = Math.max(1, active); i < wps.length; i++) {
|
||||
c += distNm({ lat: pl, lon: po }, wps[i]); pl = wps[i].lat; po = wps[i].lon;
|
||||
const t = num(wps[i].alt);
|
||||
if (t > 0 && t < alt - 50 && (wps[i].dsgn ?? true)) {
|
||||
const tan = Math.tan((3 * Math.PI) / 180);
|
||||
const d = Math.max(0, c - off); // distance to the level-off point (offset before wpt)
|
||||
const vsTgt = -gs * tan * 101.27;
|
||||
const vsReq = c > 0 ? (t - alt) / (c / gs * 60) : 0;
|
||||
const vDev = alt - (t + c * 6076.12 * tan);
|
||||
const todNm = c - (alt - t) / (6076.12 * tan);
|
||||
const vsReq = d > 0 ? (t - alt) / (d / gs * 60) : 0;
|
||||
const vDev = alt - (t + d * 6076.12 * tan);
|
||||
const todNm = d - (alt - t) / (6076.12 * tan);
|
||||
// Before TOD: time until the descent path is intercepted. After TOD (already
|
||||
// descending): time to Bottom of Descent = reaching the target waypoint.
|
||||
// descending): time to Bottom of Descent = reaching the level-off point.
|
||||
const todSec = todNm > 0 ? (todNm / gs) * 3600 : 0;
|
||||
const bodSec = todNm > 0 ? 0 : (c / gs) * 3600;
|
||||
vnav = { wptId: wps[i].id, tgtAlt: t, vsTgt, vsReq, vDev, fpa: 3.0, todSec, bodSec };
|
||||
const bodSec = todNm > 0 ? 0 : (d / gs) * 3600;
|
||||
vnav = { wptId: wps[i].id, tgtAlt: t, vsTgt, vsReq, vDev, fpa: cfg.fpa, todSec, bodSec, off };
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -134,7 +137,19 @@ export default function FplPage({ xp, full = false, onClose }) {
|
||||
: <><b>TIME TO BOD</b><span>{fmtSec(vnav.bodSec)}</span></>}
|
||||
<b>V DEV</b><span>{vnav.vDev >= 0 ? '+' : ''}{Math.round(vnav.vDev)}<u>FT</u></span>
|
||||
</div>
|
||||
) : <div className="fpl-vnav-none">— no active VNAV profile —</div>}
|
||||
) : <div className="fpl-vnav-none">{cfg.enabled ? '— no active VNAV profile —' : '— VNAV cancelled —'}</div>}
|
||||
{onVnav && (
|
||||
<div className="fpl-vnav-keys">
|
||||
<button className={cfg.enabled ? 'on' : ''} onClick={() => onVnav((c) => ({ ...c, enabled: !c.enabled }))}>{cfg.enabled ? 'CNCL VNV' : 'ENBL VNV'}</button>
|
||||
<button onClick={() => onVnav((c) => ({ ...c, fpa: +Math.max(2, c.fpa - 0.5).toFixed(1) }))}>FPA−</button>
|
||||
<span className="vk-val">{cfg.fpa.toFixed(1)}°</span>
|
||||
<button onClick={() => onVnav((c) => ({ ...c, fpa: +Math.min(6, c.fpa + 0.5).toFixed(1) }))}>FPA+</button>
|
||||
<button onClick={() => onVnav((c) => ({ ...c, offsetNm: Math.max(0, (c.offsetNm || 0) - 1) }))}>ATK−</button>
|
||||
<span className="vk-val">{cfg.offsetNm || 0}<u>NM</u></span>
|
||||
<button onClick={() => onVnav((c) => ({ ...c, offsetNm: Math.min(20, (c.offsetNm || 0) + 1) }))}>ATK+</button>
|
||||
<button className="vnvd" onClick={() => onVnav((c) => ({ ...c, enabled: true }))}>VNV-D→</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="fpl-entry">
|
||||
|
||||
@@ -38,7 +38,7 @@ const fmtEte = (s) => {
|
||||
// down the left as real bar gauges, and the moving map (X-Plane nav data) with
|
||||
// G1000 chrome (compass rose, range, NORTH UP, mode) filling the rest.
|
||||
const MFD_PAGES = [{ id: 'map', name: 'MAP' }, { id: 'fpl', name: 'FPL' }, { id: 'nrst', name: 'NRST' }];
|
||||
export default function MFD({ values: V, flightPlan, fp, mapMode, page = 'map', onCycle, xp }) {
|
||||
export default function MFD({ values: V, flightPlan, fp, mapMode, page = 'map', onCycle, xp, vnav, onVnav }) {
|
||||
const [rangeNm, setRangeNm] = useState(8);
|
||||
const idx = Math.max(0, MFD_PAGES.findIndex((p) => p.id === page));
|
||||
return (
|
||||
@@ -54,8 +54,8 @@ export default function MFD({ values: V, flightPlan, fp, mapMode, page = 'map',
|
||||
terrain={xp?.terrain} rose onView={({ rangeNm }) => setRangeNm(rangeNm)} />
|
||||
<MapChrome V={V} rangeNm={rangeNm} />
|
||||
</div>
|
||||
{page === 'nrst' && <Nearest values={V} full />}
|
||||
{page === 'fpl' && xp && <FplPage xp={xp} full />}
|
||||
{page === 'nrst' && <Nearest xp={xp} full />}
|
||||
{page === 'fpl' && xp && <FplPage xp={xp} full vnav={vnav} onVnav={onVnav} />}
|
||||
{/* page-group indicator (bottom-right), like the real G1000 — selected
|
||||
by the FMS knob; tappable as a touch fallback. */}
|
||||
<button className="mfd-pageind" onClick={() => onCycle && onCycle(1)} title="Seite (FMS-Knopf)">
|
||||
|
||||
@@ -5,7 +5,8 @@ import { num } from '../api/useXplane.js';
|
||||
// press the NRST softkey; it lists the closest airports / VORs / NDBs to the
|
||||
// aircraft with bearing + distance, straight from X-Plane's own nav data
|
||||
// (/api/nav/nearest). Tabs switch the feature type, like turning the FMS knob
|
||||
// through the NRST page group on the real unit.
|
||||
// through the NRST page group on the real unit. Each entry can be acted on:
|
||||
// load its frequency into the COM/NAV standby, or fly Direct-To it (manual p.23).
|
||||
const TABS = [
|
||||
{ id: 'apt', label: 'APT' },
|
||||
{ id: 'vor', label: 'VOR' },
|
||||
@@ -19,10 +20,11 @@ const freqStr = (f, type) => {
|
||||
return type === 'vor' ? (n / 100).toFixed(2) : String(n);
|
||||
};
|
||||
|
||||
export default function Nearest({ values, onClose, full = false }) {
|
||||
export default function Nearest({ xp, values: valuesProp, onClose, full = false }) {
|
||||
const values = xp?.values || valuesProp || {};
|
||||
const [type, setType] = useState('apt');
|
||||
const [rows, setRows] = useState([]);
|
||||
const lastRef = useRef(null);
|
||||
const [msg, setMsg] = useState('');
|
||||
const lat = num(values.lat), lon = num(values.lon);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -36,11 +38,27 @@ export default function Nearest({ values, onClose, full = false }) {
|
||||
} catch { /* aborted / offline */ }
|
||||
};
|
||||
load();
|
||||
// Refresh as the aircraft moves (cheap server scan).
|
||||
timer = setInterval(load, 5000);
|
||||
return () => { abort.abort(); clearInterval(timer); };
|
||||
}, [type, Math.round(lat * 50), Math.round(lon * 50)]); // re-key on ~1nm moves
|
||||
|
||||
const flash = (t) => { setMsg(t); setTimeout(() => setMsg(''), 1800); };
|
||||
|
||||
// Fly Direct-To: a magenta leg from present position to the chosen feature.
|
||||
const directTo = (f) => {
|
||||
if (!xp || !isFinite(f.lat)) return;
|
||||
xp.fp.set({ name: 'ACTIVE', waypoints: [
|
||||
{ id: 'PPOS', lat, lon, type: 'USR' },
|
||||
{ id: f.id, lat: f.lat, lon: f.lon, type: f.type || (type === 'apt' ? 'APT' : type === 'vor' ? 'VOR' : 'NDB') },
|
||||
] });
|
||||
xp.command('direct');
|
||||
flash(`Direct-To ${f.id}`);
|
||||
onClose && onClose();
|
||||
};
|
||||
// Load a frequency into COM1 / NAV1 standby (freq units are 10 kHz, e.g. 11990).
|
||||
const toCom = (f) => { if (xp && f.com) { xp.setDataref('com1Sb', Math.round(f.com.freq * 100)); flash(`COM1 STBY ${f.com.freq.toFixed(3)}`); } };
|
||||
const toNav = (f) => { if (xp && f.freq) { xp.setDataref('nav1Sb', Math.round(num(f.freq))); flash(`NAV1 STBY ${(num(f.freq) / 100).toFixed(2)}`); } };
|
||||
|
||||
return (
|
||||
<div className={`nrst-window ${full ? 'full' : ''}`}>
|
||||
<div className="nrst-head">
|
||||
@@ -68,6 +86,12 @@ export default function Nearest({ values, onClose, full = false }) {
|
||||
<span className="apt-rwlbl">RNWY</span>
|
||||
<span className="apt-rw">{f.rwyFt ? `${f.rwyFt}FT` : '—'}</span>
|
||||
</div>
|
||||
{xp && (
|
||||
<div className="nrst-acts">
|
||||
{f.com && <button className="nrst-act" onClick={() => toCom(f)} title="→ COM1 standby">→COM</button>}
|
||||
<button className="nrst-act dto" onClick={() => directTo(f)} title="Direct-To">D→</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
: rows.map((f, i) => (
|
||||
@@ -76,10 +100,16 @@ export default function Nearest({ values, onClose, full = false }) {
|
||||
<span className="c-brg">{String(num(f.brg)).padStart(3, '0')}°</span>
|
||||
<span className="c-dis">{num(f.dist).toFixed(1)}<u>nm</u></span>
|
||||
<span className="c-xtra">{freqStr(f.freq, type)}</span>
|
||||
{f.name && <span className="c-name">{f.name}</span>}
|
||||
{xp && (
|
||||
<span className="nrst-acts">
|
||||
{type === 'vor' && f.freq && <button className="nrst-act" onClick={() => toNav(f)} title="→ NAV1 standby">→NAV</button>}
|
||||
<button className="nrst-act dto" onClick={() => directTo(f)} title="Direct-To">D→</button>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{msg && <div className="nrst-msg">{msg}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -63,7 +63,10 @@ function fmtEte(s) {
|
||||
// target VS for a -3° flight path, required VS to make the restriction, vertical
|
||||
// deviation from that path, and time-to-top-of-descent. (Manual S.64 / S.107.)
|
||||
const VNAV_FPA = 3.0; // default flight-path angle (degrees)
|
||||
function vnavInfo(V, fp) {
|
||||
function vnavInfo(V, fp, cfg = { enabled: true, fpa: VNAV_FPA, offsetNm: 0 }) {
|
||||
if (!cfg.enabled) return null; // VNAV cancelled (CNCL VNV)
|
||||
const fpa = cfg.fpa || VNAV_FPA;
|
||||
const off = Math.max(0, cfg.offsetNm || 0);
|
||||
const wps = fp?.waypoints || [];
|
||||
const ai = Math.max(1, Math.min(wps.length - 1, fp?.activeLeg ?? 1));
|
||||
const alt = num(V.altitude);
|
||||
@@ -76,16 +79,17 @@ function vnavInfo(V, fp) {
|
||||
prevLat = wps[i].lat; prevLon = wps[i].lon;
|
||||
const tgt = num(wps[i].alt);
|
||||
if (tgt > 0 && tgt < alt - 50 && (wps[i].dsgn ?? true)) {
|
||||
const tan = Math.tan((VNAV_FPA * Math.PI) / 180);
|
||||
const tMin = (cum / gs) * 60;
|
||||
const tan = Math.tan((fpa * Math.PI) / 180);
|
||||
const d = Math.max(0, cum - off); // distance to level-off point
|
||||
const tMin = (d / gs) * 60;
|
||||
const vsReq = tMin > 0 ? (tgt - alt) / tMin : 0; // fpm to make the fix
|
||||
const vsTgt = -gs * tan * 101.27; // fpm for the FPA at this GS
|
||||
const desiredAltNow = tgt + cum * 6076.12 * tan; // path altitude at present position
|
||||
const desiredAltNow = tgt + d * 6076.12 * tan; // path altitude at present position
|
||||
const vDev = alt - desiredAltNow; // + = above path
|
||||
const descentNm = (alt - tgt) / (6076.12 * tan); // distance the descent itself takes
|
||||
const todNm = cum - descentNm; // distance ahead until TOD
|
||||
const todNm = d - descentNm; // distance ahead until TOD
|
||||
const todSec = todNm > 0 ? (todNm / gs) * 3600 : 0;
|
||||
return { wptId: wps[i].id, tgtAlt: tgt, dist: cum, vsReq, vsTgt, vDev, fpa: VNAV_FPA, todSec };
|
||||
return { wptId: wps[i].id, tgtAlt: tgt, dist: cum, vsReq, vsTgt, vDev, fpa, todSec };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -100,7 +104,7 @@ const SVT_BOX = { x: 0, y: 74, w: W, h: H - 74 };
|
||||
// The INSET moving map sits in the bottom-left corner (toggled by INSET softkey).
|
||||
const INSET_BOX = { x: 6, y: 556, w: 300, h: 172 };
|
||||
|
||||
export default function PFD({ values: V, command, connected = true, svt = true, inset = false, insetMode, nrst = false, onCloseNrst, tmr = false, onCloseTmr, dme = false, onCloseDme, alerts = false, onCloseAlerts, baroHpa = false, obs = false, minimums, onMinimums, flightPlan, fp }) {
|
||||
export default function PFD({ xp, values: V, command, connected = true, svt = true, inset = false, insetMode, nrst = false, onCloseNrst, tmr = false, onCloseTmr, dme = false, onCloseDme, alerts = false, onCloseAlerts, baroHpa = false, obs = false, minimums, onMinimums, flightPlan, fp, vnav: vnavCfg }) {
|
||||
const wrapRef = useRef(null);
|
||||
const svgRef = useRef(null);
|
||||
const [box, setBox] = useState(null);
|
||||
@@ -137,7 +141,7 @@ export default function PFD({ values: V, command, connected = true, svt = true,
|
||||
}, []);
|
||||
|
||||
const nav = activeNav(V, flightPlan);
|
||||
const vnav = vnavInfo(V, flightPlan);
|
||||
const vnav = vnavInfo(V, flightPlan, vnavCfg);
|
||||
// GPS phase annunciation: APR when an approach leg is active, TERM within 30 nm
|
||||
// of the destination, otherwise ENR (manual).
|
||||
const gpsPhase = (() => {
|
||||
@@ -203,7 +207,7 @@ export default function PFD({ values: V, command, connected = true, svt = true,
|
||||
</g>
|
||||
)}
|
||||
</svg>
|
||||
{nrst && <Nearest values={V} onClose={onCloseNrst} />}
|
||||
{nrst && <Nearest xp={xp} onClose={onCloseNrst} />}
|
||||
{tmr && <TimerRef values={V} onClose={onCloseTmr} minimums={minimums} onMinimums={onMinimums} />}
|
||||
{dme && <DmeWindow V={V} onClose={onCloseDme} />}
|
||||
{alerts && <AlertsWindow V={V} onClose={onCloseAlerts} />}
|
||||
|
||||
@@ -182,6 +182,13 @@ body {
|
||||
.apt-app.ils { color: #16d24a; }
|
||||
.apt-comlbl { color: #6f808d; font-size: 11px; }
|
||||
.apt-com { color: #fff; font-size: 13px; }
|
||||
/* NRST per-entry actions: load freq to standby, or fly Direct-To */
|
||||
.nrst-acts { display: flex; gap: 6px; margin-top: 2px; justify-content: flex-end; }
|
||||
.nrst-row .nrst-acts { display: inline-flex; margin-top: 0; margin-left: 6px; }
|
||||
.nrst-act { background: #11202a; border: 1px solid #2a4250; color: #7fd4ff; font: inherit; font-size: 10px; padding: 1px 6px; border-radius: 2px; cursor: pointer; letter-spacing: .5px; }
|
||||
.nrst-act:hover { background: #163243; }
|
||||
.nrst-act.dto { color: #e89bff; border-color: #5a3a66; }
|
||||
.nrst-msg { color: #16d24a; font-size: 11px; padding: 4px 8px; text-align: center; }
|
||||
.apt-rwlbl { color: #6f808d; font-size: 11px; }
|
||||
.apt-rw { color: #fff; font-size: 13px; text-align: right; }
|
||||
.nrst-head { display: flex; align-items: center; gap: 8px; padding: 5px 8px; background: #11161b; border-bottom: 1px solid #2c343c; }
|
||||
@@ -260,6 +267,20 @@ body {
|
||||
.fpl-vnav-grid span u { color: #6f808d; font-size: 9px; text-decoration: none; margin-left: 1px; }
|
||||
.fpl-vnav-grid .vwpt { color: #4fa8ff; }
|
||||
.fpl-vnav-none { color: #6f808d; font-size: 12px; }
|
||||
/* VNAV control keys (ENBL/CNCL VNV, FPA, along-track offset, VNV Direct-To) */
|
||||
.fpl-vnav-keys { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; margin-top: 7px; }
|
||||
.fpl-vnav-keys button { background: #11202a; border: 1px solid #2a4250; color: #7fd4ff; font: inherit; font-size: 11px; padding: 2px 7px; border-radius: 2px; cursor: pointer; letter-spacing: .5px; }
|
||||
.fpl-vnav-keys button:hover { background: #163243; }
|
||||
.fpl-vnav-keys button.on { background: #16d24a22; border-color: #16d24a; color: #16d24a; }
|
||||
.fpl-vnav-keys button.vnvd { color: #e89bff; border-color: #5a3a66; }
|
||||
.fpl-vnav-keys .vk-val { color: #fff; font-size: 13px; min-width: 36px; text-align: center; }
|
||||
.fpl-vnav-keys .vk-val u { color: #6f808d; font-size: 9px; text-decoration: none; }
|
||||
/* Direct-To VNAV editable fields */
|
||||
.dto-altedit { display: inline-flex; align-items: center; gap: 4px; color: #6f808d; font-size: 10px; }
|
||||
.dto-alt { width: 56px; background: #0a1016; border: 1px solid #2a4250; color: #36d2ff; font: inherit; font-size: 14px; text-align: right; padding: 1px 4px; border-radius: 2px; }
|
||||
.dto-unit { background: #11202a; border: 1px solid #2a4250; color: #7fd4ff; font: inherit; font-size: 10px; padding: 1px 5px; border-radius: 2px; cursor: pointer; }
|
||||
.dto-off { display: inline-flex; align-items: center; gap: 6px; color: #fff; font-size: 13px; }
|
||||
.dto-off button { background: #11202a; border: 1px solid #2a4250; color: #7fd4ff; font: inherit; width: 20px; height: 20px; border-radius: 2px; cursor: pointer; line-height: 1; }
|
||||
/* ORIG / DEST subtitle (PFD window) */
|
||||
.fpl-od { color: #36d2ff; text-align: center; font-family: 'Roboto Mono', monospace; font-size: 14px; padding: 3px 0; border-bottom: 1px solid #1c242c; letter-spacing: 1px; }
|
||||
/* compact window: DTK/DIS only (drop CUM/ALT), no editor — like the real FPL window */
|
||||
|
||||
Reference in New Issue
Block a user