Part.Clone() rebuilt the clone from BaseDrawing and then re-applied the part's absolute Rotation on top of it. Since BaseDrawing.Program.Rotation is itself absolute (baked in), this double-counted it whenever the base drawing already carried a nonzero rotation, silently corrupting the clone's orientation while its Location stayed unchanged. This only manifests for drawings needing canonical-frame axis correction (nonzero Source.Angle), since DefaultPlateFiller wraps every drawing in a rotated canonical copy before running any fill strategy. FillHelpers. BuildRotatedPattern clones parts before tiling, so any strategy that tiles interlocking pairs (Pairs, Strip/Remnant, Column/Row) could produce overlapping placements for such drawings. Fix: clone the already-composed Program directly instead of re-deriving rotation from BaseDrawing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
355 lines
11 KiB
C#
355 lines
11 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using OpenNest.CNC;
|
|
using OpenNest.Converters;
|
|
using OpenNest.Geometry;
|
|
using OpenNest.Math;
|
|
|
|
namespace OpenNest
|
|
{
|
|
public interface IPart : IBoundable
|
|
{
|
|
Vector Location { get; set; }
|
|
double Rotation { get; }
|
|
void Rotate(double angle);
|
|
void Rotate(double angle, Vector origin);
|
|
void Offset(double x, double y);
|
|
void Offset(Vector voffset);
|
|
void Update();
|
|
}
|
|
|
|
public class Part : IPart, IBoundable
|
|
{
|
|
/// <summary>
|
|
/// Chord tolerance used to polygonize arcs/circles for overlap testing in
|
|
/// <see cref="Intersects"/>. Matches the tolerance used elsewhere for
|
|
/// geometry-sensitive checks (e.g. <see cref="PartGeometry"/>).
|
|
/// </summary>
|
|
private const double IntersectsChordTolerance = 0.001;
|
|
|
|
private Vector location;
|
|
private bool ownsProgram;
|
|
private double preLeadInRotation;
|
|
|
|
public readonly Drawing BaseDrawing;
|
|
|
|
public Part(Drawing baseDrawing)
|
|
: this(baseDrawing, new Vector()) { }
|
|
|
|
public Part(Drawing baseDrawing, Vector location)
|
|
{
|
|
BaseDrawing = baseDrawing;
|
|
Program = baseDrawing.Program.Clone() as Program;
|
|
ownsProgram = true;
|
|
this.location = location;
|
|
UpdateBounds();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Location of the part.
|
|
/// </summary>
|
|
public Vector Location
|
|
{
|
|
get { return location; }
|
|
set
|
|
{
|
|
BoundingBox.Offset(value - location);
|
|
location = value;
|
|
}
|
|
}
|
|
|
|
public Program Program { get; private set; }
|
|
|
|
public bool HasManualLeadIns { get; set; }
|
|
|
|
public bool LeadInsLocked { get; set; }
|
|
|
|
public CNC.CuttingStrategy.CuttingParameters CuttingParameters { get; set; }
|
|
|
|
public void ApplyLeadIns(
|
|
CNC.CuttingStrategy.CuttingParameters parameters,
|
|
Vector approachPoint
|
|
)
|
|
{
|
|
ApplyLeadIns(parameters, approachPoint, Geometry.Vector.Invalid);
|
|
}
|
|
|
|
public void ApplyLeadIns(
|
|
CNC.CuttingStrategy.CuttingParameters parameters,
|
|
Vector approachPoint,
|
|
Vector nextPartStart
|
|
)
|
|
{
|
|
preLeadInRotation = Rotation;
|
|
var strategy = new CNC.CuttingStrategy.ContourCuttingStrategy
|
|
{
|
|
Parameters = parameters,
|
|
};
|
|
var result = strategy.Apply(Program, approachPoint, nextPartStart);
|
|
Program = result.Program;
|
|
CuttingParameters = parameters;
|
|
HasManualLeadIns = true;
|
|
UpdateBounds();
|
|
}
|
|
|
|
public void ApplySingleLeadIn(
|
|
CNC.CuttingStrategy.CuttingParameters parameters,
|
|
Geometry.Vector point,
|
|
Geometry.Entity entity,
|
|
CNC.CuttingStrategy.ContourType contourType
|
|
)
|
|
{
|
|
preLeadInRotation = Rotation;
|
|
var strategy = new CNC.CuttingStrategy.ContourCuttingStrategy
|
|
{
|
|
Parameters = parameters,
|
|
};
|
|
var result = strategy.ApplySingle(Program, point, entity, contourType);
|
|
Program = result.Program;
|
|
CuttingParameters = parameters;
|
|
HasManualLeadIns = true;
|
|
UpdateBounds();
|
|
}
|
|
|
|
public void RemoveLeadIns()
|
|
{
|
|
var rotation = preLeadInRotation;
|
|
var location = Location;
|
|
Program = BaseDrawing.Program.Clone() as Program;
|
|
ownsProgram = true;
|
|
|
|
if (!Math.Tolerance.IsEqualTo(rotation, 0))
|
|
Program.Rotate(rotation);
|
|
|
|
Location = location;
|
|
HasManualLeadIns = false;
|
|
LeadInsLocked = false;
|
|
CuttingParameters = null;
|
|
UpdateBounds();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the rotation of the part in radians.
|
|
/// </summary>
|
|
public double Rotation
|
|
{
|
|
get { return HasManualLeadIns ? preLeadInRotation : Program.Rotation; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Rotates the part.
|
|
/// </summary>
|
|
/// <param name="angle">Angle of rotation in radians.</param>
|
|
public void Rotate(double angle)
|
|
{
|
|
EnsureOwnedProgram();
|
|
Program.Rotate(angle);
|
|
location = Location.Rotate(angle);
|
|
preLeadInRotation = Program.Rotation;
|
|
UpdateBounds();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Rotates the part around the specified origin.
|
|
/// </summary>
|
|
/// <param name="angle">Angle of rotation in radians.</param>
|
|
/// <param name="origin">The origin to rotate the part around.</param>
|
|
public void Rotate(double angle, Vector origin)
|
|
{
|
|
EnsureOwnedProgram();
|
|
Program.Rotate(angle);
|
|
location = Location.Rotate(angle, origin);
|
|
preLeadInRotation = Program.Rotation;
|
|
UpdateBounds();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Offsets the part.
|
|
/// </summary>
|
|
/// <param name="x">The x-axis offset distance.</param>
|
|
/// <param name="y">The y-axis offset distance.</param>
|
|
public void Offset(double x, double y)
|
|
{
|
|
location = new Vector(location.X + x, location.Y + y);
|
|
BoundingBox.Offset(x, y);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Offsets the part.
|
|
/// </summary>
|
|
/// <param name="voffset">The vector containing the x-axis & y-axis offset distances.</param>
|
|
public void Offset(Vector voffset)
|
|
{
|
|
location += voffset;
|
|
BoundingBox.Offset(voffset);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a part normalized to the origin with optional rotation.
|
|
/// </summary>
|
|
public static Part CreateAtOrigin(Drawing drawing, double rotation = 0)
|
|
{
|
|
var part = new Part(drawing);
|
|
|
|
if (!Math.Tolerance.IsEqualTo(rotation, 0))
|
|
part.Rotate(rotation);
|
|
|
|
var bbox = part.Program.BoundingBox();
|
|
part.Offset(-bbox.Location.X, -bbox.Location.Y);
|
|
part.UpdateBounds();
|
|
|
|
return part;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the bounding box of the part.
|
|
/// </summary>
|
|
public void UpdateBounds()
|
|
{
|
|
BoundingBox = Program.BoundingBox();
|
|
BoundingBox.Offset(Location);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the part from the drawing it was derived from.
|
|
/// </summary>
|
|
public void Update()
|
|
{
|
|
var rotation = Rotation;
|
|
Program = BaseDrawing.Program.Clone() as Program;
|
|
|
|
if (!Math.Tolerance.IsEqualTo(rotation, 0))
|
|
Program.Rotate(rotation);
|
|
|
|
HasManualLeadIns = false;
|
|
LeadInsLocked = false;
|
|
CuttingParameters = null;
|
|
UpdateBounds();
|
|
}
|
|
|
|
/// <summary>
|
|
/// The smallest box that contains the part.
|
|
/// </summary>
|
|
public Box BoundingBox { get; protected set; }
|
|
|
|
public bool Intersects(Part part, out List<Vector> pts)
|
|
{
|
|
pts = new List<Vector>();
|
|
|
|
var entities1 = ConvertProgram
|
|
.ToGeometry(Program)
|
|
.Where(e => e.Layer != SpecialLayers.Rapid)
|
|
.ToList();
|
|
var entities2 = ConvertProgram
|
|
.ToGeometry(part.Program)
|
|
.Where(e => e.Layer != SpecialLayers.Rapid)
|
|
.ToList();
|
|
|
|
if (entities1.Count == 0 || entities2.Count == 0)
|
|
return false;
|
|
|
|
var perimeter1 = new ShapeProfile(entities1).Perimeter;
|
|
var perimeter2 = new ShapeProfile(entities2).Perimeter;
|
|
|
|
if (perimeter1 == null || perimeter2 == null)
|
|
return false;
|
|
|
|
var polygon1 = perimeter1.ToPolygonWithTolerance(IntersectsChordTolerance);
|
|
var polygon2 = perimeter2.ToPolygonWithTolerance(IntersectsChordTolerance);
|
|
|
|
if (polygon1 == null || polygon2 == null)
|
|
return false;
|
|
|
|
polygon1.Offset(Location);
|
|
polygon2.Offset(part.Location);
|
|
|
|
var result = Geometry.Collision.Check(polygon1, polygon2);
|
|
pts = result.IntersectionPoints.ToList();
|
|
return result.Overlaps;
|
|
}
|
|
|
|
public double Left
|
|
{
|
|
get { return BoundingBox.Left; }
|
|
}
|
|
|
|
public double Right
|
|
{
|
|
get { return BoundingBox.Right; }
|
|
}
|
|
|
|
public double Top
|
|
{
|
|
get { return BoundingBox.Top; }
|
|
}
|
|
|
|
public double Bottom
|
|
{
|
|
get { return BoundingBox.Bottom; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a deep copy of the part.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public object Clone()
|
|
{
|
|
// Clone the current Program directly rather than rebuilding from BaseDrawing and
|
|
// re-rotating by the absolute Rotation: when BaseDrawing.Program.Rotation is nonzero
|
|
// (e.g. a canonical-frame copy used internally during fill), `new Part(BaseDrawing)`
|
|
// already carries that baked rotation, so re-applying the full absolute Rotation on
|
|
// top of it double-counts the baseline and corrupts the clone's orientation.
|
|
var part = new Part(
|
|
BaseDrawing,
|
|
(Program)Program.Clone(),
|
|
Location,
|
|
new Box(BoundingBox.X, BoundingBox.Y, BoundingBox.Length, BoundingBox.Width)
|
|
);
|
|
part.ownsProgram = true;
|
|
|
|
return part;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an offset copy of the part. Clones from the already-rotated
|
|
/// program (skips re-rotation) and computes the bounding box arithmetically
|
|
/// (skips Program.BoundingBox walk).
|
|
/// </summary>
|
|
public Part CloneAtOffset(Vector offset)
|
|
{
|
|
// Share the Program instance — offset-only copies don't modify the program codes.
|
|
// This is a major performance win for tiling large patterns.
|
|
var part = new Part(
|
|
BaseDrawing,
|
|
Program,
|
|
location + offset,
|
|
new Box(
|
|
BoundingBox.X + offset.X,
|
|
BoundingBox.Y + offset.Y,
|
|
BoundingBox.Length,
|
|
BoundingBox.Width
|
|
)
|
|
);
|
|
|
|
return part;
|
|
}
|
|
|
|
private void EnsureOwnedProgram()
|
|
{
|
|
if (!ownsProgram)
|
|
{
|
|
Program = Program.Clone() as Program;
|
|
ownsProgram = true;
|
|
}
|
|
}
|
|
|
|
private Part(Drawing baseDrawing, Program program, Vector location, Box boundingBox)
|
|
{
|
|
BaseDrawing = baseDrawing;
|
|
Program = program;
|
|
this.location = location;
|
|
BoundingBox = boundingBox;
|
|
}
|
|
}
|
|
}
|