Show what a coupler connects as a stripe per connection

Under the coupler's icon runs a stripe for each connection in its own colour -
brake red, multiple-unit blue, high voltage yellow and so on - so the strip
tells the kinds of coupling apart, not just coupled from free. The editor's
checkboxes carry the same colours as the legend.

The ambient coolant flag leaves the coupler flyout: it is the vehicle's and
already sits in the vehicle panel, beside the crew and the wagon number.
This commit is contained in:
maj00r
2026-10-04 19:12:49 +02:00
parent ee87055eed
commit 868e07ff89
3 changed files with 69 additions and 38 deletions

View File

@@ -1,5 +1,8 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media;
using Material.Icons;
using StarterNG.Classes;
@@ -28,21 +31,35 @@ public enum CouplerState
}
/// <summary>
/// How a coupler is drawn on the consist strip, so its state reads at a glance:
/// an icon and a bar under it that ties the two cards, both in the state's colour.
/// Green is left out - on the strip it means selected or turned round.
/// How a coupler is drawn on the consist strip. Under an icon for the coupler as a
/// whole runs a bundle of stripes, one per connection in its own colour, so what
/// is coupled reads at a glance; the coupler editor shows the same colours as its
/// legend. Green is left out - on the strip it means selected or turned round.
/// </summary>
public static class CouplerLook
{
private static readonly IBrush Steel = Solid("#C3CBD3");
private static readonly IBrush Dim = Solid("#6B737B");
private static readonly IBrush Sky = Solid("#4FB8FF");
private static readonly IBrush Violet = Solid("#B08CFF");
private static readonly IBrush Red = Solid("#FF5A5A");
private static readonly IBrush Amber = Solid("#F0A030");
/// <summary>One per bit of <see cref="CouplingBits.BitKeys"/>, in the same order.</summary>
private static readonly IBrush[] BitBrushes =
{
Solid("#C3CBD3"), // mechanical - steel
Solid("#FF5A5A"), // brake pipe - red
Solid("#4FB8FF"), // multiple-unit control - blue
Solid("#FFD43B"), // high voltage - yellow
Solid("#B08CFF"), // gangway - violet
Solid("#FF9F43"), // auxiliary air - orange
Solid("#FF6FB5"), // heating - pink
Solid("#7A838C") // workshop lock - dark steel
};
private static IBrush Solid(string color) => new SolidColorBrush(Color.Parse(color));
public static IBrush BitBrush(int index) => BitBrushes[index];
public static CouplerState StateOf(Coupling coupling, bool trailing)
{
int flags = coupling.AbsFlags;
@@ -67,25 +84,34 @@ public static class CouplerLook
public static IBrush Brush(CouplerState state) => state switch
{
CouplerState.Free => Dim,
CouplerState.MultipleUnit => Sky,
CouplerState.Permanent => Violet,
CouplerState.EndSignals => Red,
CouplerState.NoEndSignals => Amber,
_ => Steel
};
/// <summary>The bar under the icon: none at the back, broken where nothing is coupled.</summary>
public static bool HasBar(CouplerState state) => state is not (CouplerState.EndSignals or CouplerState.NoEndSignals);
/// <summary>The bits that are set, as indexes into <see cref="CouplingBits.BitKeys"/>.</summary>
public static IEnumerable<int> SetBits(Coupling coupling) =>
Enumerable.Range(0, CouplingBits.BitKeys.Length).Where(i => coupling.Has(1 << i));
/// <summary>Thicker where the vehicles do not part in shunting.</summary>
public static double BarThickness(CouplerState state) => state == CouplerState.Permanent ? 5 : 3;
/// <summary>A connection's name with its colour beside it, for the editor's checkboxes.</summary>
public static Control BitLabel(int index) => new StackPanel
{
Orientation = Orientation.Horizontal,
Spacing = 6,
Children =
{
new Border
{
Width = 10, Height = 10, CornerRadius = new CornerRadius(2),
Background = BitBrushes[index], VerticalAlignment = VerticalAlignment.Center
},
new TextBlock { Text = App.Loc[CouplingBits.BitKeys[index]], VerticalAlignment = VerticalAlignment.Center }
}
};
/// <summary>What the coupler connects, by the names the coupler editor uses.</summary>
public static IReadOnlyList<string> Connections(Coupling coupling) =>
Enumerable.Range(0, CouplingBits.BitKeys.Length)
.Where(i => coupling.Has(1 << i))
.Select(i => App.Loc[CouplingBits.BitKeys[i]])
.ToList();
SetBits(coupling).Select(i => App.Loc[CouplingBits.BitKeys[i]]).ToList();
/// <summary>The tooltip: the code as the scenery has it, then what it means.</summary>
public static string Describe(Coupling coupling, bool trailing)

View File

@@ -416,12 +416,11 @@ public sealed class VehicleCards
{
var coupling = Consist.TailCar(item).Coupling;
var icon = new MaterialIcon { Width = 18, Height = 18, HorizontalAlignment = HorizontalAlignment.Center };
var bar = new Line
// Room for all eight, so every icon on the strip sits level.
var stripes = new StackPanel
{
StartPoint = new Point(0, 0),
EndPoint = new Point(22, 0),
HorizontalAlignment = HorizontalAlignment.Center,
StrokeLineCap = PenLineCap.Round
Spacing = 1, Height = 23,
HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Top
};
var coupler = new Button
{
@@ -429,20 +428,33 @@ public sealed class VehicleCards
MinWidth = 0,
Cursor = _hand,
VerticalAlignment = VerticalAlignment.Center,
Content = new StackPanel { Spacing = 4, Children = { icon, bar } }
Content = new StackPanel { Spacing = 4, Children = { icon, stripes } }
};
coupler.Classes.Add("Basic");
void Restyle()
{
var state = CouplerLook.StateOf(coupling, trailing);
var brush = CouplerLook.Brush(state);
icon.Kind = CouplerLook.Icon(state);
icon.Foreground = brush;
bar.Opacity = CouplerLook.HasBar(state) ? 1 : 0; // kept in the layout, so every icon sits level
bar.Stroke = brush;
bar.StrokeThickness = CouplerLook.BarThickness(state);
bar.StrokeDashArray = state == CouplerState.Free ? new AvaloniaList<double> { 1.5, 1.5 } : null;
icon.Foreground = CouplerLook.Brush(state);
// A stripe per connection, the way the hoses and cables hang between
// the buffers; nothing coupled leaves a broken line.
stripes.Children.Clear();
foreach (int bit in CouplerLook.SetBits(coupling))
stripes.Children.Add(new Border
{
Width = 22, Height = 2, CornerRadius = new CornerRadius(1),
Background = CouplerLook.BitBrush(bit)
});
if (stripes.Children.Count == 0 && !trailing)
stripes.Children.Add(new Line
{
StartPoint = new Point(0, 0), EndPoint = new Point(22, 0),
Stroke = CouplerLook.Brush(state), StrokeThickness = 2,
StrokeDashArray = new AvaloniaList<double> { 2, 2 }
});
ToolTip.SetTip(coupler, CouplerLook.Describe(coupling, trailing));
}
@@ -523,7 +535,7 @@ public sealed class VehicleCards
int bit = 1 << i;
var check = new CheckBox
{
Content = App.Loc[CouplingBits.BitKeys[i]],
Content = CouplerLook.BitLabel(i),
IsChecked = d.Coupling.Has(bit),
FontSize = 12
};
@@ -540,15 +552,8 @@ public sealed class VehicleCards
}
panel.Children.Add(grid);
var thermo = new CheckBox
{
Content = App.Loc["ThermoAmbient"],
IsChecked = d.Coupling.ThermoDynamic,
FontSize = 12,
Margin = new Thickness(0, 2, 0, 0)
};
thermo.IsCheckedChanged += (_, _) => d.Coupling.ThermoDynamic = thermo.IsChecked == true;
panel.Children.Add(thermo);
// The ambient coolant flag rides in the coupling token too, but it is the
// vehicle's: it sits in the vehicle panel, beside the crew and the number.
return new Border { Padding = new Thickness(8), Child = panel };
}

View File

@@ -196,7 +196,7 @@ public sealed class VehicleDetails
int bit = 1 << i;
var check = new CheckBox
{
Content = App.Loc[CouplingBits.BitKeys[i]],
Content = CouplerLook.BitLabel(i),
IsChecked = d.Coupling.Has(bit),
Classes = { "Checklist" }
};