diff --git a/OpenNest.Avalonia/Rendering/ProgramPaths.cs b/OpenNest.Avalonia/Rendering/ProgramPaths.cs
deleted file mode 100644
index 7cd6fd27..00000000
--- a/OpenNest.Avalonia/Rendering/ProgramPaths.cs
+++ /dev/null
@@ -1,175 +0,0 @@
-using System;
-using OpenNest.CNC;
-using OpenNest.Geometry;
-using OpenNest.Math;
-using SkiaSharp;
-
-namespace OpenNest.Avalonia.Rendering
-{
- ///
- /// 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.
- ///
- internal sealed class ProgramPaths : IDisposable
- {
- public SKPath Cut { get; } = new() { FillType = SKPathFillType.EvenOdd };
-
- public SKPath Lead { get; } = new();
-
- public SKPath Etch { get; } = new();
-
- /// The material outline to fill: cuts and tabs, without open break cuts.
- 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();
- }
- }
-}
diff --git a/OpenNest.Desktop.Tests/Rendering/ProgramGeometryTests.cs b/OpenNest.Desktop.Tests/Rendering/ProgramGeometryTests.cs
index abe32f6b..60785387 100644
--- a/OpenNest.Desktop.Tests/Rendering/ProgramGeometryTests.cs
+++ b/OpenNest.Desktop.Tests/Rendering/ProgramGeometryTests.cs
@@ -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;
diff --git a/OpenNest.Desktop.Tests/Rendering/ViewTransformTests.cs b/OpenNest.Desktop.Tests/Rendering/ViewTransformTests.cs
index 9dd2b8ad..c3450992 100644
--- a/OpenNest.Desktop.Tests/Rendering/ViewTransformTests.cs
+++ b/OpenNest.Desktop.Tests/Rendering/ViewTransformTests.cs
@@ -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]