Citation X cockpit profile: full Primus 2000 suite (PFD/MFD/EICAS/AP/RMU)
Add a switchable cockpit-profile selector (Garmin G1000 / Cessna Citation X / GA steam) and recreate the Citation X Honeywell Primus 2000 avionics line-for- line from the X-Plane Citation X + FMS manuals: - CitPFD: attitude w/ FD command bars, speed tape (Vmo barber-pole, Vfe, low- speed red/amber bands), AOA index, altitude tape + trend, VSI, round HSI with CDI/course pointer + VOR/ADF bearing pointers, radar altimeter, minimums, STD/BARO/CRS/HDG bezel. - CitEICAS: twin FAN%/ITT bar gauges, OIL °C/PSI, FUEL (flow/qty PPH·LBS), ELECTRICAL, HYDRAULICS, slat chevron, STAB trim, FLAPS, CAS message stack, softkeys NORM/FUEL-HYD/ELEC/CTRL-POS/ENG + control-position overlay. - CitMFD: Honeywell heading-up arc map, FMS route (magenta active/white future), TCAS, terrain/WX, range arc, ETE/SAT/TAS/GSPD block, clock + ET/FT timer, V-SPEEDS reference card, MFD-setup overlays (TRAFFIC/TERRAIN/APTS/VOR). - CitAP: HDG/NAV/APP/BC · ALT/VNAV/BANK/STBY · FLC/C-O/VS · pitch wheel · AP/YD/M-TRIM/PFD-SEL, FMA bar + lamps from per-mode *_status datarefs. - CitRMU: COM/NAV active+standby tuning, transponder, ADF, TCAS range/mode, IDENT + Nav Source Selector (NAV1/2/FMS, VOR/ADF/FMS bearing source). Integration: all avionics stream live via the X-Plane Web API (new datarefs for N1/N2/ITT, radar-alt, AOA, hydraulics, trim, flaps/slats/gear, control positions, ADF, mach, yaw-damper); the existing fms-sync.lua drives the Citation's built-in FMS (aircraft-agnostic XPLM FMS SDK). Demo seeds added so every panel renders offline. Verified headless via Playwright (no console errors; G1000/GA profiles unaffected). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,353 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { num } from '../../api/useXplane.js';
|
||||
|
||||
// ============================================================================
|
||||
// Cessna Citation X — Primary Flight Display (Honeywell Primus 2000).
|
||||
// Built line-for-line against the X-Plane Citation X manual (pages 30-31):
|
||||
// 1 Attitude · 2 Airspeed scale · 3 Airspeed trend · 4 Heading bug
|
||||
// 5 Desired course (CRS) · 6 Secondary NAV (bearing pointers) · 7 Desired hdg
|
||||
// 8 Minimums · 9 RA/BARO · 10 HSI · 11 STD · 12 BARO SET · 13/14 VSI
|
||||
// 15 CDI · 16 DME · 17 Altimeter setting · 18 Radar altimeter
|
||||
// 19 FD lateral bar · 20 Altitude trend · 21 Altimeter scale · 22 FD vertical bar
|
||||
// All values are live X-Plane datarefs streamed by the bridge (no Lua needed).
|
||||
// ============================================================================
|
||||
|
||||
const PXDEG = 7; // pitch ladder px per degree
|
||||
const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
|
||||
const mod360 = (d) => ((d % 360) + 360) % 360;
|
||||
const hz2mhz = (hz) => (num(hz) / 100).toFixed(2);
|
||||
|
||||
// ── airspeed tape ───────────────────────────────────────────────────────────
|
||||
// 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 }) {
|
||||
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;
|
||||
const top = ias + mid / pxkt, marks = [];
|
||||
for (let s = Math.ceil((ias - mid / pxkt) / 10) * 10; s <= top; s += 10) {
|
||||
if (s < 0) continue;
|
||||
marks.push(
|
||||
<g key={s}>
|
||||
<line x1={70} y1={y(s)} x2={s % 20 === 0 ? 58 : 64} y2={y(s)} stroke="#cfd6dc" strokeWidth="1.4" />
|
||||
{s % 20 === 0 && <text x={54} y={y(s) + 4} fontSize="17" fill="#e8edf1" textAnchor="end">{s}</text>}
|
||||
</g>,
|
||||
);
|
||||
}
|
||||
return (
|
||||
<g>
|
||||
<rect x={0} y={0} width={74} height={H} fill="#0c1116" opacity="0.82" />
|
||||
<clipPath id="spdclip"><rect x={0} y={0} width={74} height={H} /></clipPath>
|
||||
<g clipPath="url(#spdclip)">
|
||||
{/* low-speed awareness: red below Vso, amber Vso→Vs1 */}
|
||||
<rect x={0} y={y(VSO)} width={8} height={Math.max(0, H - y(VSO))} fill="#c0392b" />
|
||||
<rect x={0} y={y(VS1)} width={8} height={Math.max(0, y(VSO) - y(VS1))} fill="#ffb000" />
|
||||
{/* Vmo/Mmo barber pole: overspeed band from the top down to the Vmo line */}
|
||||
<rect x={0} y={0} width={8} height={clamp(y(vmo), 0, H)} fill="url(#barber)" />
|
||||
{/* Vfe flap-limit marker */}
|
||||
<line x1={0} y1={y(VFE)} x2={12} y2={y(VFE)} stroke="#fff" strokeWidth="3" />
|
||||
{marks}
|
||||
</g>
|
||||
<defs>
|
||||
<pattern id="barber" width="8" height="8" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
|
||||
<rect width="8" height="8" fill="#fff" /><rect width="4" height="8" fill="#c0392b" />
|
||||
</pattern>
|
||||
</defs>
|
||||
{/* selected-speed bug (magenta) */}
|
||||
{bug > 20 && <polygon points={`74,${clamp(y(bug), 6, H - 6)} 64,${clamp(y(bug), 6, H - 6) - 7} 64,${clamp(y(bug), 6, H - 6) + 7}`} fill="#d24bd2" />}
|
||||
{/* current readout box */}
|
||||
<polygon points={`0,${mid - 20} 60,${mid - 20} 74,${mid} 60,${mid + 20} 0,${mid + 20}`} fill="#000" stroke="#cfd6dc" strokeWidth="1.4" />
|
||||
<text x={50} y={mid + 8} fontSize="26" fill="#fff" textAnchor="end" fontWeight="700">{Math.round(ias)}</text>
|
||||
{mach >= 0.4 && <text x={40} y={H - 6} fontSize="16" fill="#13e000" textAnchor="middle">M{mach.toFixed(2).slice(1)}</text>}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
// ── AOA index (manual p22): normalised 0 (zero-lift) … 1.0 (stall); the
|
||||
// pilot keeps AOA below 0.6 (30% margin). alpha≈14° ≈ stall.
|
||||
function AoaIndex({ alpha }) {
|
||||
const n = clamp(alpha / 14, 0, 1.05);
|
||||
const H = 120, y = (v) => H - v / 1.05 * H;
|
||||
return (
|
||||
<g>
|
||||
<text x={0} y={-6} fontSize="11" fill="#9aa6ad" textAnchor="middle">AOA</text>
|
||||
<rect x={-7} y={0} width={14} height={H} fill="#0c1116" stroke="#2a3138" />
|
||||
<rect x={-7} y={0} width={14} height={y(0.85)} fill="#c0392b" opacity="0.55" />
|
||||
<rect x={-7} y={y(0.85)} width={14} height={y(0.6) - y(0.85)} fill="#ffb000" opacity="0.5" />
|
||||
<rect x={-7} y={y(0.6)} width={14} height={H - y(0.6)} fill="#13a800" opacity="0.4" />
|
||||
<polygon points={`8,${y(n)} 18,${y(n) - 6} 18,${y(n) + 6}`} fill="#fff" />
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
// ── altitude tape + VSI ───────────────────────────────────────────────────────
|
||||
function AltTape({ alt, bug, vs, baro, std, baroHpa, minOn, minFt, raBaro }) {
|
||||
const H = 560, mid = H / 2, pxft = 0.32; // px per foot
|
||||
const top = alt + mid / pxft, marks = [];
|
||||
for (let s = Math.ceil((alt - mid / pxft) / 100) * 100; s <= top; s += 100) {
|
||||
const y = mid + (alt - s) * pxft;
|
||||
marks.push(
|
||||
<g key={s}>
|
||||
<line x1={6} y1={y} x2={s % 500 === 0 ? 22 : 16} y2={y} stroke="#cfd6dc" strokeWidth="1.4" />
|
||||
{s % 200 === 0 && <text x={26} y={y + 4} fontSize="16" fill="#e8edf1">{s}</text>}
|
||||
</g>,
|
||||
);
|
||||
}
|
||||
const baroTxt = std ? 'STD' : baroHpa ? `${Math.round(num(baro) * 33.8639)}` : num(baro).toFixed(2);
|
||||
const minY = clamp(mid + (alt - minFt) * pxft, 6, H - 6);
|
||||
return (
|
||||
<g>
|
||||
<rect x={0} y={0} width={120} height={H} fill="#0c1116" opacity="0.82" />
|
||||
<clipPath id="altclip"><rect x={0} y={0} width={120} height={H} /></clipPath>
|
||||
<g clipPath="url(#altclip)">
|
||||
{marks}
|
||||
{/* altitude trend (green, 6 s projection of VSI) */}
|
||||
<rect x={2} y={Math.min(mid, mid - vs / 10 * pxft)} width={4} height={Math.abs(vs / 10 * pxft)} fill="#13e000" />
|
||||
{/* minimums bug (cyan) */}
|
||||
{minOn && <polygon points={`6,${minY} 22,${minY - 7} 22,${minY + 7}`} fill="#19c3e0" />}
|
||||
</g>
|
||||
{/* selected-altitude bug (magenta) */}
|
||||
<polygon points={`0,${clamp(mid + (alt - bug) * pxft, 6, H - 6) - 8} 14,${clamp(mid + (alt - bug) * pxft, 6, H - 6) - 8} 14,${clamp(mid + (alt - bug) * pxft, 6, H - 6) + 8} 0,${clamp(mid + (alt - bug) * pxft, 6, H - 6) + 8}`} fill="#d24bd2" />
|
||||
{/* current readout box */}
|
||||
<polygon points={`120,${mid - 20} 30,${mid - 20} 16,${mid} 30,${mid + 20} 120,${mid + 20}`} fill="#000" stroke="#cfd6dc" strokeWidth="1.4" />
|
||||
<text x={112} y={mid + 8} fontSize="25" fill="#fff" textAnchor="end" fontWeight="700">{Math.round(alt)}</text>
|
||||
{/* baro setting */}
|
||||
<text x={60} y={H + 26} fontSize="17" fill={std ? '#13e000' : '#19c3e0'} textAnchor="middle">{std ? 'STD' : `${baroTxt}${baroHpa ? '' : ''}`}</text>
|
||||
{minOn && <text x={60} y={H + 46} fontSize="13" fill="#19c3e0" textAnchor="middle">{raBaro ? 'RA' : 'BARO'} {Math.round(minFt)}</text>}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
function VSI({ vs }) {
|
||||
const H = 480, mid = H / 2;
|
||||
// non-linear-ish: ±2000 fpm across the scale
|
||||
const y = (fpm) => mid - clamp(fpm, -2500, 2500) / 2500 * (H / 2 - 10);
|
||||
const ticks = [0, 500, 1000, 2000];
|
||||
return (
|
||||
<g>
|
||||
<rect x={0} y={0} width={48} height={H} fill="#0c1116" opacity="0.7" rx="6" />
|
||||
{ticks.map((t) => (
|
||||
<g key={t}>
|
||||
<line x1={0} y1={y(t)} x2={t % 1000 === 0 ? 16 : 10} y2={y(t)} stroke="#9aa6ad" strokeWidth="1.2" />
|
||||
<line x1={0} y1={y(-t)} x2={t % 1000 === 0 ? 16 : 10} y2={y(-t)} stroke="#9aa6ad" strokeWidth="1.2" />
|
||||
{t > 0 && <text x={20} y={y(t) + 4} fontSize="11" fill="#9aa6ad">{t / 1000}</text>}
|
||||
{t > 0 && <text x={20} y={y(-t) + 4} fontSize="11" fill="#9aa6ad">{t / 1000}</text>}
|
||||
</g>
|
||||
))}
|
||||
<line x1={0} y1={mid} x2={48} y2={y(vs)} stroke="#13e000" strokeWidth="3" />
|
||||
{Math.abs(vs) > 100 && <text x={24} y={vs > 0 ? 14 : H - 4} fontSize="14" fill="#13e000" textAnchor="middle" fontWeight="700">{Math.round(vs / 50) * 50}</text>}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
// ── attitude ball ─────────────────────────────────────────────────────────────
|
||||
function Attitude({ pitch, roll, slip, fdP, fdR, fdOn }) {
|
||||
const R = 196, cx = 0, cy = 0;
|
||||
const ladder = [];
|
||||
for (let p = -90; p <= 90; p += 10) {
|
||||
if (p === 0) continue;
|
||||
const y = p * PXDEG;
|
||||
const w = p % 20 === 0 ? 70 : 36;
|
||||
ladder.push(
|
||||
<g key={p}>
|
||||
<line x1={-w / 2} y1={-y} x2={w / 2} y2={-y} stroke="#fff" strokeWidth="2" />
|
||||
{p % 20 === 0 && <>
|
||||
<text x={-w / 2 - 8} y={-y + 5} fontSize="14" fill="#fff" textAnchor="end">{Math.abs(p)}</text>
|
||||
<text x={w / 2 + 8} y={-y + 5} fontSize="14" fill="#fff">{Math.abs(p)}</text>
|
||||
</>}
|
||||
</g>,
|
||||
);
|
||||
}
|
||||
// bank scale arc marks (top)
|
||||
const bankMarks = [-60, -45, -30, -20, -10, 0, 10, 20, 30, 45, 60].map((b) => {
|
||||
const a = (b - 90) * Math.PI / 180, r1 = R, r2 = b % 30 === 0 || b === 0 ? R - 16 : R - 10;
|
||||
return <line key={b} x1={Math.cos(a) * r1} y1={Math.sin(a) * r1} x2={Math.cos(a) * r2} y2={Math.sin(a) * r2} stroke="#fff" strokeWidth={b === 0 ? 0 : 1.6} />;
|
||||
});
|
||||
return (
|
||||
<g>
|
||||
<clipPath id="attclip"><circle cx={cx} cy={cy} r={R} /></clipPath>
|
||||
<g clipPath="url(#attclip)">
|
||||
{/* sky / ground, rotated by roll then pitched */}
|
||||
<g transform={`rotate(${-roll})`}>
|
||||
<g transform={`translate(0 ${pitch * PXDEG})`}>
|
||||
<rect x={-600} y={-1200} width={1200} height={1200} fill="#3a86c8" />
|
||||
<rect x={-600} y={0} width={1200} height={1200} fill="#6b4a2b" />
|
||||
<line x1={-600} y1={0} x2={600} y2={0} stroke="#fff" strokeWidth="2.5" />
|
||||
{ladder}
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
{/* fixed bank pointer + arc */}
|
||||
<g transform={`rotate(${-roll})`}>
|
||||
<polygon points={`0,${-R + 2} -10,${-R + 20} 10,${-R + 20}`} fill="#ffd400" />
|
||||
</g>
|
||||
{bankMarks}
|
||||
<polygon points={`0,${-R - 2} -9,${-R - 18} 9,${-R - 18}`} fill="#fff" />
|
||||
{/* slip/skid trapezoid below the bank pointer */}
|
||||
<g transform={`rotate(${-roll})`}>
|
||||
<rect x={-14 + clamp(slip, -1, 1) * 26} y={-R + 22} width={28} height={7} fill="#ffd400" stroke="#000" strokeWidth="0.6" />
|
||||
</g>
|
||||
{/* fixed aircraft reference (yellow) */}
|
||||
<g>
|
||||
<rect x={-2.5} y={-2.5} width={5} height={5} fill="#ffd400" />
|
||||
<line x1={-90} y1={0} x2={-30} y2={0} stroke="#ffd400" strokeWidth="4" />
|
||||
<line x1={30} y1={0} x2={90} y2={0} stroke="#ffd400" strokeWidth="4" />
|
||||
<line x1={-30} y1={0} x2={-30} y2={12} stroke="#ffd400" strokeWidth="4" />
|
||||
<line x1={30} y1={0} x2={30} y2={12} stroke="#ffd400" strokeWidth="4" />
|
||||
</g>
|
||||
{/* flight-director command bars (magenta V-bars) — #19 lateral / #22 vertical */}
|
||||
{fdOn && (
|
||||
<g transform={`translate(0 ${clamp(-fdP * PXDEG, -70, 70)}) rotate(${clamp(fdR, -30, 30)})`}>
|
||||
<polyline points="-70,16 0,2 70,16" fill="none" stroke="#d24bd2" strokeWidth="4" />
|
||||
</g>
|
||||
)}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
// ── HSI (rotating compass with CDI + bearing pointers) ─────────────────────────
|
||||
function HSI({ hdg, trk, crs, hdgBug, cdi, toFrom, brg1, brg2, srcLabel }) {
|
||||
const R = 150;
|
||||
const card = [];
|
||||
for (let d = 0; d < 360; d += 5) {
|
||||
const a = (d - hdg - 90) * Math.PI / 180, r2 = d % 10 === 0 ? R - 14 : R - 8;
|
||||
card.push(<line key={d} x1={Math.cos(a) * R} y1={Math.sin(a) * R} x2={Math.cos(a) * r2} y2={Math.sin(a) * r2} stroke="#cfd6dc" strokeWidth={d % 30 === 0 ? 1.8 : 1} />);
|
||||
if (d % 30 === 0) {
|
||||
const rt = R - 30, lbl = d === 0 ? 'N' : d === 90 ? 'E' : d === 180 ? 'S' : d === 270 ? 'W' : d / 10;
|
||||
card.push(<text key={`t${d}`} x={Math.cos(a) * rt} y={Math.sin(a) * rt + 5} fontSize="14" fill="#e8edf1" textAnchor="middle">{lbl}</text>);
|
||||
}
|
||||
}
|
||||
const ptr = (deg, color, dbl) => {
|
||||
const a = (deg - hdg) * Math.PI / 180; // 0 = up
|
||||
const x = Math.sin(a), y = -Math.cos(a);
|
||||
return (
|
||||
<g stroke={color} strokeWidth="2.5" fill="none">
|
||||
<line x1={x * (R - 16)} y1={y * (R - 16)} x2={x * 40} y2={y * 40} />
|
||||
{/* arrow head */}
|
||||
<polygon points={`${x * (R - 16)},${y * (R - 16)} ${x * (R - 34) - y * 8},${y * (R - 34) + x * 8} ${x * (R - 34) + y * 8},${y * (R - 34) - x * 8}`} fill={color} />
|
||||
{dbl && <line x1={x * -40} y1={y * -40} x2={x * -(R - 16)} y2={y * -(R - 16)} />}
|
||||
</g>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<g>
|
||||
<circle cx={0} cy={0} r={R} fill="#0a0e12" stroke="#2a3138" strokeWidth="1.5" />
|
||||
<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 */}
|
||||
<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" />
|
||||
{/* CDI bar */}
|
||||
<line x1={clamp(cdi, -2, 2) * 30} y1={-46} x2={clamp(cdi, -2, 2) * 30} y2={46} stroke="#19c3e0" 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" />}
|
||||
</g>
|
||||
{/* bearing pointers — #6 secondary NAV (cyan circle = BRG1, white diamond = BRG2) */}
|
||||
{brg1 != null && ptr(brg1, '#19c3e0', false)}
|
||||
{brg2 != null && ptr(brg2, '#cfd6dc', true)}
|
||||
{/* 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>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CitPFD({ xp }) {
|
||||
const V = xp.values || {};
|
||||
const [std, setStd] = React.useState(false);
|
||||
const [raBaro, setRaBaro] = React.useState(false); // #9 RA/BARO minimums source
|
||||
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);
|
||||
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 srcLabel = num(V.cdiSrc) === 2 ? 'FMS1' : num(V.cdiSrc) === 1 ? 'VOR2' : 'VOR1';
|
||||
const dme = num(V.cdiSrc) === 1 ? num(V.nav2Dme) : num(V.nav1Dme);
|
||||
|
||||
return (
|
||||
<div className="cit-screen">
|
||||
<svg className="cit-pfd" viewBox="0 0 800 940" preserveAspectRatio="xMidYMid meet">
|
||||
<rect x={0} y={0} width={800} height={940} fill="#05080b" />
|
||||
{/* 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>
|
||||
<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>
|
||||
{/* 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) */}
|
||||
<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>
|
||||
|
||||
{/* CRS / HDG digital (#5,#7) */}
|
||||
<g fontSize="15" fontWeight="700">
|
||||
<text x={20} y={560} fill="#19c3e0">CRS</text>
|
||||
<text x={20} y={580} fill="#19c3e0" fontSize="20">{String(Math.round(mod360(crs))).padStart(3, '0')}</text>
|
||||
<text x={20} y={642} fill="#d24bd2">HDG</text>
|
||||
<text x={20} y={662} fill="#d24bd2" fontSize="20">{String(Math.round(mod360(hdgBug))).padStart(3, '0')}</text>
|
||||
</g>
|
||||
{/* secondary NAV legend (#6) */}
|
||||
<g fontSize="13">
|
||||
<circle cx={28} cy={726} r="6" fill="none" stroke="#19c3e0" strokeWidth="2" />
|
||||
<text x={42} y={731} fill="#19c3e0">VOR1</text>
|
||||
<rect x={22} y={744} width={12} height={12} fill="none" stroke="#cfd6dc" strokeWidth="2" transform="rotate(45 28 750)" />
|
||||
<text x={42} y={755} fill="#cfd6dc">VOR2</text>
|
||||
</g>
|
||||
{/* DME (#16) */}
|
||||
{dme > 0 && <text x={760} y={620} fontSize="16" fill="#13e000" textAnchor="end">{dme.toFixed(1)}NM</text>}
|
||||
{/* radar altimeter (#18) — only within 2500 ft AGL */}
|
||||
{radAlt < 2500 && <text x={400} y={420} fontSize="22" fill="#13e000" textAnchor="middle" fontWeight="700">{Math.round(radAlt)}</text>}
|
||||
{radAlt < 2500 && <text x={400} y={436} fontSize="11" fill="#9aa6ad" textAnchor="middle">RA</text>}
|
||||
{/* TCAS label */}
|
||||
<text x={690} y={840} fontSize="13" fill="#9aa6ad">TCAS</text>
|
||||
<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) */}
|
||||
<div className="cit-bezel">
|
||||
<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>
|
||||
<span className="cit-bz-val">{min.on ? min.ft : '– – –'}</span>
|
||||
<button className="cit-bz-knob" onClick={() => setMin((m) => ({ ...m, on: true, ft: m.ft + 50 }))}>›</button>
|
||||
<button className={`cit-bz-btn ${min.on ? 'on' : ''}`} onClick={() => setMin((m) => ({ ...m, on: !m.on }))}>MIN</button>
|
||||
</div>
|
||||
<button className={`cit-bz-btn ${raBaro ? 'on' : ''}`} onClick={() => setRaBaro((v) => !v)}>RA/BARO</button>
|
||||
<button className={`cit-bz-btn ${std ? 'on' : ''}`} onClick={() => setStd((v) => !v)}>STD</button>
|
||||
<div className="cit-bz-group">
|
||||
<span className="cit-bz-lbl">BARO SET</span>
|
||||
<button className="cit-bz-knob" onClick={() => xp.command('pfd_baro_down')}>‹</button>
|
||||
<span className="cit-bz-val">{std ? 'STD' : baro.toFixed(2)}</span>
|
||||
<button className="cit-bz-knob" onClick={() => xp.command('pfd_baro_up')}>›</button>
|
||||
</div>
|
||||
<div className="cit-bz-group">
|
||||
<span className="cit-bz-lbl">CRS</span>
|
||||
<button className="cit-bz-knob" onClick={() => xp.command('pfd_crs_down')}>‹</button>
|
||||
<button className="cit-bz-knob" onClick={() => xp.command('pfd_crs_up')}>›</button>
|
||||
</div>
|
||||
<div className="cit-bz-group">
|
||||
<span className="cit-bz-lbl">HDG</span>
|
||||
<button className="cit-bz-knob" onClick={() => xp.command('pfd_hdg_down')}>‹</button>
|
||||
<button className="cit-bz-knob" onClick={() => xp.command('pfd_hdg_sync')}>SYNC</button>
|
||||
<button className="cit-bz-knob" onClick={() => xp.command('pfd_hdg_up')}>›</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user