From 5fdaaaa2bfd238a6d2bf8069201b487309bd925f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=8B=D1=82=D0=BA=D0=BE=D0=B2=20=D0=A0=D0=BE=D0=BC?= =?UTF-8?q?=D0=B0=D0=BD?= Date: Sun, 29 Mar 2026 15:52:12 +0300 Subject: [PATCH] =?UTF-8?q?=D0=AD=D1=82=D0=B0=D0=BF=204?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Minint.Core/Services/Impl/Compositor.cs | 73 +++++++++++ Minint.Core/Services/Impl/PaletteService.cs | 77 ++++++++++++ Minint/Program.cs | 127 ++++++++++++++++++-- 3 files changed, 270 insertions(+), 7 deletions(-) create mode 100644 Minint.Core/Services/Impl/Compositor.cs create mode 100644 Minint.Core/Services/Impl/PaletteService.cs diff --git a/Minint.Core/Services/Impl/Compositor.cs b/Minint.Core/Services/Impl/Compositor.cs new file mode 100644 index 0000000..ce07d54 --- /dev/null +++ b/Minint.Core/Services/Impl/Compositor.cs @@ -0,0 +1,73 @@ +using Minint.Core.Models; + +namespace Minint.Core.Services.Impl; + +public sealed class Compositor : ICompositor +{ + /// + public uint[] Composite(MinintDocument document, int width, int height) + { + int pixelCount = width * height; + var result = new uint[pixelCount]; // starts as 0x00000000 (transparent black) + + var palette = document.Palette; + + foreach (var layer in document.Layers) + { + if (!layer.IsVisible) + continue; + + byte layerOpacity = layer.Opacity; + if (layerOpacity == 0) + continue; + + var pixels = layer.Pixels; + for (int i = 0; i < pixelCount; i++) + { + int idx = pixels[i]; + if (idx == 0) + continue; // transparent — skip + + var src = palette[idx]; + + // Effective source alpha = palette alpha * layer opacity / 255 + int srcA = src.A * layerOpacity / 255; + if (srcA == 0) + continue; + + if (srcA == 255) + { + // Fully opaque — fast path, no blending needed + result[i] = PackArgb(src.R, src.G, src.B, 255); + continue; + } + + // Standard "over" alpha compositing + uint dst = result[i]; + int dstA = (int)(dst >> 24); + int dstR = (int)((dst >> 16) & 0xFF); + int dstG = (int)((dst >> 8) & 0xFF); + int dstB = (int)(dst & 0xFF); + + int outA = srcA + dstA * (255 - srcA) / 255; + if (outA == 0) + continue; + + int outR = (src.R * srcA + dstR * dstA * (255 - srcA) / 255) / outA; + int outG = (src.G * srcA + dstG * dstA * (255 - srcA) / 255) / outA; + int outB = (src.B * srcA + dstB * dstA * (255 - srcA) / 255) / outA; + + result[i] = PackArgb( + (byte)Math.Min(outR, 255), + (byte)Math.Min(outG, 255), + (byte)Math.Min(outB, 255), + (byte)Math.Min(outA, 255)); + } + } + + return result; + } + + private static uint PackArgb(byte r, byte g, byte b, byte a) => + (uint)(b | (g << 8) | (r << 16) | (a << 24)); +} diff --git a/Minint.Core/Services/Impl/PaletteService.cs b/Minint.Core/Services/Impl/PaletteService.cs new file mode 100644 index 0000000..b297910 --- /dev/null +++ b/Minint.Core/Services/Impl/PaletteService.cs @@ -0,0 +1,77 @@ +using Minint.Core.Models; + +namespace Minint.Core.Services.Impl; + +public sealed class PaletteService : IPaletteService +{ + public int FindColor(MinintDocument document, RgbaColor color) + { + var palette = document.Palette; + for (int i = 0; i < palette.Count; i++) + { + if (palette[i] == color) + return i; + } + return -1; + } + + public int EnsureColor(MinintDocument document, RgbaColor color) + { + int idx = FindColor(document, color); + if (idx >= 0) + return idx; + + idx = document.Palette.Count; + document.Palette.Add(color); + return idx; + } + + public void CompactPalette(MinintDocument document) + { + var palette = document.Palette; + if (palette.Count <= 1) + return; + + // 1. Collect indices actually used across all layers + var usedIndices = new HashSet { 0 }; // always keep transparent + foreach (var layer in document.Layers) + { + foreach (int idx in layer.Pixels) + usedIndices.Add(idx); + } + + // 2. Build new palette and old→new mapping + var oldToNew = new int[palette.Count]; + var newPalette = new List(usedIndices.Count); + + // Index 0 (transparent) stays at 0 + newPalette.Add(palette[0]); + oldToNew[0] = 0; + + for (int i = 1; i < palette.Count; i++) + { + if (usedIndices.Contains(i)) + { + oldToNew[i] = newPalette.Count; + newPalette.Add(palette[i]); + } + // unused indices don't get a mapping — they'll never be looked up + } + + // 3. If nothing was removed, skip the remap + if (newPalette.Count == palette.Count) + return; + + // 4. Replace palette + palette.Clear(); + palette.AddRange(newPalette); + + // 5. Remap all pixel arrays + foreach (var layer in document.Layers) + { + var px = layer.Pixels; + for (int i = 0; i < px.Length; i++) + px[i] = oldToNew[px[i]]; + } + } +} diff --git a/Minint/Program.cs b/Minint/Program.cs index 5acf56e..f061ea8 100644 --- a/Minint/Program.cs +++ b/Minint/Program.cs @@ -1,8 +1,8 @@ using Avalonia; using System; -using System.Diagnostics; using System.IO; using Minint.Core.Models; +using Minint.Core.Services.Impl; using Minint.Infrastructure.Serialization; namespace Minint; @@ -12,10 +12,12 @@ sealed class Program [STAThread] public static void Main(string[] args) { - // TODO: remove --test-roundtrip branch after Stage 3 verification - if (args.Length > 0 && args[0] == "--test-roundtrip") + // TODO: remove --test branch after verification + if (args.Length > 0 && args[0] == "--test") { RunRoundTripTest(); + RunCompositorTest(); + RunPaletteServiceTest(); return; } @@ -28,10 +30,8 @@ sealed class Program .WithInterFont() .LogToTrace(); - /// - /// TODO: temporary round-trip test — remove after Stage 3 verification. - /// Creates a container, serializes to .minint, deserializes, and verifies equality. - /// + // TODO: temporary tests — remove after verification stages. + private static void RunRoundTripTest() { Console.WriteLine("=== Minint Round-Trip Test ===\n"); @@ -138,6 +138,119 @@ sealed class Program Console.WriteLine("\n=== All tests passed ==="); } + private static void RunCompositorTest() + { + Console.WriteLine("\n=== Compositor Test ===\n"); + + var compositor = new Compositor(); + const int W = 2, H = 2; + + // Document: 2 layers on a 2x2 canvas + var doc = new MinintDocument("test"); + doc.Palette.Add(new RgbaColor(255, 0, 0, 255)); // idx 1 = opaque red + doc.Palette.Add(new RgbaColor(0, 0, 255, 128)); // idx 2 = semi-transparent blue + + // Bottom layer: all red + var bottom = new MinintLayer("bottom", W * H); + for (int i = 0; i < bottom.Pixels.Length; i++) + bottom.Pixels[i] = 1; + doc.Layers.Add(bottom); + + // Top layer: pixel[0] = semi-blue, rest transparent + var top = new MinintLayer("top", W * H); + top.Pixels[0] = 2; + doc.Layers.Add(top); + + uint[] result = compositor.Composite(doc, W, H); + + // Pixel [1],[2],[3]: only bottom visible → opaque red → 0xFFFF0000 (ARGB) + uint opaqueRed = 0xFF_FF_00_00; + Assert(result[1] == opaqueRed, $"Pixel[1]: expected {opaqueRed:X8}, got {result[1]:X8}"); + Assert(result[2] == opaqueRed, $"Pixel[2]: expected {opaqueRed:X8}, got {result[2]:X8}"); + Assert(result[3] == opaqueRed, $"Pixel[3]: expected {opaqueRed:X8}, got {result[3]:X8}"); + + // Pixel [0]: red(255,0,0,255) under blue(0,0,255,128) + // srcA=128, dstA=255 → outA = 128 + 255*(255-128)/255 = 128+127 = 255 + // outR = (0*128 + 255*255*(127)/255) / 255 = (0 + 255*127)/255 = 127 + // outG = 0 + // outB = (255*128 + 0) / 255 = 128 + uint blended = result[0]; + byte bA = (byte)(blended >> 24); + byte bR = (byte)((blended >> 16) & 0xFF); + byte bG = (byte)((blended >> 8) & 0xFF); + byte bB = (byte)(blended & 0xFF); + + Console.WriteLine($" Blended pixel[0]: A={bA} R={bR} G={bG} B={bB}"); + Assert(bA == 255, $"Pixel[0] A: expected 255, got {bA}"); + Assert(bR >= 125 && bR <= 129, $"Pixel[0] R: expected ~127, got {bR}"); + Assert(bG == 0, $"Pixel[0] G: expected 0, got {bG}"); + Assert(bB >= 126 && bB <= 130, $"Pixel[0] B: expected ~128, got {bB}"); + + // Test hidden layer: hide top, result should be all red + top.IsVisible = false; + uint[] result2 = compositor.Composite(doc, W, H); + Assert(result2[0] == opaqueRed, $"Hidden top: Pixel[0] should be red, got {result2[0]:X8}"); + + // Test layer opacity=0: make top visible but opacity=0 + top.IsVisible = true; + top.Opacity = 0; + uint[] result3 = compositor.Composite(doc, W, H); + Assert(result3[0] == opaqueRed, $"Opacity 0: Pixel[0] should be red, got {result3[0]:X8}"); + + // Test single transparent layer + var emptyDoc = new MinintDocument("empty"); + emptyDoc.Layers.Add(new MinintLayer("bg", W * H)); + uint[] result4 = compositor.Composite(emptyDoc, W, H); + Assert(result4[0] == 0, $"Empty layer: Pixel[0] should be 0x00000000, got {result4[0]:X8}"); + + Console.WriteLine("✓ Compositor tests passed!"); + } + + private static void RunPaletteServiceTest() + { + Console.WriteLine("\n=== PaletteService Test ===\n"); + + var svc = new PaletteService(); + var doc = new MinintDocument("test"); + + // Palette starts with [Transparent] + Assert(doc.Palette.Count == 1, "Initial palette should have 1 entry"); + + // EnsureColor: new color + var red = new RgbaColor(255, 0, 0, 255); + int redIdx = svc.EnsureColor(doc, red); + Assert(redIdx == 1, $"Red index: expected 1, got {redIdx}"); + Assert(doc.Palette.Count == 2, $"Palette count after red: expected 2, got {doc.Palette.Count}"); + + // EnsureColor: same color → same index + int redIdx2 = svc.EnsureColor(doc, red); + Assert(redIdx2 == 1, $"Red re-ensure: expected 1, got {redIdx2}"); + Assert(doc.Palette.Count == 2, "Palette should not grow on duplicate"); + + // FindColor + Assert(svc.FindColor(doc, red) == 1, "FindColor red"); + Assert(svc.FindColor(doc, new RgbaColor(0, 0, 0, 255)) == -1, "FindColor missing"); + + // Compact: add unused color, then compact + var green = new RgbaColor(0, 255, 0, 255); + int greenIdx = svc.EnsureColor(doc, green); // idx 2 + doc.Layers.Add(new MinintLayer("L", 4)); + doc.Layers[0].Pixels[0] = 1; // red used + doc.Layers[0].Pixels[1] = 0; // transparent used + // green (idx 2) is NOT used by any pixel + + Console.WriteLine($" Before compact: palette has {doc.Palette.Count} colors (green at idx {greenIdx})"); + svc.CompactPalette(doc); + Console.WriteLine($" After compact: palette has {doc.Palette.Count} colors"); + + Assert(doc.Palette.Count == 2, $"After compact: expected 2 colors, got {doc.Palette.Count}"); + Assert(doc.Palette[0] == RgbaColor.Transparent, "Palette[0] should be transparent"); + Assert(doc.Palette[1] == red, $"Palette[1] should be red, got {doc.Palette[1]}"); + Assert(doc.Layers[0].Pixels[0] == 1, "Pixel[0] should still map to red (idx 1)"); + + Console.WriteLine("✓ PaletteService tests passed!"); + } + private static void Assert(bool condition, string message) { if (!condition)