Этап 4
This commit is contained in:
73
Minint.Core/Services/Impl/Compositor.cs
Normal file
73
Minint.Core/Services/Impl/Compositor.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using Minint.Core.Models;
|
||||
|
||||
namespace Minint.Core.Services.Impl;
|
||||
|
||||
public sealed class Compositor : ICompositor
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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));
|
||||
}
|
||||
77
Minint.Core/Services/Impl/PaletteService.cs
Normal file
77
Minint.Core/Services/Impl/PaletteService.cs
Normal file
@@ -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<int> { 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<RgbaColor>(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]];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
/// <summary>
|
||||
/// TODO: temporary round-trip test — remove after Stage 3 verification.
|
||||
/// Creates a container, serializes to .minint, deserializes, and verifies equality.
|
||||
/// </summary>
|
||||
// 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)
|
||||
|
||||
Reference in New Issue
Block a user