Use pole-of-inaccessibility (polylabel) to place part labels at the visual center of shapes instead of the first path vertex. Labels now stay correctly positioned regardless of part rotation or shape. Also adds project README and MIT license. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
337 lines
9.0 KiB
C#
337 lines
9.0 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 Color color;
|
|
private Brush brush;
|
|
private Pen pen;
|
|
|
|
private PointF _labelPoint;
|
|
|
|
private List<PointF[]> _offsetPolygonPoints;
|
|
private double _cachedOffsetSpacing;
|
|
private double _cachedOffsetTolerance;
|
|
private double _cachedOffsetRotation = double.NaN;
|
|
|
|
public readonly Part BasePart;
|
|
|
|
static LayoutPart()
|
|
{
|
|
programIdFont = new Font(SystemFonts.DefaultFont, FontStyle.Bold | FontStyle.Underline);
|
|
SelectedColor = Color.FromArgb(90, 150, 200, 255);
|
|
}
|
|
|
|
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 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);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
g.DrawString(id, programIdFont, Brushes.Black, _labelPoint.X, _labelPoint.Y);
|
|
}
|
|
|
|
public GraphicsPath OffsetPath { get; private set; }
|
|
|
|
public void Update(DrawControl plateView)
|
|
{
|
|
Path = GraphicsHelper.GetGraphicsPath(BasePart.Program, BasePart.Location);
|
|
Path.Transform(plateView.Matrix);
|
|
_labelPoint = ComputeLabelPoint();
|
|
IsDirty = false;
|
|
}
|
|
|
|
private PointF ComputeLabelPoint()
|
|
{
|
|
if (Path.PointCount == 0)
|
|
return PointF.Empty;
|
|
|
|
var points = Path.PathPoints;
|
|
var types = Path.PathTypes;
|
|
|
|
// Extract the largest figure from the path for polylabel.
|
|
var bestFigure = new List<Vector>();
|
|
var currentFigure = new List<Vector>();
|
|
|
|
for (var i = 0; i < points.Length; i++)
|
|
{
|
|
if ((types[i] & 0x01) == 0 && currentFigure.Count > 0)
|
|
{
|
|
// New figure starting — save previous if it's the largest so far.
|
|
if (currentFigure.Count > bestFigure.Count)
|
|
bestFigure = currentFigure;
|
|
|
|
currentFigure = new List<Vector>();
|
|
}
|
|
|
|
currentFigure.Add(new Vector(points[i].X, points[i].Y));
|
|
}
|
|
|
|
if (currentFigure.Count > bestFigure.Count)
|
|
bestFigure = currentFigure;
|
|
|
|
if (bestFigure.Count < 3)
|
|
return points[0];
|
|
|
|
// Close the polygon if needed.
|
|
var first = bestFigure[0];
|
|
var last = bestFigure[bestFigure.Count - 1];
|
|
if (first.DistanceTo(last) > 1e-6)
|
|
bestFigure.Add(first);
|
|
|
|
var label = PolyLabel.Find(bestFigure, 0.5);
|
|
return new PointF((float)label.X, (float)label.Y);
|
|
}
|
|
|
|
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 result = new List<PointF[]>();
|
|
var entities = ConvertProgram.ToGeometry(BasePart.Program);
|
|
var shapes = ShapeBuilder.GetShapes(entities.Where(e => e.Layer != SpecialLayers.Rapid));
|
|
|
|
foreach (var shape in shapes)
|
|
{
|
|
var offsetEntity = shape.OffsetEntity(spacing, OffsetSide.Left) as Shape;
|
|
|
|
if (offsetEntity == null)
|
|
continue;
|
|
|
|
var polygon = offsetEntity.ToPolygonWithTolerance(tolerance);
|
|
polygon.RemoveSelfIntersections();
|
|
|
|
if (polygon.Vertices.Count < 2)
|
|
continue;
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|