using System;
using System.Collections.Generic;
using System.Linq;
using OpenNest.Geometry;
namespace OpenNest
{
///
/// The machine's fixed clamp rail: identical rectangular clamps, each on one sheet edge
/// () with its center measured from the plate origin along that
/// edge. Plates always start at the origin, so the same rail resolves onto every sheet
/// size without re-scaling. A clamp whose footprint lies wholly beyond a sheet is skipped;
/// one that still reaches onto it is kept whole: the zone is an avoidance area, so its
/// full footprint applies even where it hangs past the sheet.
///
public sealed class ClampRail : IEquatable
{
///
/// Planner default minutes to remove one clamp and put it back after cutting (D9),
/// used when a caller does not supply its own value.
///
public const double DefaultRemovalMinutes = 5.0;
/// A rail whose clamps all sit on the origin-side edge.
public ClampRail(
double width,
double depth,
IEnumerable centers,
double removalMinutes = DefaultRemovalMinutes
)
: this(
width,
depth,
(centers ?? throw new ArgumentNullException(nameof(centers)))
.Select(c => new RailClamp(ClampEdge.OriginSide, c)),
removalMinutes
)
{ }
public ClampRail(
double width,
double depth,
IEnumerable clamps,
double removalMinutes = DefaultRemovalMinutes
)
{
ArgumentNullException.ThrowIfNull(clamps);
if (!double.IsFinite(width) || width <= 0)
throw new ArgumentOutOfRangeException(
nameof(width),
"Clamp width must be finite and positive."
);
if (!double.IsFinite(depth) || depth <= 0)
throw new ArgumentOutOfRangeException(
nameof(depth),
"Clamp depth must be finite and positive."
);
if (!double.IsFinite(removalMinutes) || removalMinutes < 0)
throw new ArgumentOutOfRangeException(
nameof(removalMinutes),
"Clamp removal time must be finite and zero or more."
);
var sorted = new SortedSet();
foreach (var clamp in clamps)
{
if (!double.IsFinite(clamp.Center) || clamp.Center < 0)
throw new ArgumentOutOfRangeException(
"centers",
"Clamp centers must be finite and measured from the origin (zero or more)."
);
if (!ClampEdges.IsDefined(clamp.Edge))
throw new ArgumentOutOfRangeException(nameof(clamps), "Unknown clamp edge.");
sorted.Add(clamp);
}
Width = width;
Depth = depth;
RemovalMinutes = removalMinutes;
Clamps = sorted.ToArray();
Centers = Clamps.Where(c => c.Edge == ClampEdge.OriginSide).Select(c => c.Center).ToArray();
}
/// Clamp extent along its edge.
public double Width { get; }
/// Clamp intrusion into the sheet from its edge.
public double Depth { get; }
///
/// Minutes to remove one clamp and put it back after cutting (D9). Added to a sheet's
/// cut-time estimate once per clamp lists there.
///
public double RemovalMinutes { get; }
///
/// Distinct clamps ordered by edge, then center. A clamp's position in this list
/// gives its clamp-N id, so a rail with only origin-side clamps keeps the ids
/// it had before clamps could sit on other edges.
///
public IReadOnlyList Clamps { get; }
/// Centers of the origin-side clamps, ascending; see for all.
public IReadOnlyList Centers { get; }
/// A copy with the same size and removal time and these clamps.
public ClampRail WithClamps(IEnumerable clamps) =>
new(Width, Depth, clamps, RemovalMinutes);
/// The clamp-N id of the clamp at in .
public static string IdOf(int index) => $"clamp-{index + 1}";
///
/// The footprint would have on a sheet, in plate coordinates
/// (lower-left corner origin), or null when it lies wholly beyond the sheet. The clamp
/// need not belong to this rail; it takes this rail's width and depth.
///
public Box Footprint(RailClamp clamp, Size size, int quadrant) =>
Footprint(clamp, Width, Depth, size, quadrant);
///
/// The footprint of a by clamp at
/// ; see .
///
public static Box Footprint(RailClamp clamp, double width, double depth, Size size, int quadrant)
{
var length = size.Length;
var sheetWidth = size.Width;
if (!(length > 0) || !(sheetWidth > 0) || !double.IsFinite(length) || !double.IsFinite(sheetWidth))
return null;
var extendsLeft = quadrant is 2 or 3;
var extendsDown = quadrant is 3 or 4;
var half = width / 2;
// Distances from the origin along the edge.
var near = clamp.Center - half;
var far = clamp.Center + half;
if (clamp.Edge.RunsAlongX())
{
if (near >= length)
return null; // wholly beyond this sheet
// The origin-side edge is y = 0 of the sheet, or its top when it extends to -Y.
var atTop = (clamp.Edge == ClampEdge.FarSide) != extendsDown;
// Plate coordinates are measured from the lower-left corner; a sheet extending
// to -X from the origin has its origin edge at x = length.
var x = extendsLeft ? length - far : near;
return new Box(x, atTop ? sheetWidth - depth : 0, width, depth);
}
if (near >= sheetWidth)
return null;
var atRight = (clamp.Edge == ClampEdge.FarEnd) != extendsLeft;
var y = extendsDown ? sheetWidth - far : near;
return new Box(atRight ? length - depth : 0, y, depth, width);
}
///
/// The zones this rail places on a sheet, in plate coordinates (the frame of
/// : origin at the sheet's lower-left corner). The
/// quadrant locates the plate origin, and with it the origin-side edge, on the sheet.
/// Zones are whole clamp footprints, never clipped, so one reaching past a sheet end
/// (or deeper than a narrow sheet) extends beyond the sheet. Ids are clamp-N by
/// rail position, so one physical clamp keeps its id on every sheet even when others
/// are skipped.
///
public IReadOnlyList ResolveFor(Size size, int quadrant)
{
var zones = new List(Clamps.Count);
for (var i = 0; i < Clamps.Count; i++)
{
var box = Footprint(Clamps[i], size, quadrant);
if (box != null)
zones.Add(new ClampZone(IdOf(i), box));
}
return zones;
}
/// True when the plate's zones are exactly (ids and bits) this rail's for its sheet.
public bool Matches(Plate plate)
{
ArgumentNullException.ThrowIfNull(plate);
return SameZones(plate.ClampZones, ResolveFor(plate.Size, plate.Quadrant));
}
///
/// True when the plate's zones are exactly this rail's resolution for its sheet minus
/// the clamps named in (D8 last-resort removal, C13).
/// A null or empty set is the same check as .
///
public bool Matches(Plate plate, IReadOnlyCollection removedClamps)
{
ArgumentNullException.ThrowIfNull(plate);
if (removedClamps == null || removedClamps.Count == 0)
return Matches(plate);
var expected = ResolveFor(plate.Size, plate.Quadrant)
.Where(z => !removedClamps.Contains(z.Id))
.ToList();
return SameZones(plate.ClampZones, expected);
}
internal static bool SameZones(IList current, IReadOnlyList expected)
{
if (current.Count != expected.Count)
return false;
for (var i = 0; i < current.Count; i++)
{
var a = current[i];
var b = expected[i];
if (
!string.Equals(a.Id, b.Id, StringComparison.Ordinal)
|| a.Bounds.X != b.Bounds.X
|| a.Bounds.Y != b.Bounds.Y
|| a.Bounds.Length != b.Bounds.Length
|| a.Bounds.Width != b.Bounds.Width
)
return false;
}
return true;
}
public bool Equals(ClampRail other) =>
other is not null
&& Width.Equals(other.Width)
&& Depth.Equals(other.Depth)
&& RemovalMinutes.Equals(other.RemovalMinutes)
&& Clamps.SequenceEqual(other.Clamps);
public override bool Equals(object obj) => Equals(obj as ClampRail);
public override int GetHashCode()
{
var hash = HashCode.Combine(Width, Depth, RemovalMinutes, Clamps.Count);
foreach (var clamp in Clamps)
hash = HashCode.Combine(hash, clamp);
return hash;
}
public override string ToString() =>
$"ClampRail {Width:R} x {Depth:R} at [{string.Join(", ", Clamps.Select(Describe))}]";
private static string Describe(RailClamp clamp) =>
clamp.Edge == ClampEdge.OriginSide
? clamp.Center.ToString("R")
: $"{clamp.Edge} {clamp.Center:R}";
}
}