mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 09:21:05 -04:00
Clamps could only sit on the origin-side edge and be edited as a list in Nest > Clamps. Setting them up against a real sheet is easier by dragging them where they are, so Plate > Edit Clamps adds an edit mode: drag a clamp along its edge or onto another edge, click near an edge to add one, click a clamp's locating dimension to type its distance from the origin, Delete to remove. Dimensions measure to the clamp center or its near edge (remembered per user). Edits replace the nest's rail so every sheet follows, like the dialog. Rail clamps now carry a ClampEdge (origin side, far side, origin end, far end), positioned from the origin along that edge. Origin-side clamps keep their ids and files; other edges persist in optional per-edge arrays in .nest files and clamps.json, omitted when empty. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
249 lines
11 KiB
C#
249 lines
11 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using OpenNest.Geometry;
|
|
|
|
namespace OpenNest
|
|
{
|
|
/// <summary>
|
|
/// The machine's fixed clamp rail: identical rectangular clamps, each on one sheet edge
|
|
/// (<see cref="ClampEdge"/>) 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.
|
|
/// </summary>
|
|
public sealed class ClampRail : IEquatable<ClampRail>
|
|
{
|
|
/// <summary>
|
|
/// Planner default minutes to remove one clamp and put it back after cutting (D9),
|
|
/// used when a caller does not supply its own value.
|
|
/// </summary>
|
|
public const double DefaultRemovalMinutes = 5.0;
|
|
|
|
/// <summary>A rail whose clamps all sit on the origin-side edge.</summary>
|
|
public ClampRail(
|
|
double width,
|
|
double depth,
|
|
IEnumerable<double> 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<RailClamp> 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<RailClamp>();
|
|
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();
|
|
}
|
|
|
|
/// <summary>Clamp extent along its edge.</summary>
|
|
public double Width { get; }
|
|
|
|
/// <summary>Clamp intrusion into the sheet from its edge.</summary>
|
|
public double Depth { get; }
|
|
|
|
/// <summary>
|
|
/// Minutes to remove one clamp and put it back after cutting (D9). Added to a sheet's
|
|
/// cut-time estimate once per clamp <see cref="Plate.RemovedClamps"/> lists there.
|
|
/// </summary>
|
|
public double RemovalMinutes { get; }
|
|
|
|
/// <summary>
|
|
/// Distinct clamps ordered by edge, then center. A clamp's position in this list
|
|
/// gives its <c>clamp-N</c> id, so a rail with only origin-side clamps keeps the ids
|
|
/// it had before clamps could sit on other edges.
|
|
/// </summary>
|
|
public IReadOnlyList<RailClamp> Clamps { get; }
|
|
|
|
/// <summary>Centers of the origin-side clamps, ascending; see <see cref="Clamps"/> for all.</summary>
|
|
public IReadOnlyList<double> Centers { get; }
|
|
|
|
/// <summary>A copy with the same size and removal time and these clamps.</summary>
|
|
public ClampRail WithClamps(IEnumerable<RailClamp> clamps) =>
|
|
new(Width, Depth, clamps, RemovalMinutes);
|
|
|
|
/// <summary>The <c>clamp-N</c> id of the clamp at <paramref name="index"/> in <see cref="Clamps"/>.</summary>
|
|
public static string IdOf(int index) => $"clamp-{index + 1}";
|
|
|
|
/// <summary>
|
|
/// The footprint <paramref name="clamp"/> 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.
|
|
/// </summary>
|
|
public Box Footprint(RailClamp clamp, Size size, int quadrant) =>
|
|
Footprint(clamp, Width, Depth, size, quadrant);
|
|
|
|
/// <summary>
|
|
/// The footprint of a <paramref name="width"/> by <paramref name="depth"/> clamp at
|
|
/// <paramref name="clamp"/>; see <see cref="Footprint(RailClamp, Size, int)"/>.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The zones this rail places on a sheet, in plate coordinates (the frame of
|
|
/// <see cref="Plate.ClampZones"/>: 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 <c>clamp-N</c> by
|
|
/// rail position, so one physical clamp keeps its id on every sheet even when others
|
|
/// are skipped.
|
|
/// </summary>
|
|
public IReadOnlyList<ClampZone> ResolveFor(Size size, int quadrant)
|
|
{
|
|
var zones = new List<ClampZone>(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;
|
|
}
|
|
|
|
/// <summary>True when the plate's zones are exactly (ids and bits) this rail's for its sheet.</summary>
|
|
public bool Matches(Plate plate)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(plate);
|
|
return SameZones(plate.ClampZones, ResolveFor(plate.Size, plate.Quadrant));
|
|
}
|
|
|
|
/// <summary>
|
|
/// True when the plate's zones are exactly this rail's resolution for its sheet minus
|
|
/// the clamps named in <paramref name="removedClamps"/> (D8 last-resort removal, C13).
|
|
/// A null or empty set is the same check as <see cref="Matches(Plate)"/>.
|
|
/// </summary>
|
|
public bool Matches(Plate plate, IReadOnlyCollection<string> 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<ClampZone> current, IReadOnlyList<ClampZone> 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}";
|
|
}
|
|
}
|