import React, { useState } from 'react'; import { num, systemAlerts } from '../api/useXplane.js'; // The physical GDU bezel of X-Plane's "XPLANE 1000" (its G1000 clone): // title bar, knob columns, the 12 softkeys along the bottom — and, on the MFD, // the autopilot mode controller built into the left bezel (just like the sim). // // EVERY control is clickable and fires the matching real X-Plane command // (sim/GPS/g1000n1_* on the PFD, g1000n3_* on the MFD) via xp.command(). // // The PFD softkeys are a two-level menu, exactly like the real unit: // root → [INSET · PFD · CDI · DME · XPDR · IDENT · TMR/REF · NRST · CAUTION] // PFD → [PATHWAY · SYN TERR · HRZN HDG · APTSIGNS · … · BACK] // SYN TERR toggles the 3D synthetic-vision terrain on/off. const PFD_MENU = { root: ['', 'INSET', '', 'PFD', '', 'CDI', 'DME', 'XPDR', 'IDENT', 'TMR/REF', 'NRST', 'CAUTION'], pfd: ['PATHWAY', 'SYN TERR', 'HRZN HDG', 'APTSIGNS', 'ALT UNIT', '', '', '', '', '', '', 'BACK'], // ALT UNIT submenu: barometric pressure units (inHg / hectopascal), like the manual. altunit: ['IN', 'HPA', '', '', '', '', '', '', '', '', '', 'BACK'], // XPDR submenu: standby/on/alt modes, VFR (1200), CODE entry, IDENT. xpdr: ['STBY', 'ON', 'ALT', 'VFR', '', 'CODE', 'IDENT', '', '', '', '', 'BACK'], // CODE entry turns the softkeys into the octal squawk keypad (digits 0–7). xpdrcode: ['0', '1', '2', '3', '4', '5', '6', '7', 'BKSP', '', 'BACK', ''], // INSET submenu: on/off, declutter, base layer, OFF, back. inset: ['INSET', 'DCLTR', '', 'TOPO', 'TERRAIN', '', '', '', '', '', 'OFF', 'BACK'], }; // MFD softkeys are a two-level menu like the real unit. MAP opens the Map-Opt // page; TOPO/TERRAIN/OSM switch the base map; BACK returns. (OSM is our tuned // extra layer in an otherwise-empty slot.) const MFD_MENU = { root: ['ENGINE', 'MAP', '', '', '', '', '', '', '', 'DCLTR', '', ''], mapopt: ['TRAFFIC', 'PROFILE', 'TOPO', 'TERRAIN', 'AIRWAYS', '', 'NEXRAD', 'OSM', '', '', 'BACK', ''], engine: ['DEC FUEL', 'INC FUEL', 'RST FUEL', '', '', '', '', '', '', '', 'BACK', ''], }; // autopilot_state bitfield (best-effort; tweak per aircraft) const AP_BITS = { fd: 1 << 0, hdg: 1 << 1, vs: 1 << 4, flc: 1 << 6, nav: 1 << 8, apr: 1 << 9, vnav: 1 << 11, altHold: 1 << 14, bc: 1 << 18 }; export default function Bezel({ variant = 'pfd', xp, svt3d, onToggleSvt, inset, onSetInset, insetMode, onInsetMode, nrst, onToggleNrst, tmr, onToggleTmr, dme, onToggleDme, alerts, onToggleAlerts, onDirect, onProc, onFpl, onClr, onFms, mapMode, onMapMode, altHpa, onAltUnit, obs, onObs, knobMode = 'arrows', children }) { const u = variant === 'mfd' ? 'mfd' : 'pfd'; // command prefix const fire = (suffix) => xp && xp.command(`${u}_${suffix}`); const [page, setPage] = useState('root'); // softkey menu page const [squawk, setSquawk] = useState(''); // XPDR code being typed const menu = variant === 'mfd' ? MFD_MENU : PFD_MENU; let keys = menu[page] || menu.root; // OBS appears in the PFD root only when a flight-plan leg is active (like the real unit) const hasLeg = (xp?.flightPlan?.waypoints?.length || 0) >= 2; if (variant !== 'mfd' && page === 'root' && hasLeg) { keys = keys.slice(); keys[4] = 'OBS'; } const setBase = (b) => onMapMode && onMapMode((m) => ({ ...m, base: m.base === b ? 'dark' : b })); const xpdrMode = num(xp?.values?.xpdrMode); const setMode = (m) => xp && xp.setDataref('xpdrMode', m); const hasAlerts = systemAlerts(xp?.values).length > 0; // lights the CAUTION key const typeDigit = (d) => { const next = (squawk + d).slice(-4); setSquawk(next); if (next.length === 4) { // 4 octal digits → write & exit xp && xp.setDataref('xpdrCode', parseInt(next, 10)); setSquawk(''); setPage('xpdr'); } }; const onSoftkey = (i, label) => { fire(`softkey${i + 1}`); // mirror to the in-sim G1000 // declutter cycles through 4 levels (0=all … 3=flight plan only), like the manual const cycleDcltr = (setter) => setter && setter((m) => ({ ...m, dcltr: (((m.dcltr || 0) + 1) % 4) })); if (variant === 'mfd') { if (label === 'MAP') setPage('mapopt'); else if (label === 'ENGINE') setPage('engine'); else if (label === 'BACK') setPage('root'); else if (label === 'TOPO') setBase('topo'); // relief on/off else if (label === 'TERRAIN') onMapMode && onMapMode((m) => ({ ...m, terrain: !m.terrain })); // awareness overlay (independent) else if (label === 'OSM') setBase('osm'); else if (label === 'DCLTR') cycleDcltr(onMapMode); else if (label === 'AIRWAYS') onMapMode && onMapMode((m) => ({ ...m, airways: !m.airways })); } else { if (label === 'PFD') setPage('pfd'); else if (label === 'BACK') setPage({ xpdrcode: 'xpdr', altunit: 'pfd' }[page] || 'root'); else if (label === 'SYN TERR') onToggleSvt && onToggleSvt(); else if (label === 'ALT UNIT') setPage('altunit'); else if (label === 'IN') { onAltUnit && onAltUnit(false); setPage('pfd'); } else if (label === 'HPA') { onAltUnit && onAltUnit(true); setPage('pfd'); } else if (label === 'INSET') { if (page === 'root') { onSetInset && onSetInset(true); setPage('inset'); } else onSetInset && onSetInset(!inset); // toggle from within the submenu } else if (label === 'OFF') { onSetInset && onSetInset(false); setPage('root'); } else if (label === 'DCLTR') cycleDcltr(onInsetMode); else if (label === 'TOPO') onInsetMode && onInsetMode((m) => ({ ...m, base: m.base === 'topo' ? 'dark' : 'topo' })); else if (label === 'TERRAIN') onInsetMode && onInsetMode((m) => ({ ...m, terrain: !m.terrain })); else if (label === 'NRST') onToggleNrst && onToggleNrst(); else if (label === 'TMR/REF') onToggleTmr && onToggleTmr(); else if (label === 'DME') onToggleDme && onToggleDme(); else if (label === 'OBS') onObs && onObs(); // suspend / OBS mode (also fires the sim softkey above) else if (label === 'CAUTION') onToggleAlerts && onToggleAlerts(); else if (label === 'XPDR') setPage('xpdr'); else if (label === 'STBY') setMode(1); else if (label === 'ON') setMode(2); else if (label === 'ALT') setMode(3); else if (label === 'VFR') xp && xp.setDataref('xpdrCode', 1200); else if (label === 'CODE') { setSquawk(''); setPage('xpdrcode'); } else if (label === 'IDENT') xp && xp.command('xpdrIdent'); else if (label === 'BKSP') setSquawk((s) => s.slice(0, -1)); else if (page === 'xpdrcode' && /^[0-7]$/.test(label)) typeDigit(label); } }; // which softkey is "lit" right now const isOn = (label) => { if (variant === 'mfd') return (label === 'TOPO' && mapMode?.base === 'topo') || (label === 'TERRAIN' && mapMode?.terrain) || (label === 'OSM' && mapMode?.base === 'osm') || (label === 'DCLTR' && mapMode?.dcltr > 0) || (label === 'AIRWAYS' && mapMode?.airways); return (label === 'SYN TERR' && svt3d) || (label === 'INSET' && inset) || (label === 'NRST' && nrst) || (label === 'TMR/REF' && tmr) || (label === 'DME' && dme) || (label === 'OBS' && obs) || (label === 'CAUTION' && (alerts || hasAlerts)) || (label === 'STBY' && xpdrMode === 1) || (label === 'ON' && xpdrMode === 2) || (label === 'ALT' && xpdrMode === 3) || (label === 'IN' && !altHpa) || (label === 'HPA' && altHpa) || (page === 'inset' && label === 'TOPO' && insetMode?.base === 'topo') || (page === 'inset' && label === 'TERRAIN' && insetMode?.terrain) || (label === 'DCLTR' && insetMode?.dcltr > 0); }; return (
xp && xp.command('nav1Swap')} /> {variant === 'mfd' && xp && }
XPLANE 1000
{children}
{page === 'xpdrcode' && (
SQUAWK {squawk.padEnd(4, '_')}
)} {/* softkey LABELS on the display (lowest line), like the real G1000 */}
{keys.map((s, i) => ( { (s === 'DCLTR' && (mapMode?.dcltr || insetMode?.dcltr)) ? `DCLTR-${mapMode?.dcltr || insetMode?.dcltr}` : s } ))}
{/* physical bezel keys — blank, aligned under the on-screen labels */}
{keys.map((s, i) => (
xp && xp.command('com1Swap')} emerg />
D→MENU FPLPROC CLRENT
); } function BtnG({ fire, cmd, onClick, children }) { return ; } // Autopilot mode controller (left bezel of the MFD). Buttons fire real X-Plane // commands; active modes light up from autopilot_state / servos_on. function APController({ xp }) { const st = num(xp.values.apState); const on = (bit) => (st & bit) !== 0; const eng = num(xp.values.apEngaged) > 0; const B = ({ label, cmd, active }) => ( ); return (
); } // Concentric G1000 knob. The outer ring rotates via the side arrows (‹ ›) and // the mouse wheel; the inner ring via the top/bottom arrows (˄ ˅) and shift+wheel. // Clicking the knob centre fires the push action (PUSH …). The RANGE knob also // pans with a directional cross. function Knob({ label, sub, outer, inner, push, big, joy, pan, fire, mode = 'arrows', swap, emerg, onTurn }) { // turn the outer ring: fire the sim command AND notify (e.g. cycle MFD page) const outerStep = (dir) => { if (!outer) return; fire(dir > 0 ? outer[0] : outer[1]); if (onTurn) onTurn(dir); }; const onWheel = (e) => { if (!outer) return; e.preventDefault(); if (e.shiftKey && inner) fire(e.deltaY < 0 ? inner[0] : inner[1]); else outerStep(e.deltaY < 0 ? 1 : -1); }; const zoneClick = (e) => { const r = e.currentTarget.getBoundingClientRect(); const dx = e.clientX - (r.left + r.width / 2); const dy = e.clientY - (r.top + r.height / 2); const rel = Math.hypot(dx, dy) / (r.width / 2); if (rel < 0.42 && push) { fire(push); return; } // centre → PUSH if (Math.abs(dy) >= Math.abs(dx)) outerStep(dy < 0 ? 1 : -1); else if (inner) fire(dx > 0 ? inner[0] : inner[1]); else outerStep(dx > 0 ? 1 : -1); }; const zones = mode === 'zones'; return (
{swap && } {emerg && EMERG} {label}
{/* arrows mode (touch-friendly): visible ˄‹›˅ buttons. zones mode: click the knob face itself (top/bottom = outer, left/right = inner). */} {!zones && inner && } {!zones && outer && } {!zones && outer && } {!zones && inner && }
{pan && (
)} {sub && {sub}}
); }