using System.Collections.Immutable; using OpenNest.CNC; using OpenNest.Converters; using OpenNest.Geometry; namespace OpenNest.Reporting; /// One part page of a printout. Values are detached from the nest. /// Bounding-box extent along Y. /// Bounding-box extent along X. /// Net part area (outside minus cutouts). /// Length of every move the head cuts: cut, break, common-line and lead moves. /// Cut runs: each starts with a pierce after a rapid, mark or tab. /// Required quantity; zero when the nest does not track one. /// Net weight of one part in ; null when unknown. public sealed record PrintoutPart(string Name, string Customer, double Width, double Length, double Area, double CutLength, int PierceCount, long Quantity, double? Weight, ReportGeometry Geometry); /// A detached value snapshot of the parts to print, one page each. public sealed record PartPrintoutSnapshot(string Title, string Material, double Thickness, string Units, DateTimeOffset GeneratedAt, ImmutableArray Parts) { /// /// The name the nest machine's config gives this thickness ("16 ga"), shown before the /// decimal; empty when it has none. The caller supplies it, as for the nest report. /// public string ThicknessName { get; init; } = ""; /// "lb" for inch jobs, "kg" for millimeter jobs. public string WeightUnits => Units == "mm" ? "kg" : "lb"; } public static class PartPrintoutBuilder { /// /// Captures (every non-cutoff drawing of the nest when null) in the /// given order. Material and thickness come from the nest, the drawing's own material being the /// fallback. is the machine catalog density (lb/in^3) used /// when the nest material carries none. Capture synchronously while the drawings are stable. /// public static PartPrintoutSnapshot Capture(Nest nest, IEnumerable? drawings, DateTimeOffset generatedAt, double catalogDensity = 0) { ArgumentNullException.ThrowIfNull(nest); if (!double.IsFinite(nest.Thickness) || nest.Thickness < 0) throw new InvalidOperationException("Nest: thickness must be finite and nonnegative."); var selected = (drawings ?? nest.Drawings.Where(drawing => !drawing.IsCutOff)).ToList(); if (selected.Count == 0) throw new InvalidOperationException("There are no drawings to print."); var parts = ImmutableArray.CreateBuilder(selected.Count); foreach (var drawing in selected) parts.Add(CapturePart(drawing, nest, catalogDensity)); var title = selected.Count == 1 ? $"{selected[0].Name} printout" : $"{nest.Name} part printouts"; return new PartPrintoutSnapshot(title.Trim(), MaterialName(nest, selected), nest.Thickness, nest.Units == Units.Millimeters ? "mm" : "in", generatedAt, parts.MoveToImmutable()); } private static PrintoutPart CapturePart(Drawing drawing, Nest nest, double catalogDensity) { ArgumentNullException.ThrowIfNull(drawing); var context = $"Drawing '{drawing.Name}'"; var geometry = NestReportGeometry.Capture(drawing.Program, new Vector(), context); var (cutLength, pierces) = CutMetrics(drawing.Program); var area = drawing.Area; if (!double.IsFinite(area) || area < 0) throw new InvalidOperationException($"{context}: net part area is invalid."); var customer = string.IsNullOrWhiteSpace(drawing.Customer) ? nest.Customer : drawing.Customer; return new PrintoutPart(drawing.Name ?? "", customer?.Trim() ?? "", geometry.Bounds.Width, geometry.Bounds.Length, area, cutLength, pierces, System.Math.Max(drawing.Quantity.Required, 0), NestReportBuilder.PartWeight(drawing, nest, catalogDensity), geometry); } /// /// Walks the program as cut. Scribe/etch marks, tab spans and display-only moves are not cut, /// so they end a run, and the next cut move starts a new run with its own pierce. A display /// perimeter's tail is never cut. /// internal static (double Length, int Pierces) CutMetrics(Program program) { var length = 0.0; var pierces = 0; var cutting = false; foreach (var entity in ConvertProgram.ToGeometry(DisplayPerimeterTail.WithoutTail(program))) { if (!IsCut(entity.Layer)) { cutting = false; continue; } length += entity.Length; if (!cutting) pierces++; cutting = true; } return (length, pierces); } private static bool IsCut(Layer layer) => layer == SpecialLayers.Cut || layer == SpecialLayers.Break || layer == SpecialLayers.Leadin || layer == SpecialLayers.Leadout || layer == SpecialLayers.CommonLine; /// The nest material, else the drawings' material when they all share one. private static string MaterialName(Nest nest, IReadOnlyList drawings) { static string Name(Material? material) => $"{material?.Name} {material?.Grade}".Trim(); var name = Name(nest.Material); if (name.Length > 0) return name; var names = drawings.Select(drawing => Name(drawing.Material)).Distinct().ToList(); return names.Count == 1 ? names[0] : ""; } }