diff --git a/web/src/components/CDU.jsx b/web/src/components/CDU.jsx index ea3a262..7e8afe4 100644 --- a/web/src/components/CDU.jsx +++ b/web/src/components/CDU.jsx @@ -30,8 +30,8 @@ function destPoint(lat, lon, brgDeg, dNm) { return { lat: deg(p2), lon: ((deg(l2) + 540) % 360) - 180 }; } -const PAGE_KEYS = [['fpln', 'FPLN'], ['legs', 'LEGS'], ['deparr', 'DEP/ARR'], ['dir', 'DIR-INTC'], ['fix', 'FIX'], ['vnav', 'VNAV'], ['prog', 'PROG'], ['menu', 'MENU']]; -const LEG_ROWS = 5; +const PAGE_KEYS = [['fpln', 'FPLN'], ['legs', 'LEGS'], ['deparr', 'DEP/ARR'], ['dir', 'DIR-INTC'], ['fix', 'FIX'], ['hold', 'HOLD'], ['vnav', 'VNAV'], ['prog', 'PROG'], ['menu', 'MENU']]; +const LEG_ROWS = 6; const VNAV_PAGES = ['CLB', 'CRZ', 'DES']; // a flight-plan leg with no coordinates is a discontinuity (e.g. a VECTORS or // heading-to-altitude procedure leg) — shown as "DISCONTINUITY" and stitched @@ -67,6 +67,13 @@ export default function CDU({ xp, vnav: vnavCfg, onVnav }) { const [fixRef, setFixRef] = useState(null); // resolved reference navaid {id,lat,lon} const [fixRad, setFixRad] = useState(''); // crossing radial (deg) const [fixDist, setFixDist] = useState(''); // distance along radial (NM) + // HOLD (manual: HOLD page): a holding pattern at a fix — inbound course, + // turn direction, leg time. Defaults to the active waypoint. + const [holdCrs, setHoldCrs] = useState(''); + const [holdTurn, setHoldTurn] = useState('R'); + const [holdTime, setHoldTime] = useState('1.0'); + // STEP (p31): a review cursor stepped through the route with the 6R key. + const [stepIdx, setStepIdx] = useState(null); // MOD/ACT: the EXEC light arms while edits are pending, like the real CDU. const [mod, setMod] = useState(false); // saved-plan list (MENU) @@ -153,8 +160,8 @@ export default function CDU({ xp, vnav: vnavCfg, onVnav }) { if (side === 'L' && r === 0 && scr) return setOrigin(scr); if (side === 'R' && r === 0 && scr) return setDest(scr); if (side === 'R' && r === 1) { if (scr) { setFltNo(scr); setScr(''); } return; } - if (side === 'L' && r === 4) return setPage('menu'); - if (side === 'R' && r === 4) return setPage('vnav'); + if (side === 'L' && r === 5) return setPage('menu'); + if (side === 'R' && r === 5) return setPage('vnav'); return; } if (page === 'legs') { @@ -165,6 +172,10 @@ export default function CDU({ xp, vnav: vnavCfg, onVnav }) { if (i < wps.length && isDisco(wps[i])) { fp.remove(i); setMod(true); return; } // clear discontinuity if (i >= 1 && i < wps.length) { fp.setActive(i); setMod(true); } } + // STEP (6R): advance a review cursor through the route, auto-paging (p31) + if (side === 'R' && r === 5 && wps.length) { + setStepIdx((s) => { const n = ((s == null ? active - 1 : s) + 1) % wps.length; setLegPage(Math.floor(n / LEG_ROWS)); return n; }); + } return; } if (page === 'deparr') { @@ -191,6 +202,12 @@ export default function CDU({ xp, vnav: vnavCfg, onVnav }) { if (side === 'R' && r === 0) return insertFix(); return; } + if (page === 'hold') { + if (side === 'L' && r === 1 && scr) { setHoldCrs(scr); setScr(''); return; } + if (side === 'R' && r === 1) { setHoldTurn((t) => (t === 'R' ? 'L' : 'R')); return; } + if (side === 'L' && r === 2 && scr) { setHoldTime(scr); setScr(''); return; } + return; + } if (page === 'vnav') { if (vnavPage === 0) { // CLB: trans alt (1L), speed/alt limits (2L, 3L) if (side === 'L' && r === 0 && scr) { setTransAlt(scr); setScr(''); return; } @@ -244,17 +261,17 @@ export default function CDU({ xp, vnav: vnavCfg, onVnav }) { const w = wps[i], prev = wps[i - 1]; if (isDisco(w)) { rows.push({ disco: true }); continue; } const linkable = prev && !isDisco(prev); - rows.push({ id: w.id, type: w.type, dtk: linkable ? Math.round(brng(prev, w)) : null, d: linkable ? distNm(prev, w) : 0, orig: i === 0, act: i === active, missed: w.missed }); + rows.push({ id: w.id, type: w.type, dtk: linkable ? Math.round(brng(prev, w)) : null, d: linkable ? distNm(prev, w) : 0, orig: i === 0, act: i === active, missed: w.missed, step: i === stepIdx }); } else if (i === wps.length) rows.push({ add: true }); else rows.push({ blank: true }); } - body = (
{rows.map((row, r) => ( -
+ body = (<>
{rows.map((row, r) => ( +
{row.blank ? · : row.add ? <----- ENTER WPT : row.disco ? ─── DISCONTINUITY ─── : (<>{row.id}{row.type}{row.dtk == null ? '---' : `${String(row.dtk).padStart(3, '0')}°`}{row.orig ? 'ORIG' : row.d.toFixed(1)})} -
))}
); +
))}
STEP through route: 6R{stepIdx != null ? ` · viewing ${wps[stepIdx]?.id || ''}` : ''}
); } else if (page === 'deparr') { const kind = cat === 'sids' ? 'DEPART' : cat === 'stars' ? 'ARRIVAL' : 'APPROACH'; if (selProc) { @@ -317,6 +334,18 @@ export default function CDU({ xp, vnav: vnavCfg, onVnav }) {
REF:1L · RADIAL:2L · DIST:3L · INSERT:1R
); + } else if (page === 'hold') { + const hf = wps[active] || wps[wps.length - 1]; + title = 'HOLD'; + body = ( +
+
{hf?.id || '----'}
+
{holdCrs ? `${holdCrs}°` : '---°'}
+
{holdTurn === 'R' ? 'RIGHT' : 'LEFT'}
+
{holdTime} MIN
+
INBD CRS:2L · TURN:2R · LEG TIME:3L
+
+ ); } else if (page === 'vnav') { title = `ACT VNAV ${VNAV_PAGES[vnavPage]}`; pageNo = `${vnavPage + 1}/3`; if (vnavPage === 0) { // CLB (manual p21-22) @@ -370,7 +399,7 @@ export default function CDU({ xp, vnav: vnavCfg, onVnav }) {
-
{[0, 1, 2, 3, 4].map((r) => )}
+
{[0, 1, 2, 3, 4, 5].map((r) => )}
{title}{pageNo}
{body}
@@ -380,7 +409,7 @@ export default function CDU({ xp, vnav: vnavCfg, onVnav }) { {exportMsg && !msg && {exportMsg.ok ? 'EXPORTED ✓' : exportMsg.error}}
-
{[0, 1, 2, 3, 4].map((r) => )}
+
{[0, 1, 2, 3, 4, 5].map((r) => )}
{/* page keys */} diff --git a/web/src/styles.css b/web/src/styles.css index 8da0467..de6daed 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -670,6 +670,7 @@ body { .cdu-k.fn.exec.arm { background: linear-gradient(#1f8f47, #146b34); color: #fff; border-color: #2ee06a; box-shadow: 0 0 9px rgba(46,224,106,.6); } .cdu-row.disco { background: rgba(255,176,0,.10); } .cdu-row.disco .cdu-add { color: #ffb000; } +.cdu-row.step { box-shadow: inset 3px 0 0 #34e0ff; background: rgba(52,224,255,.10); } /* page-key row + multi-page FMS bodies */ .cdu-pages { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; margin: 2px 0 6px; } .cdu-k.pg { font-size: 11px; letter-spacing: .3px; color: #9fb0bd; padding: 8px 2px; }