using System.Collections.Generic; using System.Linq; using OpenNest.Breakoffs; using OpenNest.Geometry; namespace OpenNest.TabbedCutouts { /// /// A resolved against a drawing's entities. When /// is set nothing else is meaningful and the cutout is not applied. /// public sealed class TabbedCutoutLayout { internal TabbedCutoutLayout(string error) => Error = error; internal TabbedCutoutLayout() { } public string Error { get; } public bool IsValid => Error == null; /// /// The hole's contour, counterclockwise from its lowest-left vertex with its joints made /// exact. It is also the slug's boundary. /// public IReadOnlyList Loop { get; internal set; } public double Length { get; internal set; } /// /// One open cut per span of the contour between tabs, in contour order, traveling /// counterclockwise (the slug on the left). /// public IReadOnlyList> Pieces { get; internal set; } /// The uncut tabs on the tab layer, each following the piece before it. public IReadOnlyList> Tabs { get; internal set; } /// Tab centers used, as distances along . public IReadOnlyList TabCenters { get; internal set; } public double TabWidth { get; internal set; } /// Where each piece is entered (its start) and the unit direction into the slug there. public IReadOnlyList<(Vector Entry, Vector SlugDirection)> Entries => Pieces .Select(piece => { var tangent = BreakoffPath.TangentAt(piece, 0); return (BreakoffPath.Start(piece[0]), new Vector(-tangent.Y, tangent.X)); }) .ToList(); /// Entities the pieces and tabs replace: the hole's original contour. internal IReadOnlyList ReplacedEntities { get; set; } /// evenly spaced tab centers, as the builder places them by default. public List EvenTabCenters(int count) => TabbedCutoutBuilder.EvenTabCenters(Length, count, Loop[0].Length / 2); /// The point at along the contour, wrapping around it. public Vector PointAt(double distance) => BreakoffPath.PointAt(Loop, Wrap(distance)); /// The distance along the contour closest to . public double DistanceOf(Vector point) => BreakoffPath.DistanceOf(Loop, point); /// /// for tab limited so the tab keeps a /// gap from its neighbors around the contour and does not pass the contour's start /// (which would reorder the tabs). /// public double ClampTabCenter(int index, double center) { var count = TabCenters.Count; if (count == 1) return Wrap(center); var spacing = TabWidth + MinGap; var min = index == 0 ? System.Math.Max(0, TabCenters[^1] - Length + spacing) : TabCenters[index - 1] + spacing; var max = index == count - 1 ? System.Math.Min(Length - Tolerance, TabCenters[0] + Length - spacing) : TabCenters[index + 1] - spacing; return max < min ? TabCenters[index] : System.Math.Clamp(center, min, max); } internal double Wrap(double distance) { if (!(Length > 0)) return 0; var wrapped = distance % Length; return wrapped < 0 ? wrapped + Length : wrapped; } // Wider than the builder's overlap tolerance, so a clamped drag always resolves. internal double MinGap => System.Math.Max(TabWidth / 2, 2 * TabbedCutoutBuilder.JoinTolerance); private const double Tolerance = 1e-9; } }