Desktop Weight & Balance calculator for the Sonaca S2 (also sold as the Sling 2) light aircraft. Built with C# / Avalonia 12 / .NET 10, MVVM via CommunityToolkit.Mvvm.
dotnet build
dotnet run
No extra setup required — all packages restore automatically.
WeightBalance/
├── Models/
│ └── AircraftConstants.cs — all POH constants (arms, envelope, fuel)
├── ViewModels/
│ └── MainViewModel.cs — inputs, computed totals, envelope check
├── Converters/
│ ├── StringToDoubleConverter.cs
│ └── BoolToEnvelopeColorConverter.cs
├── Controls/
│ └── CentogramControl.cs — custom-rendered centogram chart
├── Assets/
│ └── icon.ico — app/window icon (multi-res)
├── App.axaml / App.axaml.cs — FluentTheme + converter resources
├── MainWindow.axaml — two-panel layout (input | centogram)
└── MainWindow.axaml.cs — sets DataContext = new MainViewModel()
Compiled bindings are disabled (AvaloniaUseCompiledBindingsByDefault=false) so
reflection bindings with converters work without x:DataType annotations.
MainWindow.Title includes the maintainer contact
(ronald.de.groot@opendata.nl) after the aircraft name — keep it when editing
the title string.
CG formula (Sonair-S2-D-MAN-001 Rev B, §6-5):
- CG position (mm) = Total Moment / Total Mass
- CG % MAC = 100 × (CG_mm − 612) / 1318 [LEMAC = 612 mm, MAC = 1318 mm]
Envelope vertices (POH §2.7) — defined as (% MAC, kg) pairs in AircraftConstants:
| % MAC | Mass (kg) | POH source |
|---|---|---|
| 20 | 400 | forward lower (extrapolated) |
| 20 | 650 | forward limit ≤ 650 kg (exact) |
| 23 | 750 | forward limit at MTOW (exact) |
| 32 | 750 | aft limit at MTOW (exact) |
| 31 | 650 | aft limit ≤ 650 kg (exact) |
| 31 | 400 | aft lower (extrapolated) |
Fixed station arms (all in mm from datum):
| Station | Arm (mm) |
|---|---|
| Empty aircraft | 866 |
| Pilot & Passengers | 1150 |
| Baggage area 1 | 1500 |
| Baggage area 2 | 2296 |
| Fuel | 706 |
Limits (POH §2.6): MTOW 750 kg; baggage area 1 max 35 kg; area 2 max 25 kg; combined ≤ 35 kg; fuel density 0.72 kg/L; 2 × 70 L tanks.
With default inputs the app should show:
| Field | Value |
|---|---|
| Total mass takeoff | 761.5 kg |
| Total moment takeoff | 694,427 kg·mm |
| CG takeoff | 911.9 mm = 22.76% MAC |
| Takeoff in envelope | false (overweight + forward of 23% limit) |
| Total mass landing (−15 kg burn) | 746.5 kg |
| CG landing | 916.1 mm = 23.07% MAC |
| Landing in envelope | true |
MainViewModel.SuggestedFuelMass finds the largest fuel load (empty/pilot/baggage
held fixed) that keeps the takeoff point at/under MTOW and inside the CG
envelope. It scans down from min(MaxFuelMass, MTOW − non-fuel mass) in 0.05 kg
steps and returns the first mass for which IsPointInEnvelope is true (no
closed-form solve — the envelope's forward/aft limits are piecewise-linear in
mass, so a scan is simplest and cheap enough for a button click).
SuggestedFuelLiters divides by FuelDensity; SuggestedFuelText formats both
for the red hint label under the Fuel row. The ApplySuggestedFuelCommand
([RelayCommand]) writes the suggestion straight into FuelMass. These three
outputs are recalculated in NotifyAllOutputs(), not NotifyLandingOutputs(),
since they depend on the non-fuel masses, not on FuelMass or FuelBurnMass
itself.
DrawBackground → DrawGrid → DrawEnvelope → DrawMtowLine →
DrawAxesAndLabels → DrawMeterMarkers → DrawPoints → DrawTitle → DrawLegend
The control extends Avalonia.Controls.Control directly (not UserControl) and
overrides Render(DrawingContext). Four StyledProperty<double> trigger InvalidateVisual
via AffectsRender<CentogramControl>(...).
MainViewModel uses partial void OnXxxChanged(double value) callbacks generated by
CommunityToolkit to call NotifyAllOutputs() or NotifyLandingOutputs(). All computed
properties are pure getters — no backing fields.