Удалены лишние сервисы
This commit is contained in:
@@ -199,24 +199,20 @@ For a 64×64 image, 1 document, 1 layer, 16-color palette:
|
|||||||
| Interface | Methods (key) |
|
| Interface | Methods (key) |
|
||||||
|------------------------|--------------------------------------------------|
|
|------------------------|--------------------------------------------------|
|
||||||
| `ICompositor` | `uint[] Composite(MinintDocument, int w, int h)` |
|
| `ICompositor` | `uint[] Composite(MinintDocument, int w, int h)` |
|
||||||
| `IPaletteService` | `int EnsureColor(doc, RgbaColor)`, `void CompactPalette(doc)` |
|
|
||||||
| `IDrawingService` | `void ApplyBrush(layer, x, y, radius, colorIdx, w, h)` |
|
| `IDrawingService` | `void ApplyBrush(layer, x, y, radius, colorIdx, w, h)` |
|
||||||
| `IFloodFillService` | `void Fill(layer, x, y, newColorIdx, w, h)` |
|
| `IFloodFillService` | `void Fill(layer, x, y, newColorIdx, w, h)` |
|
||||||
| `IImageEffectService` | `void AdjustContrast(doc, float factor)`, `void ToGrayscale(doc)` |
|
| `IImageEffectService` | `void AdjustContrast(doc, float factor)`, `void ToGrayscale(doc)` |
|
||||||
| `IPatternGenerator` | `MinintDocument Generate(PatternParams)` |
|
| `IPatternGenerator` | `MinintDocument Generate(PatternParams)` |
|
||||||
| `IFragmentService` | `void CopyFragment(src, dst, rect, destPoint)` |
|
|
||||||
|
|
||||||
### 4.3 Core — Service Implementations
|
### 4.3 Core — Service Implementations
|
||||||
|
|
||||||
| Class | Implements | Notes |
|
| Class | Implements | Notes |
|
||||||
|------------------------|-------------------------|------------------------------------|
|
|------------------------|-------------------------|------------------------------------|
|
||||||
| `Compositor` | `ICompositor` | Alpha-blends layers bottom→top |
|
| `Compositor` | `ICompositor` | Alpha-blends layers bottom→top |
|
||||||
| `PaletteService` | `IPaletteService` | Color lookup, add, compact |
|
|
||||||
| `DrawingService` | `IDrawingService` | Circle mask brush/eraser |
|
| `DrawingService` | `IDrawingService` | Circle mask brush/eraser |
|
||||||
| `FloodFillService` | `IFloodFillService` | BFS flood fill on index array |
|
| `FloodFillService` | `IFloodFillService` | BFS flood fill on index array |
|
||||||
| `ImageEffectService` | `IImageEffectService` | Palette-based contrast/grayscale |
|
| `ImageEffectService` | `IImageEffectService` | Palette-based contrast/grayscale |
|
||||||
| `PatternGenerator` | `IPatternGenerator` | Checkerboard, gradient, stripes… |
|
| `PatternGenerator` | `IPatternGenerator` | Checkerboard, gradient, stripes… |
|
||||||
| `FragmentService` | `IFragmentService` | Copy rect with palette merging |
|
|
||||||
|
|
||||||
### 4.4 Infrastructure
|
### 4.4 Infrastructure
|
||||||
|
|
||||||
@@ -294,7 +290,7 @@ while queue not empty:
|
|||||||
|
|
||||||
### 5.4 Palette Compaction
|
### 5.4 Palette Compaction
|
||||||
|
|
||||||
On save (or on demand):
|
Optional optimization (not implemented as a separate service; could be applied on save or on demand):
|
||||||
1. Scan all layers, collect used indices into a `HashSet<int>`.
|
1. Scan all layers, collect used indices into a `HashSet<int>`.
|
||||||
2. Build new palette = only used colors (keep index 0 = transparent).
|
2. Build new palette = only used colors (keep index 0 = transparent).
|
||||||
3. Build old→new index mapping.
|
3. Build old→new index mapping.
|
||||||
@@ -306,12 +302,12 @@ On save (or on demand):
|
|||||||
for each pixel (sx, sy) in source rect:
|
for each pixel (sx, sy) in source rect:
|
||||||
srcIdx = srcLayer.Pixels[sy * W + sx]
|
srcIdx = srcLayer.Pixels[sy * W + sx]
|
||||||
srcColor = srcDoc.Palette[srcIdx]
|
srcColor = srcDoc.Palette[srcIdx]
|
||||||
dstIdx = dstDoc.PaletteService.EnsureColor(srcColor)
|
dstIdx = dstDoc.EnsureColorCached(srcColor)
|
||||||
dstLayer.Pixels[destY * W + destX] = dstIdx
|
dstLayer.Pixels[destY * W + destX] = dstIdx
|
||||||
destX++; ...
|
destX++; ...
|
||||||
```
|
```
|
||||||
|
|
||||||
`EnsureColor` checks if color exists in target palette; if not, appends it and returns the new index.
|
`EnsureColorCached` checks if color exists in target palette; if not, appends it and returns the new index.
|
||||||
|
|
||||||
### 5.6 Contrast (A1)
|
### 5.6 Contrast (A1)
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
using Minint.Core.Models;
|
|
||||||
|
|
||||||
namespace Minint.Core.Services;
|
|
||||||
|
|
||||||
public interface IFragmentService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Copies a rectangular region from one document/layer to another.
|
|
||||||
/// Palette colors are merged: missing colors are added to the destination palette.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="srcDoc">Source document.</param>
|
|
||||||
/// <param name="srcLayerIndex">Index of the source layer.</param>
|
|
||||||
/// <param name="srcX">Source rectangle X origin.</param>
|
|
||||||
/// <param name="srcY">Source rectangle Y origin.</param>
|
|
||||||
/// <param name="regionWidth">Width of the region to copy.</param>
|
|
||||||
/// <param name="regionHeight">Height of the region to copy.</param>
|
|
||||||
/// <param name="dstDoc">Destination document.</param>
|
|
||||||
/// <param name="dstLayerIndex">Index of the destination layer.</param>
|
|
||||||
/// <param name="dstX">Destination X origin.</param>
|
|
||||||
/// <param name="dstY">Destination Y origin.</param>
|
|
||||||
/// <param name="containerWidth">Container width (shared by both docs).</param>
|
|
||||||
/// <param name="containerHeight">Container height (shared by both docs).</param>
|
|
||||||
void CopyFragment(
|
|
||||||
MinintDocument srcDoc, int srcLayerIndex,
|
|
||||||
int srcX, int srcY, int regionWidth, int regionHeight,
|
|
||||||
MinintDocument dstDoc, int dstLayerIndex,
|
|
||||||
int dstX, int dstY,
|
|
||||||
int containerWidth, int containerHeight);
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
using Minint.Core.Models;
|
|
||||||
|
|
||||||
namespace Minint.Core.Services;
|
|
||||||
|
|
||||||
public interface IImageEffectService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Adjusts contrast of the document by modifying palette colors.
|
|
||||||
/// <paramref name="factor"/> > 1 increases contrast, < 1 decreases.
|
|
||||||
/// Index 0 (transparent) is not modified.
|
|
||||||
/// </summary>
|
|
||||||
void AdjustContrast(MinintDocument document, float factor);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converts the document to grayscale by modifying palette colors.
|
|
||||||
/// Uses ITU-R BT.601 luminance: gray = 0.299R + 0.587G + 0.114B.
|
|
||||||
/// Index 0 (transparent) is not modified.
|
|
||||||
/// </summary>
|
|
||||||
void ToGrayscale(MinintDocument document);
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
using Minint.Core.Models;
|
|
||||||
|
|
||||||
namespace Minint.Core.Services;
|
|
||||||
|
|
||||||
public interface IPaletteService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the index of <paramref name="color"/> in the document palette.
|
|
||||||
/// If the color is not present, appends it and returns the new index.
|
|
||||||
/// </summary>
|
|
||||||
int EnsureColor(MinintDocument document, RgbaColor color);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Finds index of an exact color match, or returns -1 if not found.
|
|
||||||
/// </summary>
|
|
||||||
int FindColor(MinintDocument document, RgbaColor color);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes unused colors from the palette and remaps all layer pixel indices.
|
|
||||||
/// Index 0 (transparent) is always preserved.
|
|
||||||
/// </summary>
|
|
||||||
void CompactPalette(MinintDocument document);
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Minint.Core.Models;
|
|
||||||
|
|
||||||
namespace Minint.Core.Services.Impl;
|
|
||||||
|
|
||||||
public sealed class FragmentService : IFragmentService
|
|
||||||
{
|
|
||||||
public void CopyFragment(
|
|
||||||
MinintDocument srcDoc, int srcLayerIndex,
|
|
||||||
int srcX, int srcY, int regionWidth, int regionHeight,
|
|
||||||
MinintDocument dstDoc, int dstLayerIndex,
|
|
||||||
int dstX, int dstY,
|
|
||||||
int containerWidth, int containerHeight)
|
|
||||||
{
|
|
||||||
ArgumentOutOfRangeException.ThrowIfNegative(srcLayerIndex);
|
|
||||||
ArgumentOutOfRangeException.ThrowIfNegative(dstLayerIndex);
|
|
||||||
if (srcLayerIndex >= srcDoc.Layers.Count)
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(srcLayerIndex));
|
|
||||||
if (dstLayerIndex >= dstDoc.Layers.Count)
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(dstLayerIndex));
|
|
||||||
|
|
||||||
var srcLayer = srcDoc.Layers[srcLayerIndex];
|
|
||||||
var dstLayer = dstDoc.Layers[dstLayerIndex];
|
|
||||||
|
|
||||||
int clippedSrcX = Math.Max(srcX, 0);
|
|
||||||
int clippedSrcY = Math.Max(srcY, 0);
|
|
||||||
int clippedEndX = Math.Min(srcX + regionWidth, containerWidth);
|
|
||||||
int clippedEndY = Math.Min(srcY + regionHeight, containerHeight);
|
|
||||||
|
|
||||||
for (int sy = clippedSrcY; sy < clippedEndY; sy++)
|
|
||||||
{
|
|
||||||
int dy = dstY + (sy - srcY);
|
|
||||||
if (dy < 0 || dy >= containerHeight) continue;
|
|
||||||
|
|
||||||
for (int sx = clippedSrcX; sx < clippedEndX; sx++)
|
|
||||||
{
|
|
||||||
int dx = dstX + (sx - srcX);
|
|
||||||
if (dx < 0 || dx >= containerWidth) continue;
|
|
||||||
|
|
||||||
int srcIdx = srcLayer.Pixels[sy * containerWidth + sx];
|
|
||||||
if (srcIdx == 0) continue; // skip transparent
|
|
||||||
|
|
||||||
RgbaColor color = srcDoc.Palette[srcIdx];
|
|
||||||
int dstIdx = dstDoc.EnsureColorCached(color);
|
|
||||||
dstLayer.Pixels[dy * containerWidth + dx] = dstIdx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
using Minint.Core.Models;
|
|
||||||
|
|
||||||
namespace Minint.Core.Services.Impl;
|
|
||||||
|
|
||||||
public sealed class PaletteService : IPaletteService
|
|
||||||
{
|
|
||||||
public int FindColor(MinintDocument document, RgbaColor color)
|
|
||||||
=> document.FindColorCached(color);
|
|
||||||
|
|
||||||
public int EnsureColor(MinintDocument document, RgbaColor color)
|
|
||||||
=> document.EnsureColorCached(color);
|
|
||||||
|
|
||||||
public void CompactPalette(MinintDocument document)
|
|
||||||
{
|
|
||||||
var palette = document.Palette;
|
|
||||||
if (palette.Count <= 1)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var usedIndices = new HashSet<int> { 0 };
|
|
||||||
foreach (var layer in document.Layers)
|
|
||||||
{
|
|
||||||
foreach (int idx in layer.Pixels)
|
|
||||||
usedIndices.Add(idx);
|
|
||||||
}
|
|
||||||
|
|
||||||
var oldToNew = new int[palette.Count];
|
|
||||||
var newPalette = new List<RgbaColor>(usedIndices.Count);
|
|
||||||
|
|
||||||
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]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newPalette.Count == palette.Count)
|
|
||||||
return;
|
|
||||||
|
|
||||||
palette.Clear();
|
|
||||||
palette.AddRange(newPalette);
|
|
||||||
|
|
||||||
foreach (var layer in document.Layers)
|
|
||||||
{
|
|
||||||
var px = layer.Pixels;
|
|
||||||
for (int i = 0; i < px.Length; i++)
|
|
||||||
px[i] = oldToNew[px[i]];
|
|
||||||
}
|
|
||||||
|
|
||||||
document.InvalidatePaletteCache();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
using Minint.Core.Models;
|
|
||||||
using Minint.Core.Services.Impl;
|
|
||||||
|
|
||||||
namespace Minint.Tests;
|
|
||||||
|
|
||||||
public class FragmentServiceTests
|
|
||||||
{
|
|
||||||
private readonly FragmentService _fragment = new();
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void CopyFragment_SameDocument_CopiesPixels()
|
|
||||||
{
|
|
||||||
var doc = new MinintDocument("test");
|
|
||||||
var red = new RgbaColor(255, 0, 0, 255);
|
|
||||||
doc.EnsureColorCached(red);
|
|
||||||
|
|
||||||
var src = new MinintLayer("src", 16);
|
|
||||||
src.Pixels[0] = 1; // (0,0) = red
|
|
||||||
src.Pixels[1] = 1; // (1,0) = red
|
|
||||||
doc.Layers.Add(src);
|
|
||||||
|
|
||||||
var dst = new MinintLayer("dst", 16);
|
|
||||||
doc.Layers.Add(dst);
|
|
||||||
|
|
||||||
_fragment.CopyFragment(doc, 0, 0, 0, 2, 1, doc, 1, 2, 2, 4, 4);
|
|
||||||
|
|
||||||
Assert.Equal(1, dst.Pixels[2 * 4 + 2]); // (2,2)
|
|
||||||
Assert.Equal(1, dst.Pixels[2 * 4 + 3]); // (3,2)
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void CopyFragment_DifferentDocuments_MergesPalette()
|
|
||||||
{
|
|
||||||
var srcDoc = new MinintDocument("src");
|
|
||||||
var blue = new RgbaColor(0, 0, 255, 255);
|
|
||||||
int blueIdx = srcDoc.EnsureColorCached(blue);
|
|
||||||
var srcLayer = new MinintLayer("L1", 4);
|
|
||||||
srcLayer.Pixels[0] = blueIdx;
|
|
||||||
srcDoc.Layers.Add(srcLayer);
|
|
||||||
|
|
||||||
var dstDoc = new MinintDocument("dst");
|
|
||||||
var dstLayer = new MinintLayer("L1", 4);
|
|
||||||
dstDoc.Layers.Add(dstLayer);
|
|
||||||
|
|
||||||
_fragment.CopyFragment(srcDoc, 0, 0, 0, 1, 1, dstDoc, 0, 0, 0, 2, 2);
|
|
||||||
|
|
||||||
int dstBlueIdx = dstDoc.FindColorCached(blue);
|
|
||||||
Assert.True(dstBlueIdx > 0);
|
|
||||||
Assert.Equal(dstBlueIdx, dstLayer.Pixels[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void CopyFragment_TransparentPixels_Skipped()
|
|
||||||
{
|
|
||||||
var doc = new MinintDocument("test");
|
|
||||||
var src = new MinintLayer("src", 4); // all zeros (transparent)
|
|
||||||
doc.Layers.Add(src);
|
|
||||||
|
|
||||||
var dst = new MinintLayer("dst", 4);
|
|
||||||
Array.Fill(dst.Pixels, 0);
|
|
||||||
dst.Pixels[0] = 0; // explicitly 0
|
|
||||||
doc.Layers.Add(dst);
|
|
||||||
|
|
||||||
_fragment.CopyFragment(doc, 0, 0, 0, 2, 2, doc, 1, 0, 0, 2, 2);
|
|
||||||
|
|
||||||
Assert.Equal(0, dst.Pixels[0]); // stays transparent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,5 @@
|
|||||||
using Avalonia;
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using Avalonia;
|
||||||
using Minint.Core.Models;
|
|
||||||
using Minint.Core.Services.Impl;
|
|
||||||
using Minint.Infrastructure.Serialization;
|
|
||||||
|
|
||||||
namespace Minint;
|
namespace Minint;
|
||||||
|
|
||||||
@@ -11,18 +7,7 @@ sealed class Program
|
|||||||
{
|
{
|
||||||
[STAThread]
|
[STAThread]
|
||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
{
|
=> BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
||||||
// TODO: remove --test branch after verification
|
|
||||||
if (args.Length > 0 && args[0] == "--test")
|
|
||||||
{
|
|
||||||
RunRoundTripTest();
|
|
||||||
RunCompositorTest();
|
|
||||||
RunPaletteServiceTest();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static AppBuilder BuildAvaloniaApp()
|
public static AppBuilder BuildAvaloniaApp()
|
||||||
=> AppBuilder.Configure<App>()
|
=> AppBuilder.Configure<App>()
|
||||||
@@ -30,231 +15,4 @@ sealed class Program
|
|||||||
.WithInterFont()
|
.WithInterFont()
|
||||||
.LogToTrace()
|
.LogToTrace()
|
||||||
.With(new X11PlatformOptions { OverlayPopups = true });
|
.With(new X11PlatformOptions { OverlayPopups = true });
|
||||||
|
|
||||||
// TODO: temporary tests — remove after verification stages.
|
|
||||||
|
|
||||||
private static void RunRoundTripTest()
|
|
||||||
{
|
|
||||||
Console.WriteLine("=== Minint Round-Trip Test ===\n");
|
|
||||||
|
|
||||||
var container = new MinintContainer(8, 4);
|
|
||||||
|
|
||||||
var doc1 = container.AddNewDocument("Frame 1");
|
|
||||||
doc1.FrameDelayMs = 200;
|
|
||||||
doc1.Palette.Add(new RgbaColor(255, 0, 0, 255)); // idx 1 = red
|
|
||||||
doc1.Palette.Add(new RgbaColor(0, 255, 0, 255)); // idx 2 = green
|
|
||||||
doc1.Palette.Add(new RgbaColor(0, 0, 255, 128)); // idx 3 = semi-transparent blue
|
|
||||||
var layer1 = doc1.Layers[0];
|
|
||||||
for (int i = 0; i < layer1.Pixels.Length; i++)
|
|
||||||
layer1.Pixels[i] = i % 4; // cycle 0,1,2,3
|
|
||||||
|
|
||||||
doc1.Layers.Add(new MinintLayer("Overlay", container.PixelCount));
|
|
||||||
var layer2 = doc1.Layers[1];
|
|
||||||
layer2.Opacity = 128;
|
|
||||||
layer2.Pixels[0] = 3;
|
|
||||||
layer2.Pixels[5] = 2;
|
|
||||||
|
|
||||||
var doc2 = container.AddNewDocument("Frame 2");
|
|
||||||
doc2.FrameDelayMs = 150;
|
|
||||||
doc2.Palette.Add(new RgbaColor(255, 255, 0, 255)); // idx 1 = yellow
|
|
||||||
var layer3 = doc2.Layers[0];
|
|
||||||
for (int i = 0; i < layer3.Pixels.Length; i++)
|
|
||||||
layer3.Pixels[i] = i % 2;
|
|
||||||
|
|
||||||
Console.WriteLine($"Original: {container.Width}x{container.Height}, {container.Documents.Count} docs");
|
|
||||||
Console.WriteLine($" Doc1: palette={doc1.Palette.Count} colors, layers={doc1.Layers.Count}, indexWidth={doc1.IndexByteWidth}");
|
|
||||||
Console.WriteLine($" Doc2: palette={doc2.Palette.Count} colors, layers={doc2.Layers.Count}, indexWidth={doc2.IndexByteWidth}");
|
|
||||||
|
|
||||||
var serializer = new MinintSerializer();
|
|
||||||
|
|
||||||
using var ms = new MemoryStream();
|
|
||||||
serializer.Write(ms, container);
|
|
||||||
byte[] data = ms.ToArray();
|
|
||||||
Console.WriteLine($"\nSerialized: {data.Length} bytes");
|
|
||||||
Console.WriteLine($" Signature: {System.Text.Encoding.ASCII.GetString(data, 0, 6)}");
|
|
||||||
|
|
||||||
ms.Position = 0;
|
|
||||||
var loaded = serializer.Read(ms);
|
|
||||||
|
|
||||||
Assert(loaded.Width == container.Width, "Width mismatch");
|
|
||||||
Assert(loaded.Height == container.Height, "Height mismatch");
|
|
||||||
Assert(loaded.Documents.Count == container.Documents.Count, "Document count mismatch");
|
|
||||||
|
|
||||||
for (int d = 0; d < container.Documents.Count; d++)
|
|
||||||
{
|
|
||||||
var orig = container.Documents[d];
|
|
||||||
var copy = loaded.Documents[d];
|
|
||||||
Assert(copy.Name == orig.Name, $"Doc[{d}] name mismatch");
|
|
||||||
Assert(copy.FrameDelayMs == orig.FrameDelayMs, $"Doc[{d}] frameDelay mismatch");
|
|
||||||
Assert(copy.Palette.Count == orig.Palette.Count, $"Doc[{d}] palette count mismatch");
|
|
||||||
|
|
||||||
for (int c = 0; c < orig.Palette.Count; c++)
|
|
||||||
Assert(copy.Palette[c] == orig.Palette[c], $"Doc[{d}] palette[{c}] mismatch");
|
|
||||||
|
|
||||||
Assert(copy.Layers.Count == orig.Layers.Count, $"Doc[{d}] layer count mismatch");
|
|
||||||
|
|
||||||
for (int l = 0; l < orig.Layers.Count; l++)
|
|
||||||
{
|
|
||||||
var oLayer = orig.Layers[l];
|
|
||||||
var cLayer = copy.Layers[l];
|
|
||||||
Assert(cLayer.Name == oLayer.Name, $"Doc[{d}].Layer[{l}] name mismatch");
|
|
||||||
Assert(cLayer.IsVisible == oLayer.IsVisible, $"Doc[{d}].Layer[{l}] visibility mismatch");
|
|
||||||
Assert(cLayer.Opacity == oLayer.Opacity, $"Doc[{d}].Layer[{l}] opacity mismatch");
|
|
||||||
Assert(cLayer.Pixels.Length == oLayer.Pixels.Length, $"Doc[{d}].Layer[{l}] pixel count mismatch");
|
|
||||||
|
|
||||||
for (int p = 0; p < oLayer.Pixels.Length; p++)
|
|
||||||
Assert(cLayer.Pixels[p] == oLayer.Pixels[p],
|
|
||||||
$"Doc[{d}].Layer[{l}].Pixels[{p}] mismatch: expected {oLayer.Pixels[p]}, got {cLayer.Pixels[p]}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.WriteLine("\n✓ All assertions passed — round-trip is correct!");
|
|
||||||
|
|
||||||
// Test invalid signature
|
|
||||||
Console.Write("\nTest: invalid signature... ");
|
|
||||||
data[0] = (byte)'X';
|
|
||||||
try
|
|
||||||
{
|
|
||||||
serializer.Read(new MemoryStream(data));
|
|
||||||
Console.WriteLine("FAIL (no exception)");
|
|
||||||
}
|
|
||||||
catch (InvalidDataException)
|
|
||||||
{
|
|
||||||
Console.WriteLine("OK (InvalidDataException)");
|
|
||||||
}
|
|
||||||
data[0] = (byte)'M'; // restore
|
|
||||||
|
|
||||||
// Test truncated stream
|
|
||||||
Console.Write("Test: truncated stream... ");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
serializer.Read(new MemoryStream(data, 0, 10));
|
|
||||||
Console.WriteLine("FAIL (no exception)");
|
|
||||||
}
|
|
||||||
catch (Exception ex) when (ex is InvalidDataException or EndOfStreamException)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"OK ({ex.GetType().Name})");
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
throw new Exception($"Assertion failed: {message}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,10 +21,8 @@ public sealed record ClipboardFragment(int Width, int Height, RgbaColor[] Pixels
|
|||||||
public partial class EditorViewModel : ViewModelBase
|
public partial class EditorViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
private readonly ICompositor _compositor = new Compositor();
|
private readonly ICompositor _compositor = new Compositor();
|
||||||
private readonly IPaletteService _paletteService = new PaletteService();
|
|
||||||
private readonly IDrawingService _drawingService = new DrawingService();
|
private readonly IDrawingService _drawingService = new DrawingService();
|
||||||
private readonly IFloodFillService _floodFillService = new FloodFillService();
|
private readonly IFloodFillService _floodFillService = new FloodFillService();
|
||||||
private readonly IFragmentService _fragmentService = new FragmentService();
|
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
[NotifyPropertyChangedFor(nameof(HasContainer))]
|
[NotifyPropertyChangedFor(nameof(HasContainer))]
|
||||||
@@ -740,7 +738,5 @@ public partial class EditorViewModel : ViewModelBase
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public ICompositor Compositor => _compositor;
|
public ICompositor Compositor => _compositor;
|
||||||
public IPaletteService PaletteService => _paletteService;
|
|
||||||
public IDrawingService DrawingService => _drawingService;
|
public IDrawingService DrawingService => _drawingService;
|
||||||
public IFragmentService FragmentService => _fragmentService;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ dotnet run --project Minint
|
|||||||
dotnet test
|
dotnet test
|
||||||
```
|
```
|
||||||
|
|
||||||
37 тестов покрывают: сериализацию (round-trip), композитинг, инструменты рисования, flood fill, эффекты (контраст, grayscale), генерацию паттернов, копирование фрагмента, экспорт BMP/GIF.
|
34 теста покрывают: сериализацию (round-trip), композитинг, инструменты рисования, flood fill, эффекты (контраст, grayscale), генерацию паттернов, экспорт BMP/GIF.
|
||||||
|
|
||||||
## Управление
|
## Управление
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user