G1000: deepen VNAV/PFD — V-DEV & VS-TGT chevrons, GPS phase, designated toggle

- PFD VNAV: magenta flight-plan target altitude on the alt scale (S.110), V DEV
  deviation scale + chevron (left, shown in VNAV when not on an ILS), VS TGT
  chevron on the VSI (S.113)
- GPS phase annunciation is now dynamic: APR (approach leg) / TERM (<30 nm to
  destination) / ENR, instead of a fixed label
- flight-plan ALT can be toggled designated(blue) <-> reference(white) by clicking
  the cell (S.106); only designated altitudes drive the VNAV profile
- setPlan now preserves the dsgn/appr waypoint flags across the shared plan
- AFCS vertical mode labelled VPTH (manual) instead of VNV

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 12:07:06 +02:00
parent 053d362245
commit 6738e6085b
5 changed files with 52 additions and 14 deletions
+8 -2
View File
@@ -74,7 +74,7 @@ export default function FplPage({ xp, full = false, onClose }) {
for (let i = Math.max(1, active); i < wps.length; i++) {
c += distNm({ lat: pl, lon: po }, wps[i]); pl = wps[i].lat; po = wps[i].lon;
const t = num(wps[i].alt);
if (t > 0 && t < alt - 50) {
if (t > 0 && t < alt - 50 && (wps[i].dsgn ?? true)) {
const tan = Math.tan((3 * Math.PI) / 180);
const vsTgt = -gs * tan * 101.27;
const vsReq = c > 0 ? (t - alt) / (c / gs * 60) : 0;
@@ -106,7 +106,13 @@ export default function FplPage({ xp, full = false, onClose }) {
<span className="r-dtk">{dtk == null ? '___' : `${String(dtk).padStart(3, '0')}°`}</span>
<span className="r-dis">{orig ? '—' : d.toFixed(1)}</span>
<span className="r-cum">{orig ? '—' : cum.toFixed(0)}</span>
<span className={`r-alt ${w.alt ? 'dsgn' : ''}`}>{w.alt ? `${w.alt}FT` : '_____'}</span>
<span className={`r-alt ${w.alt ? ((w.dsgn ?? true) ? 'dsgn' : 'refr') : ''}`}
title={w.alt ? 'Klick: Designated ↔ Reference' : ''}
onClick={(e) => {
e.stopPropagation(); if (!w.alt) return;
const next = wps.map((x, j) => (j === i ? { ...x, dsgn: !(x.dsgn ?? true) } : x));
fp.set({ name: 'ACTIVE', waypoints: next, activeLeg: flightPlan.activeLeg ?? 1 });
}}>{w.alt ? `${w.alt}FT` : '_____'}</span>
<button className="r-del" onClick={(e) => { e.stopPropagation(); fp.remove(i); }}></button>
</div>
))}
+40 -10
View File
@@ -74,7 +74,7 @@ function vnavInfo(V, fp) {
cum += brgDist(prevLat, prevLon, wps[i].lat, wps[i].lon).dist;
prevLat = wps[i].lat; prevLon = wps[i].lon;
const tgt = num(wps[i].alt);
if (tgt > 0 && tgt < alt - 50) {
if (tgt > 0 && tgt < alt - 50 && (wps[i].dsgn ?? true)) {
const tan = Math.tan((VNAV_FPA * Math.PI) / 180);
const tMin = (cum / gs) * 60;
const vsReq = tMin > 0 ? (tgt - alt) / tMin : 0; // fpm to make the fix
@@ -181,6 +181,16 @@ export default function PFD({ values: V, command, connected = true, svt = true,
const nav = activeNav(V, flightPlan);
const vnav = vnavInfo(V, flightPlan);
// GPS phase annunciation: APR when an approach leg is active, TERM within 30 nm
// of the destination, otherwise ENR (manual).
const gpsPhase = (() => {
const wps = flightPlan?.waypoints || [];
if (!wps.length) return 'ENR';
if (wps.some((w) => w.appr)) return 'APR';
const d = wps[wps.length - 1];
const dd = brgDist(num(V.lat), num(V.lon), d.lat, d.lon).dist;
return dd < 30 ? 'TERM' : 'ENR';
})();
const [tune, setTune] = useState(null); // radio being tuned (tap a freq)
// Eased values so the tapes + heading rose glide between X-Plane's ~20 Hz
// samples (VSI a touch softer; attitude is smoothed separately, imperatively).
@@ -219,9 +229,9 @@ export default function PFD({ values: V, command, connected = true, svt = true,
<AFCS V={V} />
<Marker V={V} />
<AirspeedTape V={V} ias={iasS} />
<AltitudeTape V={V} alt={altS} vs={vsS} baroHpa={baroHpa} minimums={minimums} />
<AltitudeTape V={V} alt={altS} vs={vsS} baroHpa={baroHpa} minimums={minimums} vnav={vnav} />
<GlideSlope V={V} />
<HSI V={V} nav={nav} hdg={hdgS} obs={obs} />
<HSI V={V} nav={nav} hdg={hdgS} obs={obs} phase={gpsPhase} />
<HdgCrsBoxes V={V} nav={nav} />
<Wind V={V} />
<DataStrip V={V} />
@@ -386,7 +396,7 @@ function AFCS({ V }) {
]);
const vrt = pick([
['GS', st('gsStatus')], ['ALT', st('altStatus')], ['VS', st('vsStatus')],
['FLC', st('flcStatus')], ['VNV', st('vnavStatus')],
['FLC', st('flcStatus')], ['VPTH', st('vnavStatus')],
]);
if (!lat.act && fd) lat.act = 'ROL'; // wings-level default
if (!vrt.act && fd) vrt.act = 'PIT'; // pitch-hold default
@@ -599,7 +609,7 @@ function AirspeedTape({ V, ias: iasProp }) {
}
/* ---------------- altitude tape + VSI + baro ---------------- */
function AltitudeTape({ V, alt: altProp, vs: vsProp, baroHpa = false, minimums }) {
function AltitudeTape({ V, alt: altProp, vs: vsProp, baroHpa = false, minimums, vnav }) {
const alt = altProp != null ? altProp : num(V.altitude);
const vs = vsProp != null ? vsProp : num(V.vspeed);
const altBug = num(V.apAltBug), baro = num(V.baro, 29.92);
@@ -683,17 +693,37 @@ function AltitudeTape({ V, alt: altProp, vs: vsProp, baroHpa = false, minimums }
)}
</g>
)}
{/* VSI to the right */}
<VSI x={x + W2 + 34} cy={cy} h={h} vs={vs} bug={num(V.apVsBug)} />
{/* VNAV: magenta flight-plan target altitude (upper-right of the scale, S.110)
+ V DEV chevron on a small deviation scale left of the tape (S.113) */}
{vnav && (
<g fontFamily="monospace">
<text x={x + W2 - 4} y={top + 15} textAnchor="end" fill="#ff20ff" fontSize="14">{vnav.tgtAlt}<tspan fill="#c060c0" fontSize="9">FT</tspan></text>
{/* V DEV scale (left) — only in VNAV, not on an ILS (where the GS shows there) */}
{!isILS(V.nav1) && (() => {
const dy = Math.max(-1, Math.min(1, -vnav.vDev / 300)) * (h / 2 - 26);
return (<g>
<line x1={x - 16} y1={cy - (h / 2 - 26)} x2={x - 16} y2={cy + (h / 2 - 26)} stroke="#6a6a6a" strokeWidth="1.2" />
{[-1, 1].map((k) => <circle key={k} cx={x - 16} cy={cy + k * (h / 2 - 26) / 2} r="2.5" fill="none" stroke="#9aa" strokeWidth="1" />)}
<text x={x - 16} y={cy - (h / 2 - 26) - 4} textAnchor="middle" fill="#c060c0" fontSize="9">V</text>
<polygon points={`${x - 9},${cy + dy} ${x - 23},${cy + dy - 7} ${x - 23},${cy + dy + 7}`} fill="#ff20ff" />
</g>);
})()}
</g>
)}
{/* VSI to the right (+ magenta VS TGT chevron in VNAV) */}
<VSI x={x + W2 + 34} cy={cy} h={h} vs={vs} bug={num(V.apVsBug)} vsTgt={vnav?.vsTgt} />
</g>
);
}
function VSI({ x, cy, h, vs, bug }) {
function VSI({ x, cy, h, vs, bug, vsTgt }) {
const max = 2000, top = cy - h / 2 + 10, bot = cy + h / 2 - 10;
const yOf = (v) => cy - (Math.max(-max, Math.min(max, v)) / max) * (h / 2 - 10);
return (
<g fontFamily="monospace">
<rect x={x} y={top} width="30" height={bot - top} fill="#0e1626a0" />
{vsTgt != null && Math.abs(vsTgt) > 20 && (
<polygon points={`${x + 30},${yOf(vsTgt)} ${x + 42},${yOf(vsTgt) - 7} ${x + 42},${yOf(vsTgt) + 7}`} fill="#ff20ff" />
)}
{[2000, 1000, 0, -1000, -2000].map((v) => (
<g key={v}>
<line x1={x} y1={yOf(v)} x2={x + 8} y2={yOf(v)} stroke="#9aa" strokeWidth="2" />
@@ -709,7 +739,7 @@ function VSI({ x, cy, h, vs, bug }) {
}
/* ---------------- HSI compass rose ---------------- */
function HSI({ V, nav, hdg: hdgProp, obs = false }) {
function HSI({ V, nav, hdg: hdgProp, obs = false, phase = 'ENR' }) {
const hdg = hdgProp != null ? hdgProp : ((num(V.heading) % 360) + 360) % 360;
const bug = num(V.apHdgBug);
// CDI source mirrors the in-sim G1000: 2 = GPS (magenta), 0/1 = VLOC1/2 (green).
@@ -762,7 +792,7 @@ function HSI({ V, nav, hdg: hdgProp, obs = false }) {
</g>
{/* CDI source label (GPS magenta / VLOC green) */}
<text x={cx - 56} y={cy - 10} textAnchor="middle" fill={C} fontSize="15">{srcLabel}</text>
{isGps && <text x={cx + 56} y={cy - 10} textAnchor="middle" fill={C} fontSize="15">ENR</text>}
{isGps && <text x={cx + 56} y={cy - 10} textAnchor="middle" fill={C} fontSize="15">{phase}</text>}
{/* course pointer + CDI */}
<g transform={`rotate(${crsA} ${cx} ${cy})`}>
<line x1={cx} y1={cy - r + 18} x2={cx} y2={cy - 40} stroke={C} strokeWidth="4" />