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:
+69
-21
@@ -1,26 +1,67 @@
|
||||
import React, { useState } from 'react';
|
||||
import { num } from '../api/useXplane.js';
|
||||
import MapView from './MapView.jsx';
|
||||
import Nearest from './Nearest.jsx';
|
||||
import FplPage from './FplPage.jsx';
|
||||
|
||||
const arr = (v, i = 0, d = 0) => (Array.isArray(v) ? num(v[i], d) : num(v, d));
|
||||
const KG_PER_GAL = 2.72; // avgas
|
||||
const navF = (v) => (num(v) / 100).toFixed(2);
|
||||
const comF = (v) => (num(v) / 100).toFixed(3);
|
||||
|
||||
// Active flight-plan leg: distance / desired track / ETE to the active waypoint
|
||||
// (great-circle from the aircraft), for the MFD nav data bar. Mirrors the PFD's
|
||||
// activeNav so the two displays agree.
|
||||
const R_NM = 3440.065, D2R = Math.PI / 180, R2D = 180 / Math.PI;
|
||||
function legNav(V, fp) {
|
||||
const wps = fp?.waypoints || [];
|
||||
const ai = Math.max(1, Math.min(wps.length - 1, fp?.activeLeg ?? 1));
|
||||
const wp = wps[ai];
|
||||
const lat = num(V.lat), lon = num(V.lon);
|
||||
if (!wp || (!lat && !lon)) return null;
|
||||
const dLat = (wp.lat - lat) * D2R, dLon = (wp.lon - lon) * D2R;
|
||||
const a = Math.sin(dLat / 2) ** 2 + Math.cos(lat * D2R) * Math.cos(wp.lat * D2R) * Math.sin(dLon / 2) ** 2;
|
||||
const dist = 2 * R_NM * Math.asin(Math.min(1, Math.sqrt(a)));
|
||||
const y = Math.sin(dLon) * Math.cos(wp.lat * D2R);
|
||||
const x = Math.cos(lat * D2R) * Math.sin(wp.lat * D2R) - Math.sin(lat * D2R) * Math.cos(wp.lat * D2R) * Math.cos(dLon);
|
||||
const dtk = (Math.atan2(y, x) * R2D + 360) % 360;
|
||||
const gs = num(V.groundspeed) * 1.94384;
|
||||
return { id: wp.id, dist, dtk, ete: gs > 20 ? (dist / gs) * 3600 : null };
|
||||
}
|
||||
const fmtEte = (s) => {
|
||||
if (s == null) return '__:__';
|
||||
const m = Math.floor(s / 60), ss = Math.round(s % 60);
|
||||
return m < 60 ? `${m}:${String(ss).padStart(2, '0')}` : `${Math.floor(m / 60)}+${String(m % 60).padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
// G1000 MFD — full-width NAV/COM bar on top, the engine instrument strip (EIS)
|
||||
// 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.
|
||||
export default function MFD({ values: V, flightPlan, fp, mapMode }) {
|
||||
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 }) {
|
||||
const [rangeNm, setRangeNm] = useState(8);
|
||||
const idx = Math.max(0, MFD_PAGES.findIndex((p) => p.id === page));
|
||||
return (
|
||||
<div className="mfd-g1000">
|
||||
<MfdTopBar V={V} />
|
||||
<MfdTopBar V={V} fp={flightPlan} />
|
||||
<div className="mfd-body">
|
||||
<EisStrip V={V} />
|
||||
<div className="mfd-map">
|
||||
<MapView values={V} flightPlan={flightPlan} fp={fp} hud={false}
|
||||
mapMode={mapMode} dcltr={mapMode?.dcltr || 0} onView={({ rangeNm }) => setRangeNm(rangeNm)} />
|
||||
<MapChrome V={V} rangeNm={rangeNm} />
|
||||
{/* MapView stays mounted (keeps tiles warm) but is hidden under NRST */}
|
||||
<div style={{ position: 'absolute', inset: 0, visibility: page === 'map' ? 'visible' : 'hidden' }}>
|
||||
<MapView values={V} flightPlan={flightPlan} fp={fp} hud={false}
|
||||
mapMode={mapMode} dcltr={mapMode?.dcltr || 0} rangeNm={num(V.uiMapRange) || undefined}
|
||||
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-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)">
|
||||
<span>{MFD_PAGES[idx].name}</span>
|
||||
{MFD_PAGES.map((p, i) => <em key={p.id} className={i === idx ? 'on' : ''} />)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -28,9 +69,11 @@ export default function MFD({ values: V, flightPlan, fp, mapMode }) {
|
||||
}
|
||||
|
||||
/* ---------------- top NAV/COM bar ---------------- */
|
||||
function MfdTopBar({ V }) {
|
||||
function MfdTopBar({ V, fp }) {
|
||||
const gs = Math.round(num(V.groundspeed) * 1.94384);
|
||||
const trk = String(Math.round(num(V.track)) % 360).padStart(3, '0');
|
||||
const leg = legNav(V, fp);
|
||||
const dtk = leg ? `${String(Math.round(leg.dtk) % 360).padStart(3, '0')}°` : '___°';
|
||||
const swap = (x, y) => <text x={x} y={y} fill="#0ff" fontSize="16" textAnchor="middle">⇔</text>;
|
||||
return (
|
||||
<svg className="mfd-topbar" viewBox="0 0 1000 70" preserveAspectRatio="none" fontFamily="monospace">
|
||||
@@ -51,10 +94,22 @@ function MfdTopBar({ V }) {
|
||||
<text x="350" y="27" fill="#e040fb" fontSize="15" fontWeight="bold">{gs}</text>
|
||||
<text x="378" y="27" fill="#0c9" fontSize="11">KT</text>
|
||||
<text x="410" y="27" fill="#fff" fontSize="13">DTK</text>
|
||||
<text x="448" y="27" fill="#e040fb" fontSize="15" fontWeight="bold">{dtk}</text>
|
||||
<text x="520" y="27" fill="#fff" fontSize="13">TRK</text>
|
||||
<text x="560" y="27" fill="#e040fb" fontSize="15" fontWeight="bold">{trk}°</text>
|
||||
<text x="610" y="27" fill="#fff" fontSize="13">ETE</text>
|
||||
<text x="480" y="58" fill="#0ff" fontSize="15" textAnchor="middle">NAV – DEFAULT NAV</text>
|
||||
<text x="648" y="27" fill="#fff" fontSize="15">{fmtEte(leg?.ete)}</text>
|
||||
{/* active leg (centre): → waypoint + distance, or no-flight-plan note */}
|
||||
{leg ? (
|
||||
<g>
|
||||
<text x="412" y="58" fill="#e040fb" fontSize="16">→</text>
|
||||
<text x="432" y="58" fill="#fff" fontSize="16" fontWeight="bold">{leg.id}</text>
|
||||
<text x="520" y="58" fill="#0ff" fontSize="15">{leg.dist.toFixed(1)}</text>
|
||||
<text x="566" y="58" fill="#0c9" fontSize="11">NM</text>
|
||||
</g>
|
||||
) : (
|
||||
<text x="480" y="58" fill="#777" fontSize="14" textAnchor="middle">NO ACTIVE WAYPOINT</text>
|
||||
)}
|
||||
{/* COM1 / COM2 */}
|
||||
<text x="690" y="27" fill="#0f0" fontSize="17">{comF(V.com1)}</text>
|
||||
{swap(818, 27)}
|
||||
@@ -191,24 +246,17 @@ function niceRange(nm) { let r = NICE[0]; for (const s of NICE) if (nm >= s) r =
|
||||
function MapChrome({ V, rangeNm }) {
|
||||
const gs = Math.round(num(V.groundspeed) * 1.94384);
|
||||
const rng = niceRange(rangeNm);
|
||||
const cx = 160, cy = 160, r = 150;
|
||||
const ticks = [];
|
||||
for (let d = 0; d < 360; d += 10) {
|
||||
const a = ((d - 90) * Math.PI) / 180;
|
||||
const big = d % 30 === 0;
|
||||
const r2 = r - (big ? 12 : 7);
|
||||
ticks.push(<line key={d} x1={cx + r * Math.cos(a)} y1={cy + r * Math.sin(a)} x2={cx + r2 * Math.cos(a)} y2={cy + r2 * Math.sin(a)} stroke="#cfd6dd" strokeWidth={big ? 2 : 1} />);
|
||||
if (big) {
|
||||
const lbl = d === 0 ? 'N' : d === 90 ? 'E' : d === 180 ? 'S' : d === 270 ? 'W' : d / 10;
|
||||
ticks.push(<text key={'l' + d} x={cx + (r - 26) * Math.cos(a)} y={cy + (r - 26) * Math.sin(a) + 5} fill="#fff" fontSize="15" textAnchor="middle" fontFamily="monospace">{lbl}</text>);
|
||||
}
|
||||
}
|
||||
const wd = ((Math.round(num(V.windDir)) % 360) + 360) % 360, ws = Math.round(num(V.windSpd));
|
||||
return (
|
||||
<div className="map-chrome">
|
||||
<svg className="map-rose" viewBox="0 0 320 320">{ticks}</svg>
|
||||
{/* the compass rose now lives in MapView, anchored to the aircraft */}
|
||||
<div className="mc-tr"><b>{gs} KT</b><span>NORTH UP</span></div>
|
||||
<div className="mc-wind">
|
||||
{ws >= 1
|
||||
? (<><span className="mc-windarr" style={{ transform: `rotate(${wd + 180}deg)` }}>↑</span><span>{String(wd).padStart(3, '0')}° {ws}<i>kt</i></span></>)
|
||||
: <span>CALM</span>}
|
||||
</div>
|
||||
<div className="mc-range">{rng} NM</div>
|
||||
<div className="mc-mode">NAV <em className="on" /><em /><em /><em /><em /></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user