mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 05:01:05 -04:00
A hole can now be declared a tabbed cutout in the CAD Converter: pick a circle or any edge of a closed hole, set the tab count and width, and drag the tabs around the hole. The definition persists on the drawing and in the nest file, and stays editable from the source entities. The builder replaces the hole with open cut pieces and uncut spans on a new Tab layer. Tabs are material, so area, nesting and overlap checks still see a closed hole, but they are never cut. Plan Cutting and Assign Lead-ins cut each piece from the slug with the internal lead-in and no lead-out. A lead that would reach back over a tab is mirrored, and an arc lead onto an arc pierces straight in. Planning accounts for each span between tabs and runs the open-cut proof, so its own tabbed cutouts certify; tabs followed by a lead-out are still refused. Pre-post review flags programs that still contain tab motions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
98 lines
4.1 KiB
C#
98 lines
4.1 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using OpenNest.Breakoffs;
|
|
using OpenNest.Geometry;
|
|
|
|
namespace OpenNest.TabbedCutouts
|
|
{
|
|
/// <summary>
|
|
/// A <see cref="TabbedCutout"/> resolved against a drawing's entities. When <see cref="Error"/>
|
|
/// is set nothing else is meaningful and the cutout is not applied.
|
|
/// </summary>
|
|
public sealed class TabbedCutoutLayout
|
|
{
|
|
internal TabbedCutoutLayout(string error) => Error = error;
|
|
|
|
internal TabbedCutoutLayout() { }
|
|
|
|
public string Error { get; }
|
|
|
|
public bool IsValid => Error == null;
|
|
|
|
/// <summary>
|
|
/// The hole's contour, counterclockwise from its lowest-left vertex with its joints made
|
|
/// exact. It is also the slug's boundary.
|
|
/// </summary>
|
|
public IReadOnlyList<Entity> Loop { get; internal set; }
|
|
|
|
public double Length { get; internal set; }
|
|
|
|
/// <summary>
|
|
/// One open cut per span of the contour between tabs, in contour order, travelling
|
|
/// counterclockwise (the slug on the left).
|
|
/// </summary>
|
|
public IReadOnlyList<IReadOnlyList<Entity>> Pieces { get; internal set; }
|
|
|
|
/// <summary>The uncut tabs on the tab layer, each following the piece before it.</summary>
|
|
public IReadOnlyList<IReadOnlyList<Entity>> Tabs { get; internal set; }
|
|
|
|
/// <summary>Tab centers used, as distances along <see cref="Loop"/>.</summary>
|
|
public IReadOnlyList<double> TabCenters { get; internal set; }
|
|
|
|
public double TabWidth { get; internal set; }
|
|
|
|
/// <summary>Where each piece is entered (its start) and the unit direction into the slug there.</summary>
|
|
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();
|
|
|
|
/// <summary>Entities the pieces and tabs replace: the hole's original contour.</summary>
|
|
internal IReadOnlyList<Entity> ReplacedEntities { get; set; }
|
|
|
|
/// <summary><paramref name="count"/> evenly spaced tab centers, as the builder places them by default.</summary>
|
|
public List<double> EvenTabCenters(int count) =>
|
|
TabbedCutoutBuilder.EvenTabCenters(Length, count, Loop[0].Length / 2);
|
|
|
|
/// <summary>The point at <paramref name="distance"/> along the contour, wrapping around it.</summary>
|
|
public Vector PointAt(double distance) => BreakoffPath.PointAt(Loop, Wrap(distance));
|
|
|
|
/// <summary>The distance along the contour closest to <paramref name="point"/>.</summary>
|
|
public double DistanceOf(Vector point) => BreakoffPath.DistanceOf(Loop, point);
|
|
|
|
/// <summary>
|
|
/// <paramref name="center"/> for tab <paramref name="index"/> limited so the tab keeps a
|
|
/// gap from its neighbours around the contour and does not pass the contour's start
|
|
/// (which would reorder the tabs).
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|