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}"; } }