Files
OpenNest/OpenNest.Core/Drawing.cs
T
ajandClaude Opus 5.5 b026bd19d2 feat(common-line): place parts on common lines automatically at a chosen level
Slice 3 of common-line cutting. On costly plate (N26-295: 3/16 RA310S at about
$2,500 a sheet) pairing parts one kerf apart fits the part the spacing cannot:
the last plate's 38 parts fit on one 48x120 with Auto Nest only with pairs.

- Drawing Properties gains "Allow common line" (Drawing.AllowCommonLine, off by
  default because customer edge-quality needs vary). The nest's CommonLineUsage
  (Off / When it adds parts / Whenever available) is set in Auto Nest and seeded
  from Tools > Nest Defaults; both persist (nest.json, defaults.json).
- NestPipeline runs the ordinary solve, then, with a level on, trials where
  eligible parts are offered as kerf-gap pair compounds. Placed pairs expand into
  parts plus explicit NestJobCommonLine groups, are validated again, and
  CommonLineRanking keeps a trial only by the level: count first, then sheets;
  "when it adds parts" keeps the plain layout on a tie, "whenever" also takes a
  shorter cut or less material, never at the cost of a part. Engines, FillScore
  and their comparers are unchanged; Off asks the engine exactly what it did.
- NestLayoutCheck exempts members only through a sheet's explicit group list and
  only when the job allows common lines; materializing and committing create
  Plate.CommonLines.
- Server nesting writes common-line jobs as version 3 and replays the recorded
  trial answers, so the desktop binds the same groups.
- Fill Area honors the level through a PlateFillService overload.
- The benchmark gains --common-lines <level> --kerf <k>.

Benchmark (synthetic nests + N26-295, --parallel 1): with no flag and with
--common-lines off every engine matches the base commit; with a level on no
layout becomes invalid and three runs save a sheet at equal nested area.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 19:38:00 -05:00

263 lines
9.4 KiB
C#

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Threading;
using OpenNest.Bending;
using OpenNest.CNC;
using OpenNest.Converters;
using OpenNest.Geometry;
namespace OpenNest
{
public class Drawing
{
private static int nextId;
private static int nextColorIndex;
private Program program;
public static Color[] PartColors = new Color[]
{
Color.FromArgb(205, 92, 92), // Indian Red
Color.FromArgb(148, 103, 189), // Medium Purple
Color.FromArgb(75, 180, 175), // Teal
Color.FromArgb(210, 190, 75), // Goldenrod
Color.FromArgb(190, 85, 175), // Orchid
Color.FromArgb(185, 115, 85), // Sienna
Color.FromArgb(120, 100, 190), // Slate Blue
Color.FromArgb(200, 100, 140), // Rose
Color.FromArgb(80, 175, 155), // Sea Green
Color.FromArgb(195, 160, 85), // Dark Khaki
Color.FromArgb(175, 95, 160), // Plum
Color.FromArgb(215, 130, 130), // Light Coral
};
public static bool UseGoldenAngleColors { get; set; }
public static Color GetPartColor(int index)
{
ArgumentOutOfRangeException.ThrowIfNegative(index);
return UseGoldenAngleColors
? PartColorPalette.GoldenAngle(index)
: PartColors[index % PartColors.Length];
}
public static Color GetNextColor() => GetPartColor(unchecked(nextColorIndex++) & int.MaxValue);
public Drawing()
: this(string.Empty, new Program()) { }
public Drawing(string name)
: this(name, new Program()) { }
public Drawing(string name, Program pgm)
{
Id = Interlocked.Increment(ref nextId);
Name = name;
Material = new Material();
Constraints = new NestConstraints();
Source = new SourceInfo();
Program = pgm;
}
public int Id { get; }
public string Name { get; set; }
public string Customer { get; set; }
/// <summary>Nesting order: higher numbers nest first. Zero is the default.</summary>
public int Priority { get; set; }
public DwgQty Quantity;
public Material Material { get; set; }
public Program Program
{
get { return program; }
set
{
program = value;
UpdateArea();
RecomputeCanonicalAngle();
}
}
/// <summary>
/// Recomputes and stores the canonical angle from the current Program.
/// Callers that mutate Program in place (rather than reassigning it) must invoke this explicitly.
/// Cut-off drawings are left with Angle=0.
/// </summary>
public void RecomputeCanonicalAngle()
{
if (Source == null)
Source = new SourceInfo();
if (program == null || IsCutOff)
{
Source.Angle = 0.0;
return;
}
Source.Angle = CanonicalAngle.Compute(this);
}
public Color Color { get; set; }
/// <summary>
/// True when someone chose <see cref="Color"/> on purpose. Otherwise the color comes
/// from the active scheme's palette and is reassigned when the scheme changes or the
/// nest is opened.
/// </summary>
public bool HasCustomColor { get; set; }
public bool IsCutOff { get; set; }
/// <summary>
/// Cut this drawing's parts before every other part when automatic sequencing runs,
/// e.g. a test coupon checked before the rest of the sheet is cut.
/// </summary>
public bool CutFirst { get; set; }
/// <summary>
/// Lets automatic nesting place this drawing's parts one kerf apart so they share a cut
/// (see docs/common-line-cutting.md), when the nest's common-line usage allows it. Off by
/// default: a shared edge is cut once from the centerline, which some customers'
/// edge-quality requirements do not accept.
/// </summary>
public bool AllowCommonLine { get; set; }
public NestConstraints Constraints { get; set; }
public SourceInfo Source { get; set; }
public List<Bend> Bends { get; set; } = new List<Bend>();
/// <summary>
/// The library revision this drawing was copied from, or null when it did not come
/// from a drawing library. Editing the drawing in a nest keeps the link as provenance.
/// </summary>
public DrawingLibraryLink Library { get; set; }
/// <summary>
/// Contours this drawing says must be cut a fixed way, in the source frame. At most one
/// pin per contour is used; <see cref="CNC.CuttingPlanning.CutPinResolver"/> maps them to
/// a placement. Pins do not follow geometry into new drawings (split pieces).
/// </summary>
public List<CNC.CuttingStrategy.ContourCutPin> CutPins { get; set; } = new List<CNC.CuttingStrategy.ContourCutPin>();
/// <summary>
/// Scrap pieces tabbed to the part, in the source frame. <see cref="Program"/> already has
/// them applied; they are kept so an editor can change them from <see cref="SourceEntities"/>.
/// </summary>
public List<Breakoffs.Breakoff> Breakoffs { get; set; } = new List<Breakoffs.Breakoff>();
/// <summary>
/// Holes whose slugs stay attached by tabs, in the source frame. <see cref="Program"/> already
/// has them applied (cut pieces plus <see cref="CNC.LayerType.Tab"/> spans); they are kept so
/// an editor can change them from <see cref="SourceEntities"/>.
/// </summary>
public List<TabbedCutouts.TabbedCutout> TabbedCutouts { get; set; } = new List<TabbedCutouts.TabbedCutout>();
/// <summary>
/// Annotations shown in the CAD Converter. They store placement only, are measured from the
/// geometry when shown and never affect <see cref="Program"/>.
/// </summary>
public List<Dimensions.DrawingDimension> Dimensions { get; set; } = new List<Dimensions.DrawingDimension>();
/// <summary>
/// Complete set of source entities with stable GUIDs.
/// Null when the drawing was created from G-code or an older nest file.
/// </summary>
public List<Entity> SourceEntities { get; set; }
/// <summary>
/// IDs of entities in <see cref="SourceEntities"/> that are suppressed (hidden).
/// Suppressed entities are excluded from the active Program but preserved for re-enabling.
/// </summary>
public HashSet<Guid> SuppressedEntityIds { get; set; } = new HashSet<Guid>();
/// <summary>
/// Role overrides for entities in <see cref="SourceEntities"/>; entities not listed keep the
/// role their layer implies (see <see cref="Converters.EntityRoles"/>). <see cref="Program"/> already has
/// them applied; display-only entities are not in it and come from
/// <see cref="Converters.EntityRoles.DisplayGeometry"/>.
/// </summary>
public Dictionary<Guid, EntityRole> EntityRoles { get; set; } = new Dictionary<Guid, EntityRole>();
public double Area { get; protected set; }
public void UpdateArea()
{
var geometry = ConvertProgram
.ToGeometry(Program)
.Where(entity => SpecialLayers.IsMaterial(entity.Layer));
var shapes = ShapeBuilder.GetShapes(geometry);
if (shapes.Count == 0)
return;
var areas = new double[shapes.Count];
for (int i = 0; i < shapes.Count; i++)
{
var shape = shapes[i];
areas[i] = shape.Area();
}
int largestAreaIndex = 0;
for (int i = 1; i < areas.Length; i++)
{
var area = areas[i];
if (area > areas[largestAreaIndex])
largestAreaIndex = i;
}
var outerArea = areas[largestAreaIndex];
Area = outerArea - (areas.Sum() - outerArea);
}
public override bool Equals(object obj)
{
if (obj is Drawing == false)
return false;
var dwg = (Drawing)obj;
return Name == dwg.Name;
}
public override int GetHashCode()
{
return Name.GetHashCode();
}
}
public class SourceInfo
{
/// <summary>
/// Path to the source file.
/// </summary>
public string Path { get; set; }
/// <summary>
/// Offset distances to the original location.
/// </summary>
public Vector Offset { get; set; }
/// <summary>
/// Rotation (radians) that maps the source program geometry to its canonical
/// (MBR-axis-aligned) frame. Populated automatically by the <see cref="Drawing.Program"/>
/// setter via <see cref="CanonicalAngle.Compute"/>. A value of 0 means the drawing is
/// already canonical or <see cref="Drawing.IsCutOff"/> is true. Callers that mutate
/// <see cref="Drawing.Program"/> in place must invoke
/// <see cref="Drawing.RecomputeCanonicalAngle"/> to refresh.
/// </summary>
public double Angle { get; set; }
}
}