033a9d406a
Aligned to the official X-Plane 1000 manual: - NAV radio: active RIGHT / standby LEFT (boxed) per S.12 (COM already correct) - ALT UNIT softkey (IN / HPA) in the PFD submenu, baro readout converts (S.20) - DCLTR cycles 3 levels (land / +NDB / flight-plan only) with DCLTR-n label (S.56) - TOPO and TERRAIN are now independent toggles (relief vs awareness overlay) (S.57) - Barometric MINIMUMS: BARO MIN bug + readout on the altimeter, amber "MINIMUMS" annunciation at/below the decision altitude; set via TMR/REF (lifted to App) - OBS mode: HSI course follows the CRS knob (magenta "OBS"), sequencing suspended - New Audio Panel tab (COM mic/receive, MKR/DME/ADF, intercom, Display Backup) (S.91) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
92 lines
4.0 KiB
React
92 lines
4.0 KiB
React
import React, { useEffect, useRef, useState } from 'react';
|
||
import { num } from '../api/useXplane.js';
|
||
|
||
// G1000 TMR/REF window (PFD). The real unit shows a generic timer plus the
|
||
// reference V-speeds and barometric minimums. This implements the timer
|
||
// (count-up or count-down with START/STOP/RESET) and the V-speed / minimums
|
||
// references with simple on/off bugs. Self-contained — no sim dependency.
|
||
const VSPEEDS = [
|
||
{ key: 'vr', label: 'Vr', def: 55 },
|
||
{ key: 'vx', label: 'Vx', def: 62 },
|
||
{ key: 'vy', label: 'Vy', def: 74 },
|
||
{ key: 'vg', label: 'Vg', def: 68 }, // best glide
|
||
];
|
||
|
||
function fmt(sec) {
|
||
const s = Math.max(0, Math.floor(sec));
|
||
const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), ss = s % 60;
|
||
const pad = (n) => String(n).padStart(2, '0');
|
||
return h > 0 ? `${pad(h)}:${pad(m)}:${pad(ss)}` : `${pad(m)}:${pad(ss)}`;
|
||
}
|
||
|
||
export default function TimerRef({ values, onClose, minimums = { on: false, ft: 500 }, onMinimums }) {
|
||
const [dir, setDir] = useState('up'); // 'up' | 'dn'
|
||
const [running, setRunning] = useState(false);
|
||
const [elapsed, setElapsed] = useState(0); // seconds
|
||
const [target, setTarget] = useState(300); // count-down start (s)
|
||
const [vbugs, setVbugs] = useState({}); // key -> bool (shown on tape, future)
|
||
// Minimums are lifted to App so the PFD altimeter can show the BARO MIN bug.
|
||
const minsOn = minimums.on, mins = minimums.ft;
|
||
const setMins = (fn) => onMinimums && onMinimums((m) => ({ ...m, ft: Math.max(0, typeof fn === 'function' ? fn(m.ft) : fn) }));
|
||
const setMinsOn = (fn) => onMinimums && onMinimums((m) => ({ ...m, on: typeof fn === 'function' ? fn(m.on) : fn }));
|
||
const tickRef = useRef(null);
|
||
|
||
useEffect(() => {
|
||
if (!running) return;
|
||
const t0 = Date.now() - elapsed * 1000;
|
||
tickRef.current = setInterval(() => setElapsed((Date.now() - t0) / 1000), 250);
|
||
return () => clearInterval(tickRef.current);
|
||
}, [running]); // eslint-disable-line
|
||
|
||
const shown = dir === 'dn' ? Math.max(0, target - elapsed) : elapsed;
|
||
const alt = num(values.altitude);
|
||
const belowMins = minsOn && alt > 0 && alt <= mins;
|
||
|
||
const reset = () => { setRunning(false); setElapsed(0); };
|
||
|
||
return (
|
||
<div className="tmr-window">
|
||
<div className="nrst-head">
|
||
<span className="nrst-title">TIMER / REFERENCES</span>
|
||
</div>
|
||
<div className="tmr-body">
|
||
<div className="tmr-clock">{fmt(shown)}</div>
|
||
<div className="tmr-dir">
|
||
<button className={dir === 'up' ? 'on' : ''} onClick={() => { setDir('up'); }}>UP</button>
|
||
<button className={dir === 'dn' ? 'on' : ''} onClick={() => { setDir('dn'); }}>DN</button>
|
||
</div>
|
||
<div className="tmr-ctl">
|
||
<button className="fbtn add" onClick={() => setRunning((r) => !r)}>{running ? 'STOP' : 'START'}</button>
|
||
<button className="fbtn" onClick={reset}>RESET</button>
|
||
</div>
|
||
{dir === 'dn' && (
|
||
<div className="tmr-target">
|
||
<label>FROM</label>
|
||
<button onClick={() => setTarget((t) => Math.max(60, t - 60))}>−</button>
|
||
<span>{fmt(target)}</span>
|
||
<button onClick={() => setTarget((t) => t + 60)}>+</button>
|
||
</div>
|
||
)}
|
||
|
||
<div className="tmr-sec">REFERENCES — V-SPEEDS</div>
|
||
<div className="tmr-vspeeds">
|
||
{VSPEEDS.map((v) => (
|
||
<button key={v.key} className={vbugs[v.key] ? 'on' : ''}
|
||
onClick={() => setVbugs((b) => ({ ...b, [v.key]: !b[v.key] }))}>
|
||
<i>{v.label}</i><b>{v.def}</b><u>KT</u>
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
<div className="tmr-mins">
|
||
<button className={minsOn ? 'on' : ''} onClick={() => setMinsOn((m) => !m)}>MINIMUMS</button>
|
||
<button onClick={() => setMins((m) => Math.max(0, m - 100))}>−</button>
|
||
<span className={belowMins ? 'alert' : ''}>{mins} FT</span>
|
||
<button onClick={() => setMins((m) => m + 100)}>+</button>
|
||
</div>
|
||
{belowMins && <div className="tmr-minalert">MINIMUMS</div>}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|