using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using OpenNest.Collections; using OpenNest.Geometry; using OpenNest.Math; using OpenNest.Shapes; using OpenNest.Support; namespace OpenNest { public class Plate { private int quadrant; public event EventHandler> PartAdded { add { Parts.ItemAdded += value; } remove { Parts.ItemAdded -= value; } } public event EventHandler> PartRemoved { add { Parts.ItemRemoved += value; } remove { Parts.ItemRemoved -= value; } } public event EventHandler> PartChanged { add { Parts.ItemChanged += value; } remove { Parts.ItemChanged -= value; } } /// /// Raised once after the part order changes without adding or removing parts, including /// a committed cutting plan that also installed planned programs. Refresh order- and /// program-derived views; quantity accounting is unchanged. /// public event EventHandler PartsReordered { add { Parts.ItemsReordered += value; } remove { Parts.ItemsReordered -= value; } } public Plate() : this(60, 120) { } public Plate(double width, double length) : this(new Size(width, length)) { } public Plate(Size size) { EdgeSpacing = new Spacing(); Size = size; Parts = new ObservableList(); Parts.ItemAdded += Parts_PartAdded; Parts.ItemRemoved += Parts_PartRemoved; CutOffs = new ObservableList(); ClampZones = new ObservableList(); ClampZones.ItemAdded += (_, _) => OnClampZonesChanged(); ClampZones.ItemRemoved += (_, _) => OnClampZonesChanged(); ClampZones.ItemChanged += (_, _) => OnClampZonesChanged(); Quadrant = 1; } private void Parts_PartAdded(object sender, ItemAddedEventArgs e) { if (!e.Item.BaseDrawing.IsCutOff) e.Item.BaseDrawing.Quantity.Nested += Quantity; } private void Parts_PartRemoved(object sender, ItemRemovedEventArgs e) { if (!e.Item.BaseDrawing.IsCutOff) e.Item.BaseDrawing.Quantity.Nested -= Quantity; } /// /// The spacing between parts. /// public double PartSpacing { get; set; } /// /// The spacing along the edges of the plate. /// public Spacing EdgeSpacing; /// /// The size of the plate. A plate following a re-resolves /// its clamp zones for the new size. /// public Size Size { get { return size; } set { // A turned sheet is the same sheet; any other size is no longer the coded item. if (stockItemCodes.Count > 0 && !SameSheet(size, value)) stockItemCodes = Array.Empty(); size = value; ResolveClampRail(); } } private Size size; private static bool SameSheet(Size a, Size b) => a.Width == b.Width && a.Length == b.Length || a.Width == b.Length && a.Length == b.Width; /// /// The inventory item each physical sheet of this plate is cut from: entry i names sheet /// i of , and sheets past the end are uncoded. Empty when no sheet /// is coded. A resize (other than turning the sheet) clears it, lowering /// truncates it, and raising it leaves the extra sheets uncoded. /// Display and hand-off data only: quantity accounting never reads it. /// public IReadOnlyList StockItemCodes { get { return stockItemCodes; } set { ArgumentNullException.ThrowIfNull(value); if (value.Any(string.IsNullOrWhiteSpace)) throw new ArgumentException("Stock item codes must not be blank.", nameof(value)); stockItemCodes = value.Take(System.Math.Max(quantity, 0)).ToArray(); } } private IReadOnlyList stockItemCodes = Array.Empty(); public CNC.CuttingStrategy.CuttingParameters CuttingParameters { get; set; } /// /// Material grain direction in radians. 0 = horizontal. /// public double GrainAngle { get; set; } /// /// The parts that the plate contains. /// public ObservableList Parts { get; set; } /// /// The cut-off lines defined on this plate. /// public ObservableList CutOffs { get; set; } /// /// Fixed rectangular machine-clamp keep-outs in plate coordinates /// (). Never parts: excluded from quantity, utilization, /// cut sequence and CNC output; placement rules and display consume them. Mutate /// through this list; fires for every change. /// public ObservableList ClampZones { get; } /// /// Raised after content changes so views can refresh /// the zone overlay. Carries no geometry arguments. /// public event EventHandler ClampZonesChanged; private void OnClampZonesChanged() => ClampZonesChanged?.Invoke(this, EventArgs.Empty); /// /// The machine clamp rail this plate follows, or null when its /// are explicit (engine sheets, files without a rail). While /// set, the zones are derived: re-resolved whenever or /// changes, because the clamps are fixed to the machine. /// public ClampRail ClampRail { get; private set; } /// /// The machine support bed this sheet rests on (its nest's, see /// ), or null for none. Display, analysis and support /// tabs only: never material, spacing or a keep-out. /// public SupportBed SupportBed { get; set; } private static readonly IReadOnlySet NoRemovedClamps = ImmutableHashSet.Empty; /// /// Rail clamp ids taken off this sheet, by Auto Nest as a last resort (D8) so a part /// that could not be nested anywhere with every clamp in place still fits, or by hand /// (). Empty unless /// is set; already excludes them, so /// nothing else needs to consult this list to avoid them. Ids stay listed across a /// resize/rotate even when the matching clamp is temporarily beyond the sheet, so it /// re-appears removed if a later resize brings it back onto the sheet (C13). /// public IReadOnlySet RemovedClamps { get; private set; } = NoRemovedClamps; /// /// Minutes this sheet's cut-time estimate gains from (D9): /// each listed clamp the rail places on this sheet adds /// once; a listed clamp that is beyond /// the sheet (or unknown to the rail) is not pulled and costs nothing. Zero when there /// is no rail (removal is meaningless without one to remove clamps from). /// public double ClampRemovalMinutes => ClampRail == null || RemovedClamps.Count == 0 ? 0 : ClampRail.ResolveFor(Size, Quadrant).Count(zone => RemovedClamps.Contains(zone.Id)) * ClampRail.RemovalMinutes; /// /// Follows and replaces the zones with its resolution for /// this sheet; null detaches and removes every zone. Unchanged zones raise no events. /// public void SetClampRail(ClampRail rail) => SetClampRail(rail, null); /// /// Follows with the clamps named in /// taken off this sheet (D8): zones become the rail's resolution for this sheet minus /// those ids. Null detaches, clears every zone and forgets the /// removed set (removal only means something while following a rail). /// public void SetClampRail(ClampRail rail, IEnumerable removedClamps) { ClampRail = rail; RemovedClamps = rail == null || removedClamps == null ? NoRemovedClamps : removedClamps.ToImmutableHashSet(StringComparer.Ordinal); ReplaceClampZones(ResolveRailZones()); } /// /// The rail clamp this sheet places at (the parts' placement /// frame, see ), whether in place or removed, grown by /// on every side; null without a rail or a clamp there. /// Returned bounds are in the placement frame. /// public ClampZone RailClampAt(Vector point, double tolerance = 0) { if (ClampRail == null) return null; var left = Quadrant is 2 or 3 ? -Size.Length : 0; var bottom = Quadrant is 3 or 4 ? -Size.Width : 0; foreach (var zone in ClampRail.ResolveFor(Size, Quadrant)) { var box = zone.Bounds.Translate(left, bottom); if (box.Offset(tolerance).Contains(point)) return new ClampZone(zone.Id, box); } return null; } /// /// Takes the rail clamp off this sheet, or puts it back, as a /// manual counterpart to Auto Nest's last-resort removal (D8): the zones and /// follow. False when nothing changed: no rail, a clamp /// the rail does not place on this sheet, or one already in the requested state. /// public bool SetClampRemoved(string id, bool removed) { if (ClampRail == null || id == null || RemovedClamps.Contains(id) == removed) return false; if (removed && !ClampRail.ResolveFor(Size, Quadrant).Any(zone => zone.Id == id)) return false; var ids = removed ? RemovedClamps.Append(id) : RemovedClamps.Where(x => x != id); SetClampRail(ClampRail, ids); return true; } private void ResolveClampRail() { // Called from the constructor's Size assignment before the zone list exists. if (ClampRail != null) ReplaceClampZones(ResolveRailZones()); } private IReadOnlyList ResolveRailZones() { if (ClampRail == null) return Array.Empty(); var resolved = ClampRail.ResolveFor(Size, Quadrant); if (RemovedClamps.Count == 0) return resolved; var kept = new List(resolved.Count); foreach (var zone in resolved) if (!RemovedClamps.Contains(zone.Id)) kept.Add(zone); return kept; } private void ReplaceClampZones(IReadOnlyList zones) { if (ClampRail.SameZones(ClampZones, zones)) return; ClampZones.Clear(); foreach (var zone in zones) ClampZones.Add(zone); } /// /// Regenerates all cut-off drawings and materializes each of their trimmed segments as /// a part. Existing cut-off parts are removed first, then each regenerated segment goes /// back to the place in the cut sequence ( order) of the saved segment /// of its cut-off it overlaps most, so a part drag or cut-off move doesn't send it to the /// end of the sequence; a segment with no saved counterpart follows the segment before /// it. New cut-offs are added at the end. /// public void RegenerateCutOffs(CutOffSettings settings) { var saved = new Dictionary>(); for (var i = Parts.Count - 1; i >= 0; i--) { if (!Parts[i].BaseDrawing.IsCutOff) continue; var cutoff = CutOffs.FirstOrDefault(c => ReferenceEquals(c.Drawing, Parts[i].BaseDrawing)); if (cutoff != null) { if (!saved.TryGetValue(cutoff, out var segments)) saved[cutoff] = segments = new List<(int, double, double)>(); var extent = CutOffSegments.Extent(Parts[i], cutoff.Axis == CutOffAxis.Vertical); segments.Add((i, extent?.Start ?? double.NaN, extent?.End ?? double.NaN)); } Parts.RemoveAt(i); } RegenerateCutOffs(settings, (cutoff, extents) => { if (!saved.TryGetValue(cutoff, out var segments)) return null; var indices = new int[extents.Count]; var previous = segments.Min(s => s.Index); for (var k = 0; k < extents.Count; k++) { var (start, end) = extents[k]; var best = -1; var bestOverlap = 0.0; foreach (var (index, savedStart, savedEnd) in segments) { var overlap = System.Math.Min(end, savedEnd) - System.Math.Max(start, savedStart); if (overlap > bestOverlap) { best = index; bestOverlap = overlap; } } indices[k] = previous = best >= 0 ? best : previous; } return indices; }); } /// /// Regenerates all cut-off drawings and materializes each of their trimmed segments as a /// part, placing a cut-off's segments consecutively from its index in /// (its place in order). Cut-offs missing /// from it are added at the end. Callers must remove existing cut-off parts first. /// public void RegenerateCutOffs(CutOffSettings settings, IReadOnlyDictionary sequence) => RegenerateCutOffs(settings, (cutoff, extents) => sequence != null && sequence.TryGetValue(cutoff, out var index) ? Enumerable.Range(index, extents.Count).ToArray() : null); /// /// As , /// but each cut-off lists one index per segment in ascending order along its axis. A list /// whose length differs from the regenerated segment count places the segments /// consecutively from its smallest index. /// public void RegenerateCutOffs(CutOffSettings settings, IReadOnlyDictionary> sequence) => RegenerateCutOffs(settings, (cutoff, extents) => { if (sequence == null || !sequence.TryGetValue(cutoff, out var indices) || indices == null || indices.Count == 0) return null; if (indices.Count != extents.Count) return Enumerable.Range(indices.Min(), extents.Count).ToArray(); // Program order can run toward the origin; the file lists segments ascending. var ascending = Enumerable.Range(0, extents.Count).OrderBy(k => extents[k].Start).ToArray(); var result = new int[extents.Count]; for (var rank = 0; rank < ascending.Length; rank++) result[ascending[rank]] = indices[rank]; return result; }); // sequence gives a cut-off's segment indices in program order from its segments' extents // along its axis (NaN when unmeasurable), or null to append them. private void RegenerateCutOffs(CutOffSettings settings, Func, IReadOnlyList> sequence) { var cache = BuildPerimeterCache(this); var placed = new List<(int Index, int Order, Part Part)>(); // Regenerate and materialize each cut-off's segments foreach (var cutoff in CutOffs) { cutoff.Regenerate(this, settings, cache); var programs = cutoff.SegmentPrograms(); if (programs.Count == 0) continue; var vertical = cutoff.Axis == CutOffAxis.Vertical; var parts = programs.Select(program => new Part(cutoff.Drawing, program)).ToArray(); var extents = parts.Select(part => CutOffSegments.Extent(part, vertical) ?? (double.NaN, double.NaN)).ToArray(); var indices = sequence?.Invoke(cutoff, extents); for (var k = 0; k < parts.Length; k++) { if (indices != null && k < indices.Count) placed.Add((indices[k], placed.Count, parts[k])); else Parts.Add(parts[k]); } } // Lowest index first: each insert then lands on its saved index, because every part // sequenced before it is already in place. Segments sharing an index keep their // program order (a later insert at the same index lands before an earlier one). foreach (var (index, _, part) in placed.OrderBy(p => p.Index).ThenByDescending(p => p.Order)) Parts.Insert(System.Math.Clamp(index, 0, Parts.Count), part); } /// /// Builds a dictionary mapping each non-cut-off part to its perimeter entity. /// Closed shapes use ShapeProfile; open contours fall back to ConvexHull. /// public static Dictionary BuildPerimeterCache(Plate plate) { var cache = new Dictionary(); foreach (var part in plate.Parts) { if (part.BaseDrawing.IsCutOff) continue; cache[part] = BuildPartPerimeter(part); } return cache; } /// /// Converts one part's own program into its placed perimeter entity (closed /// profile, or a convex hull fallback for a genuinely open contour). This is /// the same conversion runs per part; callers /// without a pre-built cache (e.g. a direct, uncached /// call) use it to get the same conservative, converted-material envelope instead /// of trusting the part's cached bounding box alone. /// public static Geometry.Entity BuildPartPerimeter(Part part) { try { var entities = Converters .ConvertProgram.ToGeometry(part.Program) .Where(e => SpecialLayers.IsMaterial(e.Layer)) .ToList(); if (entities.Count == 0) return null; // Leads are cutting paths in scrap, not boundary edges. Chaining them // into the outline can make a closed part appear open and replace its // recesses with a convex hull. Keep the full geometry for the existing // conservative fallback when the material contour really is open. var outline = entities.Where(e => e.Layer != SpecialLayers.Leadin && e.Layer != SpecialLayers.Leadout).ToList(); var profile = new Geometry.ShapeProfile(outline); if (profile.Perimeter.IsClosed()) { var perimeter = profile.Perimeter; perimeter.Offset(part.Location); return perimeter; } var points = entities.CollectPoints(); if (points.Count < 3) return null; var hull = Geometry.ConvexHull.Compute(points); hull.Offset(part.Location); return hull; } catch { return null; } } /// /// The number of times to cut the plate. Lowering it drops the /// of the sheets no longer cut. /// public int Quantity { get { return quantity; } set { quantity = value; if (stockItemCodes.Count > System.Math.Max(value, 0)) stockItemCodes = stockItemCodes.Take(System.Math.Max(value, 0)).ToArray(); } } private int quantity; /// /// The quadrant the plate is located in. /// 1 = TopRight /// 2 = TopLeft /// 3 = BottomLeft /// 4 = BottomRight /// public int Quadrant { get { return quadrant; } set { quadrant = value <= 4 && value > 0 ? value : 1; ResolveClampRail(); } } /// /// Rotates the plate clockwise or counter-clockwise along with all parts. /// /// /// public void Rotate90(RotationType rotationDirection, bool keepSameQuadrant = true) { const double oneAndHalfPI = System.Math.PI * 1.5; Size = new Size(Size.Length, Size.Width); // After Size swap above, new Size.Width = old Length (old X extent), // new Size.Length = old Width (old Y extent). // Convention: Length = X axis, Width = Y axis. if (rotationDirection == RotationType.CW) { Rotate(oneAndHalfPI); if (keepSameQuadrant) { switch (Quadrant) { case 1: Offset(0, Size.Width); break; case 2: Offset(-Size.Length, 0); break; case 3: Offset(0, -Size.Width); break; case 4: Offset(Size.Length, 0); break; default: return; } } else { Quadrant = Quadrant < 2 ? 4 : Quadrant - 1; } } else { Rotate(Angle.HalfPI); if (keepSameQuadrant) { switch (Quadrant) { case 1: Offset(Size.Length, 0); break; case 2: Offset(0, Size.Width); break; case 3: Offset(-Size.Length, 0); break; case 4: Offset(0, -Size.Width); break; default: return; } } else { Quadrant = Quadrant > 3 ? 1 : Quadrant + 1; } } } /// /// Rotates the plate 180 degrees along with all parts. /// /// public void Rotate180(bool keepSameQuadrant = true) { if (keepSameQuadrant) { Vector centerpt; switch (Quadrant) { case 1: centerpt = new Vector(Size.Length * 0.5, Size.Width * 0.5); break; case 2: centerpt = new Vector(-Size.Length * 0.5, Size.Width * 0.5); break; case 3: centerpt = new Vector(-Size.Length * 0.5, -Size.Width * 0.5); break; case 4: centerpt = new Vector(Size.Length * 0.5, -Size.Width * 0.5); break; default: return; } Rotate(System.Math.PI, centerpt); } else { Rotate(System.Math.PI); Quadrant = (Quadrant + 2) % 4; if (Quadrant == 0) Quadrant = 4; } } /// /// Rotates the parts on the plate. /// /// public void Rotate(double angle) { for (var i = Parts.Count - 1; i >= 0; i--) { if (Parts[i].BaseDrawing.IsCutOff) Parts.RemoveAt(i); } for (var i = 0; i < Parts.Count; ++i) { var part = Parts[i]; part.Rotate(angle); } foreach (var cutoff in CutOffs) cutoff.Position = cutoff.Position.Rotate(angle); } /// /// Rotates the parts on the plate around the specified origin. /// /// /// public void Rotate(double angle, Vector origin) { for (var i = Parts.Count - 1; i >= 0; i--) { if (Parts[i].BaseDrawing.IsCutOff) Parts.RemoveAt(i); } for (var i = 0; i < Parts.Count; ++i) { var part = Parts[i]; part.Rotate(angle, origin); } foreach (var cutoff in CutOffs) { var pos = cutoff.Position - origin; pos = pos.Rotate(angle); cutoff.Position = pos + origin; } } /// /// Offsets the parts on the plate. /// /// /// public void Offset(double x, double y) { // Remove cut-off parts before transforming for (var i = Parts.Count - 1; i >= 0; i--) { if (Parts[i].BaseDrawing.IsCutOff) Parts.RemoveAt(i); } for (var i = 0; i < Parts.Count; ++i) { var part = Parts[i]; part.Offset(x, y); } // Transform cut-off positions foreach (var cutoff in CutOffs) cutoff.Position = new Vector(cutoff.Position.X + x, cutoff.Position.Y + y); } /// /// Offsets the parts on the plate. /// /// public void Offset(Vector voffset) { for (var i = Parts.Count - 1; i >= 0; i--) { if (Parts[i].BaseDrawing.IsCutOff) Parts.RemoveAt(i); } for (var i = 0; i < Parts.Count; ++i) { var part = Parts[i]; part.Offset(voffset); } foreach (var cutoff in CutOffs) cutoff.Position = new Vector( cutoff.Position.X + voffset.X, cutoff.Position.Y + voffset.Y ); } /// /// The smallest box that contains the plate. /// /// /// public Box BoundingBox(bool includeParts = true) { var plateBox = new Box(); // Width = Y axis (vertical), Length = X axis (horizontal) switch (Quadrant) { case 1: plateBox.X = 0; plateBox.Y = 0; break; case 2: plateBox.X = (float)-Size.Length; plateBox.Y = 0; break; case 3: plateBox.X = (float)-Size.Length; plateBox.Y = (float)-Size.Width; break; case 4: plateBox.X = 0; plateBox.Y = (float)-Size.Width; break; default: return new Box(); } plateBox.Width = Size.Width; plateBox.Length = Size.Length; if (!includeParts) return plateBox; var boundingBox = new Box(); var partsBox = Parts.GetBoundingBox(); boundingBox.X = partsBox.Left < plateBox.Left ? partsBox.Left : plateBox.Left; boundingBox.Y = partsBox.Bottom < plateBox.Bottom ? partsBox.Bottom : plateBox.Bottom; boundingBox.Length = partsBox.Right > plateBox.Right ? partsBox.Right - boundingBox.X : plateBox.Right - boundingBox.X; boundingBox.Width = partsBox.Top > plateBox.Top ? partsBox.Top - boundingBox.Y : plateBox.Top - boundingBox.Y; return boundingBox; } /// /// The area within the edge spacing. /// /// public Box WorkArea() { var box = BoundingBox(false); box.X += EdgeSpacing.Left; box.Y += EdgeSpacing.Bottom; box.Length -= EdgeSpacing.Left + EdgeSpacing.Right; box.Width -= EdgeSpacing.Top + EdgeSpacing.Bottom; return box; } /// /// The offset that centers the parts' combined bounds in the , /// so uneven edge spacing (such as a wider clamp-side margin) shifts them with it. /// Zero when the plate has no parts. /// public Vector OffsetToCenterPartsInWorkArea() => Parts.Count == 0 ? new Vector() : WorkArea().Center - Parts.GetBoundingBox().Center; /// /// Automatically sizes the plate to fit the parts. /// /// The factor to round the actual size up to. /// /// AutoSize 9.7 x 10.1 /// * roundingFactor=1.0 new Size=10 x 11 /// * roundingFactor=0.5 new Size=10 x 10.5 /// * roundingFactor=0.25 new Size=9.75 x 10.25 /// * roundingFactor=0.0 new Size=9.7 x 10.1 /// public void AutoSize(double roundingFactor = 1.0) { if (Parts.Count == 0) return; var bounds = Parts.GetBoundingBox(); // Convention: Length = X axis, Width = Y axis double xExtent; double yExtent; switch (Quadrant) { case 1: xExtent = System.Math.Abs(bounds.Right) + EdgeSpacing.Right; yExtent = System.Math.Abs(bounds.Top) + EdgeSpacing.Top; break; case 2: xExtent = System.Math.Abs(bounds.Left) + EdgeSpacing.Left; yExtent = System.Math.Abs(bounds.Top) + EdgeSpacing.Top; break; case 3: xExtent = System.Math.Abs(bounds.Left) + EdgeSpacing.Left; yExtent = System.Math.Abs(bounds.Bottom) + EdgeSpacing.Bottom; break; case 4: xExtent = System.Math.Abs(bounds.Right) + EdgeSpacing.Right; yExtent = System.Math.Abs(bounds.Bottom) + EdgeSpacing.Bottom; break; default: return; } Size = new Size( Rounding.RoundUpToNearest(yExtent, roundingFactor), Rounding.RoundUpToNearest(xExtent, roundingFactor) ); } /// /// Sizes the plate using the catalog: small /// layouts snap to an increment, larger ones round up to the next /// standard mill sheet. The plate's long-axis orientation (X vs Y) /// is preserved. Does nothing if the plate has no parts. /// public PlateSizeResult SnapToStandardSize(PlateSizeOptions options = null) { if (Parts.Count == 0) return default; var bounds = Parts.GetBoundingBox(); // Quadrant-aware extents relative to the plate origin, matching AutoSize. double xExtent; double yExtent; switch (Quadrant) { case 1: xExtent = System.Math.Abs(bounds.Right) + EdgeSpacing.Right; yExtent = System.Math.Abs(bounds.Top) + EdgeSpacing.Top; break; case 2: xExtent = System.Math.Abs(bounds.Left) + EdgeSpacing.Left; yExtent = System.Math.Abs(bounds.Top) + EdgeSpacing.Top; break; case 3: xExtent = System.Math.Abs(bounds.Left) + EdgeSpacing.Left; yExtent = System.Math.Abs(bounds.Bottom) + EdgeSpacing.Bottom; break; case 4: xExtent = System.Math.Abs(bounds.Right) + EdgeSpacing.Right; yExtent = System.Math.Abs(bounds.Bottom) + EdgeSpacing.Bottom; break; default: return default; } // PlateSizes.Recommend takes (short, long); canonicalize then map // the result back so the plate's long axis stays aligned with the // parts' long axis. var shortDim = System.Math.Min(xExtent, yExtent); var longDim = System.Math.Max(xExtent, yExtent); var result = PlateSizes.Recommend(shortDim, longDim, options); // Plate convention: Length = X axis, Width = Y axis. if (xExtent >= yExtent) Size = new Size(result.Width, result.Length); // X is the long axis else Size = new Size(result.Length, result.Width); // Y is the long axis return result; } /// /// Gets the area of the top surface of the plate. /// /// public double Area() { return Size.Width * Size.Length; } /// /// Gets the volume of the plate. /// public double Volume(double thickness) { return Area() * thickness; } /// /// Gets the weight of the plate. /// public double Weight(double thickness, double density) { return Volume(thickness) * density; } /// /// Percentage of the material used. /// /// Returns a number between 0.0 and 1.0 public double Utilization() { return Parts.Where(p => !p.BaseDrawing.IsCutOff).Sum(part => part.BaseDrawing.Area) / Area(); } public bool HasOverlappingParts(out List pts) { pts = new List(); var realParts = Parts.Where(p => !p.BaseDrawing.IsCutOff).ToList(); for (var i = 0; i < realParts.Count; i++) { var part1 = realParts[i]; var b1 = part1.BoundingBox; for (var j = i + 1; j < realParts.Count; j++) { var part2 = realParts[j]; var b2 = part2.BoundingBox; // Skip pairs whose bounding boxes don't meaningfully overlap. // Floating-point rounding can produce sub-epsilon overlaps for // parts that are merely edge-touching, so require the overlap // region to exceed Epsilon in both dimensions. var overlapX = System.Math.Min(b1.Right, b2.Right) - System.Math.Max(b1.Left, b2.Left); var overlapY = System.Math.Min(b1.Top, b2.Top) - System.Math.Max(b1.Bottom, b2.Bottom); if (overlapX <= Math.Tolerance.Epsilon || overlapY <= Math.Tolerance.Epsilon) continue; if (part1.Intersects(part2, out var pts2)) pts.AddRange(pts2); } } return pts.Count > 0; } } }