test(desktop): pin sub-program frame origin and height-limited fit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-11-11 22:18:14 -05:00
1 parent 049b45b394
commit 5edebc6144
3 files changed
+6 -175

No files matched your search

-175
View File
@@ -1,175 +0,0 @@
using System;
using OpenNest.CNC;
using OpenNest.Geometry;
using OpenNest.Math;
using SkiaSharp;
namespace OpenNest.Avalonia.Rendering
{
/// <summary>
/// A program's display geometry as Skia paths in the program frame, before placement.
/// A Skia port of the desktop's GraphicsHelper.GetDisplayPaths, without tab bridging.
/// </summary>
internal sealed class ProgramPaths : IDisposable
{
public SKPath Cut { get; } = new() { FillType = SKPathFillType.EvenOdd };
public SKPath Lead { get; } = new();
public SKPath Etch { get; } = new();
/// <summary>The material outline to fill: cuts and tabs, without open break cuts.</summary>
public SKPath Fill { get; } = new() { FillType = SKPathFillType.EvenOdd };
public SKRect Bounds { get; private set; }
public static ProgramPaths Build(CNC.Program program)
{
var paths = new ProgramPaths();
var position = Vector.Zero;
paths.AddProgram(program, ref position);
var bounds = paths.Cut.Bounds;
if (!paths.Etch.IsEmpty)
bounds = paths.Cut.IsEmpty ? paths.Etch.Bounds : SKRect.Union(bounds, paths.Etch.Bounds);
paths.Bounds = bounds;
return paths;
}
private SKPath TargetFor(LayerType layer) => layer switch
{
LayerType.Leadin or LayerType.Leadout => Lead,
LayerType.Scribe => Etch,
LayerType.Display or LayerType.Tab => null,
_ => Cut,
};
private void AddProgram(CNC.Program program, ref Vector position)
{
// Sub-program offsets are relative to the frame origin at entry, not the tool position.
var frameOrigin = position;
var mode = program.Mode;
SKPath open = null;
for (var i = 0; i < program.Length; ++i)
{
var code = program[i];
switch (code)
{
case LinearMove line:
{
var end = mode == Mode.Incremental ? line.EndPoint + position : line.EndPoint;
var target = line.Suppressed ? null : TargetFor(line.Layer);
if (target != null)
{
Continue(ref open, target, position);
target.LineTo((float)end.X, (float)end.Y);
}
if (!line.Suppressed && line.Layer != LayerType.Break)
AddFillLine(line.Layer, position, end);
position = end;
break;
}
case ArcMove arc:
{
var end = arc.EndPoint;
var center = arc.CenterPoint;
if (mode == Mode.Incremental)
{
end += position;
center += position;
}
var target = arc.Suppressed ? null : TargetFor(arc.Layer);
if (target != null)
{
Continue(ref open, target, position);
AddArc(target, position, end, center, arc.Rotation);
}
if (!arc.Suppressed && arc.Layer != LayerType.Break)
AddFillArc(arc.Layer, position, end, center, arc.Rotation);
position = end;
break;
}
case RapidMove rapid:
{
var end = mode == Mode.Incremental ? rapid.EndPoint + position : rapid.EndPoint;
if (end.DistanceTo(position) > 0.001)
open = null;
position = end;
break;
}
case SubProgramCall call when call.Program != null:
open = null;
position = new Vector(frameOrigin.X + call.Offset.X, frameOrigin.Y + call.Offset.Y);
AddProgram(call.Program, ref position);
open = null;
break;
}
}
}
// Starts a new contour unless the move continues the one already open on the same path.
private SKPath openFill;
private static void Continue(ref SKPath open, SKPath target, Vector from)
{
if (!ReferenceEquals(open, target) || target.LastPoint != new SKPoint((float)from.X, (float)from.Y))
target.MoveTo((float)from.X, (float)from.Y);
open = target;
}
private bool IsMaterial(LayerType layer) =>
layer is not (LayerType.Scribe or LayerType.Leadin or LayerType.Leadout or LayerType.Display);
private void AddFillLine(LayerType layer, Vector from, Vector to)
{
if (!IsMaterial(layer))
return;
Continue(ref openFill, Fill, from);
Fill.LineTo((float)to.X, (float)to.Y);
}
private void AddFillArc(LayerType layer, Vector from, Vector to, Vector center, RotationType rotation)
{
if (!IsMaterial(layer))
return;
Continue(ref openFill, Fill, from);
AddArc(Fill, from, to, center, rotation);
}
private static void AddArc(SKPath path, Vector from, Vector to, Vector center, RotationType rotation)
{
var startAngle = Angle.NormalizeDeg(Angle.ToDegrees(System.Math.Atan2(from.Y - center.Y, from.X - center.X)));
var endAngle = Angle.NormalizeDeg(Angle.ToDegrees(System.Math.Atan2(to.Y - center.Y, to.X - center.X)));
var radius = (float)center.DistanceTo(to);
var oval = new SKRect((float)center.X - radius, (float)center.Y - radius,
(float)center.X + radius, (float)center.Y + radius);
if (startAngle.IsEqualTo(endAngle))
{
// A full circle: two half sweeps keep the contour starting at the current point.
var sweep = rotation == RotationType.CCW ? 180f : -180f;
path.ArcTo(oval, (float)startAngle, sweep, false);
path.ArcTo(oval, (float)startAngle + sweep, sweep, false);
return;
}
if (rotation == RotationType.CCW && endAngle < startAngle)
endAngle += 360.0;
else if (rotation == RotationType.CW && startAngle < endAngle)
startAngle += 360.0;
path.ArcTo(oval, (float)startAngle, (float)(endAngle - startAngle), false);
}
public void Dispose()
{
Cut.Dispose();
Lead.Dispose();
Etch.Dispose();
Fill.Dispose();
}
}
}
@@ -266,6 +266,8 @@ public class ProgramGeometryTests
hole.Codes.Add(new RapidMove(new Vector(1, 0)));
hole.Codes.Add(new ArcMove(Vector.Zero, new Vector(-1, 0), RotationType.CW));
var program = Square();
// The tool is elsewhere when the call runs; the offset is from the frame origin.
program.Codes.Add(new RapidMove(new Vector(20, 20)));
program.Codes.Add(new SubProgramCall(hole, 0) { Offset = new Vector(5, 5) });
var cut = ProgramGeometry.Layers(program).Cut;
@@ -44,6 +44,10 @@ public class ViewTransformTests
var centre = view.WorldToScreen(new Vector(60, 45));
Assert.Equal(400, centre.X, 9);
Assert.Equal(300, centre.Y, 9);
// Height-limited: (600 - 50) / 100.
Assert.True(view.ZoomToArea(new Box(0, 0, 10, 100)));
Assert.Equal(5.5, view.Scale, 9);
}
[Fact]