import React from 'react'; import { num } from '../api/useXplane.js'; // Classic analog "six-pack" VFR panel: airspeed, attitude, altimeter, turn // coordinator, heading indicator, vertical speed — round steam gauges driven by // the same X-Plane datarefs. For steam/VFR aircraft (and just because it looks // great). Each gauge is a self-contained SVG on a dark instrument panel. const arr0 = (v, d = 0) => (Array.isArray(v) ? num(v[0], d) : num(v, d)); const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v)); // point on a dial: ang in degrees, 0 = up (12 o'clock), clockwise positive. const pt = (cx, cy, r, ang) => { const a = (ang - 90) * Math.PI / 180; return [cx + r * Math.cos(a), cy + r * Math.sin(a)]; }; function Bezel({ title, children }) { return (
{children} {title}
); } const Needle = ({ ang, len = 70, w = 4, color = '#fff', tail = 14 }) => ( ); // generic tick ring function ticks(min, max, a0, a1, step, big = 1, r = 84, lab) { const out = []; let i = 0; for (let v = min; v <= max + 1e-6; v += step, i++) { const ang = a0 + ((v - min) / (max - min)) * (a1 - a0); const isBig = i % big === 0; const [x1, y1] = pt(100, 100, r, ang); const [x2, y2] = pt(100, 100, r - (isBig ? 12 : 7), ang); out.push(); if (lab && isBig) { const [lx, ly] = pt(100, 100, r - 24, ang); out.push({lab(v)}); } } return out; } /* ---------- Airspeed ---------- */ function ASI({ V }) { const kt = num(V.airspeed); const A0 = -150, A1 = 150, MIN = 0, MAX = 200; const ang = A0 + (clamp(kt, MIN, MAX) - MIN) / (MAX - MIN) * (A1 - A0); const arc = (lo, hi, color, rr, wdt) => { const [x1, y1] = pt(100, 100, rr, A0 + (lo / MAX) * (A1 - A0)); const [x2, y2] = pt(100, 100, rr, A0 + (hi / MAX) * (A1 - A0)); const large = ((hi - lo) / MAX) * 300 > 180 ? 1 : 0; return ; }; return ( {arc(33, 85, '#fff', 70, 4)} {/* white flap arc */} {arc(48, 129, '#21d04a', 78, 5)} {/* green normal */} {arc(129, 163, '#e6c200', 78, 5)}{/* yellow caution */} {(() => { const [x, y] = pt(100, 100, 78, A0 + (163 / MAX) * (A1 - A0)); const [x2, y2] = pt(100, 100, 70, A0 + (163 / MAX) * (A1 - A0)); return ; })()} {ticks(0, 200, A0, A1, 10, 2, 84, (v) => (v % 20 === 0 && v >= 40 ? v : ''))} KT ); } /* ---------- Attitude ---------- */ function AI({ V }) { const pitch = num(V.pitch), roll = num(V.roll); const PPD = 2.0; // px per degree pitch const off = clamp(pitch, -25, 25) * PPD; return ( {[-20, -10, 10, 20].map((d) => ( {d % 20 === 0 && {Math.abs(d)}} ))} {/* bank arc + pointer */} {[-60, -30, -20, -10, 0, 10, 20, 30, 60].map((b) => { const [x1, y1] = pt(100, 100, 84, b); const [x2, y2] = pt(100, 100, 78, b); return ; })} {/* fixed aircraft reference */} ); } /* ---------- Altimeter (3-pointer) ---------- */ function ALT({ V }) { const alt = num(V.altitude), baro = num(V.baro, 29.92); const a100 = (alt % 1000) / 1000 * 360; const a1000 = (alt % 10000) / 10000 * 360; const a10000 = (alt % 100000) / 100000 * 360; return ( {ticks(0, 1000, 0, 360, 100, 1, 84, (v) => (v < 1000 ? v / 100 : ''))} {ticks(0, 1000, 0, 360, 20, 5, 84)} {baro.toFixed(2)} {/* 10000 ft thin pointer */} {/* 1000 ft short fat */} {/* 100 ft long */} ); } /* ---------- Turn coordinator ---------- */ function TC({ V }) { const roll = num(V.roll), slip = num(V.slip); const bank = clamp(roll, -30, 30); // little-plane bank (approx turn rate) const ballX = 100 + clamp(slip, -8, 8) * 3.0; return ( {/* standard-rate marks */} {[-25, 25].map((b) => { const [x1, y1] = pt(100, 100, 80, b); const [x2, y2] = pt(100, 100, 66, b); return ; })} L R {/* little airplane */} 2 MIN {/* inclinometer (slip ball) */} ); } /* ---------- Heading indicator ---------- */ function HI({ V }) { const hdg = ((num(V.heading) % 360) + 360) % 360; const card = []; for (let d = 0; d < 360; d += 5) { const big = d % 30 === 0; const [x1, y1] = pt(100, 100, 84, d); const [x2, y2] = pt(100, 100, 84 - (big ? 12 : 7), d); card.push(); if (big) { const [lx, ly] = pt(100, 100, 62, d); const lbl = d === 0 ? 'N' : d === 90 ? 'E' : d === 180 ? 'S' : d === 270 ? 'W' : d / 10; card.push({lbl}); } } return ( {card} {/* fixed aircraft */} ); } /* ---------- Vertical speed ---------- */ function VSI({ V }) { const vs = clamp(num(V.vspeed), -2000, 2000); // 0 at 9 o'clock (270°), climb sweeps up (toward 0/up), descent down. const ang = 270 + (vs / 2000) * 160; // -2000→110°, 0→270°, +2000→430°(=70°) return ( {ticks(-2000, 2000, 110, 430, 500, 1, 84, (v) => Math.abs(v) / 1000)} {ticks(-2000, 2000, 110, 430, 100, 5, 84)} FPM x1000 0 ); } /* ---------- small gauges for the engine/fuel cluster ---------- */ const KG_GAL = 2.72; function SmallBezel({ title, children }) { return (
{children} {title}
); } // dual half-dial gauge: left needle (lower-left sweep) + right needle (lower-right) function Dual({ title, l, r }) { const sg = (v, min, max, a0, a1) => a0 + (clamp(v, min, max) - min) / (max - min) * (a1 - a0); const sp = (cx, cy, rr, ang) => { const a = (ang - 90) * Math.PI / 180; return [cx + rr * Math.cos(a), cy + rr * Math.sin(a)]; }; const band = (lo, hi, min, max, a0, a1, color) => { const [x1, y1] = sp(60, 60, 46, sg(lo, min, max, a0, a1)); const [x2, y2] = sp(60, 60, 46, sg(hi, min, max, a0, a1)); return ; }; const La = sg(l.value, l.min, l.max, -150, -20), Ra = sg(r.value, r.min, r.max, 20, 150); return ( {l.green && band(l.green[0], l.green[1], l.min, l.max, -150, -20, '#21d04a')} {r.green && band(r.green[0], r.green[1], r.min, r.max, 20, 150, '#21d04a')} {l.tag} {r.tag} ); } /* ---------- tachometer ---------- */ function Tach({ V }) { const rpm = arr0(V.engRpm); const A0 = -150, A1 = 150; const ang = A0 + clamp(rpm, 0, 3500) / 3500 * (A1 - A0); return ( {ticks(0, 3500, A0, A1, 500, 1, 84, (v) => v / 100)} {(() => { const [x1, y1] = pt(100, 100, 84, A0 + 2700 / 3500 * (A1 - A0)); const [x2, y2] = pt(100, 100, 72, A0 + 2700 / 3500 * (A1 - A0)); return ; })()} {(() => { const [x1, y1] = pt(100, 100, 80, A0 + 2100 / 3500 * (A1 - A0)); const [x2, y2] = pt(100, 100, 80, A0 + 2600 / 3500 * (A1 - A0)); return ; })()} RPM x100 {Math.round(rpm)} ); } /* ---------- digital OAT / Volts plate ---------- */ function Clock({ V }) { const oatC = num(V.oat), oatF = Math.round(oatC * 9 / 5 + 32); const volts = arr0(V.volts, 28); return (
{oatF}°FO.A.T.
{volts.toFixed(1)}VOLT
); } export default function VFR({ values: V }) { const fuelL = arr0(V.fuelQty, 0) / KG_GAL, fuelR = (Array.isArray(V.fuelQty) ? num(V.fuelQty[1]) : 0) / KG_GAL; const oilF = arr0(V.oilTemp) * 9 / 5 + 32, oilP = arr0(V.oilPress); const egtF = arr0(V.egt) * 9 / 5 + 32, ffGph = (arr0(V.fuelFlow) * 3600) / KG_GAL; const amps = arr0(V.amps); return (
); }