Skip to content

Commit 877d177

Browse files
authored
Custom palettes (#108)
* add named colour swatches * add ability to load custom palettes
1 parent 9782a85 commit 877d177

File tree

6 files changed

+154
-27
lines changed

6 files changed

+154
-27
lines changed

Dat/PaletteMap.cs

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ public PaletteMap(string filename)
1313

1414
public PaletteMap(Image<Rgba32> img)
1515
{
16-
Palette = new (Color, byte)[256];
16+
Verify.AreEqual(16, img.Height);
17+
Verify.AreEqual(16, img.Height);
18+
19+
Palette = new (Color, byte)[img.Width * img.Height];
1720
for (var y = 0; y < img.Height; ++y)
1821
{
1922
for (var x = 0; x < img.Width; ++x)
@@ -26,23 +29,21 @@ public PaletteMap(Image<Rgba32> img)
2629

2730
public PaletteMap(Color[] _palette)
2831
{
29-
_ = Verify.Equals(_palette.Length, 256);
30-
Palette = new (Color, byte)[256];
32+
Verify.AreEqual(256, _palette.Length);
33+
Palette = new (Color, byte)[_palette.Length];
3134

32-
for (var i = 0; i < 256; ++i)
35+
for (var i = 0; i < _palette.Length; ++i)
3336
{
3437
Palette[i] = (_palette[i], (byte)i);
3538
}
3639
}
3740

3841
public (Color Color, byte Index)[] Palette { get; set; }
3942

40-
public Color[] PaletteColours => Palette.Select(x => x.Color).ToArray();
41-
4243
public (Color Color, byte Index) Transparent
4344
=> (Color.FromRgba(0, 0, 0, 0), 0); //Palette[0];
4445

45-
public (Color Color, byte Index)[] DirectXReserved
46+
public (Color Color, byte Index)[] TextRendering
4647
=> Palette[1..7];
4748

4849
public (Color Color, byte Index)[] PrimaryRemapColours
@@ -58,7 +59,57 @@ public PaletteMap(Color[] _palette)
5859
=> [.. Palette[10..202], .. Palette[214..246]];
5960

6061
public (Color Color, byte Index)[] ReservedColours
61-
=> [Transparent, .. DirectXReserved, .. PrimaryRemapColours, .. SecondaryRemapColours, ChunkedTransparent];
62+
=> [Transparent, .. TextRendering, .. PrimaryRemapColours, .. SecondaryRemapColours, ChunkedTransparent];
63+
64+
#region Colour Swatches
65+
66+
public (Color Color, byte Index)[] Black
67+
=> Palette[10..22];
68+
public (Color Color, byte Index)[] Bronze
69+
=> Palette[22..34];
70+
public (Color Color, byte Index)[] Copper
71+
=> Palette[34..46];
72+
public (Color Color, byte Index)[] Yellow
73+
=> Palette[46..58];
74+
public (Color Color, byte Index)[] Rose
75+
=> Palette[58..70];
76+
public (Color Color, byte Index)[] GrassGreen
77+
=> Palette[70..82];
78+
public (Color Color, byte Index)[] AvocadoGreen
79+
=> Palette[82..94];
80+
public (Color Color, byte Index)[] Green
81+
=> Palette[94..106];
82+
public (Color Color, byte Index)[] Brass
83+
=> Palette[106..118];
84+
public (Color Color, byte Index)[] Lavender
85+
=> Palette[118..130];
86+
public (Color Color, byte Index)[] Blue
87+
=> Palette[130..142];
88+
public (Color Color, byte Index)[] SeaGreen
89+
=> Palette[142..154];
90+
public (Color Color, byte Index)[] Purple
91+
=> Palette[154..166];
92+
public (Color Color, byte Index)[] Red
93+
=> Palette[166..178];
94+
public (Color Color, byte Index)[] Orange
95+
=> Palette[178..190];
96+
public (Color Color, byte Index)[] Teal
97+
=> Palette[190..202];
98+
public (Color Color, byte Index)[] Brown
99+
=> Palette[214..226];
100+
public (Color Color, byte Index)[] Amber
101+
=> [.. Palette[230..240], .. Palette[244..246]];
102+
103+
#endregion
104+
105+
#region Misc Usable Colours
106+
107+
public (Color Color, byte Index)[] MiscGrey
108+
=> [Palette[226], Palette[240], Palette[241], Palette[242]];
109+
public (Color Color, byte Index)[] MiscYellow
110+
=> [Palette[227], Palette[228], Palette[229]];
111+
112+
#endregion
62113

63114
public byte[] ConvertRgba32ImageToG1Data(Image<Rgba32> img, G1ElementFlags flags)
64115
{

Gui/PlatformSpecific.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,10 @@ public static async Task<IReadOnlyList<IStorageFolder>> OpenFolderPicker()
6868
});
6969
}
7070

71-
public static async Task<IReadOnlyList<IStorageFile>> OpenFilePicker()
71+
public static readonly IReadOnlyList<FilePickerFileType> DatFileTypes = [new("Locomotion DAT Files") { Patterns = ["*.dat", "*.DAT"] }];
72+
public static readonly IReadOnlyList<FilePickerFileType> PngFileTypes = [new("PNG Files") { Patterns = ["*.png", "*.PNG"] }];
73+
74+
public static async Task<IReadOnlyList<IStorageFile>> OpenFilePicker(IReadOnlyList<FilePickerFileType> filetypes)
7275
{
7376
// See IoCFileOps project for an example of how to accomplish this.
7477
if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop
@@ -81,7 +84,7 @@ public static async Task<IReadOnlyList<IStorageFile>> OpenFilePicker()
8184
{
8285
Title = "Select a Locomotion object file",
8386
AllowMultiple = false,
84-
FileTypeFilter = [new("Locomotion DAT Files") { Patterns = ["*.dat", "*.DAT"] }]
87+
FileTypeFilter = filetypes,
8588
});
8689
}
8790

Gui/ViewModels/MainWindowViewModel.cs

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,16 @@ public class MainWindowViewModel : ViewModelBase
4040

4141
public ObservableCollection<LogLine> Logs => Model.LoggerObservableLogs;
4242

43-
public ReactiveCommand<Unit, Unit> LoadPalette { get; }
43+
//public ReactiveCommand<Unit, Unit> LoadPalette { get; }
4444

4545
public ReactiveCommand<Unit, Unit> OpenDownloadFolder { get; }
4646
public ReactiveCommand<Unit, Unit> OpenSettingsFolder { get; }
4747
public ReactiveCommand<Unit, Task> OpenSingleObject { get; }
4848
public ReactiveCommand<Unit, Task> OpenG1 { get; }
4949

50+
public ReactiveCommand<Unit, Task> UseDefaultPalette { get; }
51+
public ReactiveCommand<Unit, Task> UseCustomPalette { get; }
52+
5053
public const string GithubApplicationName = "ObjectEditor";
5154
public const string GithubIssuePage = "https://github.com/OpenLoco/ObjectEditor/issues";
5255
public const string GithubLatestReleaseDownloadPage = "https://github.com/OpenLoco/ObjectEditor/releases";
@@ -60,15 +63,15 @@ public class MainWindowViewModel : ViewModelBase
6063
[Reactive]
6164
public string LatestVersionText { get; set; } = "Up-to-date";
6265

66+
const string DefaultPaletteImageString = "avares://ObjectEditor/Assets/palette.png";
67+
Image<Rgba32> DefaultPaletteImage { get; init; }
68+
6369
public MainWindowViewModel()
6470
{
65-
var paletteUri = new Uri("avares://ObjectEditor/Assets/palette.png");
66-
var palette = Image.Load<Rgba32>(AssetLoader.Open(paletteUri));
71+
DefaultPaletteImage = Image.Load<Rgba32>(AssetLoader.Open(new Uri(DefaultPaletteImageString)));
6772

68-
Model = new()
69-
{
70-
PaletteMap = new PaletteMap(palette)
71-
};
73+
Model = new();
74+
LoadDefaultPalette();
7275

7376
FolderTreeViewModel = new FolderTreeViewModel(Model);
7477

@@ -93,6 +96,9 @@ public MainWindowViewModel()
9396
OpenSettingsFolder = ReactiveCommand.Create(() => PlatformSpecific.FolderOpenInDesktop(ObjectEditorModel.SettingsPath));
9497
OpenG1 = ReactiveCommand.Create(LoadG1);
9598

99+
UseDefaultPalette = ReactiveCommand.Create(LoadDefaultPalette);
100+
UseCustomPalette = ReactiveCommand.Create(LoadCustomPalette);
101+
96102
#region Version
97103

98104
_ = this.WhenAnyValue(o => o.ApplicationVersion)
@@ -123,9 +129,42 @@ public MainWindowViewModel()
123129
#endregion
124130
}
125131

132+
async Task LoadDefaultPalette()
133+
{
134+
Model.PaletteMap = new PaletteMap(DefaultPaletteImage);
135+
if (CurrentEditorModel != null)
136+
{
137+
_ = await CurrentEditorModel.ReloadCommand.Execute();
138+
}
139+
}
140+
141+
async Task LoadCustomPalette()
142+
{
143+
// file picker
144+
var openFile = await PlatformSpecific.OpenFilePicker(PlatformSpecific.PngFileTypes);
145+
if (openFile == null)
146+
{
147+
return;
148+
}
149+
150+
var path = openFile.SingleOrDefault()?.Path.LocalPath;
151+
if (path == null)
152+
{
153+
return;
154+
}
155+
156+
//
157+
Model.PaletteMap = new PaletteMap(path);
158+
// could use reactive here, but its simple for now so we won't. just reload the current model, which will in turn reload the images with the new palette
159+
if (CurrentEditorModel != null)
160+
{
161+
_ = await CurrentEditorModel.ReloadCommand.Execute();
162+
}
163+
}
164+
126165
public async Task LoadSingleObject()
127166
{
128-
var openFile = await PlatformSpecific.OpenFilePicker();
167+
var openFile = await PlatformSpecific.OpenFilePicker(PlatformSpecific.DatFileTypes);
129168
if (openFile == null)
130169
{
131170
return;
@@ -153,7 +192,7 @@ public async Task LoadSingleObject()
153192

154193
public async Task LoadG1()
155194
{
156-
var openFile = await PlatformSpecific.OpenFilePicker();
195+
var openFile = await PlatformSpecific.OpenFilePicker(PlatformSpecific.DatFileTypes);
157196
if (openFile == null)
158197
{
159198
return;

Gui/ViewModels/SubObjectTypes/ImageTableViewModel.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,32 @@
2323

2424
namespace AvaGui.ViewModels
2525
{
26+
public enum ColourSwatches
27+
{
28+
Black,
29+
Bronze,
30+
Copper,
31+
Yellow,
32+
Rose,
33+
GrassGreen,
34+
AvocadoGreen,
35+
Green,
36+
Brass,
37+
Lavender,
38+
Blue,
39+
SeaGreen,
40+
Purple,
41+
Red,
42+
Orange,
43+
Teal,
44+
Brown,
45+
Amber,
46+
MiscGrey,
47+
MiscYellow,
48+
PrimaryRemap,
49+
SecondaryRemap,
50+
}
51+
2652
public record SpriteOffset(
2753
[property: JsonPropertyName("path")] string Path,
2854
[property: JsonPropertyName("x")] int16_t X,
@@ -34,6 +60,9 @@ public class ImageTableViewModel : ReactiveObject, IExtraContentViewModel
3460
readonly IImageTableNameProvider NameProvider;
3561
readonly ILogger Logger;
3662

63+
public ColourSwatches[] ColourSwatchesArr { get; } = (ColourSwatches[])Enum.GetValues(typeof(ColourSwatches));
64+
[Reactive] public ColourSwatches SelectedColourSwatch { get; set; } = ColourSwatches.PrimaryRemap;
65+
3766
public ImageTableViewModel(IHasG1Elements g1ElementProvider, IImageTableNameProvider imageNameProvider, PaletteMap paletteMap, IList<Image<Rgba32>> images, ILogger logger)
3867
{
3968
G1Provider = g1ElementProvider;

Gui/Views/MainWindow.axaml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -362,8 +362,6 @@
362362
<TextBlock HorizontalAlignment="Stretch" VerticalAlignment="Center" Margin="4">Export to Directory</TextBlock>
363363
</DockPanel>
364364
</Button>
365-
<!--<Label VerticalAlignment="Center">🔍 Image Scaling (1-10):</Label>
366-
<NumericUpDown Value="{Binding Zoom}" FormatString="0" Watermark="1" Width="108" Minimum="1" Maximum="10" />-->
367365
<Button HorizontalAlignment="Stretch" Margin="2" Padding="2" ToolTip.Tip="Select background colour">
368366
<DockPanel>
369367
<materialIcons:MaterialIcon Kind="Palette" Width="24" Height="24" Margin="2" />
@@ -375,6 +373,7 @@
375373
</Flyout>
376374
</Button.Flyout>
377375
</Button>
376+
<!--<ComboBox ItemsSource="{Binding ColourSwatchesArr}" SelectedItem="{Binding SelectedColourSwatch}" HorizontalAlignment="Stretch" VerticalAlignment="Center" />-->
378377
</StackPanel>
379378
<Grid ColumnDefinitions="*, Auto, 256">
380379
<ScrollViewer>
@@ -477,7 +476,11 @@
477476
<MenuItem Header="Open downloads folder" Command="{Binding OpenDownloadFolder}" />
478477
<MenuItem Header="Open G1.dat" Command="{Binding OpenG1}" />
479478
</MenuItem>
480-
<MenuItem Header="_ObjData" ItemsSource="{Binding ObjDataItems}" Classes="SubItems" >
479+
<MenuItem Header="_Palette">
480+
<MenuItem Header="Load default palette" Command="{Binding UseDefaultPalette}" />
481+
<MenuItem Header="Load custom palette" Command="{Binding UseCustomPalette}" />
482+
</MenuItem>
483+
<MenuItem Header="_Objects" ItemsSource="{Binding ObjDataItems}" Classes="SubItems" >
481484
<MenuItem.Styles>
482485
<Style Selector="MenuItem.SubItems MenuItem" x:DataType="vm:MenuItemViewModel">
483486
<Setter Property="Header" Value="{Binding Name}"/>

Tests/ImagePaletteConversionTests.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@ public class ImagePaletteConversionTests
1212
const string BaseObjDataPath = "Q:\\Games\\Locomotion\\OriginalObjects\\GoG\\";
1313
const string BaseImagePath = "Q:\\Games\\Locomotion\\ExportedImagesFromObjectEditor\\";
1414
const string BasePalettePath = "Q:\\Games\\Locomotion\\Palettes\\";
15+
const string PaletteFileName = "palette.png";
1516
readonly ILogger Logger = new Logger();
1617

1718
[Test]
1819
public void TestWrite00000000ToIndex0()
1920
{
20-
var paletteFile = Path.Combine(BasePalettePath, "palette.png");
21+
var paletteFile = Path.Combine(BasePalettePath, PaletteFileName);
2122
var paletteMap = Image.Load<Rgba32>(paletteFile);
2223
paletteMap[0, 0] = Color.Transparent;
2324
paletteMap.SaveAsPng(paletteFile);
@@ -26,7 +27,7 @@ public void TestWrite00000000ToIndex0()
2627
[Test]
2728
public void PaletteIndex0IsTransparent()
2829
{
29-
var paletteFile = Path.Combine(BasePalettePath, "palette.png");
30+
var paletteFile = Path.Combine(BasePalettePath, PaletteFileName);
3031
var paletteMap = new PaletteMap(paletteFile);
3132

3233
Assert.That(paletteMap.Transparent.Color, Is.EqualTo(Color.Transparent));
@@ -35,11 +36,12 @@ public void PaletteIndex0IsTransparent()
3536
[Test]
3637
public void PaletteHasUniqueColours()
3738
{
38-
var paletteFile = Path.Combine(BasePalettePath, "palette.png");
39+
var paletteFile = Path.Combine(BasePalettePath, PaletteFileName);
3940
var paletteMap = new PaletteMap(paletteFile);
41+
var paletteColours = paletteMap.Palette.Select(x => x.Color).ToArray();
4042

4143
Assert.That(paletteMap.Transparent.Color, Is.EqualTo(Color.Transparent));
42-
Assert.That(paletteMap.PaletteColours.Length, Is.EqualTo(paletteMap.PaletteColours.ToHashSet().Count));
44+
Assert.That(paletteColours.Length, Is.EqualTo(paletteColours.ToHashSet().Count));
4345
}
4446

4547
[TestCase("AIRPORT1.DAT")]
@@ -49,7 +51,7 @@ public void PaletteHasUniqueColours()
4951
//[TestCase("WATER1.DAT")] // these files use different palettes
5052
public void G1ElementToPNGAndBack(string objectSource)
5153
{
52-
var paletteFile = Path.Combine(BasePalettePath, "palette.png");
54+
var paletteFile = Path.Combine(BasePalettePath, PaletteFileName);
5355
var paletteMap = new PaletteMap(paletteFile);
5456
var obj = SawyerStreamReader.LoadFullObjectFromFile(Path.Combine(BaseObjDataPath, objectSource), Logger);
5557

0 commit comments

Comments
 (0)