From 6756acab4ab7162e6f31a4ad1ff4ac89f7b1edf4 Mon Sep 17 00:00:00 2001 From: karim Date: Thu, 4 Jun 2026 12:33:04 +0200 Subject: [PATCH] Citation: combined PFD+MFD view, hardware AP look, FMS build-out, fluid easing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- web/src/App.jsx | 9 +- web/src/citation.css | 87 +++++++++++----- web/src/components/CDU.jsx | 122 ++++++++++++++++++----- web/src/components/citation/CitDuo.jsx | 15 +++ web/src/components/citation/CitEICAS.jsx | 5 +- web/src/components/citation/CitMFD.jsx | 7 +- web/src/components/citation/CitPFD.jsx | 31 ++++-- 7 files changed, 211 insertions(+), 65 deletions(-) create mode 100644 web/src/components/citation/CitDuo.jsx diff --git a/web/src/App.jsx b/web/src/App.jsx index 8fca01a..a5d2ced 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -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' && } {profile === 'citation' && tab === 'pfd' && } {profile === 'citation' && tab === 'mfd' && } {profile === 'citation' && tab === 'eicas' && } diff --git a/web/src/citation.css b/web/src/citation.css index 7334489..8b32a93 100644 --- a/web/src/citation.css +++ b/web/src/citation.css @@ -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 ============================================================================ */ diff --git a/web/src/components/CDU.jsx b/web/src/components/CDU.jsx index a7361df..b2c12f6 100644 --- a/web/src/components/CDU.jsx +++ b/web/src/components/CDU.jsx @@ -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 (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; } + 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; } + } 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 }) { : (<>{row.id}{row.type}{row.dtk == null ? '---' : `${String(row.dtk).padStart(3, '0')}°`}{row.orig ? 'ORIG' : row.d.toFixed(1)})} ))}); } else if (page === 'deparr') { - title = `${dest || '----'} ${cat === 'sids' ? 'DEPART' : cat === 'stars' ? 'ARRIVAL' : 'APPROACH'}`; pageNo = `${procPage + 1}/${procPages}`; - const shown = procList.slice(procPage * 5, procPage * 5 + 5); + const kind = cat === 'sids' ? 'DEPART' : cat === 'stars' ? 'ARRIVAL' : 'APPROACH'; + if (selProc) { + title = `${selProc.name} TRANS`; pageNo = `1/1`; + body = ( +
+
<NO TRANSdirect
+ {(selProc.transitions || []).slice(0, 4).map((t) =>
<{t}
)} +
Transition wählen (LSK) · oder NO TRANS (1L)
+
+ ); + } else { + title = `${dest || '----'} ${kind}`; pageNo = `${procPage + 1}/${procPages}`; + const shown = procList.slice(procPage * 5, procPage * 5 + 5); + body = ( +
+
SID>STAR>APPR>
+ {!procs &&
{dest ? 'loading…' : 'set DEST on FPLN'}
} + {procs && shown.length === 0 &&
none
} + {shown.map((p, i) =>
<{p.name}{p.transitions?.length ? {p.transitions.length} TR> : null}
)} +
+ ); + } + } 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 = ( -
-
SID>STAR>APPR>
- {!procs &&
{dest ? 'loading…' : 'set DEST on FPLN'}
} - {procs && shown.length === 0 &&
none
} - {shown.map((p, i) =>
<{p.name}{p.transitions?.length ? {p.transitions.length} TR : null}
)} +
+
{actW?.id || '----'}
+
{dA.toFixed(1)} NM
+
{ete(dA)}
+
{destW?.id || '----'}
+
{dD.toFixed(0)} NM
+
{ete(dD)}
+
GS {gs} KT · TAS {Math.round(num(xp.values.tas))} · SAT {Math.round(num(xp.values.oat))}°C
); } else if (page === 'dir') { @@ -203,15 +256,34 @@ export default function CDU({ xp, vnav: vnavCfg, onVnav }) {
); } else if (page === 'vnav') { - title = 'ACT VNAV'; - body = ( -
-
{crzAlt || '-----'}
-
{tgtSpd || '---'}
-
{(cfg.fpa || 3).toFixed(1)}°
-
CRZ ALT: LSK1L · TGT SPD: LSK1R · VPA: LSK3R
-
- ); + title = `ACT VNAV ${VNAV_PAGES[vnavPage]}`; pageNo = `${vnavPage + 1}/3`; + if (vnavPage === 0) { // CLB (manual p21-22) + body = ( +
+
{transAlt || '18000'}
+
{tgtSpd || '290/.74'}
+
{clbLim}
+
TRANS ALT: 1L · SPD/ALT LIMIT: 2L · NEXT→CRZ
+
+ ); + } else if (vnavPage === 1) { // CRZ (manual p23) + body = ( +
+
{crzAlt ? `FL${crzAlt}` : '-----'}
+
{tgtSpd || '.80'}
+
CRZ ALT: 1L (e.g. 280=FL280) · TGT SPD: 1R
+
+ ); + } else { // DES (manual p23-24) + body = ( +
+
{tgtSpd || '/200'}
+
{(cfg.fpa || 3).toFixed(1)}°
+
{desLim}
+
TGT SPD: 1L · VPA: 1R (2.0-6.0°) · SPD/ALT: 2L
+
+ ); + } } 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 */}
{PAGE_KEYS.map(([id, lbl]) => ( - + ))}
- - + + diff --git a/web/src/components/citation/CitDuo.jsx b/web/src/components/citation/CitDuo.jsx new file mode 100644 index 0000000..88cadb0 --- /dev/null +++ b/web/src/components/citation/CitDuo.jsx @@ -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 ( +
+
+
+
+ ); +} diff --git a/web/src/components/citation/CitEICAS.jsx b/web/src/components/citation/CitEICAS.jsx index 4f7a064..e17b4c6 100644 --- a/web/src/components/citation/CitEICAS.jsx +++ b/web/src/components/citation/CitEICAS.jsx @@ -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)]; diff --git a/web/src/components/citation/CitMFD.jsx b/web/src/components/citation/CitMFD.jsx index a6afb28..059e56a 100644 --- a/web/src/components/citation/CitMFD.jsx +++ b/web/src/components/citation/CitMFD.jsx @@ -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; diff --git a/web/src/components/citation/CitPFD.jsx b/web/src/components/citation/CitPFD.jsx index c08035e..4a0cd77 100644 --- a/web/src/components/citation/CitPFD.jsx +++ b/web/src/components/citation/CitPFD.jsx @@ -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 }) { KIAS {/* AOA index (#manual p22) */} - + {/* altitude tape (#20,#21) + baro (#12,#17) */} {/* VSI (#13,#14) */}