mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-10 07:34:14 -04:00
Keep cut outlines dark and etch strokes bright green. Assign drawing fills by a golden-angle hue walk with varied saturation/lightness tiers, preserving legacy schemes and rendering-only behavior. Include palette, visibility, persistence, and contrast regressions.
263 lines
13 KiB
C#
263 lines
13 KiB
C#
using System.Drawing;
|
|
using System.Drawing.Drawing2D;
|
|
using OpenNest.CNC;
|
|
using OpenNest.Controls;
|
|
using OpenNest.Geometry;
|
|
using Program = OpenNest.CNC.Program;
|
|
|
|
namespace OpenNest.WinForms.Tests;
|
|
|
|
public class EtchDisplayTests
|
|
{
|
|
private static readonly Color EtchColor = Color.Lime;
|
|
private static readonly Color CutColor = Color.FromArgb(48, 48, 48);
|
|
private static readonly Color PartColor = Color.FromArgb(216, 49, 49);
|
|
|
|
[Theory]
|
|
[InlineData(false, false, Mode.Absolute)]
|
|
[InlineData(false, true, Mode.Absolute)]
|
|
[InlineData(true, false, Mode.Absolute)]
|
|
[InlineData(true, true, Mode.Absolute)]
|
|
[InlineData(false, false, Mode.Incremental)]
|
|
[InlineData(false, true, Mode.Incremental)]
|
|
[InlineData(true, false, Mode.Incremental)]
|
|
[InlineData(true, true, Mode.Incremental)]
|
|
public void PlateSeparatesEtchFromCutWithoutChangingProgram(bool leads, bool selected, Mode mode)
|
|
=> StaTestThread.Run(() =>
|
|
{
|
|
var program = MixedProgram();
|
|
program.Mode = mode;
|
|
var before = Snapshot(program);
|
|
var drawing = new Drawing("Synthetic etch display", program) { Color = PartColor };
|
|
var part = new Part(drawing, new Vector(10, 10)) { HasManualLeadIns = leads };
|
|
using var view = new PlateView(ColorSchemeRegistry.Get("Workshop"));
|
|
view.Matrix = new Matrix();
|
|
var layout = LayoutPart.Create(part, view);
|
|
layout.IsSelected = selected;
|
|
using var image = new Bitmap(130, 130);
|
|
using var g = Graphics.FromImage(image);
|
|
g.Clear(Color.White);
|
|
layout.Draw(g);
|
|
|
|
var renderedFill = image.GetPixel(20, 20);
|
|
if (!selected)
|
|
Assert.Equal(PartColor.ToArgb(), renderedFill.ToArgb());
|
|
else
|
|
Assert.NotEqual(PartColor.ToArgb(), renderedFill.ToArgb());
|
|
Assert.True(Luminance(CutColor) < Luminance(renderedFill));
|
|
Assert.True(Distinctness(EtchColor, renderedFill) >= 15);
|
|
// A closed etch is not a hole; an actual closed cut still is.
|
|
Assert.Equal(image.GetPixel(20, 20), image.GetPixel(40, 40));
|
|
Assert.Equal(Color.White.ToArgb(), image.GetPixel(90, 90).ToArgb());
|
|
Assert.Equal(EtchColor.ToArgb(), image.GetPixel(30, 40).ToArgb());
|
|
Assert.Equal(EtchColor.ToArgb(), image.GetPixel(40, 70).ToArgb());
|
|
Assert.Equal(EtchColor.ToArgb(), image.GetPixel(75, 40).ToArgb());
|
|
Assert.Equal(CutColor.ToArgb(), image.GetPixel(10, 50).ToArgb());
|
|
Assert.Equal(CutColor.ToArgb(), image.GetPixel(80, 90).ToArgb());
|
|
// Suppression must not leave a visible cut or etch stroke.
|
|
Assert.Equal(image.GetPixel(20, 20), image.GetPixel(40, 90));
|
|
Assert.Equal(before, Snapshot(program));
|
|
}, TimeSpan.FromMinutes(1), "Etch display test timed out.");
|
|
|
|
[Fact]
|
|
public void ActualCutWinsWhenEtchOccupiesTheSameLine() => StaTestThread.Run(() =>
|
|
{
|
|
var program = MixedProgram();
|
|
program.MoveTo(20, 60);
|
|
program.Codes.Add(new LinearMove(40, 60) { Layer = LayerType.Cut });
|
|
using var view = new PlateView(ColorSchemeRegistry.Get("Workshop"));
|
|
view.Matrix = new Matrix();
|
|
var layout = LayoutPart.Create(new Part(new Drawing("Overlaid cut", program)), view);
|
|
using var image = new Bitmap(120, 120);
|
|
using var g = Graphics.FromImage(image);
|
|
layout.Draw(g);
|
|
Assert.Equal(CutColor.ToArgb(), image.GetPixel(30, 60).ToArgb());
|
|
}, TimeSpan.FromMinutes(1), "Overlaid cut test timed out.");
|
|
|
|
[Fact]
|
|
public void ThumbnailKeepsEtchOutOfFillAndUsesDedicatedColor()
|
|
{
|
|
var program = MixedProgram();
|
|
using var fill = new SolidBrush(Color.FromArgb(184, 207, 224));
|
|
using var image = (Bitmap)program.GetImage(new System.Drawing.Size(110, 110), Pens.Black, fill);
|
|
Assert.Equal(image.GetPixel(15, 95), image.GetPixel(35, 75));
|
|
Assert.Equal(0, image.GetPixel(85, 25).A);
|
|
Assert.Equal(ColorScheme.Default.EtchColor.ToArgb(), image.GetPixel(25, 75).ToArgb());
|
|
Assert.Equal(Color.Black.ToArgb(), image.GetPixel(75, 25).ToArgb());
|
|
}
|
|
|
|
[Fact]
|
|
public void PlateRendererKeepsEtchesInNumberedPartsAndPlacementPreviews() => StaTestThread.Run(() =>
|
|
{
|
|
using var view = new PlateView(ColorSchemeRegistry.Get("Workshop"))
|
|
{
|
|
Size = new System.Drawing.Size(250, 130),
|
|
DrawBounds = false,
|
|
};
|
|
view.Matrix = new Matrix();
|
|
view.Plate.Parts.Add(new Part(new Drawing("Placed", MixedProgram())));
|
|
view.Previews.SetActiveParts(new List<Part>
|
|
{
|
|
new(new Drawing("Preview", MixedProgram()), new Vector(120, 0)),
|
|
});
|
|
using var image = new Bitmap(250, 130);
|
|
using var g = Graphics.FromImage(image);
|
|
g.Clear(Color.White);
|
|
view.Renderer.DrawParts(g);
|
|
Assert.Equal(EtchColor.ToArgb(), image.GetPixel(20, 30).ToArgb());
|
|
Assert.Equal(EtchColor.ToArgb(), image.GetPixel(140, 30).ToArgb());
|
|
Assert.Equal(CutColor.ToArgb(), image.GetPixel(0, 50).ToArgb());
|
|
}, TimeSpan.FromMinutes(1), "Plate preview test timed out.");
|
|
|
|
[Fact]
|
|
public void WorkshopIsAnAdditionalPaletteNotAReplacementForSavedSchemes()
|
|
{
|
|
var scheme = ColorSchemeRegistry.Get("Workshop");
|
|
Assert.Equal("Workshop", scheme.Name);
|
|
Assert.Equal("Classic", ColorSchemeRegistry.Get("Classic").Name);
|
|
Assert.Equal("Pastel", ColorSchemeRegistry.Get("Pastel").Name);
|
|
Assert.Equal("Dark", ColorSchemeRegistry.Get("Dark").Name);
|
|
Assert.NotEmpty(scheme.PartColors);
|
|
Assert.True(scheme.UseGoldenAngleColors);
|
|
Assert.Equal(PartColor, scheme.PartColors[0]);
|
|
Assert.Equal(Color.FromArgb(242, 242, 242), scheme.LayoutFillColor);
|
|
Assert.True(scheme.SelectedPartColor.IsEmpty);
|
|
Assert.All(Enumerable.Range(0, 1000).Select(PartColorPalette.GoldenAngle), color =>
|
|
{
|
|
Assert.True(Distinctness(color, EtchColor) >= 15, "Etch must stay distinguishable from generated fills.");
|
|
Assert.True(Distinctness(color, CutColor) >= 15, "Cuts must stay distinguishable from generated fills.");
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void SchemeRoundTripAndOldFilesPreserveOutlineFallback()
|
|
{
|
|
var workshop = ColorSchemeRegistry.Get("Workshop");
|
|
var restored = ColorSchemeSerializer.Deserialize(ColorSchemeSerializer.Serialize(workshop));
|
|
Assert.Equal(CutColor.ToArgb(), restored.PartOutlineColor.ToArgb());
|
|
Assert.Equal(EtchColor.ToArgb(), restored.EtchColor.ToArgb());
|
|
Assert.True(restored.SelectedPartColor.IsEmpty);
|
|
Assert.True(restored.UseGoldenAngleColors);
|
|
Assert.Equal(workshop.PartColors.Select(c => c.ToArgb()), restored.PartColors.Select(c => c.ToArgb()));
|
|
var legacy = System.Text.Json.Nodes.JsonNode.Parse(ColorSchemeSerializer.Serialize(ColorSchemeRegistry.Get("Classic")))!.AsObject();
|
|
legacy.Remove("etchColor");
|
|
legacy.Remove("partOutlineColor");
|
|
legacy.Remove("selectedPartColor");
|
|
legacy.Remove("useGoldenAngleColors");
|
|
var oldScheme = ColorSchemeSerializer.Deserialize(legacy.ToJsonString());
|
|
Assert.True(oldScheme.PartOutlineColor.IsEmpty);
|
|
Assert.True(oldScheme.SelectedPartColor.IsEmpty);
|
|
Assert.False(oldScheme.UseGoldenAngleColors);
|
|
Assert.Equal(System.Windows.Forms.ControlPaint.Dark(Color.Coral), oldScheme.GetPartOutlineColor(Color.Coral));
|
|
Assert.Equal(new ColorScheme().EtchColor.ToArgb(), oldScheme.EtchColor.ToArgb());
|
|
}
|
|
|
|
[Fact]
|
|
public void PaletteChangeRepaintsExistingPartWithoutMovingIt() => StaTestThread.Run(() =>
|
|
{
|
|
var scheme = ColorSchemeSerializer.Deserialize(ColorSchemeSerializer.Serialize(ColorSchemeRegistry.Get("Classic")));
|
|
var drawing = new Drawing("Recolor", MixedProgram()) { Color = Color.Coral };
|
|
using var view = new PlateView(scheme);
|
|
view.Matrix = new Matrix();
|
|
var layout = LayoutPart.Create(new Part(drawing), view);
|
|
var path = layout.Path;
|
|
scheme.PartOutlineColor = CutColor;
|
|
scheme.EtchColor = EtchColor;
|
|
drawing.Color = Color.FromArgb(184, 207, 224);
|
|
using var image = new Bitmap(120, 120);
|
|
using var g = Graphics.FromImage(image);
|
|
layout.Draw(g);
|
|
Assert.Same(path, layout.Path);
|
|
Assert.Equal(drawing.Color.ToArgb(), image.GetPixel(10, 10).ToArgb());
|
|
Assert.Equal(CutColor.ToArgb(), image.GetPixel(0, 50).ToArgb());
|
|
Assert.Equal(EtchColor.ToArgb(), image.GetPixel(20, 30).ToArgb());
|
|
}, TimeSpan.FromMinutes(1), "Scheme repaint test timed out.");
|
|
|
|
[Fact]
|
|
public void LayerChangesAndSubprogramsDoNotCreateConnectingStrokes()
|
|
{
|
|
var program = new Program();
|
|
program.MoveTo(10, 10);
|
|
program.Codes.Add(new LinearMove(20, 10) { Layer = LayerType.Cut });
|
|
program.Codes.Add(new LinearMove(30, 10) { Layer = LayerType.Scribe });
|
|
program.Codes.Add(new LinearMove(40, 10) { Layer = LayerType.Cut });
|
|
program.Codes.Add(new LinearMove(50, 10) { Layer = LayerType.Leadin });
|
|
program.Codes.Add(new LinearMove(60, 10) { Layer = LayerType.Leadout });
|
|
var child = new Program(Mode.Incremental);
|
|
child.MoveTo(2, 3);
|
|
child.Codes.Add(new LinearMove(6, 0) { Layer = LayerType.Scribe });
|
|
program.Codes.Add(new SubProgramCall(child, 0) { Offset = new Vector(70, 20) });
|
|
var before = Snapshot(program);
|
|
program.GetDisplayPaths(new Vector(5, 5), out var cuts, out var leads, out var etches);
|
|
using (cuts)
|
|
using (leads)
|
|
using (etches)
|
|
using (var pen = new Pen(Color.Black, 1))
|
|
{
|
|
Assert.True(cuts.IsOutlineVisible(20, 15, pen));
|
|
Assert.True(cuts.IsOutlineVisible(40, 15, pen));
|
|
Assert.False(cuts.IsOutlineVisible(30, 15, pen));
|
|
Assert.True(etches.IsOutlineVisible(30, 15, pen));
|
|
Assert.False(etches.IsOutlineVisible(40, 15, pen));
|
|
Assert.True(leads.IsOutlineVisible(50, 15, pen));
|
|
Assert.True(leads.IsOutlineVisible(60, 15, pen));
|
|
Assert.False(cuts.IsOutlineVisible(50, 15, pen));
|
|
Assert.True(etches.IsOutlineVisible(80, 28, pen));
|
|
Assert.False(etches.IsOutlineVisible(60, 21, pen));
|
|
}
|
|
Assert.Equal(before, Snapshot(program));
|
|
}
|
|
|
|
private static double Contrast(Color a, Color b)
|
|
{
|
|
var first = Luminance(a);
|
|
var second = Luminance(b);
|
|
return (System.Math.Max(first, second) + 0.05) / (System.Math.Min(first, second) + 0.05);
|
|
}
|
|
|
|
private static double Luminance(Color color)
|
|
{
|
|
static double Linear(byte channel)
|
|
{
|
|
var value = channel / 255.0;
|
|
return value <= 0.04045 ? value / 12.92 : System.Math.Pow((value + 0.055) / 1.055, 2.4);
|
|
}
|
|
return 0.2126 * Linear(color.R) + 0.7152 * Linear(color.G) + 0.0722 * Linear(color.B);
|
|
}
|
|
|
|
private static double Distinctness(Color a, Color b) => System.Math.Sqrt(
|
|
(a.R - (double)b.R) * (a.R - b.R)
|
|
+ (a.G - (double)b.G) * (a.G - b.G)
|
|
+ (a.B - (double)b.B) * (a.B - b.B));
|
|
|
|
private static Program MixedProgram()
|
|
{
|
|
var program = new Program();
|
|
AddSquare(program, 0, 0, 100, LayerType.Cut);
|
|
AddSquare(program, 20, 20, 20, LayerType.Scribe);
|
|
AddSquare(program, 70, 70, 20, LayerType.Cut);
|
|
program.MoveTo(20, 60);
|
|
program.Codes.Add(new LinearMove(40, 60) { Layer = LayerType.Scribe });
|
|
program.MoveTo(65, 30);
|
|
program.Codes.Add(new ArcMove(65, 30, 60, 30) { Layer = LayerType.Scribe });
|
|
program.MoveTo(20, 80);
|
|
program.Codes.Add(new LinearMove(40, 80) { Layer = LayerType.Scribe, Suppressed = true });
|
|
return program;
|
|
}
|
|
|
|
private static void AddSquare(Program program, double x, double y, double size, LayerType layer)
|
|
{
|
|
program.MoveTo(x, y);
|
|
foreach (var end in new[] { new Vector(x + size, y), new Vector(x + size, y + size), new Vector(x, y + size), new Vector(x, y) })
|
|
program.Codes.Add(new LinearMove(end) { Layer = layer });
|
|
}
|
|
|
|
private static string[] Snapshot(Program program) => program.Codes.Select(code => code switch
|
|
{
|
|
ArcMove arc => $"arc:{arc.Layer}:{arc.Suppressed}:{arc.EndPoint.X:R}:{arc.EndPoint.Y:R}:{arc.CenterPoint.X:R}:{arc.CenterPoint.Y:R}:{arc.Rotation}",
|
|
LinearMove line => $"line:{line.Layer}:{line.Suppressed}:{line.EndPoint.X:R}:{line.EndPoint.Y:R}",
|
|
Motion motion => $"{motion.Type}:{motion.Suppressed}:{motion.EndPoint.X:R}:{motion.EndPoint.Y:R}",
|
|
_ => code.ToString()!
|
|
}).Prepend(program.Mode.ToString()).ToArray();
|
|
}
|