Files
OpenNest/OpenNest/Controls/PlateRenderer.cs
T
aj 6fdf0ad3c5 refactor(cnc): extract rapid enumeration into RapidEnumerator
Pulls the rapid-walk logic (sub-program unwrapping, first-pierce lookup,
incremental-vs-absolute handling, first-rapid skipping) out of
PlateRenderer.DrawRapids into a reusable RapidEnumerator in Core so it
can be unit-tested and reused outside the renderer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 12:49:04 -04:00

493 lines
17 KiB
C#

using OpenNest.Bending;
using OpenNest.CNC;
using OpenNest.Geometry;
using OpenNest.Math;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
namespace OpenNest.Controls
{
internal class PlateRenderer
{
private readonly PlateView view;
public PlateRenderer(PlateView view)
{
this.view = view;
}
public void DrawPlate(Graphics g)
{
var plate = view.Plate;
var plateRect = new RectangleF
{
Width = view.LengthWorldToGui(plate.Size.Length),
Height = view.LengthWorldToGui(plate.Size.Width)
};
var edgeSpacingRect = new RectangleF
{
Width = view.LengthWorldToGui(plate.Size.Length - plate.EdgeSpacing.Left - plate.EdgeSpacing.Right),
Height = view.LengthWorldToGui(plate.Size.Width - plate.EdgeSpacing.Top - plate.EdgeSpacing.Bottom)
};
switch (plate.Quadrant)
{
case 1:
plateRect.Location = view.PointWorldToGraph(0, 0);
edgeSpacingRect.Location = view.PointWorldToGraph(
plate.EdgeSpacing.Left,
plate.EdgeSpacing.Bottom);
break;
case 2:
plateRect.Location = view.PointWorldToGraph(-plate.Size.Length, 0);
edgeSpacingRect.Location = view.PointWorldToGraph(
plate.EdgeSpacing.Left - plate.Size.Length,
plate.EdgeSpacing.Bottom);
break;
case 3:
plateRect.Location = view.PointWorldToGraph(-plate.Size.Length, -plate.Size.Width);
edgeSpacingRect.Location = view.PointWorldToGraph(
plate.EdgeSpacing.Left - plate.Size.Length,
plate.EdgeSpacing.Bottom - plate.Size.Width);
break;
case 4:
plateRect.Location = view.PointWorldToGraph(0, -plate.Size.Width);
edgeSpacingRect.Location = view.PointWorldToGraph(
plate.EdgeSpacing.Left,
plate.EdgeSpacing.Bottom - plate.Size.Width);
break;
default:
return;
}
plateRect.Y -= plateRect.Height;
edgeSpacingRect.Y -= edgeSpacingRect.Height;
g.FillRectangle(view.ColorScheme.LayoutFillBrush, plateRect);
var viewBounds = view.GetViewBounds();
if (!edgeSpacingRect.Contains(viewBounds))
{
g.DrawRectangle(view.ColorScheme.EdgeSpacingPen,
edgeSpacingRect.X,
edgeSpacingRect.Y,
edgeSpacingRect.Width,
edgeSpacingRect.Height);
}
g.DrawRectangle(view.ColorScheme.LayoutOutlinePen,
plateRect.X,
plateRect.Y,
plateRect.Width,
plateRect.Height);
}
public void DrawParts(Graphics g)
{
var viewBounds = view.GetViewBounds();
var layoutParts = view.LayoutParts;
for (var i = 0; i < layoutParts.Count; ++i)
{
var part = layoutParts[i];
if (part.IsDirty)
part.Update(view);
var path = part.Path;
var pathBounds = path.GetBounds();
if (!pathBounds.IntersectsWith(viewBounds))
continue;
part.Draw(g, (i + 1).ToString());
DrawBendLines(g, part.BasePart);
DrawEtchMarks(g, part.BasePart);
DrawGrainWarning(g, part.BasePart);
}
var previewParts = view.PreviewParts;
var previewBrush = view.PreviewBrush;
var previewPen = view.PreviewPen;
for (var i = 0; i < previewParts.Count; i++)
{
var part = previewParts[i];
if (part.IsDirty)
part.Update(view);
var path = part.Path;
if (!path.GetBounds().IntersectsWith(viewBounds))
continue;
g.FillPath(previewBrush, path);
g.DrawPath(previewPen, path);
}
if (view.DrawOffset && view.Plate.PartSpacing > 0)
DrawOffsetGeometry(g);
if (view.DrawBounds)
{
var bounds = view.SelectedParts.Select(p => p.BasePart).ToList().GetBoundingBox();
DrawBox(g, bounds);
}
if (view.DrawRapid)
DrawRapids(g);
if (view.DrawPiercePoints)
DrawAllPiercePoints(g);
if (view.DrawCutDirection)
DrawAllCutDirectionArrows(g);
}
public void DrawCutOffs(Graphics g)
{
var plate = view.Plate;
if (plate?.CutOffs == null || plate.CutOffs.Count == 0)
return;
using var pen = new Pen(Color.FromArgb(64, 64, 64), 1.5f);
using var selectedPen = new Pen(Color.FromArgb(0, 120, 255), 3.5f);
foreach (var cutoff in plate.CutOffs)
{
var program = cutoff.Drawing?.Program;
if (program == null || program.Codes.Count == 0)
continue;
var activePen = view.Selection.SelectedCutOffs.Contains(cutoff) ? selectedPen : pen;
for (var i = 0; i < program.Codes.Count - 1; i += 2)
{
if (program.Codes[i] is RapidMove rapid &&
program.Codes[i + 1] is LinearMove linear)
{
DrawLine(g, rapid.EndPoint, linear.EndPoint, activePen);
}
}
}
}
public void DrawActiveWorkArea(Graphics g)
{
var workArea = view.ActiveWorkArea;
if (workArea == null)
return;
var rect = new RectangleF
{
Location = view.PointWorldToGraph(workArea.Location),
Width = view.LengthWorldToGui(workArea.Length),
Height = view.LengthWorldToGui(workArea.Width)
};
rect.Y -= rect.Height;
using var pen = new Pen(Color.Red, 1.5f)
{
DashStyle = DashStyle.Dash
};
g.DrawRectangle(pen, rect.X, rect.Y, rect.Width, rect.Height);
}
private static readonly Color[] PriorityFills =
{
Color.FromArgb(60, Color.LimeGreen),
Color.FromArgb(60, Color.Gold),
Color.FromArgb(60, Color.Salmon),
};
private static readonly Color[] PriorityBorders =
{
Color.FromArgb(180, Color.Green),
Color.FromArgb(180, Color.DarkGoldenrod),
Color.FromArgb(180, Color.DarkRed),
};
public void DrawDebugRemnants(Graphics g)
{
var remnants = view.DebugRemnants;
if (remnants == null || remnants.Count == 0)
return;
for (var i = 0; i < remnants.Count; i++)
{
var box = remnants[i];
var loc = view.PointWorldToGraph(box.Location);
var w = view.LengthWorldToGui(box.Length);
var h = view.LengthWorldToGui(box.Width);
var rect = new RectangleF(loc.X, loc.Y - h, w, h);
var priority = view.DebugRemnantPriorities != null && i < view.DebugRemnantPriorities.Count
? System.Math.Min(view.DebugRemnantPriorities[i], 2)
: 0;
using var brush = new SolidBrush(PriorityFills[priority]);
g.FillRectangle(brush, rect);
using var pen = new Pen(PriorityBorders[priority], 1.5f);
g.DrawRectangle(pen, rect.X, rect.Y, rect.Width, rect.Height);
var label = $"P{priority} {box.Width:F1}x{box.Length:F1}";
using var font = new Font("Segoe UI", 8f);
using var sf = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center };
g.DrawString(label, font, Brushes.Black, rect, sf);
}
}
private void DrawBendLines(Graphics g, Part part)
{
if (!view.ShowBendLines || part.BaseDrawing.Bends == null || part.BaseDrawing.Bends.Count == 0)
return;
using var bendPen = new Pen(Color.Yellow, 1.5f)
{
DashStyle = System.Drawing.Drawing2D.DashStyle.Dash
};
foreach (var bend in part.BaseDrawing.Bends)
{
var start = bend.StartPoint;
var end = bend.EndPoint;
if (part.Rotation != 0)
{
start = start.Rotate(part.Rotation);
end = end.Rotate(part.Rotation);
}
start = start + part.Location;
end = end + part.Location;
var pt1 = view.PointWorldToGraph(start);
var pt2 = view.PointWorldToGraph(end);
g.DrawLine(bendPen, pt1, pt2);
}
}
private void DrawEtchMarks(Graphics g, Part part)
{
if (!view.ShowBendLines || part.BaseDrawing.Bends == null || part.BaseDrawing.Bends.Count == 0)
return;
using var etchPen = new Pen(Color.Green, 1.5f);
var etchLength = 1.0;
foreach (var bend in part.BaseDrawing.Bends)
{
if (bend.Direction != BendDirection.Up)
continue;
var start = bend.StartPoint;
var end = bend.EndPoint;
if (part.Rotation != 0)
{
start = start.Rotate(part.Rotation);
end = end.Rotate(part.Rotation);
}
start = start + part.Location;
end = end + part.Location;
var length = bend.Length;
var angle = bend.StartPoint.AngleTo(bend.EndPoint) + part.Rotation;
if (length < etchLength * 3.0)
{
var pt1 = view.PointWorldToGraph(start);
var pt2 = view.PointWorldToGraph(end);
g.DrawLine(etchPen, pt1, pt2);
}
else
{
var dx = System.Math.Cos(angle) * etchLength;
var dy = System.Math.Sin(angle) * etchLength;
var s1 = view.PointWorldToGraph(start);
var e1 = view.PointWorldToGraph(new Vector(start.X + dx, start.Y + dy));
g.DrawLine(etchPen, s1, e1);
var s2 = view.PointWorldToGraph(end);
var e2 = view.PointWorldToGraph(new Vector(end.X - dx, end.Y - dy));
g.DrawLine(etchPen, s2, e2);
}
}
}
private void DrawGrainWarning(Graphics g, Part part)
{
var plate = view.Plate;
if (!view.ShowBendLines || plate == null || part.BaseDrawing.Bends == null || part.BaseDrawing.Bends.Count == 0)
return;
var grainAngle = plate.GrainAngle;
var tolerance = Angle.ToRadians(5);
foreach (var bend in part.BaseDrawing.Bends)
{
var bendAngle = bend.LineAngle + part.Rotation;
bendAngle = bendAngle % System.Math.PI;
if (bendAngle < 0) bendAngle += System.Math.PI;
var grainNormalized = grainAngle % System.Math.PI;
if (grainNormalized < 0) grainNormalized += System.Math.PI;
var diff = System.Math.Abs(bendAngle - grainNormalized);
diff = System.Math.Min(diff, System.Math.PI - diff);
if (diff > tolerance)
{
var box = part.BaseDrawing.Program.BoundingBox();
var location = part.Location;
var pt1 = view.PointWorldToGraph(location);
var pt2 = view.PointWorldToGraph(new Vector(
location.X + box.Length, location.Y + box.Width));
using var warnPen = new Pen(Color.FromArgb(180, 255, 140, 0), 2f);
g.DrawRectangle(warnPen, pt1.X, pt2.Y,
System.Math.Abs(pt2.X - pt1.X), System.Math.Abs(pt2.Y - pt1.Y));
return;
}
}
}
private void DrawOffsetGeometry(Graphics g)
{
var layoutParts = view.LayoutParts;
using var offsetPen = new Pen(Color.FromArgb(120, 255, 100, 100));
for (var i = 0; i < layoutParts.Count; i++)
{
var layoutPart = layoutParts[i];
if (layoutPart.IsDirty)
layoutPart.Update(view);
layoutPart.UpdateOffset(view.Plate.PartSpacing, view.OffsetTolerance, view.Matrix);
if (layoutPart.OffsetPath != null)
g.DrawPath(offsetPen, layoutPart.OffsetPath);
}
}
private void DrawRapids(Graphics g)
{
var pen = view.ColorScheme.RapidPen;
var pos = new Vector(0, 0);
for (var i = 0; i < view.Plate.Parts.Count; ++i)
{
var part = view.Plate.Parts[i];
var segments = RapidEnumerator.Enumerate(part.Program, part.Location, pos);
foreach (var seg in segments)
{
DrawLine(g, seg.From, seg.To, pen);
pos = seg.To;
}
}
}
private void DrawAllPiercePoints(Graphics g)
{
using var brush = new SolidBrush(Color.Red);
using var pen = new Pen(Color.DarkRed, 1f);
for (var i = 0; i < view.Plate.Parts.Count; ++i)
{
var part = view.Plate.Parts[i];
var pgm = part.Program;
var pos = part.Location;
DrawProgramPiercePoints(g, pgm, part.Location, ref pos, brush, pen);
}
}
private void DrawProgramPiercePoints(Graphics g, Program pgm, Vector basePos, ref Vector pos, Brush brush, Pen pen)
{
for (var i = 0; i < pgm.Length; ++i)
{
var code = pgm[i];
if (code.Type == CodeType.SubProgramCall)
{
var subpgm = (SubProgramCall)code;
if (subpgm.Program != null)
{
var holeBase = basePos + subpgm.Offset;
pos = holeBase;
DrawProgramPiercePoints(g, subpgm.Program, holeBase, ref pos, brush, pen);
}
}
else
{
var motion = code as Motion;
if (motion == null) continue;
var endpt = pgm.Mode == Mode.Incremental
? motion.EndPoint + pos
: motion.EndPoint + basePos;
if (code.Type == CodeType.RapidMove)
{
var pt = view.PointWorldToGraph(endpt);
var radius = 2f;
g.FillEllipse(brush, pt.X - radius, pt.Y - radius, radius * 2, radius * 2);
g.DrawEllipse(pen, pt.X - radius, pt.Y - radius, radius * 2, radius * 2);
}
pos = endpt;
}
}
}
private void DrawAllCutDirectionArrows(Graphics g)
{
using var pen = new Pen(Color.FromArgb(60, 60, 60), 1.5f);
var arrowSpacingWorld = view.LengthGuiToWorld(60f);
var arrowSize = 6f;
for (var i = 0; i < view.Plate.Parts.Count; ++i)
{
var part = view.Plate.Parts[i];
var pgm = part.Program;
var pos = part.Location;
CutDirectionArrows.DrawProgram(g, view, pgm, ref pos, pen, arrowSpacingWorld, arrowSize);
}
}
private void DrawLine(Graphics g, Vector pt1, Vector pt2, Pen pen)
{
var point1 = view.PointWorldToGraph(pt1);
var point2 = view.PointWorldToGraph(pt2);
g.DrawLine(pen, point1, point2);
}
private void DrawBox(Graphics g, Box box)
{
var rect = new RectangleF
{
Location = view.PointWorldToGraph(box.Location),
Width = view.LengthWorldToGui(box.Length),
Height = view.LengthWorldToGui(box.Width)
};
g.DrawRectangle(view.ColorScheme.BoundingBoxPen, rect.X, rect.Y - rect.Height, rect.Width, rect.Height);
}
}
}