using System; using System.Collections.Generic; using System.Linq; using OpenNest.Clamps; using OpenNest.Collections; using OpenNest.Geometry; using OpenNest.Support; namespace OpenNest { public partial 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(); CommonLines = 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; // A removed part leaves its common-line group; a group left with one part is gone. for (var i = CommonLines.Count - 1; i >= 0; i--) if (CommonLines[i].Remove(e.Item) && CommonLines[i].Members.Count < 2) CommonLines.RemoveAt(i); } /// /// 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(); // An outline is a particular sheet; any other size is a rectangle again. if (outline != null && (outline.Size.Width != value.Width || outline.Size.Length != value.Length)) outline = null; size = value; ResolveClampRail(); } } private Size size; /// /// The sheet's material when it is not a rectangle (see ), in /// plate coordinates; null for a rectangular sheet. Setting one makes its /// bounding box, and setting any other size drops it. Only the Irregular whole-job engine /// nests on an outlined sheet; cut-offs are trimmed to it, automatic cut-offs refuse it, and /// is its material area. /// public SheetOutline Outline { get { return outline; } set { outline = value; if (value != null) Size = value.Size; } } private SheetOutline outline; 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 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; } /// /// A new, unowned plate with this plate's sheet and layout: size, outline, spacing, /// quadrant, grain, clamps, support bed, parts (programs and lead-in state included), /// common-line groups and cut-off definitions, regenerated in their places in the /// sequence. It is one sheet ( 1) with no stock item codes, since /// those name particular physical sheets. Adding the parts counts them as nested at once, /// as every plate does, so callers add the copy to a nest. /// is shared; callers that need an independent copy /// replace it. /// public Plate CopyLayout() { var copy = new Plate(Size) { Quantity = 1, Quadrant = Quadrant, PartSpacing = PartSpacing, EdgeSpacing = EdgeSpacing, GrainAngle = GrainAngle, CuttingParameters = CuttingParameters, SupportBed = SupportBed, CutCommonLinesTwice = CutCommonLinesTwice, }; copy.Outline = Outline; if (ClampRail != null) copy.SetClampRail(ClampRail, RemovedClamps); else foreach (var zone in ClampZones) copy.ClampZones.Add(new ClampZone(zone.Id, zone.Bounds)); var copies = new Dictionary(); foreach (var part in Parts.Where(p => !p.BaseDrawing.IsCutOff)) { var clone = (Part)part.Clone(); copy.Parts.Add(clone); copies[part] = clone; } foreach (var group in CommonLines) { var members = group.Members.Where(copies.ContainsKey).Select(member => copies[member]).ToList(); if (members.Count >= 2) copy.CommonLines.Add(new CommonLineGroup(members, group.Kerf)); } if (CutOffs.Count == 0) return copy; // Cut-off segments keep their places among the parts, as a save and reload does. var places = new Dictionary>(); foreach (var cutoff in CutOffs) { var duplicate = new CutOff(cutoff.Position, cutoff.Axis) { StartLimit = cutoff.StartLimit, EndLimit = cutoff.EndLimit, HeadLockOvercut = cutoff.HeadLockOvercut, FarEdgeFirst = cutoff.FarEdgeFirst, Gaps = cutoff.Gaps, }; copy.CutOffs.Add(duplicate); var segments = CutOffSegments.Of(this, cutoff) .Select(segment => Parts.IndexOf(segment.Part)) .Where(index => index >= 0) .ToArray(); if (segments.Length > 0) places[duplicate] = segments; } copy.RegenerateCutOffs(CutOffSettings ?? new CutOffSettings(), places, null); return copy; } /// /// 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(); } } } }