Files
OpenNest/OpenNest/LayoutPart.cs
T
ajandClaude Opus 5.5 a6bc9d8be6 feat(geometry): add ClipperBridge for region offsetting
Offsetting entity by entity leaves spikes and inverted loops wherever a
feature is narrower than the spacing, and RemoveSelfIntersections only
catches proper crossings. ClipperBridge flattens a ShapeProfile into one
region (perimeter positive, cutouts negative) and inflates it in a single
Clipper pass with round joins, so narrow features collapse and holes that
close up disappear.

Conservative mode circumscribes perimeter arcs, inscribes cutout arcs and
pads the inflation by the chord tolerance, so the result never
under-estimates the spacing. It replaces the circumscribed-polygon
guarantee the BestFit/PartBoundary callers rely on.

Clipper stays confined to CPU preparation whose output is cached; the
per-pair Collision path remains hand-rolled for GPU portability.
LayoutPart's display offset now goes through the bridge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 09:12:34 -04:00

378 lines
11 KiB
C#

using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Windows.Forms;
using OpenNest.Controls;
using OpenNest.Converters;
using OpenNest.Geometry;
namespace OpenNest
{
public class LayoutPart : IPart
{
private static Font programIdFont;
private static Color selectedColor;
private static Pen selectedPen;
private static Brush selectedBrush;
private static Pen leadInPen;
private Color color;
private Brush brush;
private Pen pen;
private List<PointF[]> _offsetPolygonPoints;
private double _cachedOffsetSpacing;
private double _cachedOffsetTolerance;
private double _cachedOffsetRotation = double.NaN;
private Vector? _labelPoint;
private PointF _labelScreenPoint;
public readonly Part BasePart;
static LayoutPart()
{
programIdFont = new Font(SystemFonts.DefaultFont, FontStyle.Bold | FontStyle.Underline);
SelectedColor = Color.FromArgb(90, 150, 200, 255);
leadInPen = new Pen(Color.OrangeRed, 1.5f);
}
private LayoutPart(Part part)
{
this.BasePart = part;
if (part.BaseDrawing.Color.IsEmpty)
part.BaseDrawing.Color = Color.FromArgb(130, 204, 130);
Color = part.BaseDrawing.Color;
}
internal bool IsDirty { get; set; }
public bool IsSelected { get; set; }
public GraphicsPath Path { get; private set; }
public GraphicsPath LeadInPath { get; private set; }
public Color Color
{
get { return color; }
set
{
color = value;
if (brush != null)
brush.Dispose();
brush = new SolidBrush(value);
if (pen != null)
pen.Dispose();
pen = new Pen(ControlPaint.Dark(value));
}
}
public void Draw(Graphics g)
{
if (IsSelected)
{
g.FillPath(selectedBrush, Path);
g.DrawPath(selectedPen, Path);
}
else
{
g.FillPath(brush, Path);
g.DrawPath(pen, Path);
}
if (LeadInPath != null)
g.DrawPath(leadInPen, LeadInPath);
}
public void Draw(Graphics g, string id)
{
if (IsSelected)
{
g.FillPath(selectedBrush, Path);
g.DrawPath(selectedPen, Path);
}
else
{
g.FillPath(brush, Path);
g.DrawPath(pen, Path);
}
if (LeadInPath != null)
g.DrawPath(leadInPen, LeadInPath);
using var sf = new StringFormat
{
Alignment = StringAlignment.Center,
LineAlignment = StringAlignment.Center,
};
g.DrawString(
id,
programIdFont,
Brushes.Black,
_labelScreenPoint.X,
_labelScreenPoint.Y,
sf
);
}
public GraphicsPath OffsetPath { get; private set; }
private Vector ComputeLabelPoint()
{
var entities = ConvertProgram.ToGeometry(BasePart.BaseDrawing.Program);
var nonRapid = entities.Where(e => e.Layer != SpecialLayers.Rapid).ToList();
var shapes = ShapeBuilder.GetShapes(nonRapid);
if (shapes.Count == 0)
{
var bbox = BasePart.BaseDrawing.Program.BoundingBox();
return new Vector(
bbox.Location.X + bbox.Length / 2,
bbox.Location.Y + bbox.Width / 2
);
}
var profile = new ShapeProfile(nonRapid);
var outer = profile.Perimeter.ToPolygonWithTolerance(0.1);
List<Polygon> holes = null;
if (profile.Cutouts.Count > 0)
{
holes = new List<Polygon>();
foreach (var cutout in profile.Cutouts)
holes.Add(cutout.ToPolygonWithTolerance(0.1));
}
return PolyLabel.Find(outer, holes);
}
public void Update(DrawControl plateView)
{
if (BasePart.HasManualLeadIns)
{
BasePart.Program.GetGraphicsPaths(
BasePart.Location,
out var cutPath,
out var leadPath
);
cutPath.Transform(plateView.Matrix);
leadPath.Transform(plateView.Matrix);
Path = cutPath;
LeadInPath?.Dispose();
LeadInPath = leadPath;
}
else
{
Path = GraphicsHelper.GetGraphicsPath(BasePart.Program, BasePart.Location);
Path.Transform(plateView.Matrix);
LeadInPath?.Dispose();
LeadInPath = null;
}
// _labelPoint is computed from BaseDrawing.Program's current geometry, which already
// carries BaseDrawing.Program.Rotation (nonzero for canonical-frame drawings, e.g. in
// BestFitViewerForm). BasePart.Rotation is cumulative from that same baseline, so it
// must be re-applied net of the baseline already baked into _labelPoint — otherwise
// the baseline rotation is counted twice. Mirrors CanonicalFrame.RebindToOriginal.
_labelPoint ??= ComputeLabelPoint();
var baseRotation = BasePart.BaseDrawing.Program.Rotation;
var rotatedLabel = _labelPoint.Value.Rotate(BasePart.Rotation - baseRotation);
var labelPt = new PointF(
(float)(rotatedLabel.X + BasePart.Location.X),
(float)(rotatedLabel.Y + BasePart.Location.Y)
);
var pts = new[] { labelPt };
plateView.Matrix.TransformPoints(pts);
_labelScreenPoint = pts[0];
IsDirty = false;
}
public void UpdateOffset(double spacing, double tolerance, Matrix matrix)
{
if (
_offsetPolygonPoints == null
|| spacing != _cachedOffsetSpacing
|| tolerance != _cachedOffsetTolerance
|| BasePart.Rotation != _cachedOffsetRotation
)
{
_offsetPolygonPoints = ComputeOffsetPolygons(spacing, tolerance);
_cachedOffsetSpacing = spacing;
_cachedOffsetTolerance = tolerance;
_cachedOffsetRotation = BasePart.Rotation;
}
RebuildOffsetPath(matrix);
}
public void InvalidateOffset()
{
_offsetPolygonPoints = null;
}
private List<PointF[]> ComputeOffsetPolygons(double spacing, double tolerance)
{
var entities = ConvertProgram.ToGeometry(BasePart.Program);
var profile = new ShapeProfile(
entities.Where(e => e.Layer != SpecialLayers.Rapid).ToList()
);
var offset = ClipperBridge.Offset(profile, spacing, tolerance);
var result = new List<PointF[]>(offset.Outers.Count + offset.Holes.Count);
foreach (var polygon in offset.Outers.Concat(offset.Holes))
{
var pts = new PointF[polygon.Vertices.Count];
for (var j = 0; j < pts.Length; j++)
pts[j] = new PointF((float)polygon.Vertices[j].X, (float)polygon.Vertices[j].Y);
result.Add(pts);
}
return result;
}
private void RebuildOffsetPath(Matrix matrix)
{
OffsetPath?.Dispose();
if (_offsetPolygonPoints == null || _offsetPolygonPoints.Count == 0)
{
OffsetPath = null;
return;
}
var path = new GraphicsPath();
var dx = (float)BasePart.Location.X;
var dy = (float)BasePart.Location.Y;
foreach (var pts in _offsetPolygonPoints)
{
var offsetPts = new PointF[pts.Length];
for (var i = 0; i < pts.Length; i++)
offsetPts[i] = new PointF(pts[i].X + dx, pts[i].Y + dy);
path.AddLines(offsetPts);
path.StartFigure();
}
path.Transform(matrix);
OffsetPath = path;
}
public static LayoutPart Create(Part part, PlateView plateView)
{
var layoutPart = new LayoutPart(part);
layoutPart.Update(plateView);
return layoutPart;
}
public static Color SelectedColor
{
get { return selectedColor; }
set
{
selectedColor = value;
if (selectedBrush != null)
selectedBrush.Dispose();
selectedBrush = new SolidBrush(value);
if (selectedPen != null)
selectedPen.Dispose();
selectedPen = new Pen(ControlPaint.Dark(value));
}
}
public Vector Location
{
get { return BasePart.Location; }
set
{
BasePart.Location = value;
IsDirty = true;
}
}
public double Rotation
{
get { return BasePart.Rotation; }
}
public void Rotate(double angle)
{
BasePart.Rotate(angle);
IsDirty = true;
}
public void Rotate(double angle, Vector origin)
{
BasePart.Rotate(angle, origin);
IsDirty = true;
}
public void Offset(double x, double y)
{
BasePart.Offset(x, y);
IsDirty = true;
}
public void Offset(Vector voffset)
{
BasePart.Offset(voffset);
IsDirty = true;
}
public Box BoundingBox
{
get { return BasePart.BoundingBox; }
}
public double Left
{
get { return BasePart.Left; }
}
public double Right
{
get { return BasePart.Right; }
}
public double Top
{
get { return BasePart.Top; }
}
public double Bottom
{
get { return BasePart.Bottom; }
}
public void UpdateBounds()
{
BasePart.UpdateBounds();
}
public void Update()
{
Color = BasePart.BaseDrawing.Color;
}
}
}