mirror of
https://github.com/haven1433/HexManiacAdvance.git
synced 2026-05-27 18:42:45 -05:00
287 lines
12 KiB
C#
287 lines
12 KiB
C#
using HavenSoft.HexManiac.Core.Models;
|
|
using HavenSoft.HexManiac.Core.Models.Runs.Sprites;
|
|
using HavenSoft.HexManiac.Core.ViewModels.DataFormats;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.Diagnostics;
|
|
using System.Globalization;
|
|
using System.IO.Packaging;
|
|
using System.Linq;
|
|
using System.Windows.Input;
|
|
|
|
namespace HavenSoft.HexManiac.Core.ViewModels.Tools {
|
|
public class PaletteCollection : ViewModelCore {
|
|
private readonly ViewPort viewPort;
|
|
private readonly ChangeHistory<ModelDelta> history;
|
|
|
|
private int sourcePalette;
|
|
public int SourcePalette { get => sourcePalette; set => Set(ref sourcePalette, value); }
|
|
public ObservableCollection<SelectableColor> Elements { get; } = new ObservableCollection<SelectableColor>();
|
|
|
|
public int ColorWidth => (int)Math.Ceiling(Math.Sqrt(Elements.Count));
|
|
public int ColorHeight => (int)Math.Sqrt(Elements.Count);
|
|
|
|
private int selectionStart;
|
|
public int SelectionStart {
|
|
get => selectionStart;
|
|
set {
|
|
var first = Math.Min(selectionStart, selectionEnd);
|
|
var last = Math.Max(selectionStart, selectionEnd);
|
|
if (first <= value && value <= last) {
|
|
for (int i = 0; i < Elements.Count; i++) Elements[i].Selected = first <= i && i <= last;
|
|
return;
|
|
}
|
|
if (!TryUpdate(ref selectionStart, value)) return;
|
|
SelectionEnd = selectionStart;
|
|
if (selectionStart == -1) history.ChangeCompleted();
|
|
}
|
|
}
|
|
|
|
private int selectionEnd;
|
|
public int SelectionEnd {
|
|
get => selectionEnd;
|
|
set {
|
|
TryUpdate(ref selectionEnd, value);
|
|
var first = Math.Min(selectionStart, selectionEnd);
|
|
var last = Math.Max(selectionStart, selectionEnd);
|
|
for (int i = 0; i < Elements.Count; i++) Elements[i].Selected = first <= i && i <= last;
|
|
createGradient?.CanExecuteChanged.Invoke(createGradient, EventArgs.Empty);
|
|
}
|
|
}
|
|
|
|
private int page;
|
|
public int Page { get => page; set => Set(ref page, value); }
|
|
|
|
private bool hasMultiplePages;
|
|
public bool HasMultiplePages { get => hasMultiplePages; set => Set(ref hasMultiplePages, value); }
|
|
|
|
private StubCommand copy;
|
|
public ICommand Copy => StubCommand<IFileSystem>(ref copy, ExecuteCopy, CanExecuteCopy);
|
|
|
|
private StubCommand paste;
|
|
public ICommand Paste => StubCommand<IFileSystem>(ref paste, ExecutePaste, CanExecutePaste);
|
|
|
|
private StubCommand createGradient;
|
|
public ICommand CreateGradient => StubCommand(ref createGradient, ExecuteCreateGradient, CanExecuteCreateGradient);
|
|
|
|
public event EventHandler<int> RequestPageSet;
|
|
|
|
public PaletteCollection(ViewPort viewPort, ChangeHistory<ModelDelta> history) {
|
|
this.viewPort = viewPort;
|
|
this.history = history;
|
|
}
|
|
|
|
public IList<(int index, int direction)> HandleMove(int originalIndex, int newIndex) {
|
|
var otherMovedElements = new List<(int, int)>();
|
|
if (originalIndex == newIndex) return otherMovedElements;
|
|
|
|
var first = Math.Min(selectionStart, selectionEnd);
|
|
var last = Math.Max(selectionStart, selectionEnd);
|
|
newIndex = Math.Max(newIndex, originalIndex - first);
|
|
newIndex = Math.Min(newIndex, Elements.Count - 1 - last + originalIndex);
|
|
var targetElements = Enumerable.Range(first, last - first + 1).ToList();
|
|
|
|
if (originalIndex < newIndex) targetElements.Reverse();
|
|
foreach (var index in targetElements) {
|
|
var element = Elements[index];
|
|
Elements.RemoveAt(index);
|
|
Elements.Insert(newIndex - originalIndex + index, element);
|
|
}
|
|
|
|
for (int i = first; i < newIndex - (originalIndex - first); i++) {
|
|
otherMovedElements.Add((i, last - first + 1));
|
|
}
|
|
for (int i = last; i > newIndex + (last - originalIndex); i--) {
|
|
otherMovedElements.Add((i, first - last - 1));
|
|
}
|
|
|
|
selectionStart += newIndex - originalIndex;
|
|
SelectionEnd += newIndex - originalIndex;
|
|
return otherMovedElements;
|
|
}
|
|
|
|
public void CompleteCurrentInteraction() {
|
|
ReorderPalette();
|
|
history.ChangeCompleted();
|
|
}
|
|
|
|
public void SetContents(IReadOnlyList<short> colors) {
|
|
Elements.Clear();
|
|
foreach (var element in Enumerable.Range(0, colors.Count)
|
|
.Select(i => new SelectableColor { Color = colors[i], Index = i })) {
|
|
Elements.Add(element);
|
|
}
|
|
NotifyPropertyChanged(nameof(ColorWidth));
|
|
NotifyPropertyChanged(nameof(ColorHeight));
|
|
}
|
|
|
|
public void PushColorsToModel() {
|
|
var model = viewPort.Model;
|
|
if (!(model.GetNextRun(sourcePalette) is IPaletteRun source)) return;
|
|
|
|
// update model
|
|
var newPalette = source;
|
|
newPalette = newPalette.SetPalette(model, history.CurrentChange, page, Elements.Select(e => e.Color).ToList());
|
|
if (source.Start != newPalette.Start) viewPort.RaiseMessage($"Palette was moved to {newPalette.Start:X6}. Pointers were updated.");
|
|
|
|
// update UI
|
|
var selectionRange = (selectionStart, selectionEnd);
|
|
Refresh();
|
|
(SelectionStart, SelectionEnd) = selectionRange;
|
|
}
|
|
|
|
private void ReorderPalette() {
|
|
var model = viewPort.Model;
|
|
if (!(model.GetNextRun(sourcePalette) is IPaletteRun source)) return;
|
|
|
|
var oldToNew = Enumerable.Range(0, Elements.Count).Select(i => Elements.IndexOf(Elements.Single(element => element.Index == i))).ToArray();
|
|
var newElements = Enumerable.Range(0, Elements.Count).Select(i => new SelectableColor {
|
|
Index = i,
|
|
Color = Elements[Elements[i].Index].Color,
|
|
Selected = Elements[i].Selected,
|
|
}).ToList();
|
|
|
|
// early exit if there's no actual changes
|
|
if (Enumerable.Range(0, Elements.Count).All(i => Elements[i].Index == newElements[i].Index)) return;
|
|
|
|
var palettesToUpdate = new List<IPaletteRun> { source };
|
|
foreach (var sprite in source.FindDependentSprites(model).Distinct()) {
|
|
var newSprite = sprite;
|
|
|
|
// TODO this doesn't currently work for tilemaps with multiple palettes, such as the first-person-views
|
|
for (int page = 0; page < newSprite.Pages; page++) {
|
|
if (hasMultiplePages) page += this.page;
|
|
var pixels = newSprite.GetPixels(model, page % newSprite.Pages);
|
|
for (int y = 0; y < pixels.GetLength(1); y++) {
|
|
for (int x = 0; x < pixels.GetLength(0); x++) {
|
|
pixels[x, y] = oldToNew[pixels[x, y]];
|
|
}
|
|
}
|
|
newSprite = newSprite.SetPixels(model, history.CurrentChange, page % newSprite.Pages, pixels);
|
|
if (hasMultiplePages) break;
|
|
}
|
|
|
|
if (newSprite.Start != sprite.Start) viewPort.RaiseMessage($"Sprite was moved to {newSprite.Start:X6}. Pointers were updated.");
|
|
palettesToUpdate.AddRange(newSprite.FindRelatedPalettes(model));
|
|
}
|
|
|
|
foreach (var palette in palettesToUpdate.Distinct()) {
|
|
var newPalette = palette;
|
|
var colors = newPalette.GetPalette(model, page);
|
|
var newColors = Enumerable.Range(0, Elements.Count).Select(i => colors[Elements[i].Index]).ToList();
|
|
newPalette = newPalette.SetPalette(model, history.CurrentChange, page, newColors);
|
|
if (palette.Start != newPalette.Start) viewPort.RaiseMessage($"Palette was moved to {newPalette.Start:X6}. Pointers were updated.");
|
|
}
|
|
|
|
Refresh();
|
|
for (int i = 0; i < Elements.Count; i++) Elements[i].Selected = newElements[i].Selected;
|
|
}
|
|
|
|
private void Refresh() {
|
|
var currentPage = page;
|
|
viewPort.Refresh();
|
|
if (hasMultiplePages) RequestPageSet?.Invoke(this, currentPage);
|
|
}
|
|
|
|
#region Commands
|
|
|
|
private void ExecuteCopy(IFileSystem fileSystem) {
|
|
var start = Math.Min(selectionStart, selectionEnd) + 1;
|
|
var end = Math.Max(selectionStart, selectionEnd) + 1;
|
|
var result = UncompressedPaletteColor.Convert(Elements[start - 1].Color);
|
|
for (int i = start; i < end; i++) {
|
|
result += " " + UncompressedPaletteColor.Convert(Elements[i].Color);
|
|
}
|
|
fileSystem.CopyText = result;
|
|
}
|
|
|
|
private bool CanExecuteCopy(IFileSystem fileSystem) => 0 <= selectionStart && selectionStart < Elements.Count;
|
|
|
|
private static IReadOnlyList<short> ParseColor(string stream) {
|
|
var results = new List<short>();
|
|
var parts = stream.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
|
for (int i = 0; i < parts.Length; i++) {
|
|
if (parts[i].Contains(":")) {
|
|
var channels = parts[i].Split(':');
|
|
if (channels.Length != 3) return null;
|
|
if (!int.TryParse(channels[0], out var red) || !int.TryParse(channels[1], out var green) || !int.TryParse(channels[2], out var blue)) return null;
|
|
results.Add(UncompressedPaletteColor.Pack(red, green, blue));
|
|
} else if (parts[i].Length == 4) {
|
|
if (!short.TryParse(parts[i], NumberStyles.HexNumber, CultureInfo.CurrentCulture, out var color)) return null;
|
|
results.Add(color);
|
|
} else if (parts[i].Length == 2 && i + 1 < parts.Length && parts[i + 1].Length == 2) {
|
|
if (!byte.TryParse(parts[i + 0], NumberStyles.HexNumber, CultureInfo.CurrentCulture, out var low)) return null;
|
|
if (!byte.TryParse(parts[i + 1], NumberStyles.HexNumber, CultureInfo.CurrentCulture, out var high)) return null;
|
|
i += 1;
|
|
results.Add((byte)((high << 8) | low));
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
private void ExecutePaste(IFileSystem fileSystem) {
|
|
var model = viewPort.Model;
|
|
if (!(model.GetNextRun(sourcePalette) is IPaletteRun)) return;
|
|
|
|
// paste data into elements
|
|
var colors = ParseColor(fileSystem.CopyText);
|
|
if (colors == null) return;
|
|
var start = Math.Min(selectionStart, selectionEnd);
|
|
for (int i = 0; i < colors.Count; i++) {
|
|
Elements[start].Color = colors[i];
|
|
if (start < Elements.Count - 1) start += 1;
|
|
}
|
|
|
|
PushColorsToModel();
|
|
SelectionStart = start;
|
|
}
|
|
|
|
private bool CanExecutePaste(IFileSystem fileSystem) => CanExecuteCopy(fileSystem) && ParseColor(fileSystem.CopyText) != null;
|
|
|
|
private void ExecuteCreateGradient() {
|
|
var left = Math.Min(SelectionStart, SelectionEnd);
|
|
var (r, g, b) = UncompressedPaletteColor.ToRGB(Elements[left].Color);
|
|
var leftHSB = Theme.ToHSB((byte)(r << 3), (byte)(g << 3), (byte)(b << 3));
|
|
|
|
var right = Math.Max(SelectionStart, SelectionEnd);
|
|
var rightRGB = UncompressedPaletteColor.ToRGB(Elements[right].Color);
|
|
var rightHSB = Theme.ToHSB((byte)(rightRGB.r << 3), (byte)(rightRGB.g << 3), (byte)(rightRGB.b << 3));
|
|
|
|
var deltaHue = rightHSB.hue - leftHSB.hue;
|
|
var deltaSat = rightHSB.sat - leftHSB.sat;
|
|
var deltaBright = rightHSB.bright - leftHSB.bright;
|
|
|
|
var distance = right - left;
|
|
for (int i = 1; i < distance; i++) {
|
|
var part = (double)i / distance;
|
|
var hue = leftHSB.hue + deltaHue * part;
|
|
var sat = leftHSB.sat + deltaSat * part;
|
|
var bright = leftHSB.bright + deltaBright * part;
|
|
var (red, green, blue) = Theme.FromHSB(hue, sat, bright);
|
|
Elements[left + i].Color = UncompressedPaletteColor.Pack(red >> 3, green >> 3, blue >> 3);
|
|
}
|
|
|
|
PushColorsToModel();
|
|
}
|
|
|
|
private bool CanExecuteCreateGradient() => Elements.Count(element => element.Selected) > 2;
|
|
|
|
#endregion
|
|
}
|
|
|
|
[DebuggerDisplay("{Index}:{Color}")]
|
|
public class SelectableColor : ViewModelCore {
|
|
private bool selected;
|
|
public bool Selected { get => selected; set => Set(ref selected, value); }
|
|
|
|
private short color;
|
|
public short Color { get => color; set => Set(ref color, value); }
|
|
|
|
private int index;
|
|
public int Index { get => index; set => Set(ref index, value); }
|
|
}
|
|
}
|