Citation: match PFD/MFD size (portrait DU-870), Nav Source switch, manual audit

- MFD reworked to the same portrait DU-870 format as the PFD (800x940) so both
  tubes are identical size side-by-side in the PFD+MFD view, like the real panel.
- Nav Source Selector now on the PFD bezel (sits under the PFD per manual p24):
  NAV (VOR1/VOR2) / FMS buttons drive HSI_source_select; the HSI course pointer,
  CDI and source label colour by source — FMS magenta, VOR green (Honeywell
  convention). MFD source label (FMS1/VOR) follows the same coupling.
- Added the airspeed trend vector (PFD #3, was missing): smoothed acceleration
  projected 10 s, magenta, on the speed tape.
- Removed dead MFD soft-keys per manual: PFD SETUP → IN/HPA baro unit; EICAS SYS
  → FUEL-HYD/ELEC/APU/ENG sub-set readout (#11/#14) with RTN.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 14:16:15 +02:00
parent 6756acab4a
commit e8dfa84266
3 changed files with 94 additions and 28 deletions
+39 -14
View File
@@ -22,7 +22,7 @@ const hz2mhz = (hz) => (num(hz) / 100).toFixed(2);
// Citation X operating speeds (manual p80): Vso 115 · Vs1 136 · Vfe 180 ·
// Vmo 270 (SL-8000') / 350 (above) · Mmo 0.935.
const VSO = 115, VS1 = 136, VFE = 180;
function SpeedTape({ ias, mach, bug, alt }) {
function SpeedTape({ ias, mach, bug, alt, trendKt }) {
const H = 560, mid = H / 2, pxkt = 3.4; // 3.4 px per knot
const y = (s) => mid + (ias - s) * pxkt;
const vmo = alt > 8000 ? 350 : 270;
@@ -49,6 +49,13 @@ function SpeedTape({ ias, mach, bug, alt }) {
{/* Vfe flap-limit marker */}
<line x1={0} y1={y(VFE)} x2={12} y2={y(VFE)} stroke="#fff" strokeWidth="3" />
{marks}
{/* airspeed trend vector (#3): magenta line from the index up/down */}
{Math.abs(trendKt) > 1 && (
<g>
<line x1={71} y1={mid} x2={71} y2={mid - trendKt * pxkt} stroke="#d24bd2" strokeWidth="3" />
<polygon points={`71,${mid - trendKt * pxkt} 67,${mid - trendKt * pxkt + (trendKt > 0 ? 8 : -8)} 75,${mid - trendKt * pxkt + (trendKt > 0 ? 8 : -8)}`} fill="#d24bd2" />
</g>
)}
</g>
<defs>
<pattern id="barber" width="8" height="8" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
@@ -208,7 +215,7 @@ function Attitude({ pitch, roll, slip, fdP, fdR, fdOn }) {
}
// ── HSI (rotating compass with CDI + bearing pointers) ─────────────────────────
function HSI({ hdg, trk, crs, hdgBug, cdi, toFrom, brg1, brg2, srcLabel }) {
function HSI({ hdg, trk, crs, hdgBug, cdi, toFrom, brg1, brg2, srcLabel, srcColor }) {
const R = 150;
const card = [];
for (let d = 0; d < 360; d += 5) {
@@ -237,15 +244,15 @@ function HSI({ hdg, trk, crs, hdgBug, cdi, toFrom, brg1, brg2, srcLabel }) {
<g>{card}</g>
{/* heading bug (magenta) */}
<g transform={`rotate(${mod360(hdgBug - hdg)})`}><polygon points={`0,${-R} -9,${-R + 14} 9,${-R + 14}`} fill="#d24bd2" /></g>
{/* course pointer + CDI deviation (cyan), #5 + #15 */}
{/* course pointer + CDI deviation (#5 + #15) — FMS magenta / VOR green */}
<g transform={`rotate(${mod360(crs - hdg)})`}>
<line x1={0} y1={-R + 18} x2={0} y2={-50} stroke="#19c3e0" strokeWidth="3" />
<polygon points={`0,${-R + 6} -8,${-R + 22} 8,${-R + 22}`} fill="#19c3e0" />
<line x1={0} y1={50} x2={0} y2={R - 18} stroke="#19c3e0" strokeWidth="3" />
<line x1={0} y1={-R + 18} x2={0} y2={-50} stroke={srcColor} strokeWidth="3" />
<polygon points={`0,${-R + 6} -8,${-R + 22} 8,${-R + 22}`} fill={srcColor} />
<line x1={0} y1={50} x2={0} y2={R - 18} stroke={srcColor} strokeWidth="3" />
{/* CDI bar */}
<line x1={clamp(cdi, -2, 2) * 30} y1={-46} x2={clamp(cdi, -2, 2) * 30} y2={46} stroke="#19c3e0" strokeWidth="3.5" />
<line x1={clamp(cdi, -2, 2) * 30} y1={-46} x2={clamp(cdi, -2, 2) * 30} y2={46} stroke={srcColor} strokeWidth="3.5" />
{[-2, -1, 1, 2].map((d) => <circle key={d} cx={d * 30} cy={0} r="3" fill="none" stroke="#9aa6ad" strokeWidth="1.2" />)}
{toFrom !== 0 && <polygon points={toFrom > 0 ? '0,-14 -8,2 8,2' : '0,14 -8,-2 8,-2'} fill="#19c3e0" />}
{toFrom !== 0 && <polygon points={toFrom > 0 ? '0,-14 -8,2 8,2' : '0,14 -8,-2 8,-2'} fill={srcColor} />}
</g>
{/* bearing pointers — #6 secondary NAV (cyan circle = BRG1, white diamond = BRG2) */}
{brg1 != null && ptr(brg1, '#19c3e0', false)}
@@ -253,7 +260,7 @@ function HSI({ hdg, trk, crs, hdgBug, cdi, toFrom, brg1, brg2, srcLabel }) {
{/* fixed lubber line + aircraft */}
<polygon points={`0,${-R - 2} -8,${-R - 16} 8,${-R - 16}`} fill="#fff" />
<text x={0} y={-R - 22} fontSize="13" fill="#fff" textAnchor="middle" fontWeight="700">{String(Math.round(mod360(hdg))).padStart(3, '0')}</text>
<text x={0} y={6} fontSize="12" fill="#13e000" textAnchor="middle">{srcLabel}</text>
<text x={0} y={6} fontSize="12" fill={srcColor} textAnchor="middle">{srcLabel}</text>
</g>
);
}
@@ -289,8 +296,20 @@ export default function CitPFD({ xp }) {
// bearing pointers only when a station is received (finite, nonzero)
const brg1 = (num(V.nav1Dme) > 0 || num(V.nav1Brg) > 0) ? brg1e : null;
const brg2 = (num(V.nav2Dme) > 0 || num(V.nav2Brg) > 0) ? brg2e : null;
const srcLabel = num(V.cdiSrc) === 2 ? 'FMS1' : num(V.cdiSrc) === 1 ? 'VOR2' : 'VOR1';
const dme = num(V.cdiSrc) === 1 ? num(V.nav2Dme) : num(V.nav1Dme);
// Nav source (Nav Source Selector, manual p24): 0 VOR1 · 1 VOR2 · 2 FMS.
// FMS course is magenta, VOR course is green (Honeywell convention).
const cdiSrc = num(V.cdiSrc);
const srcLabel = cdiSrc === 2 ? 'FMS1' : cdiSrc === 1 ? 'VOR2' : 'VOR1';
const srcColor = cdiSrc === 2 ? '#d24bd2' : '#13e000';
const cycleNav = () => xp.setDataref('cdiSrc', cdiSrc === 1 ? 0 : 1); // toggle VOR1↔VOR2
const setFms = () => xp.setDataref('cdiSrc', 2);
const dme = cdiSrc === 1 ? num(V.nav2Dme) : num(V.nav1Dme);
// airspeed trend vector (#3): smoothed acceleration projected 10 s ahead
const t = trend.current, nowS = (typeof performance !== 'undefined' ? performance.now() : Date.now()) / 1000;
const dt = Math.min(0.5, Math.max(0.001, nowS - (t.t || nowS)));
const rate = (ias - (t.ias != null ? t.ias : ias)) / dt; // kt/s
t.s = (t.s || 0) * 0.92 + rate * 0.08; t.ias = ias; t.t = nowS;
const trendKt = clamp(t.s * 10, -45, 45);
return (
<div className="cit-screen">
@@ -299,7 +318,7 @@ export default function CitPFD({ xp }) {
{/* attitude */}
<g transform="translate(400 270)"><Attitude pitch={pitch} roll={roll} slip={slip} fdP={num(V.fdPitch)} fdR={num(V.fdRoll)} fdOn={fdOn} /></g>
{/* speed tape (#2,#3) */}
<g transform="translate(96 90)"><SpeedTape ias={ias} mach={mach} bug={num(V.apSpdBug)} alt={alt} /></g>
<g transform="translate(96 90)"><SpeedTape ias={ias} mach={mach} bug={num(V.apSpdBug)} alt={alt} trendKt={trendKt} /></g>
<text x={120} y={78} fontSize="14" fill="#9aa6ad" textAnchor="middle">KIAS</text>
{/* AOA index (#manual p22) */}
<g transform="translate(48 600)"><AoaIndex alpha={aoa} /></g>
@@ -308,7 +327,7 @@ export default function CitPFD({ xp }) {
{/* VSI (#13,#14) */}
<g transform="translate(716 130)"><VSI vs={vs} /></g>
{/* HSI (#10) */}
<g transform="translate(400 690)"><HSI hdg={hdg} trk={trk} crs={crs} hdgBug={hdgBug} cdi={cdi} toFrom={toFrom} brg1={brg1} brg2={brg2} srcLabel={srcLabel} /></g>
<g transform="translate(400 690)"><HSI hdg={hdg} trk={trk} crs={crs} hdgBug={hdgBug} cdi={cdi} toFrom={toFrom} brg1={brg1} brg2={brg2} srcLabel={srcLabel} srcColor={srcColor} /></g>
{/* CRS / HDG digital (#5,#7) */}
<g fontSize="15" fontWeight="700">
@@ -334,8 +353,14 @@ export default function CitPFD({ xp }) {
<text x={760} y={84} fontSize="13" fill="#9aa6ad" textAnchor="end">FT</text>
</svg>
{/* bezel buttons — MINIMUMS rotary (#8), RA/BARO (#9), STD (#11), BARO SET (#12) */}
{/* bezel buttons — Nav Source Selector (p24, sits under the PFD), MINIMUMS
rotary (#8), RA/BARO (#9), STD (#11), BARO SET (#12), CRS, HDG */}
<div className="cit-bezel">
<div className="cit-bz-group">
<span className="cit-bz-lbl">NAV SRC</span>
<button className={`cit-bz-btn ${cdiSrc !== 2 ? 'on' : ''}`} onClick={cycleNav}>{cdiSrc === 1 ? 'VOR2' : 'VOR1'}</button>
<button className={`cit-bz-btn ${cdiSrc === 2 ? 'on' : ''}`} onClick={setFms}>FMS</button>
</div>
<div className="cit-bz-group">
<span className="cit-bz-lbl">MINIMUMS</span>
<button className="cit-bz-knob" onClick={() => setMin((m) => ({ ...m, on: true, ft: m.ft - 50 }))}></button>