Citation: combined PFD+MFD view, hardware AP look, FMS build-out, fluid easing
- CitDuo: PFD + MFD side-by-side on one tablet screen (new 'PFD+MFD' tab, first in the Citation profile) — the two pilot DU-870 tubes at once. - Autopilot restyled to the real Primus FGC: machined dark bezel w/ corner screws, engraved square keys with green annunciator triangles (lit when active), ridged pitch thumbwheel. - FMS more complete per the FMS manual: DEP/ARR now does the two-step procedure→transition pick (NO TRANS / RWxx / named transitions), VNAV split into CLB/CRZ/DES pages (trans-alt, speed/alt limits, cruise alt, target speed, VPA) via PREV/NEXT, and a new PROG page (TO/DEST distance-to-go + ETE at GS). Page keys: FPLN/LEGS/DEP-ARR/DIR-INTC/VNAV/PROG/MENU. - Fluidity: Citation PFD/MFD/EICAS now use the same rAF time-constant easing as the G1000 (useEased/useEasedAngle) for attitude, speed/alt/VS tapes, HSI, compass, map ownship and N1/ITT gauges — smooth 60 fps instead of stepping. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+6
-3
@@ -14,6 +14,7 @@ import AudioPanel from './components/AudioPanel.jsx';
|
||||
import KAP140 from './components/KAP140.jsx';
|
||||
import CitPFD from './components/citation/CitPFD.jsx';
|
||||
import CitMFD from './components/citation/CitMFD.jsx';
|
||||
import CitDuo from './components/citation/CitDuo.jsx';
|
||||
import CitEICAS from './components/citation/CitEICAS.jsx';
|
||||
import CitAP from './components/citation/CitAP.jsx';
|
||||
import CitRMU from './components/citation/CitRMU.jsx';
|
||||
@@ -29,6 +30,7 @@ const ICONS = {
|
||||
audio: 'M11 4a6 6 0 00-6 6v5M17 15v-5a6 6 0 00-6-6M4 14h2.5v4.5H4zM15.5 14H18v4.5h-2.5z',
|
||||
eicas: 'M5 4v14M9 4v14M5 11h4M13 7h5M13 11h5M13 15h5',
|
||||
rmu: 'M4 5h14v12H4zM7 8h8M7 11h8M7 14h4',
|
||||
duo: 'M3 5h7v12H3zM12 5h7v12h-7z',
|
||||
};
|
||||
function Icon({ name }) {
|
||||
return (
|
||||
@@ -54,9 +56,9 @@ const PROFILES = {
|
||||
citation: {
|
||||
label: 'Cessna Citation X', short: 'CITATION X',
|
||||
tabs: [
|
||||
{ id: 'pfd', label: 'PFD' }, { id: 'mfd', label: 'MFD' }, { id: 'eicas', label: 'EICAS' },
|
||||
{ id: 'fms', label: 'CDU/FMS' }, { id: 'ap', label: 'Autopilot' }, { id: 'rmu', label: 'Radios' },
|
||||
{ id: 'map', label: 'Map' },
|
||||
{ id: 'duo', label: 'PFD+MFD' }, { id: 'pfd', label: 'PFD' }, { id: 'mfd', label: 'MFD' },
|
||||
{ id: 'eicas', label: 'EICAS' }, { id: 'fms', label: 'CDU/FMS' }, { id: 'ap', label: 'Autopilot' },
|
||||
{ id: 'rmu', label: 'Radios' }, { id: 'map', label: 'Map' },
|
||||
],
|
||||
},
|
||||
ga: {
|
||||
@@ -234,6 +236,7 @@ export default function App() {
|
||||
)}
|
||||
|
||||
{/* ---- Cessna Citation X suite (Honeywell Primus 2000) ---- */}
|
||||
{profile === 'citation' && tab === 'duo' && <CitDuo xp={xp} />}
|
||||
{profile === 'citation' && tab === 'pfd' && <CitPFD xp={xp} />}
|
||||
{profile === 'citation' && tab === 'mfd' && <CitMFD xp={xp} />}
|
||||
{profile === 'citation' && tab === 'eicas' && <CitEICAS xp={xp} />}
|
||||
|
||||
+61
-26
@@ -57,46 +57,81 @@
|
||||
.cit-bz-knob:hover { background: #2e3740; }
|
||||
|
||||
/* ============================================================================
|
||||
AUTOPILOT (Flight Guidance Controller)
|
||||
AUTOPILOT — Honeywell Primus 2000 Flight Guidance Controller (hardware look)
|
||||
============================================================================ */
|
||||
.citap-screen { gap: 10px; }
|
||||
.citap-refs { display: flex; gap: 26px; color: #cdd6dd; font-family: 'Roboto Mono',monospace; }
|
||||
.citap-screen { gap: 14px; }
|
||||
.citap-refs { display: flex; gap: 30px; color: #cdd6dd; font-family: 'Roboto Mono',monospace; }
|
||||
.citap-refs div { text-align: center; }
|
||||
.citap-refs span { display: block; font-size: 10px; color: #8b97a0; letter-spacing: .08em; }
|
||||
.citap-refs b { font-size: 22px; color: #d24bd2; }
|
||||
.citap-fma {
|
||||
display: flex; gap: 0; background: #05080b; border: 1px solid #2a3138; border-radius: 6px; overflow: hidden;
|
||||
display: flex; gap: 0; background: #05080b; border: 1px solid #2a3138; border-radius: 4px; overflow: hidden;
|
||||
font-family: 'Roboto Mono',monospace; font-weight: 700; min-width: 360px;
|
||||
}
|
||||
.citap-fma span { flex: 1; text-align: center; padding: 6px 14px; font-size: 14px; }
|
||||
.fma-act { color: #16e000; } .fma-arm { color: #fff; } .citap-ap, .citap-fma .fma-ap { color: #16e000; border-left: 1px solid #2a3138; border-right: 1px solid #2a3138; }
|
||||
.fma-act { color: #16e000; } .fma-arm { color: #fff; }
|
||||
.citap-fma .fma-ap { color: #16e000; border-left: 1px solid #2a3138; border-right: 1px solid #2a3138; }
|
||||
|
||||
/* the controller body: machined dark-grey bezel with screws */
|
||||
.citap-panel {
|
||||
display: flex; align-items: stretch; gap: 14px; padding: 16px 22px;
|
||||
background: linear-gradient(#20262c,#14181d); border: 1px solid #2c333a; border-radius: 12px;
|
||||
box-shadow: inset 0 1px 0 #353d45, 0 6px 20px rgba(0,0,0,.45);
|
||||
position: relative; display: flex; align-items: stretch; gap: 10px; padding: 20px 26px;
|
||||
background: linear-gradient(#3a4047 0%,#23282e 8%,#1a1e23 92%,#2b3137 100%);
|
||||
border: 1px solid #0c0f12; border-radius: 10px;
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,.08), inset 0 0 0 1px #0c0f12, 0 8px 26px rgba(0,0,0,.6);
|
||||
}
|
||||
.citap-col { display: flex; flex-direction: column; gap: 10px; justify-content: flex-start; }
|
||||
.citap-master { margin-left: 6px; }
|
||||
.citap-panel::before, .citap-panel::after {
|
||||
content: ''; position: absolute; width: 7px; height: 7px; border-radius: 50%;
|
||||
background: radial-gradient(circle at 35% 35%,#5a626a,#1b1f24); top: 7px;
|
||||
}
|
||||
.citap-panel::before { left: 8px; } .citap-panel::after { right: 8px; }
|
||||
.citap-col { display: flex; flex-direction: column; gap: 9px; justify-content: flex-start; }
|
||||
.citap-master { margin-left: 8px; border-left: 1px solid #0c0f12; padding-left: 14px; }
|
||||
|
||||
/* engraved square key with a green annunciator triangle (lit when active) */
|
||||
.citap-btn {
|
||||
position: relative; min-width: 96px; padding: 11px 14px 11px 22px; text-align: left;
|
||||
font-size: 13px; font-weight: 700; letter-spacing: .05em; color: #cdd6dd;
|
||||
background: linear-gradient(#2c343c,#1c2228); border: 1px solid #3a434c; border-radius: 6px;
|
||||
cursor: pointer; font-family: 'Roboto Mono',monospace;
|
||||
position: relative; width: 82px; padding: 9px 8px 9px 20px; text-align: left;
|
||||
font-size: 12px; font-weight: 700; letter-spacing: .06em; color: #e4e9ee;
|
||||
background: linear-gradient(#33393f,#1c2025); border: 1px solid #0e1114;
|
||||
border-radius: 4px; cursor: pointer; font-family: 'Roboto Mono',monospace;
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,.10), inset 0 -2px 3px rgba(0,0,0,.5), 0 1px 2px rgba(0,0,0,.6);
|
||||
text-shadow: 0 1px 1px #000;
|
||||
}
|
||||
.citap-btn .citap-arrow { position: absolute; left: 8px; color: #4a545d; font-size: 10px; }
|
||||
.citap-btn:hover { background: #333d46; }
|
||||
.citap-btn.active { background: #0e5a2a; border-color: #1b8a43; color: #c9ffd6; box-shadow: 0 0 8px rgba(25,190,80,.55); }
|
||||
.citap-btn.active .citap-arrow { color: #16e000; }
|
||||
.citap-btn.armed { background: #20262c; border-color: #6a7178; color: #fff; }
|
||||
.citap-btn.armed .citap-arrow { color: #fff; }
|
||||
.citap-btn.dim { opacity: .45; cursor: default; }
|
||||
.citap-wheel { display: flex; flex-direction: column; align-items: center; gap: 6px; justify-content: center; padding: 0 6px; }
|
||||
.citap-wlbl { font-size: 9px; color: #8b97a0; letter-spacing: .1em; }
|
||||
.citap-wbtn { width: 46px; padding: 8px 0; font-size: 14px; color: #cdd6dd; background: #2a323a; border: 1px solid #3a434c; border-radius: 5px; cursor: pointer; }
|
||||
.citap-wheelface { width: 46px; height: 56px; border-radius: 6px; background: repeating-linear-gradient(#1a2026,#1a2026 3px,#2b343c 3px,#2b343c 6px); border: 1px solid #3a434c; }
|
||||
.citap-foot { font-size: 11px; color: #8b97a0; max-width: 640px; text-align: center; line-height: 1.5; }
|
||||
.citap-btn .citap-arrow {
|
||||
position: absolute; left: 7px; top: 50%; transform: translateY(-50%);
|
||||
width: 0; height: 0; border-top: 5px solid transparent; border-bottom: 5px solid transparent;
|
||||
border-left: 7px solid #2b3137; font-size: 0; line-height: 0; color: transparent;
|
||||
}
|
||||
.citap-btn:hover { background: linear-gradient(#3b424a,#23282e); }
|
||||
.citap-btn:active { box-shadow: inset 0 2px 4px rgba(0,0,0,.7); }
|
||||
.citap-btn.active .citap-arrow { border-left-color: #1fff4e; filter: drop-shadow(0 0 4px #16e000); }
|
||||
.citap-btn.active { color: #fff; }
|
||||
.citap-btn.armed .citap-arrow { border-left-color: #fff; }
|
||||
.citap-btn.armed { color: #fff; }
|
||||
.citap-btn.dim { opacity: .4; cursor: default; }
|
||||
|
||||
/* pitch wheel: NOSE UP/DN labels + a ridged thumbwheel */
|
||||
.citap-wheel { display: flex; flex-direction: column; align-items: center; gap: 4px; justify-content: center; padding: 0 10px; }
|
||||
.citap-wlbl { font-size: 8px; color: #aeb8bf; letter-spacing: .14em; }
|
||||
.citap-wbtn { width: 30px; padding: 3px 0; font-size: 12px; color: #cdd6dd; background: #21262b; border: 1px solid #0e1114; border-radius: 3px; cursor: pointer; }
|
||||
.citap-wheelface {
|
||||
width: 30px; height: 64px; border-radius: 5px; border: 1px solid #0e1114;
|
||||
background: repeating-linear-gradient(#0d0f12,#0d0f12 2px,#3a424a 3px,#22272c 5px,#0d0f12 7px);
|
||||
box-shadow: inset 2px 0 4px rgba(0,0,0,.7), inset -2px 0 4px rgba(0,0,0,.7);
|
||||
}
|
||||
.citap-foot { font-size: 11px; color: #8b97a0; max-width: 660px; text-align: center; line-height: 1.5; }
|
||||
.citap-foot b { color: #7fd4ff; }
|
||||
|
||||
/* ---- combined PFD + MFD (one tablet screen) ---- */
|
||||
.cit-duo { display: flex; width: 100%; height: 100%; background: #05080b; }
|
||||
.cit-duo-half { flex: 1; min-width: 0; height: 100%; display: flex; }
|
||||
.cit-duo .cit-screen { padding: 8px; gap: 8px; }
|
||||
.cit-duo .cit-bezel { padding: 5px 7px; gap: 5px; }
|
||||
.cit-duo .cit-bz-btn, .cit-duo .cit-sk { min-width: 0; padding: 5px 7px; font-size: 10px; }
|
||||
.cit-duo .cit-bz-group { padding: 1px 5px; }
|
||||
.cit-duo .cit-bz-val { min-width: 38px; font-size: 11px; }
|
||||
.cit-duo .cit-bz-knob { width: 22px; padding: 4px 0; }
|
||||
@media (max-width: 900px) { .cit-duo { flex-direction: column; } }
|
||||
|
||||
/* ============================================================================
|
||||
RADIO MANAGEMENT UNIT + Nav source selector
|
||||
============================================================================ */
|
||||
|
||||
+86
-14
@@ -23,8 +23,9 @@ function brng(a, b) {
|
||||
return (deg(Math.atan2(y, x)) + 360) % 360;
|
||||
}
|
||||
|
||||
const PAGE_KEYS = [['fpln', 'FPLN'], ['legs', 'LEGS'], ['deparr', 'DEP/ARR'], ['dir', 'DIR'], ['vnav', 'VNAV'], ['menu', 'MENU']];
|
||||
const PAGE_KEYS = [['fpln', 'FPLN'], ['legs', 'LEGS'], ['deparr', 'DEP/ARR'], ['dir', 'DIR-INTC'], ['vnav', 'VNAV'], ['prog', 'PROG'], ['menu', 'MENU']];
|
||||
const LEG_ROWS = 5;
|
||||
const VNAV_PAGES = ['CLB', 'CRZ', 'DES'];
|
||||
|
||||
export default function CDU({ xp, vnav: vnavCfg, onVnav }) {
|
||||
const { flightPlan, fp, exportMsg, command } = xp;
|
||||
@@ -42,10 +43,15 @@ export default function CDU({ xp, vnav: vnavCfg, onVnav }) {
|
||||
const [procs, setProcs] = useState(null);
|
||||
const [cat, setCat] = useState('sids'); // sids | stars | approaches
|
||||
const [procPage, setProcPage] = useState(0);
|
||||
const [selProc, setSelProc] = useState(null); // procedure awaiting a transition pick
|
||||
// VNAV perf (FMS init values; VPA feeds the shared descent profile)
|
||||
const cfg = vnavCfg || { fpa: 3, offsetNm: 0, enabled: true };
|
||||
const [crzAlt, setCrzAlt] = useState('');
|
||||
const [tgtSpd, setTgtSpd] = useState('');
|
||||
const [transAlt, setTransAlt] = useState('18000'); // CLB transition altitude (manual default)
|
||||
const [clbLim, setClbLim] = useState('250/10000'); // climb speed/alt restriction
|
||||
const [desLim, setDesLim] = useState('250/10000'); // descent speed/alt restriction
|
||||
const [vnavPage, setVnavPage] = useState(0); // 0 CLB · 1 CRZ · 2 DES
|
||||
// saved-plan list (MENU)
|
||||
const [plans, setPlans] = useState(null);
|
||||
|
||||
@@ -128,21 +134,37 @@ export default function CDU({ xp, vnav: vnavCfg, onVnav }) {
|
||||
return;
|
||||
}
|
||||
if (page === 'deparr') {
|
||||
if (side === 'R' && r < 3) { setCat(['sids', 'stars', 'approaches'][r]); setProcPage(0); return; }
|
||||
if (side === 'R' && r < 3) { setCat(['sids', 'stars', 'approaches'][r]); setProcPage(0); setSelProc(null); return; }
|
||||
if (side === 'L') {
|
||||
// step 1: a procedure is selected → if it has transitions, pick one; else load
|
||||
if (selProc) {
|
||||
if (r === 0) return loadProc(selProc.name, ''); // NO TRANS / direct
|
||||
const tr = (selProc.transitions || [])[r - 1];
|
||||
if (tr) return loadProc(selProc.name, tr);
|
||||
return;
|
||||
}
|
||||
const list = (procs && procs[cat]) || [];
|
||||
const p = list[procPage * 5 + r];
|
||||
if (p) return loadProc(p.name, p.transitions && p.transitions[0]);
|
||||
if (p) { if (p.transitions && p.transitions.length) return setSelProc(p); return loadProc(p.name, ''); }
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (page === 'dir') { if (scr) return directTo(scr); return; }
|
||||
if (page === 'vnav') {
|
||||
if (vnavPage === 0) { // CLB: trans alt (1L), speed/alt limit (2L)
|
||||
if (side === 'L' && r === 0 && scr) { setTransAlt(scr); setScr(''); return; }
|
||||
if (side === 'L' && r === 1 && scr) { setClbLim(scr); setScr(''); return; }
|
||||
} else if (vnavPage === 1) { // CRZ: cruise alt (1L), target speed (1R)
|
||||
if (side === 'L' && r === 0 && scr) { setCrzAlt(scr); setScr(''); return; }
|
||||
if (side === 'R' && r === 0 && scr) { setTgtSpd(scr.replace('/', '')); setScr(''); return; }
|
||||
if (side === 'R' && r === 2 && scr && onVnav) { const v = parseFloat(scr); if (v >= 2 && v <= 6) onVnav((c) => ({ ...c, fpa: v })); setScr(''); return; }
|
||||
} else { // DES: VPA (1R), speed/alt limit (2L), target speed (1L)
|
||||
if (side === 'R' && r === 0 && scr && onVnav) { const v = parseFloat(scr); if (v >= 2 && v <= 6) onVnav((c) => ({ ...c, fpa: v })); setScr(''); return; }
|
||||
if (side === 'L' && r === 0 && scr) { setTgtSpd(scr.replace('/', '')); setScr(''); return; }
|
||||
if (side === 'L' && r === 1 && scr) { setDesLim(scr); setScr(''); return; }
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (page === 'prog') return;
|
||||
if (page === 'menu') {
|
||||
if (side === 'L' && r === 0) return openLoad();
|
||||
if (side === 'L' && r === 1) return exec();
|
||||
@@ -183,14 +205,45 @@ export default function CDU({ xp, vnav: vnavCfg, onVnav }) {
|
||||
: (<><span className="cdu-wpt">{row.id}<i>{row.type}</i></span><span className="cdu-dtk">{row.dtk == null ? '---' : `${String(row.dtk).padStart(3, '0')}°`}</span><span className="cdu-dist">{row.orig ? 'ORIG' : row.d.toFixed(1)}</span></>)}
|
||||
</div>))}</div>);
|
||||
} else if (page === 'deparr') {
|
||||
title = `${dest || '----'} ${cat === 'sids' ? 'DEPART' : cat === 'stars' ? 'ARRIVAL' : 'APPROACH'}`; pageNo = `${procPage + 1}/${procPages}`;
|
||||
const kind = cat === 'sids' ? 'DEPART' : cat === 'stars' ? 'ARRIVAL' : 'APPROACH';
|
||||
if (selProc) {
|
||||
title = `${selProc.name} TRANS`; pageNo = `1/1`;
|
||||
body = (
|
||||
<div className="cdu-deparr">
|
||||
<div className="cdu-prow act"><span><NO TRANS</span><i>direct</i></div>
|
||||
{(selProc.transitions || []).slice(0, 4).map((t) => <div className="cdu-prow" key={t}><span><{t}</span></div>)}
|
||||
<div className="cdu-note small">Transition wählen (LSK) · oder NO TRANS (1L)</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
title = `${dest || '----'} ${kind}`; pageNo = `${procPage + 1}/${procPages}`;
|
||||
const shown = procList.slice(procPage * 5, procPage * 5 + 5);
|
||||
body = (
|
||||
<div className="cdu-deparr">
|
||||
<div className="cdu-tabs"><span className={cat === 'sids' ? 'on' : ''}>SID></span><span className={cat === 'stars' ? 'on' : ''}>STAR></span><span className={cat === 'approaches' ? 'on' : ''}>APPR></span></div>
|
||||
{!procs && <div className="cdu-note">{dest ? 'loading…' : 'set DEST on FPLN'}</div>}
|
||||
{procs && shown.length === 0 && <div className="cdu-note">none</div>}
|
||||
{shown.map((p, i) => <div className="cdu-prow" key={p.name + i}><span><{p.name}</span>{p.transitions?.length ? <i>{p.transitions.length} TR</i> : null}</div>)}
|
||||
{shown.map((p, i) => <div className="cdu-prow" key={p.name + i}><span><{p.name}</span>{p.transitions?.length ? <i>{p.transitions.length} TR></i> : null}</div>)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} else if (page === 'prog') {
|
||||
title = 'PROGRESS';
|
||||
const here = { lat: num(xp.values.lat), lon: num(xp.values.lon) };
|
||||
const gs = Math.max(1, Math.round(num(xp.values.groundspeed) * 1.94384));
|
||||
const actW = wps[active], destW = wps[wps.length - 1];
|
||||
const dA = actW && here.lat ? distNm(here, actW) : 0;
|
||||
const dD = destW && here.lat ? distNm(here, destW) : 0;
|
||||
const ete = (d) => (gs > 5 ? `${Math.floor(d / gs * 60)}:${String(Math.round((d / gs * 60 % 1) * 60)).padStart(2, '0')}` : '--:--');
|
||||
body = (
|
||||
<div className="cdu-vnav">
|
||||
<div className="cdu-fl"><label>TO</label><b>{actW?.id || '----'}</b></div>
|
||||
<div className="cdu-fl r"><label>DTG</label><b>{dA.toFixed(1)} NM</b></div>
|
||||
<div className="cdu-fl r"><label>ETE</label><b>{ete(dA)}</b></div>
|
||||
<div className="cdu-fl"><label>DEST</label><b>{destW?.id || '----'}</b></div>
|
||||
<div className="cdu-fl r"><label>DEST DTG</label><b>{dD.toFixed(0)} NM</b></div>
|
||||
<div className="cdu-fl r"><label>DEST ETE</label><b>{ete(dD)}</b></div>
|
||||
<div className="cdu-note small">GS {gs} KT · TAS {Math.round(num(xp.values.tas))} · SAT {Math.round(num(xp.values.oat))}°C</div>
|
||||
</div>
|
||||
);
|
||||
} else if (page === 'dir') {
|
||||
@@ -203,15 +256,34 @@ export default function CDU({ xp, vnav: vnavCfg, onVnav }) {
|
||||
</div>
|
||||
);
|
||||
} else if (page === 'vnav') {
|
||||
title = 'ACT VNAV';
|
||||
title = `ACT VNAV ${VNAV_PAGES[vnavPage]}`; pageNo = `${vnavPage + 1}/3`;
|
||||
if (vnavPage === 0) { // CLB (manual p21-22)
|
||||
body = (
|
||||
<div className="cdu-vnav">
|
||||
<div className="cdu-fl"><label>CRZ ALT</label><b>{crzAlt || '-----'}</b></div>
|
||||
<div className="cdu-fl r"><label>TGT SPD</label><b>{tgtSpd || '---'}</b></div>
|
||||
<div className="cdu-fl r"><label>FPA / VPA</label><b>{(cfg.fpa || 3).toFixed(1)}°</b></div>
|
||||
<div className="cdu-note small">CRZ ALT: LSK1L · TGT SPD: LSK1R · VPA: LSK3R</div>
|
||||
<div className="cdu-fl"><label>TRANS ALT</label><b>{transAlt || '18000'}</b></div>
|
||||
<div className="cdu-fl r"><label>TGT SPD</label><b>{tgtSpd || '290/.74'}</b></div>
|
||||
<div className="cdu-fl"><label>SPD/ALT LIMIT</label><b>{clbLim}</b></div>
|
||||
<div className="cdu-note small">TRANS ALT: 1L · SPD/ALT LIMIT: 2L · NEXT→CRZ</div>
|
||||
</div>
|
||||
);
|
||||
} else if (vnavPage === 1) { // CRZ (manual p23)
|
||||
body = (
|
||||
<div className="cdu-vnav">
|
||||
<div className="cdu-fl"><label>CRZ ALT</label><b>{crzAlt ? `FL${crzAlt}` : '-----'}</b></div>
|
||||
<div className="cdu-fl r"><label>TGT SPD</label><b>{tgtSpd || '.80'}</b></div>
|
||||
<div className="cdu-note small">CRZ ALT: 1L (e.g. 280=FL280) · TGT SPD: 1R</div>
|
||||
</div>
|
||||
);
|
||||
} else { // DES (manual p23-24)
|
||||
body = (
|
||||
<div className="cdu-vnav">
|
||||
<div className="cdu-fl"><label>TGT SPD</label><b>{tgtSpd || '/200'}</b></div>
|
||||
<div className="cdu-fl r"><label>VPA</label><b>{(cfg.fpa || 3).toFixed(1)}°</b></div>
|
||||
<div className="cdu-fl"><label>SPD/ALT LIMIT</label><b>{desLim}</b></div>
|
||||
<div className="cdu-note small">TGT SPD: 1L · VPA: 1R (2.0-6.0°) · SPD/ALT: 2L</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} else if (page === 'menu') {
|
||||
title = plans ? 'CO ROUTE LIST' : 'ROUTE MENU';
|
||||
body = plans ? (
|
||||
@@ -247,13 +319,13 @@ export default function CDU({ xp, vnav: vnavCfg, onVnav }) {
|
||||
{/* page keys */}
|
||||
<div className="cdu-pages">
|
||||
{PAGE_KEYS.map(([id, lbl]) => (
|
||||
<button key={id} className={`cdu-k pg ${page === id ? 'on' : ''}`} onClick={() => { setPage(id); setScr(''); setDel(false); setPlans(null); }}>{lbl}</button>
|
||||
<button key={id} className={`cdu-k pg ${page === id ? 'on' : ''}`} onClick={() => { setPage(id); setScr(''); setDel(false); setPlans(null); setSelProc(null); }}>{lbl}</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="cdu-fn">
|
||||
<button className="cdu-k fn" onClick={() => { if (page === 'legs') setLegPage((p) => Math.max(0, p - 1)); else if (page === 'deparr') setProcPage((p) => Math.max(0, p - 1)); }}>PREV</button>
|
||||
<button className="cdu-k fn" onClick={() => { if (page === 'legs') setLegPage((p) => Math.min(legPages - 1, p + 1)); else if (page === 'deparr') setProcPage((p) => Math.min(procPages - 1, p + 1)); }}>NEXT</button>
|
||||
<button className="cdu-k fn" onClick={() => { if (page === 'legs') setLegPage((p) => Math.max(0, p - 1)); else if (page === 'deparr') setProcPage((p) => Math.max(0, p - 1)); else if (page === 'vnav') setVnavPage((p) => Math.max(0, p - 1)); }}>PREV</button>
|
||||
<button className="cdu-k fn" onClick={() => { if (page === 'legs') setLegPage((p) => Math.min(legPages - 1, p + 1)); else if (page === 'deparr') setProcPage((p) => Math.min(procPages - 1, p + 1)); else if (page === 'vnav') setVnavPage((p) => Math.min(2, p + 1)); }}>NEXT</button>
|
||||
<button className={`cdu-k fn ${del ? 'arm' : ''}`} onClick={() => { setDel((d) => !d); setScr(''); }}>DEL</button>
|
||||
<button className="cdu-k fn" onClick={clr}>CLR</button>
|
||||
<button className="cdu-k fn exec" onClick={exec}>EXEC</button>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import CitPFD from './CitPFD.jsx';
|
||||
import CitMFD from './CitMFD.jsx';
|
||||
|
||||
// Side-by-side PFD + MFD — the two pilot tubes of the Citation X panel on one
|
||||
// tablet screen (landscape). Each keeps its own bezel/soft-keys; they scale to
|
||||
// fill half the width like the real instrument panel (DU-870 displays).
|
||||
export default function CitDuo({ xp }) {
|
||||
return (
|
||||
<div className="cit-duo">
|
||||
<div className="cit-duo-half"><CitPFD xp={xp} /></div>
|
||||
<div className="cit-duo-half"><CitMFD xp={xp} /></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { num } from '../../api/useXplane.js';
|
||||
import { useEased } from '../../api/ease.js';
|
||||
|
||||
// ============================================================================
|
||||
// Citation X — Engine Indicating & Crew Alerting System (EICAS).
|
||||
@@ -48,8 +49,8 @@ export default function CitEICAS({ xp }) {
|
||||
const V = xp.values || {};
|
||||
const [page, setPage] = useState('norm'); // norm | fuel | elec | ctrl | eng
|
||||
|
||||
const n1 = [arr(V.n1, 0), arr(V.n1, 1)];
|
||||
const itt = [arr(V.itt, 0), arr(V.itt, 1)];
|
||||
const n1 = [useEased(arr(V.n1, 0), 0.16), useEased(arr(V.n1, 1), 0.16)];
|
||||
const itt = [useEased(arr(V.itt, 0), 0.2), useEased(arr(V.itt, 1), 0.2)];
|
||||
const oilT = [arr(V.oilTemp, 0), arr(V.oilTemp, 1)];
|
||||
const oilP = [arr(V.oilPress, 0), arr(V.oilPress, 1)];
|
||||
const ff = [arr(V.fuelFlow, 0), arr(V.fuelFlow, 1)];
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { num } from '../../api/useXplane.js';
|
||||
import { useEased, useEasedAngle } from '../../api/ease.js';
|
||||
|
||||
// ============================================================================
|
||||
// Citation X — Multi-Function Display (Honeywell Primus 2000 arc map).
|
||||
@@ -35,7 +36,11 @@ export default function CitMFD({ xp }) {
|
||||
const etRun = useRef(false);
|
||||
useEffect(() => { const id = setInterval(() => etRun.current && setEt((t) => t + 1), 1000); return () => clearInterval(id); }, []);
|
||||
|
||||
const lat = num(V.lat), lon = num(V.lon), hdg = num(V.heading), trk = num(V.track);
|
||||
// smooth ownship + compass (same rAF glide as the G1000 map)
|
||||
const lat = useEased(num(V.lat), 0.14);
|
||||
const lon = useEased(num(V.lon), 0.14);
|
||||
const hdg = useEasedAngle(num(V.heading), 0.10);
|
||||
const trk = num(V.track);
|
||||
// arc map geometry: ownship near bottom, ~120° forward arc
|
||||
const W = 760, H = 760, cx = W / 2, cy = 600, R = 470; // compass radius
|
||||
const pxPerNm = R / rng;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { num } from '../../api/useXplane.js';
|
||||
import { useEased, useEasedAngle } from '../../api/ease.js';
|
||||
|
||||
// ============================================================================
|
||||
// Cessna Citation X — Primary Flight Display (Honeywell Primus 2000).
|
||||
@@ -264,16 +265,30 @@ export default function CitPFD({ xp }) {
|
||||
const [min, setMin] = React.useState({ on: false, ft: 200 });
|
||||
const trend = useRef({ ias: 0, t: 0 });
|
||||
|
||||
const ias = num(V.airspeed), alt = num(V.altitude), vs = num(V.vspeed);
|
||||
const pitch = num(V.pitch), roll = num(V.roll), slip = num(V.slip);
|
||||
const hdg = num(V.heading), trk = num(V.track), crs = num(V.obsCrs);
|
||||
const hdgBug = num(V.apHdgBug), cdi = num(V.hsiDef), toFrom = num(V.hsiToFrom);
|
||||
const baro = num(V.baro, 29.92), mach = num(V.mach);
|
||||
// Smooth the moving symbology toward the live datarefs (frame-rate-independent
|
||||
// easing) — the same rAF glide the G1000 uses, so a 10-20 Hz stream renders as
|
||||
// fluid 60 fps motion instead of stepping.
|
||||
const ias = useEased(num(V.airspeed), 0.10);
|
||||
const alt = useEased(num(V.altitude), 0.12);
|
||||
const vs = useEased(num(V.vspeed), 0.18);
|
||||
const pitch = useEased(num(V.pitch), 0.07);
|
||||
const roll = useEased(num(V.roll), 0.07);
|
||||
const slip = useEased(num(V.slip), 0.12);
|
||||
const hdg = useEasedAngle(num(V.heading), 0.08);
|
||||
const crs = useEasedAngle(num(V.obsCrs), 0.10);
|
||||
const hdgBug = useEasedAngle(num(V.apHdgBug), 0.10);
|
||||
const cdi = useEased(num(V.hsiDef), 0.12);
|
||||
const mach = useEased(num(V.mach), 0.2);
|
||||
const aoa = useEased(num(V.aoa), 0.12);
|
||||
const brg1e = useEasedAngle(num(V.nav1Brg), 0.12);
|
||||
const brg2e = useEasedAngle(num(V.nav2Brg), 0.12);
|
||||
const trk = num(V.track), toFrom = num(V.hsiToFrom);
|
||||
const baro = num(V.baro, 29.92);
|
||||
const radAlt = num(V.radioAlt, 99999);
|
||||
const fdOn = num(V.apMode) >= 1 || num(V.apEngaged) > 0;
|
||||
// bearing pointers only when a station is received (finite, nonzero)
|
||||
const brg1 = (num(V.nav1Dme) > 0 || num(V.nav1Brg) > 0) ? num(V.nav1Brg) : null;
|
||||
const brg2 = (num(V.nav2Dme) > 0 || num(V.nav2Brg) > 0) ? num(V.nav2Brg) : null;
|
||||
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);
|
||||
|
||||
@@ -287,7 +302,7 @@ export default function CitPFD({ xp }) {
|
||||
<g transform="translate(96 90)"><SpeedTape ias={ias} mach={mach} bug={num(V.apSpdBug)} alt={alt} /></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={num(V.aoa)} /></g>
|
||||
<g transform="translate(48 600)"><AoaIndex alpha={aoa} /></g>
|
||||
{/* altitude tape (#20,#21) + baro (#12,#17) */}
|
||||
<g transform="translate(584 90)"><AltTape alt={alt} bug={num(V.apAltBug)} vs={vs} baro={baro} std={std} baroHpa={false} minOn={min.on} minFt={min.ft} raBaro={raBaro} /></g>
|
||||
{/* VSI (#13,#14) */}
|
||||
|
||||
Reference in New Issue
Block a user