diff --git a/web/src/App.jsx b/web/src/App.jsx index 411f82a..454d81d 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -74,6 +74,9 @@ export default function App() { // 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 }); + // Synthetic-vision display options (PFD submenu): PATHWAY draws the flight-plan + // route on the 3D terrain; APTSIGNS shows runway/airport labels. + const [svtOpts, setSvtOpts] = useState({ pathway: true, aptSigns: true }); // 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. @@ -139,6 +142,7 @@ export default function App() {
{tab === 'pfd' && ( setSvt3d((v) => !v)} + svtOpts={svtOpts} onSvtOpt={setSvtOpts} inset={inset} onSetInset={setInset} insetMode={insetMode} onInsetMode={setInsetMode} nrst={nrst} onToggleNrst={() => toggleWin('nrst')} onDirect={() => toggleWin('dto')} tmr={tmr} onToggleTmr={() => toggleWin('tmr')} dme={dme} onToggleDme={() => toggleWin('dme')} @@ -147,7 +151,7 @@ export default function App() { 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} vnav={vnavCfg} /> + minimums={minimums} onMinimums={setMinimums} flightPlan={xp.flightPlan} fp={xp.fp} vnav={vnavCfg} svtOpts={svtOpts} /> {dialogs} )} diff --git a/web/src/components/Bezel.jsx b/web/src/components/Bezel.jsx index 6188a7a..576a30c 100644 --- a/web/src/components/Bezel.jsx +++ b/web/src/components/Bezel.jsx @@ -34,10 +34,7 @@ const MFD_MENU = { }; const KG_PER_GAL = 2.72; // fuel totalizer steps in US gallons (matches the EIS readout) -// 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, dme, onToggleDme, alerts, onToggleAlerts, onDirect, onProc, onFpl, onClr, onFms, mapMode, onMapMode, altHpa, onAltUnit, obs, onObs, knobMode = 'arrows', children }) { +export default function Bezel({ variant = 'pfd', xp, svt3d, onToggleSvt, svtOpts, onSvtOpt, inset, onSetInset, insetMode, onInsetMode, nrst, onToggleNrst, tmr, onToggleTmr, dme, onToggleDme, alerts, onToggleAlerts, onDirect, onProc, onFpl, onClr, onFms, mapMode, onMapMode, altHpa, onAltUnit, obs, onObs, 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 @@ -83,6 +80,8 @@ export default function Bezel({ variant = 'pfd', xp, svt3d, onToggleSvt, inset, if (label === 'PFD') setPage('pfd'); else if (label === 'BACK') setPage({ xpdrcode: 'xpdr', altunit: 'pfd' }[page] || 'root'); else if (label === 'SYN TERR') onToggleSvt && onToggleSvt(); + else if (label === 'PATHWAY') onSvtOpt && onSvtOpt((o) => ({ ...o, pathway: !o.pathway })); // route on the 3D terrain + else if (label === 'APTSIGNS') onSvtOpt && onSvtOpt((o) => ({ ...o, aptSigns: !o.aptSigns })); // runway/airport labels in SVT else if (label === 'ALT UNIT') setPage('altunit'); else if (label === 'IN') { onAltUnit && onAltUnit(false); setPage('pfd'); } else if (label === 'HPA') { onAltUnit && onAltUnit(true); setPage('pfd'); } @@ -120,7 +119,8 @@ export default function Bezel({ variant = 'pfd', xp, svt3d, onToggleSvt, inset, || (label === 'TERRAIN' && mapMode?.terrain) || (label === 'OSM' && mapMode?.base === 'osm') || (label === 'DCLTR' && mapMode?.dcltr > 0) || (label === 'AIRWAYS' && mapMode?.airways) || (label === 'AIRSPACE' && mapMode?.airspace); - return (label === 'SYN TERR' && svt3d) || (label === 'INSET' && inset) || (label === 'NRST' && nrst) || (label === 'TMR/REF' && tmr) + return (label === 'SYN TERR' && svt3d) || (label === 'PATHWAY' && svtOpts?.pathway) || (label === 'APTSIGNS' && svtOpts?.aptSigns) + || (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) || (label === 'IN' && !altHpa) || (label === 'HPA' && altHpa) @@ -195,26 +195,30 @@ function BtnG({ fire, cmd, onClick, children }) { } // Autopilot mode controller (left bezel of the MFD). Buttons fire real X-Plane -// commands; active modes light up from autopilot_state / servos_on. +// commands; active modes light from the per-mode *_status datarefs (off/armed/ +// active) — the same reliable source as the PFD bar and the AutopilotPanel, not +// the autopilot_state bitfield (whose bit positions don't match X-Plane). function APController({ xp }) { - const st = num(xp.values.apState); - const on = (bit) => (st & bit) !== 0; - const eng = num(xp.values.apEngaged) > 0; + const V = xp.values; + const lit = (k) => num(V[k]) > 0; + const apMode = num(V.apMode); + const eng = num(V.apEngaged) > 0 || apMode >= 2; + const fdOn = apMode >= 1 || eng; const B = ({ label, cmd, active }) => ( ); return (
- - - - - - - - - + + + + + + + + +
diff --git a/web/src/components/PFD.jsx b/web/src/components/PFD.jsx index 904e013..0424121 100644 --- a/web/src/components/PFD.jsx +++ b/web/src/components/PFD.jsx @@ -104,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({ 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 }) { +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, svtOpts }) { const wrapRef = useRef(null); const svgRef = useRef(null); const [box, setBox] = useState(null); @@ -164,7 +164,7 @@ export default function PFD({ xp, values: V, command, connected = true, svt = tr
{svt && box && (
- }> + }>
)} {inset && insetBox && ( diff --git a/web/src/components/SVT.jsx b/web/src/components/SVT.jsx index c643a9b..561e0c0 100644 --- a/web/src/components/SVT.jsx +++ b/web/src/components/SVT.jsx @@ -83,11 +83,27 @@ function cameraPitchForAircraft(aircraftPitchDeg) { return Math.max(60, Math.min(85, pitch)); } -export default function SVT({ values }) { +// Flight-plan route as a GeoJSON LineString (drawn on the terrain for PATHWAY). +function routeGeo(plan) { + const wps = plan?.waypoints || []; + return { type: 'Feature', geometry: { type: 'LineString', coordinates: wps.map((w) => [w.lon, w.lat]) } }; +} + +// Toggle the PATHWAY route line and APTSIGNS runway labels (default on). +function setSvtVisibility(map, opts) { + if (!map) return; + const set = (id, vis) => { try { if (map.getLayer(id)) map.setLayoutProperty(id, 'visibility', vis ? 'visible' : 'none'); } catch { /* not ready */ } }; + set('fpl-line', !opts || opts.pathway !== false); + ['rwy-fill', 'rwy-line', 'rwy-num'].forEach((id) => set(id, !opts || opts.aptSigns !== false)); +} + +export default function SVT({ values, flightPlan, opts }) { const elRef = useRef(null); const mapRef = useRef(null); const dataRef = useRef(values); dataRef.current = values; + const planRef = useRef(flightPlan); planRef.current = flightPlan; + const optsRef = useRef(opts); optsRef.current = opts; useEffect(() => { let map; @@ -131,6 +147,12 @@ export default function SVT({ values }) { }, paint: { 'text-color': '#fff', 'text-halo-color': '#000', 'text-halo-width': 1.4 }, }); + // PATHWAY: the active flight-plan route, draped magenta on the terrain. + map.addSource('fplroute', { type: 'geojson', data: routeGeo(planRef.current) }); + map.addLayer({ id: 'fpl-line', type: 'line', source: 'fplroute', + layout: { 'line-cap': 'round', 'line-join': 'round' }, + paint: { 'line-color': '#ff20ff', 'line-width': 3, 'line-opacity': 0.9 } }); + setSvtVisibility(map, optsRef.current); // honour PATHWAY / APTSIGNS from the start let last = null; const refresh = async () => { const v = dataRef.current, lat = num(v.lat), lon = num(v.lon); @@ -186,6 +208,14 @@ export default function SVT({ values }) { return () => { cancelAnimationFrame(raf); clearInterval(rwyTimer); map.remove(); mapRef.current = null; }; }, []); // eslint-disable-line + // Keep the PATHWAY route in sync with the flight plan, and apply PATHWAY / + // APTSIGNS visibility when the softkeys toggle them. + useEffect(() => { + const m = mapRef.current; + if (m && m.getSource && m.getSource('fplroute')) { try { m.getSource('fplroute').setData(routeGeo(flightPlan)); } catch { /* not ready */ } } + }, [flightPlan]); + useEffect(() => { setSvtVisibility(mapRef.current, opts); }, [opts]); // eslint-disable-line + // Bank: rotate the whole terrain canvas opposite to aircraft roll; scale up so // the corners stay covered while rotated. const roll = num(values.roll);