Compare commits
36
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4cc8b8f9b7 | ||
|
|
1f159d5dcc | ||
|
|
f626fbe063 | ||
|
|
d5b5ab57e3 | ||
|
|
6916f5ecca | ||
|
|
e1bcb7498f | ||
|
|
a7f8972722 | ||
|
|
6d1a3f5e2c | ||
|
|
52eca5f5c2 | ||
|
|
3bce45be5f | ||
|
|
3f0a4c57b5 | ||
|
|
ededc7b6b4 | ||
|
|
5f74afeda1 | ||
|
|
574a8f2c38 | ||
|
|
dd2892a9fe | ||
|
|
7056f8816f | ||
|
|
c2a470f79c | ||
|
|
39f8a79cfd | ||
|
|
df18b72881 | ||
|
|
cd8adc97d6 | ||
|
|
ba7aa39941 | ||
|
|
5d93ddb2c4 | ||
|
|
15b2043048 | ||
|
|
aa8b6f3d9e | ||
|
|
3686d074e6 | ||
|
|
8f1a3fb6b7 | ||
|
|
60ce297d6a | ||
|
|
addd7acc3c | ||
|
|
d91ffccfa3 | ||
|
|
adb8ed12d7 | ||
|
|
4acd8b8bad | ||
|
|
d7b095cf2d | ||
|
|
499e0425b5 | ||
|
|
c2c3e23024 | ||
|
|
5afb311ac7 | ||
|
|
765a862440 |
@@ -31,6 +31,7 @@ Domain model, geometry, and CNC primitives organized into namespaces:
|
||||
- **CNC/CuttingStrategy** (`CNC/CuttingStrategy/`, `namespace OpenNest.CNC`): `ContourCuttingStrategy` orchestrates cut ordering, lead-ins/lead-outs, and tabs. Includes `LeadIn`/`LeadOut` hierarchies (line, arc, clean-hole variants), `Tab` hierarchy (normal, machine, breaker), and `CuttingParameters`/`AssignmentParameters`/`SequenceParameters` configuration.
|
||||
- **Collections** (`Collections/`, `namespace OpenNest.Collections`): `ObservableList<T>`, `DrawingCollection`.
|
||||
- **CutOffs** (`namespace OpenNest`): `CutOff` (axis-aligned cut line with position, axis, optional start/end limits), `CutOffAxis` enum (`Horizontal`, `Vertical`), `CutOffSettings` (clearance, overtravel, min segment length, direction), `CutDirection` enum (`TowardOrigin`, `AwayFromOrigin`). Cut-offs generate CNC `Program` objects with trimmed line segments that avoid parts.
|
||||
- **Splitting** (`Splitting/`, `namespace OpenNest`): `DrawingSplitter` splits a Drawing into multiple pieces along split lines. `ISplitFeature` strategy pattern with implementations: `StraightSplit` (clean edge), `WeldGapTabSplit` (rectangular tab spacers on one side), `SpikeGrooveSplit` (interlocking spike/V-groove pairs). `AutoSplitCalculator` computes split lines for fit-to-plate and split-by-count modes. Supporting types: `SplitLine`, `SplitParameters`, `SplitFeatureResult`.
|
||||
- **Quadrant system**: Plates use quadrants 1-4 (like Cartesian quadrants) to determine coordinate origin placement. This affects bounding box calculation, rotation, and part positioning.
|
||||
|
||||
### OpenNest.Engine (class library, depends on Core)
|
||||
@@ -78,7 +79,7 @@ MCP server for Claude Code integration. Exposes nesting operations as MCP tools
|
||||
### OpenNest (WinForms WinExe, depends on Core + Engine + IO)
|
||||
The UI application with MDI interface.
|
||||
|
||||
- **Forms/**: `MainForm` (MDI parent), `EditNestForm` (MDI child per nest), plus dialogs for plate editing, auto-nesting, DXF conversion, cut parameters, etc.
|
||||
- **Forms/**: `MainForm` (MDI parent), `EditNestForm` (MDI child per nest), `SplitDrawingForm` (split oversized drawings into smaller pieces, launched from CadConverterForm), plus dialogs for plate editing, auto-nesting, DXF conversion, cut parameters, etc.
|
||||
- **Controls/**: `PlateView` (2D plate renderer with zoom/pan, supports temporary preview parts), `DrawingListBox`, `DrawControl`, `QuadrantSelect`.
|
||||
- **Actions/**: User interaction modes — `ActionSelect`, `ActionClone`, `ActionFillArea`, `ActionSelectArea`, `ActionZoomWindow`, `ActionSetSequence`, `ActionCutOff`.
|
||||
- **Post-processing**: `IPostProcessor` plugin interface loaded from DLLs in a `Posts/` directory at runtime.
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Bending
|
||||
{
|
||||
public class Bend
|
||||
{
|
||||
public Vector StartPoint { get; set; }
|
||||
public Vector EndPoint { get; set; }
|
||||
public BendDirection Direction { get; set; }
|
||||
public double? Angle { get; set; }
|
||||
public double? Radius { get; set; }
|
||||
public string NoteText { get; set; }
|
||||
|
||||
public double Length => StartPoint.DistanceTo(EndPoint);
|
||||
|
||||
public double AngleRadians => Angle.HasValue
|
||||
? OpenNest.Math.Angle.ToRadians(Angle.Value)
|
||||
: 0;
|
||||
|
||||
public Line ToLine() => new Line(StartPoint, EndPoint);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the angle of the bend line itself (not the bend angle).
|
||||
/// Used for grain direction comparison.
|
||||
/// </summary>
|
||||
public double LineAngle => StartPoint.AngleTo(EndPoint);
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var dir = Direction.ToString();
|
||||
var angle = Angle?.ToString("0.##") ?? "?";
|
||||
var radius = Radius?.ToString("0.###") ?? "?";
|
||||
return $"{dir} {angle}° R{radius}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace OpenNest.Bending
|
||||
{
|
||||
public enum BendDirection
|
||||
{
|
||||
Unknown,
|
||||
Up,
|
||||
Down
|
||||
}
|
||||
}
|
||||
@@ -59,9 +59,11 @@ namespace OpenNest.Converters
|
||||
if (mode == Mode.Incremental)
|
||||
pt += curpos;
|
||||
|
||||
var layer = ConvertLayer(linearMove.Layer);
|
||||
var line = new Line(curpos, pt)
|
||||
{
|
||||
Layer = ConvertLayer(linearMove.Layer)
|
||||
Layer = layer,
|
||||
Color = layer.Color
|
||||
};
|
||||
geometry.Add(line);
|
||||
curpos = pt;
|
||||
@@ -76,7 +78,8 @@ namespace OpenNest.Converters
|
||||
|
||||
var line = new Line(curpos, pt)
|
||||
{
|
||||
Layer = SpecialLayers.Rapid
|
||||
Layer = SpecialLayers.Rapid,
|
||||
Color = SpecialLayers.Rapid.Color
|
||||
};
|
||||
geometry.Add(line);
|
||||
curpos = pt;
|
||||
@@ -103,9 +106,9 @@ namespace OpenNest.Converters
|
||||
var layer = ConvertLayer(arcMove.Layer);
|
||||
|
||||
if (startAngle.IsEqualTo(endAngle))
|
||||
geometry.Add(new Circle(center, radius) { Layer = layer });
|
||||
geometry.Add(new Circle(center, radius) { Layer = layer, Color = layer.Color });
|
||||
else
|
||||
geometry.Add(new Arc(center, radius, startAngle, endAngle, arcMove.Rotation == RotationType.CW) { Layer = layer });
|
||||
geometry.Add(new Arc(center, radius, startAngle, endAngle, arcMove.Rotation == RotationType.CW) { Layer = layer, Color = layer.Color });
|
||||
|
||||
curpos = endpt;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Bending;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
@@ -62,6 +64,8 @@ namespace OpenNest
|
||||
|
||||
public SourceInfo Source { get; set; }
|
||||
|
||||
public List<Bend> Bends { get; set; } = new List<Bend>();
|
||||
|
||||
public double Area { get; protected set; }
|
||||
|
||||
public void UpdateArea()
|
||||
|
||||
@@ -219,6 +219,14 @@ namespace OpenNest.Geometry
|
||||
}
|
||||
|
||||
internal static bool Intersects(Line line1, Line line2, out Vector pt)
|
||||
{
|
||||
if (!IntersectsUnbounded(line1, line2, out pt))
|
||||
return false;
|
||||
|
||||
return line1.BoundingBox.Contains(pt) && line2.BoundingBox.Contains(pt);
|
||||
}
|
||||
|
||||
internal static bool IntersectsUnbounded(Line line1, Line line2, out Vector pt)
|
||||
{
|
||||
var a1 = line1.EndPoint.Y - line1.StartPoint.Y;
|
||||
var b1 = line1.StartPoint.X - line1.EndPoint.X;
|
||||
@@ -240,7 +248,7 @@ namespace OpenNest.Geometry
|
||||
var y = (a1 * c2 - a2 * c1) / d;
|
||||
|
||||
pt = new Vector(x, y);
|
||||
return line1.BoundingBox.Contains(pt) && line2.BoundingBox.Contains(pt);
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static bool Intersects(Line line, Shape shape, out List<Vector> pts)
|
||||
|
||||
@@ -534,7 +534,7 @@ namespace OpenNest.Geometry
|
||||
{
|
||||
Vector intersection;
|
||||
|
||||
if (Intersect.Intersects(offsetLine, lastOffsetLine, out intersection))
|
||||
if (Intersect.IntersectsUnbounded(offsetLine, lastOffsetLine, out intersection))
|
||||
{
|
||||
offsetLine.StartPoint = intersection;
|
||||
lastOffsetLine.EndPoint = intersection;
|
||||
@@ -558,6 +558,46 @@ namespace OpenNest.Geometry
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Offsets the shape outward by the given distance, detecting winding direction
|
||||
/// to choose the correct offset side. Falls back to the opposite side if the
|
||||
/// bounding box shrinks (indicating the offset went inward).
|
||||
/// </summary>
|
||||
public Shape OffsetOutward(double distance)
|
||||
{
|
||||
var poly = ToPolygon();
|
||||
var side = poly.Vertices.Count >= 3 && poly.RotationDirection() == RotationType.CW
|
||||
? OffsetSide.Left
|
||||
: OffsetSide.Right;
|
||||
|
||||
var result = OffsetEntity(distance, side) as Shape;
|
||||
|
||||
if (result == null)
|
||||
return null;
|
||||
|
||||
UpdateBounds();
|
||||
var originalBB = BoundingBox;
|
||||
result.UpdateBounds();
|
||||
var offsetBB = result.BoundingBox;
|
||||
|
||||
if (offsetBB.Width < originalBB.Width || offsetBB.Length < originalBB.Length)
|
||||
{
|
||||
Trace.TraceWarning(
|
||||
"Shape.OffsetOutward: offset shrank bounding box " +
|
||||
$"(original={originalBB.Width:F3}x{originalBB.Length:F3}, " +
|
||||
$"offset={offsetBB.Width:F3}x{offsetBB.Length:F3}). " +
|
||||
"Retrying with opposite side.");
|
||||
|
||||
var opposite = side == OffsetSide.Left ? OffsetSide.Right : OffsetSide.Left;
|
||||
var retry = OffsetEntity(distance, opposite) as Shape;
|
||||
|
||||
if (retry != null)
|
||||
result = retry;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the closest point on the shape to the given point.
|
||||
/// </summary>
|
||||
|
||||
@@ -49,7 +49,7 @@ namespace OpenNest
|
||||
{
|
||||
// Add chord tolerance to compensate for inscribed polygon chords
|
||||
// being inside the actual offset arcs.
|
||||
var offsetEntity = shape.OffsetEntity(spacing + chordTolerance, OffsetSide.Left) as Shape;
|
||||
var offsetEntity = shape.OffsetOutward(spacing + chordTolerance);
|
||||
|
||||
if (offsetEntity == null)
|
||||
continue;
|
||||
@@ -71,7 +71,7 @@ namespace OpenNest
|
||||
|
||||
foreach (var shape in shapes)
|
||||
{
|
||||
var offsetEntity = shape.OffsetEntity(spacing + chordTolerance, OffsetSide.Left) as Shape;
|
||||
var offsetEntity = shape.OffsetOutward(spacing + chordTolerance);
|
||||
|
||||
if (offsetEntity == null)
|
||||
continue;
|
||||
@@ -109,7 +109,7 @@ namespace OpenNest
|
||||
|
||||
foreach (var shape in shapes)
|
||||
{
|
||||
var offsetEntity = shape.OffsetEntity(spacing + chordTolerance, OffsetSide.Left) as Shape;
|
||||
var offsetEntity = shape.OffsetOutward(spacing + chordTolerance);
|
||||
|
||||
if (offsetEntity == null)
|
||||
continue;
|
||||
|
||||
@@ -88,6 +88,11 @@ namespace OpenNest
|
||||
/// </summary>
|
||||
public Material Material { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Material grain direction in radians. 0 = horizontal.
|
||||
/// </summary>
|
||||
public double GrainAngle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The parts that the plate contains.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Drawing;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
public static class SpecialLayers
|
||||
{
|
||||
public static readonly Layer Default = new Layer("0");
|
||||
public static readonly Layer Default = new Layer("0") { Color = Color.White };
|
||||
|
||||
public static readonly Layer Cut = new Layer("CUT");
|
||||
public static readonly Layer Cut = new Layer("CUT") { Color = Color.White };
|
||||
|
||||
public static readonly Layer Rapid = new Layer("RAPID");
|
||||
public static readonly Layer Rapid = new Layer("RAPID") { Color = Color.Gray };
|
||||
|
||||
public static readonly Layer Display = new Layer("DISPLAY");
|
||||
public static readonly Layer Display = new Layer("DISPLAY") { Color = Color.Cyan };
|
||||
|
||||
public static readonly Layer Leadin = new Layer("LEADIN");
|
||||
public static readonly Layer Leadin = new Layer("LEADIN") { Color = Color.Yellow };
|
||||
|
||||
public static readonly Layer Leadout = new Layer("LEADOUT");
|
||||
public static readonly Layer Leadout = new Layer("LEADOUT") { Color = Color.Yellow };
|
||||
|
||||
public static readonly Layer Scribe = new Layer("SCRIBE");
|
||||
public static readonly Layer Scribe = new Layer("SCRIBE") { Color = Color.Magenta };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
public static class AutoSplitCalculator
|
||||
{
|
||||
public static List<SplitLine> FitToPlate(Box partBounds, double plateWidth, double plateHeight,
|
||||
double edgeSpacing, double featureOverhang)
|
||||
{
|
||||
var usableWidth = plateWidth - 2 * edgeSpacing - featureOverhang;
|
||||
var usableHeight = plateHeight - 2 * edgeSpacing - featureOverhang;
|
||||
|
||||
var lines = new List<SplitLine>();
|
||||
|
||||
var verticalSplits = usableWidth > 0 ? (int)System.Math.Ceiling(partBounds.Width / usableWidth) - 1 : 0;
|
||||
var horizontalSplits = usableHeight > 0 ? (int)System.Math.Ceiling(partBounds.Length / usableHeight) - 1 : 0;
|
||||
|
||||
if (verticalSplits < 0) verticalSplits = 0;
|
||||
if (horizontalSplits < 0) horizontalSplits = 0;
|
||||
|
||||
if (verticalSplits > 0)
|
||||
{
|
||||
var spacing = partBounds.Width / (verticalSplits + 1);
|
||||
for (var i = 1; i <= verticalSplits; i++)
|
||||
lines.Add(new SplitLine(partBounds.X + spacing * i, CutOffAxis.Vertical));
|
||||
}
|
||||
|
||||
if (horizontalSplits > 0)
|
||||
{
|
||||
var spacing = partBounds.Length / (horizontalSplits + 1);
|
||||
for (var i = 1; i <= horizontalSplits; i++)
|
||||
lines.Add(new SplitLine(partBounds.Y + spacing * i, CutOffAxis.Horizontal));
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
public static List<SplitLine> SplitByCount(Box partBounds, int horizontalPieces, int verticalPieces)
|
||||
{
|
||||
var lines = new List<SplitLine>();
|
||||
|
||||
if (verticalPieces > 1)
|
||||
{
|
||||
var spacing = partBounds.Width / verticalPieces;
|
||||
for (var i = 1; i < verticalPieces; i++)
|
||||
lines.Add(new SplitLine(partBounds.X + spacing * i, CutOffAxis.Vertical));
|
||||
}
|
||||
|
||||
if (horizontalPieces > 1)
|
||||
{
|
||||
var spacing = partBounds.Length / horizontalPieces;
|
||||
for (var i = 1; i < horizontalPieces; i++)
|
||||
lines.Add(new SplitLine(partBounds.Y + spacing * i, CutOffAxis.Horizontal));
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,505 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
/// <summary>
|
||||
/// Splits a Drawing into multiple pieces along split lines with optional feature geometry.
|
||||
/// </summary>
|
||||
public static class DrawingSplitter
|
||||
{
|
||||
public static List<Drawing> Split(Drawing drawing, List<SplitLine> splitLines, SplitParameters parameters)
|
||||
{
|
||||
if (splitLines.Count == 0)
|
||||
return new List<Drawing> { drawing };
|
||||
|
||||
var profile = BuildProfile(drawing);
|
||||
DecomposeCircles(profile);
|
||||
|
||||
var perimeter = profile.Perimeter;
|
||||
var bounds = perimeter.BoundingBox;
|
||||
|
||||
var sortedLines = splitLines
|
||||
.Where(l => IsLineInsideBounds(l, bounds))
|
||||
.OrderBy(l => l.Position)
|
||||
.ToList();
|
||||
|
||||
if (sortedLines.Count == 0)
|
||||
return new List<Drawing> { drawing };
|
||||
|
||||
var regions = BuildClipRegions(sortedLines, bounds);
|
||||
var feature = GetFeature(parameters.Type);
|
||||
|
||||
var results = new List<Drawing>();
|
||||
var pieceIndex = 1;
|
||||
|
||||
foreach (var region in regions)
|
||||
{
|
||||
var pieceEntities = ClipPerimeterToRegion(perimeter, region, sortedLines, feature, parameters);
|
||||
if (pieceEntities.Count == 0)
|
||||
continue;
|
||||
|
||||
var cutoutEntities = CollectCutouts(profile.Cutouts, region, sortedLines);
|
||||
|
||||
var allEntities = new List<Entity>();
|
||||
allEntities.AddRange(pieceEntities);
|
||||
allEntities.AddRange(cutoutEntities);
|
||||
|
||||
var piece = BuildPieceDrawing(drawing, allEntities, pieceIndex);
|
||||
results.Add(piece);
|
||||
pieceIndex++;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static ShapeProfile BuildProfile(Drawing drawing)
|
||||
{
|
||||
var entities = ConvertProgram.ToGeometry(drawing.Program)
|
||||
.Where(e => e.Layer != SpecialLayers.Rapid)
|
||||
.ToList();
|
||||
return new ShapeProfile(entities);
|
||||
}
|
||||
|
||||
private static List<Entity> CollectCutouts(List<Shape> cutouts, Box region, List<SplitLine> splitLines)
|
||||
{
|
||||
var entities = new List<Entity>();
|
||||
foreach (var cutout in cutouts)
|
||||
{
|
||||
if (IsCutoutInRegion(cutout, region))
|
||||
entities.AddRange(cutout.Entities);
|
||||
else if (DoesCutoutCrossSplitLine(cutout, splitLines))
|
||||
{
|
||||
var clipped = ClipCutoutToRegion(cutout, region, splitLines);
|
||||
if (clipped.Count > 0)
|
||||
entities.AddRange(clipped);
|
||||
}
|
||||
}
|
||||
return entities;
|
||||
}
|
||||
|
||||
private static Drawing BuildPieceDrawing(Drawing source, List<Entity> entities, int pieceIndex)
|
||||
{
|
||||
var pieceBounds = entities.Select(e => e.BoundingBox).ToList().GetBoundingBox();
|
||||
var offsetX = -pieceBounds.X;
|
||||
var offsetY = -pieceBounds.Y;
|
||||
|
||||
foreach (var e in entities)
|
||||
e.Offset(offsetX, offsetY);
|
||||
|
||||
var pgm = ConvertGeometry.ToProgram(entities);
|
||||
var piece = new Drawing($"{source.Name}-{pieceIndex}", pgm);
|
||||
piece.Color = source.Color;
|
||||
piece.Priority = source.Priority;
|
||||
piece.Material = source.Material;
|
||||
piece.Constraints = source.Constraints;
|
||||
piece.Customer = source.Customer;
|
||||
piece.Source = source.Source;
|
||||
piece.Quantity.Required = source.Quantity.Required;
|
||||
return piece;
|
||||
}
|
||||
|
||||
private static void DecomposeCircles(ShapeProfile profile)
|
||||
{
|
||||
DecomposeCirclesInShape(profile.Perimeter);
|
||||
foreach (var cutout in profile.Cutouts)
|
||||
DecomposeCirclesInShape(cutout);
|
||||
}
|
||||
|
||||
private static void DecomposeCirclesInShape(Shape shape)
|
||||
{
|
||||
for (var i = shape.Entities.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (shape.Entities[i] is Circle circle)
|
||||
{
|
||||
var arc1 = new Arc(circle.Center, circle.Radius, 0, System.Math.PI);
|
||||
var arc2 = new Arc(circle.Center, circle.Radius, System.Math.PI, System.Math.PI * 2);
|
||||
shape.Entities.RemoveAt(i);
|
||||
shape.Entities.Insert(i, arc2);
|
||||
shape.Entities.Insert(i, arc1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsLineInsideBounds(SplitLine line, Box bounds)
|
||||
{
|
||||
return line.Axis == CutOffAxis.Vertical
|
||||
? line.Position > bounds.Left + OpenNest.Math.Tolerance.Epsilon
|
||||
&& line.Position < bounds.Right - OpenNest.Math.Tolerance.Epsilon
|
||||
: line.Position > bounds.Bottom + OpenNest.Math.Tolerance.Epsilon
|
||||
&& line.Position < bounds.Top - OpenNest.Math.Tolerance.Epsilon;
|
||||
}
|
||||
|
||||
private static List<Box> BuildClipRegions(List<SplitLine> sortedLines, Box bounds)
|
||||
{
|
||||
var verticals = sortedLines.Where(l => l.Axis == CutOffAxis.Vertical).OrderBy(l => l.Position).ToList();
|
||||
var horizontals = sortedLines.Where(l => l.Axis == CutOffAxis.Horizontal).OrderBy(l => l.Position).ToList();
|
||||
|
||||
var xEdges = new List<double> { bounds.Left };
|
||||
xEdges.AddRange(verticals.Select(v => v.Position));
|
||||
xEdges.Add(bounds.Right);
|
||||
|
||||
var yEdges = new List<double> { bounds.Bottom };
|
||||
yEdges.AddRange(horizontals.Select(h => h.Position));
|
||||
yEdges.Add(bounds.Top);
|
||||
|
||||
var regions = new List<Box>();
|
||||
for (var yi = 0; yi < yEdges.Count - 1; yi++)
|
||||
for (var xi = 0; xi < xEdges.Count - 1; xi++)
|
||||
regions.Add(new Box(xEdges[xi], yEdges[yi], xEdges[xi + 1] - xEdges[xi], yEdges[yi + 1] - yEdges[yi]));
|
||||
|
||||
return regions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clip perimeter to a region by walking entities, splitting at split line crossings,
|
||||
/// and stitching in feature edges. No polygon clipping library needed.
|
||||
/// </summary>
|
||||
private static List<Entity> ClipPerimeterToRegion(Shape perimeter, Box region,
|
||||
List<SplitLine> splitLines, ISplitFeature feature, SplitParameters parameters)
|
||||
{
|
||||
var boundarySplitLines = GetBoundarySplitLines(region, splitLines);
|
||||
var entities = new List<Entity>();
|
||||
var splitPoints = new List<(Vector Point, SplitLine Line, bool IsExit)>();
|
||||
|
||||
foreach (var entity in perimeter.Entities)
|
||||
{
|
||||
ProcessEntity(entity, region, boundarySplitLines, entities, splitPoints);
|
||||
}
|
||||
|
||||
if (entities.Count == 0)
|
||||
return new List<Entity>();
|
||||
|
||||
InsertFeatureEdges(entities, splitPoints, region, boundarySplitLines, feature, parameters);
|
||||
EnsurePerimeterWinding(entities);
|
||||
return entities;
|
||||
}
|
||||
|
||||
private static void ProcessEntity(Entity entity, Box region,
|
||||
List<SplitLine> boundarySplitLines, List<Entity> entities,
|
||||
List<(Vector Point, SplitLine Line, bool IsExit)> splitPoints)
|
||||
{
|
||||
// Find the first boundary split line this entity crosses
|
||||
SplitLine crossedLine = null;
|
||||
Vector? intersectionPt = null;
|
||||
|
||||
foreach (var sl in boundarySplitLines)
|
||||
{
|
||||
if (SplitLineIntersect.CrossesSplitLine(entity, sl))
|
||||
{
|
||||
var pt = SplitLineIntersect.FindIntersection(entity, sl);
|
||||
if (pt != null)
|
||||
{
|
||||
crossedLine = sl;
|
||||
intersectionPt = pt;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (crossedLine != null)
|
||||
{
|
||||
// Entity crosses a split line — split it and keep the half inside the region
|
||||
var regionSide = RegionSideOf(region, crossedLine);
|
||||
var startPt = GetStartPoint(entity);
|
||||
var startSide = SplitLineIntersect.SideOf(startPt, crossedLine);
|
||||
var startInRegion = startSide == regionSide || startSide == 0;
|
||||
|
||||
SplitEntityAtPoint(entity, intersectionPt.Value, startInRegion, crossedLine, entities, splitPoints);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Entity doesn't cross any boundary split line — check if it's inside the region
|
||||
var mid = MidPoint(entity);
|
||||
if (region.Contains(mid))
|
||||
entities.Add(entity);
|
||||
}
|
||||
}
|
||||
|
||||
private static void SplitEntityAtPoint(Entity entity, Vector point, bool startInRegion,
|
||||
SplitLine crossedLine, List<Entity> entities,
|
||||
List<(Vector Point, SplitLine Line, bool IsExit)> splitPoints)
|
||||
{
|
||||
if (entity is Line line)
|
||||
{
|
||||
var (first, second) = line.SplitAt(point);
|
||||
if (startInRegion)
|
||||
{
|
||||
if (first != null) entities.Add(first);
|
||||
splitPoints.Add((point, crossedLine, true));
|
||||
}
|
||||
else
|
||||
{
|
||||
splitPoints.Add((point, crossedLine, false));
|
||||
if (second != null) entities.Add(second);
|
||||
}
|
||||
}
|
||||
else if (entity is Arc arc)
|
||||
{
|
||||
var (first, second) = arc.SplitAt(point);
|
||||
if (startInRegion)
|
||||
{
|
||||
if (first != null) entities.Add(first);
|
||||
splitPoints.Add((point, crossedLine, true));
|
||||
}
|
||||
else
|
||||
{
|
||||
splitPoints.Add((point, crossedLine, false));
|
||||
if (second != null) entities.Add(second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns split lines whose position matches a boundary edge of the region.
|
||||
/// </summary>
|
||||
private static List<SplitLine> GetBoundarySplitLines(Box region, List<SplitLine> splitLines)
|
||||
{
|
||||
var result = new List<SplitLine>();
|
||||
foreach (var sl in splitLines)
|
||||
{
|
||||
if (sl.Axis == CutOffAxis.Vertical)
|
||||
{
|
||||
if (System.Math.Abs(sl.Position - region.Left) < OpenNest.Math.Tolerance.Epsilon
|
||||
|| System.Math.Abs(sl.Position - region.Right) < OpenNest.Math.Tolerance.Epsilon)
|
||||
result.Add(sl);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (System.Math.Abs(sl.Position - region.Bottom) < OpenNest.Math.Tolerance.Epsilon
|
||||
|| System.Math.Abs(sl.Position - region.Top) < OpenNest.Math.Tolerance.Epsilon)
|
||||
result.Add(sl);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns -1 or +1 indicating which side of the split line the region center is on.
|
||||
/// </summary>
|
||||
private static int RegionSideOf(Box region, SplitLine sl)
|
||||
{
|
||||
return SplitLineIntersect.SideOf(region.Center, sl);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the midpoint of an entity. For lines: average of endpoints.
|
||||
/// For arcs: point at the mid-angle.
|
||||
/// </summary>
|
||||
private static Vector MidPoint(Entity entity)
|
||||
{
|
||||
if (entity is Line line)
|
||||
return line.MidPoint;
|
||||
|
||||
if (entity is Arc arc)
|
||||
{
|
||||
var midAngle = (arc.StartAngle + arc.EndAngle) / 2;
|
||||
return new Vector(
|
||||
arc.Center.X + arc.Radius * System.Math.Cos(midAngle),
|
||||
arc.Center.Y + arc.Radius * System.Math.Sin(midAngle));
|
||||
}
|
||||
|
||||
return new Vector(0, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Groups split points by split line, pairs exits with entries, and generates feature edges.
|
||||
/// </summary>
|
||||
private static void InsertFeatureEdges(List<Entity> entities,
|
||||
List<(Vector Point, SplitLine Line, bool IsExit)> splitPoints,
|
||||
Box region, List<SplitLine> boundarySplitLines,
|
||||
ISplitFeature feature, SplitParameters parameters)
|
||||
{
|
||||
// Group split points by their split line
|
||||
var groups = new Dictionary<SplitLine, List<(Vector Point, bool IsExit)>>();
|
||||
foreach (var sp in splitPoints)
|
||||
{
|
||||
if (!groups.ContainsKey(sp.Line))
|
||||
groups[sp.Line] = new List<(Vector, bool)>();
|
||||
groups[sp.Line].Add((sp.Point, sp.IsExit));
|
||||
}
|
||||
|
||||
foreach (var kvp in groups)
|
||||
{
|
||||
var sl = kvp.Key;
|
||||
var points = kvp.Value;
|
||||
|
||||
// Pair each exit with the next entry
|
||||
var exits = points.Where(p => p.IsExit).Select(p => p.Point).ToList();
|
||||
var entries = points.Where(p => !p.IsExit).Select(p => p.Point).ToList();
|
||||
|
||||
if (exits.Count == 0 || entries.Count == 0)
|
||||
continue;
|
||||
|
||||
// For each exit, find the matching entry to form the feature edge span
|
||||
// Sort exits and entries by their position along the split line
|
||||
var isVertical = sl.Axis == CutOffAxis.Vertical;
|
||||
exits = exits.OrderBy(p => isVertical ? p.Y : p.X).ToList();
|
||||
entries = entries.OrderBy(p => isVertical ? p.Y : p.X).ToList();
|
||||
|
||||
// Pair them up: each exit with the next entry (or vice versa)
|
||||
var pairCount = System.Math.Min(exits.Count, entries.Count);
|
||||
for (var i = 0; i < pairCount; i++)
|
||||
{
|
||||
var exitPt = exits[i];
|
||||
var entryPt = entries[i];
|
||||
|
||||
var extentStart = isVertical
|
||||
? System.Math.Min(exitPt.Y, entryPt.Y)
|
||||
: System.Math.Min(exitPt.X, entryPt.X);
|
||||
var extentEnd = isVertical
|
||||
? System.Math.Max(exitPt.Y, entryPt.Y)
|
||||
: System.Math.Max(exitPt.X, entryPt.X);
|
||||
|
||||
var featureResult = feature.GenerateFeatures(sl, extentStart, extentEnd, parameters);
|
||||
|
||||
var isNegativeSide = RegionSideOf(region, sl) < 0;
|
||||
var featureEdge = isNegativeSide ? featureResult.NegativeSideEdge : featureResult.PositiveSideEdge;
|
||||
|
||||
if (featureEdge.Count > 0)
|
||||
featureEdge = AlignFeatureDirection(featureEdge, exitPt, entryPt, sl.Axis);
|
||||
|
||||
entities.AddRange(featureEdge);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Entity> AlignFeatureDirection(List<Entity> featureEdge, Vector start, Vector end, CutOffAxis axis)
|
||||
{
|
||||
var featureStart = GetStartPoint(featureEdge[0]);
|
||||
var featureEnd = GetEndPoint(featureEdge[^1]);
|
||||
var isVertical = axis == CutOffAxis.Vertical;
|
||||
|
||||
var edgeGoesForward = isVertical ? start.Y < end.Y : start.X < end.X;
|
||||
var featureGoesForward = isVertical ? featureStart.Y < featureEnd.Y : featureStart.X < featureEnd.X;
|
||||
|
||||
if (edgeGoesForward != featureGoesForward)
|
||||
{
|
||||
featureEdge = new List<Entity>(featureEdge);
|
||||
featureEdge.Reverse();
|
||||
foreach (var e in featureEdge)
|
||||
e.Reverse();
|
||||
}
|
||||
|
||||
return featureEdge;
|
||||
}
|
||||
|
||||
private static void EnsurePerimeterWinding(List<Entity> entities)
|
||||
{
|
||||
var shape = new Shape();
|
||||
shape.Entities.AddRange(entities);
|
||||
var poly = shape.ToPolygon();
|
||||
if (poly != null && poly.RotationDirection() != RotationType.CW)
|
||||
shape.Reverse();
|
||||
|
||||
entities.Clear();
|
||||
entities.AddRange(shape.Entities);
|
||||
}
|
||||
|
||||
private static bool IsCutoutInRegion(Shape cutout, Box region)
|
||||
{
|
||||
if (cutout.Entities.Count == 0) return false;
|
||||
var pt = GetStartPoint(cutout.Entities[0]);
|
||||
return region.Contains(pt);
|
||||
}
|
||||
|
||||
private static bool DoesCutoutCrossSplitLine(Shape cutout, List<SplitLine> splitLines)
|
||||
{
|
||||
var bb = cutout.BoundingBox;
|
||||
foreach (var sl in splitLines)
|
||||
{
|
||||
if (sl.Axis == CutOffAxis.Vertical && bb.Left < sl.Position && bb.Right > sl.Position)
|
||||
return true;
|
||||
if (sl.Axis == CutOffAxis.Horizontal && bb.Bottom < sl.Position && bb.Top > sl.Position)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clip a cutout shape to a region by walking entities, splitting at split line
|
||||
/// intersections, keeping portions inside the region, and closing gaps with
|
||||
/// straight lines. No polygon clipping library needed.
|
||||
/// </summary>
|
||||
private static List<Entity> ClipCutoutToRegion(Shape cutout, Box region, List<SplitLine> splitLines)
|
||||
{
|
||||
var boundarySplitLines = GetBoundarySplitLines(region, splitLines);
|
||||
var entities = new List<Entity>();
|
||||
var splitPoints = new List<(Vector Point, SplitLine Line, bool IsExit)>();
|
||||
|
||||
foreach (var entity in cutout.Entities)
|
||||
{
|
||||
ProcessEntity(entity, region, boundarySplitLines, entities, splitPoints);
|
||||
}
|
||||
|
||||
if (entities.Count == 0)
|
||||
return new List<Entity>();
|
||||
|
||||
// Close gaps with straight lines (connect exit→entry pairs)
|
||||
var groups = new Dictionary<SplitLine, List<(Vector Point, bool IsExit)>>();
|
||||
foreach (var sp in splitPoints)
|
||||
{
|
||||
if (!groups.ContainsKey(sp.Line))
|
||||
groups[sp.Line] = new List<(Vector, bool)>();
|
||||
groups[sp.Line].Add((sp.Point, sp.IsExit));
|
||||
}
|
||||
|
||||
foreach (var kvp in groups)
|
||||
{
|
||||
var sl = kvp.Key;
|
||||
var points = kvp.Value;
|
||||
var isVertical = sl.Axis == CutOffAxis.Vertical;
|
||||
|
||||
var exits = points.Where(p => p.IsExit).Select(p => p.Point)
|
||||
.OrderBy(p => isVertical ? p.Y : p.X).ToList();
|
||||
var entries = points.Where(p => !p.IsExit).Select(p => p.Point)
|
||||
.OrderBy(p => isVertical ? p.Y : p.X).ToList();
|
||||
|
||||
var pairCount = System.Math.Min(exits.Count, entries.Count);
|
||||
for (var i = 0; i < pairCount; i++)
|
||||
entities.Add(new Line(exits[i], entries[i]));
|
||||
}
|
||||
|
||||
// Ensure CCW winding for cutouts
|
||||
var shape = new Shape();
|
||||
shape.Entities.AddRange(entities);
|
||||
var poly = shape.ToPolygon();
|
||||
if (poly != null && poly.RotationDirection() != RotationType.CCW)
|
||||
shape.Reverse();
|
||||
|
||||
return shape.Entities;
|
||||
}
|
||||
|
||||
private static Vector GetStartPoint(Entity entity)
|
||||
{
|
||||
return entity switch
|
||||
{
|
||||
Line l => l.StartPoint,
|
||||
Arc a => a.StartPoint(),
|
||||
_ => new Vector(0, 0)
|
||||
};
|
||||
}
|
||||
|
||||
private static Vector GetEndPoint(Entity entity)
|
||||
{
|
||||
return entity switch
|
||||
{
|
||||
Line l => l.EndPoint,
|
||||
Arc a => a.EndPoint(),
|
||||
_ => new Vector(0, 0)
|
||||
};
|
||||
}
|
||||
|
||||
private static ISplitFeature GetFeature(SplitType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
SplitType.Straight => new StraightSplit(),
|
||||
SplitType.WeldGapTabs => new WeldGapTabSplit(),
|
||||
SplitType.SpikeGroove => new SpikeGrooveSplit(),
|
||||
_ => new StraightSplit()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
public class SplitFeatureResult
|
||||
{
|
||||
public List<Entity> NegativeSideEdge { get; }
|
||||
public List<Entity> PositiveSideEdge { get; }
|
||||
|
||||
public SplitFeatureResult(List<Entity> negativeSideEdge, List<Entity> positiveSideEdge)
|
||||
{
|
||||
NegativeSideEdge = negativeSideEdge;
|
||||
PositiveSideEdge = positiveSideEdge;
|
||||
}
|
||||
}
|
||||
|
||||
public interface ISplitFeature
|
||||
{
|
||||
string Name { get; }
|
||||
SplitFeatureResult GenerateFeatures(SplitLine line, double extentStart, double extentEnd, SplitParameters parameters);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
/// <summary>
|
||||
/// Generates interlocking spike/V-groove pairs along the split edge.
|
||||
/// Spikes protrude from the positive side into the negative side.
|
||||
/// V-grooves on the negative side receive the spikes for self-alignment during welding.
|
||||
/// The weld gap (grooveDepth - spikeDepth) is the clearance at the tip when assembled.
|
||||
/// </summary>
|
||||
public class SpikeGrooveSplit : ISplitFeature
|
||||
{
|
||||
public string Name => "Spike / V-Groove";
|
||||
|
||||
public SplitFeatureResult GenerateFeatures(SplitLine line, double extentStart, double extentEnd, SplitParameters parameters)
|
||||
{
|
||||
var extent = extentEnd - extentStart;
|
||||
var pairCount = parameters.SpikePairCount;
|
||||
var spikeDepth = parameters.SpikeDepth;
|
||||
var grooveDepth = parameters.GrooveDepth;
|
||||
var angleRad = OpenNest.Math.Angle.ToRadians(parameters.SpikeAngle / 2);
|
||||
var spikeHalfWidth = spikeDepth * System.Math.Tan(angleRad);
|
||||
var grooveHalfWidth = grooveDepth * System.Math.Tan(angleRad);
|
||||
|
||||
var isVertical = line.Axis == CutOffAxis.Vertical;
|
||||
var pos = line.Position;
|
||||
|
||||
// Use custom positions if provided, otherwise place evenly with margin
|
||||
var pairPositions = new List<double>();
|
||||
if (line.FeaturePositions.Count > 0)
|
||||
{
|
||||
pairPositions.AddRange(line.FeaturePositions);
|
||||
}
|
||||
else if (pairCount == 1)
|
||||
{
|
||||
pairPositions.Add(extentStart + extent / 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
var margin = extent * 0.15;
|
||||
var usable = extent - 2 * margin;
|
||||
for (var i = 0; i < pairCount; i++)
|
||||
pairPositions.Add(extentStart + margin + usable * i / (pairCount - 1));
|
||||
}
|
||||
|
||||
var negEntities = BuildGrooveSide(pairPositions, grooveHalfWidth, grooveDepth, extentStart, extentEnd, pos, isVertical);
|
||||
var posEntities = BuildSpikeSide(pairPositions, spikeHalfWidth, spikeDepth, extentStart, extentEnd, pos, isVertical);
|
||||
|
||||
return new SplitFeatureResult(negEntities, posEntities);
|
||||
}
|
||||
|
||||
private static List<Entity> BuildGrooveSide(List<double> pairPositions, double halfWidth, double depth,
|
||||
double extentStart, double extentEnd, double pos, bool isVertical)
|
||||
{
|
||||
var entities = new List<Entity>();
|
||||
var cursor = extentStart;
|
||||
|
||||
foreach (var center in pairPositions)
|
||||
{
|
||||
var grooveStart = center - halfWidth;
|
||||
var grooveEnd = center + halfWidth;
|
||||
|
||||
if (grooveStart > cursor + OpenNest.Math.Tolerance.Epsilon)
|
||||
entities.Add(MakeLine(pos, cursor, pos, grooveStart, isVertical));
|
||||
|
||||
entities.Add(MakeLine(pos, grooveStart, pos - depth, center, isVertical));
|
||||
entities.Add(MakeLine(pos - depth, center, pos, grooveEnd, isVertical));
|
||||
|
||||
cursor = grooveEnd;
|
||||
}
|
||||
|
||||
if (extentEnd > cursor + OpenNest.Math.Tolerance.Epsilon)
|
||||
entities.Add(MakeLine(pos, cursor, pos, extentEnd, isVertical));
|
||||
|
||||
return entities;
|
||||
}
|
||||
|
||||
private static List<Entity> BuildSpikeSide(List<double> pairPositions, double halfWidth, double depth,
|
||||
double extentStart, double extentEnd, double pos, bool isVertical)
|
||||
{
|
||||
var entities = new List<Entity>();
|
||||
var cursor = extentEnd;
|
||||
|
||||
for (var i = pairPositions.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var center = pairPositions[i];
|
||||
var spikeEnd = center + halfWidth;
|
||||
var spikeStart = center - halfWidth;
|
||||
|
||||
if (cursor > spikeEnd + OpenNest.Math.Tolerance.Epsilon)
|
||||
entities.Add(MakeLine(pos, cursor, pos, spikeEnd, isVertical));
|
||||
|
||||
entities.Add(MakeLine(pos, spikeEnd, pos - depth, center, isVertical));
|
||||
entities.Add(MakeLine(pos - depth, center, pos, spikeStart, isVertical));
|
||||
|
||||
cursor = spikeStart;
|
||||
}
|
||||
|
||||
if (cursor > extentStart + OpenNest.Math.Tolerance.Epsilon)
|
||||
entities.Add(MakeLine(pos, cursor, pos, extentStart, isVertical));
|
||||
|
||||
return entities;
|
||||
}
|
||||
|
||||
private static Line MakeLine(double splitAxis1, double along1, double splitAxis2, double along2, bool isVertical)
|
||||
{
|
||||
return isVertical
|
||||
? new Line(new Vector(splitAxis1, along1), new Vector(splitAxis2, along2))
|
||||
: new Line(new Vector(along1, splitAxis1), new Vector(along2, splitAxis2));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a split line at a position along an axis.
|
||||
/// For Vertical, Position is the X coordinate. For Horizontal, Position is the Y coordinate.
|
||||
/// </summary>
|
||||
public class SplitLine
|
||||
{
|
||||
public double Position { get; }
|
||||
public CutOffAxis Axis { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional custom center positions for features (tabs/spikes) along the split line.
|
||||
/// Values are absolute coordinates on the perpendicular axis.
|
||||
/// When empty, feature generators use their default even spacing.
|
||||
/// </summary>
|
||||
public List<double> FeaturePositions { get; set; } = new();
|
||||
|
||||
public SplitLine(double position, CutOffAxis axis)
|
||||
{
|
||||
Position = position;
|
||||
Axis = axis;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a Line entity at the split position spanning the given extent range.
|
||||
/// For Vertical: line from (Position, extentStart) to (Position, extentEnd).
|
||||
/// For Horizontal: line from (extentStart, Position) to (extentEnd, Position).
|
||||
/// </summary>
|
||||
public Line ToLine(double extentStart, double extentEnd)
|
||||
{
|
||||
return Axis == CutOffAxis.Vertical
|
||||
? new Line(Position, extentStart, Position, extentEnd)
|
||||
: new Line(extentStart, Position, extentEnd, Position);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
/// <summary>
|
||||
/// Static helpers for testing entity-splitline intersections.
|
||||
/// </summary>
|
||||
public static class SplitLineIntersect
|
||||
{
|
||||
/// <summary>
|
||||
/// Finds the intersection point between an entity and a split line.
|
||||
/// Returns null if no intersection or the entity doesn't straddle the split line.
|
||||
/// </summary>
|
||||
public static Vector? FindIntersection(Entity entity, SplitLine sl)
|
||||
{
|
||||
if (!CrossesSplitLine(entity, sl))
|
||||
return null;
|
||||
|
||||
var bbox = entity.BoundingBox;
|
||||
var margin = 1.0;
|
||||
|
||||
// Create a line at the split position spanning the entity's bbox extent (with margin)
|
||||
Line splitLine;
|
||||
|
||||
if (sl.Axis == CutOffAxis.Vertical)
|
||||
splitLine = sl.ToLine(bbox.Bottom - margin, bbox.Top + margin);
|
||||
else
|
||||
splitLine = sl.ToLine(bbox.Left - margin, bbox.Right + margin);
|
||||
|
||||
switch (entity.Type)
|
||||
{
|
||||
case EntityType.Line:
|
||||
var line = (Line)entity;
|
||||
if (Intersect.Intersects(line, splitLine, out var pt))
|
||||
return pt;
|
||||
return null;
|
||||
|
||||
case EntityType.Arc:
|
||||
var arc = (Arc)entity;
|
||||
if (Intersect.Intersects(arc, splitLine, out var pts))
|
||||
return pts.Count > 0 ? pts[0] : null;
|
||||
return null;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the entity's bounding box straddles the split line,
|
||||
/// meaning it extends to both sides of the split position (not just touching).
|
||||
/// </summary>
|
||||
public static bool CrossesSplitLine(Entity entity, SplitLine sl)
|
||||
{
|
||||
var bbox = entity.BoundingBox;
|
||||
|
||||
if (sl.Axis == CutOffAxis.Vertical)
|
||||
return bbox.Left < sl.Position - Tolerance.Epsilon
|
||||
&& bbox.Right > sl.Position + Tolerance.Epsilon;
|
||||
else
|
||||
return bbox.Bottom < sl.Position - Tolerance.Epsilon
|
||||
&& bbox.Top > sl.Position + Tolerance.Epsilon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns -1 if the point is below/left of the split line,
|
||||
/// +1 if above/right, or 0 if on the line (within tolerance).
|
||||
/// </summary>
|
||||
public static int SideOf(Vector pt, SplitLine sl)
|
||||
{
|
||||
var value = sl.Axis == CutOffAxis.Vertical ? pt.X : pt.Y;
|
||||
var diff = value - sl.Position;
|
||||
|
||||
if (System.Math.Abs(diff) <= Tolerance.Epsilon)
|
||||
return 0;
|
||||
|
||||
return diff < 0 ? -1 : 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
namespace OpenNest;
|
||||
|
||||
public enum SplitType
|
||||
{
|
||||
Straight,
|
||||
WeldGapTabs,
|
||||
SpikeGroove
|
||||
}
|
||||
|
||||
public class SplitParameters
|
||||
{
|
||||
public SplitType Type { get; set; } = SplitType.Straight;
|
||||
|
||||
// Tab parameters
|
||||
public double TabWidth { get; set; } = 1.0;
|
||||
public double TabHeight { get; set; } = 0.125;
|
||||
public int TabCount { get; set; } = 3;
|
||||
|
||||
// Spike/Groove parameters
|
||||
public double SpikeDepth { get; set; } = 0.5;
|
||||
public double GrooveDepth { get; set; } = 0.625;
|
||||
public double SpikeWeldGap { get; set; } = 0.125;
|
||||
public double SpikeAngle { get; set; } = 60.0; // degrees
|
||||
public int SpikePairCount { get; set; } = 2;
|
||||
|
||||
/// <summary>
|
||||
/// Max protrusion from the split edge (for auto-fit plate size calculation).
|
||||
/// </summary>
|
||||
public double FeatureOverhang => Type switch
|
||||
{
|
||||
SplitType.WeldGapTabs => TabHeight,
|
||||
SplitType.SpikeGroove => System.Math.Max(SpikeDepth, GrooveDepth),
|
||||
_ => 0
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
public class StraightSplit : ISplitFeature
|
||||
{
|
||||
public string Name => "Straight";
|
||||
|
||||
public SplitFeatureResult GenerateFeatures(SplitLine line, double extentStart, double extentEnd, SplitParameters parameters)
|
||||
{
|
||||
var (negEdge, posEdge) = line.Axis == CutOffAxis.Vertical
|
||||
? (new Line(new Vector(line.Position, extentStart), new Vector(line.Position, extentEnd)),
|
||||
new Line(new Vector(line.Position, extentEnd), new Vector(line.Position, extentStart)))
|
||||
: (new Line(new Vector(extentStart, line.Position), new Vector(extentEnd, line.Position)),
|
||||
new Line(new Vector(extentEnd, line.Position), new Vector(extentStart, line.Position)));
|
||||
|
||||
return new SplitFeatureResult(
|
||||
new List<Entity> { negEdge },
|
||||
new List<Entity> { posEdge });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
/// <summary>
|
||||
/// Generates rectangular tabs on one side of the split edge (negative side).
|
||||
/// The positive side remains a straight line. Tabs act as weld-gap spacers.
|
||||
/// </summary>
|
||||
public class WeldGapTabSplit : ISplitFeature
|
||||
{
|
||||
public string Name => "Weld-Gap Tabs";
|
||||
|
||||
public SplitFeatureResult GenerateFeatures(SplitLine line, double extentStart, double extentEnd, SplitParameters parameters)
|
||||
{
|
||||
var extent = extentEnd - extentStart;
|
||||
var tabCount = parameters.TabCount;
|
||||
var tabWidth = parameters.TabWidth;
|
||||
var tabHeight = parameters.TabHeight;
|
||||
|
||||
// Use custom positions if provided, otherwise evenly space
|
||||
var tabCenters = new List<double>();
|
||||
if (line.FeaturePositions.Count > 0)
|
||||
{
|
||||
tabCenters.AddRange(line.FeaturePositions);
|
||||
}
|
||||
else
|
||||
{
|
||||
var spacing = extent / (tabCount + 1);
|
||||
for (var i = 0; i < tabCount; i++)
|
||||
tabCenters.Add(extentStart + spacing * (i + 1));
|
||||
}
|
||||
|
||||
var negEntities = new List<Entity>();
|
||||
var isVertical = line.Axis == CutOffAxis.Vertical;
|
||||
var pos = line.Position;
|
||||
|
||||
// Tabs protrude toward the negative side (lower coordinate on the split axis)
|
||||
var tabDir = -1.0;
|
||||
|
||||
var cursor = extentStart;
|
||||
|
||||
for (var i = 0; i < tabCenters.Count; i++)
|
||||
{
|
||||
var tabCenter = tabCenters[i];
|
||||
var tabStart = tabCenter - tabWidth / 2;
|
||||
var tabEnd = tabCenter + tabWidth / 2;
|
||||
|
||||
if (isVertical)
|
||||
{
|
||||
if (tabStart > cursor + OpenNest.Math.Tolerance.Epsilon)
|
||||
negEntities.Add(new Line(new Vector(pos, cursor), new Vector(pos, tabStart)));
|
||||
|
||||
negEntities.Add(new Line(new Vector(pos, tabStart), new Vector(pos + tabDir * tabHeight, tabStart)));
|
||||
negEntities.Add(new Line(new Vector(pos + tabDir * tabHeight, tabStart), new Vector(pos + tabDir * tabHeight, tabEnd)));
|
||||
negEntities.Add(new Line(new Vector(pos + tabDir * tabHeight, tabEnd), new Vector(pos, tabEnd)));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (tabStart > cursor + OpenNest.Math.Tolerance.Epsilon)
|
||||
negEntities.Add(new Line(new Vector(cursor, pos), new Vector(tabStart, pos)));
|
||||
|
||||
negEntities.Add(new Line(new Vector(tabStart, pos), new Vector(tabStart, pos + tabDir * tabHeight)));
|
||||
negEntities.Add(new Line(new Vector(tabStart, pos + tabDir * tabHeight), new Vector(tabEnd, pos + tabDir * tabHeight)));
|
||||
negEntities.Add(new Line(new Vector(tabEnd, pos + tabDir * tabHeight), new Vector(tabEnd, pos)));
|
||||
}
|
||||
|
||||
cursor = tabEnd;
|
||||
}
|
||||
|
||||
// Final segment from last tab to extent end
|
||||
if (isVertical)
|
||||
{
|
||||
if (extentEnd > cursor + OpenNest.Math.Tolerance.Epsilon)
|
||||
negEntities.Add(new Line(new Vector(pos, cursor), new Vector(pos, extentEnd)));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (extentEnd > cursor + OpenNest.Math.Tolerance.Epsilon)
|
||||
negEntities.Add(new Line(new Vector(cursor, pos), new Vector(extentEnd, pos)));
|
||||
}
|
||||
|
||||
// Positive side: plain straight line (reversed direction)
|
||||
var posEntities = new List<Entity>();
|
||||
if (isVertical)
|
||||
posEntities.Add(new Line(new Vector(pos, extentEnd), new Vector(pos, extentStart)));
|
||||
else
|
||||
posEntities.Add(new Line(new Vector(extentEnd, pos), new Vector(extentStart, pos)));
|
||||
|
||||
return new SplitFeatureResult(negEntities, posEntities);
|
||||
}
|
||||
}
|
||||
@@ -157,7 +157,7 @@ namespace OpenNest.Engine.Fill
|
||||
continue;
|
||||
|
||||
var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction);
|
||||
var d = gap - partSpacing;
|
||||
var d = gap - partSpacing - 2 * ChordTolerance;
|
||||
if (d < 0) d = 0;
|
||||
if (d < distance)
|
||||
distance = d;
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
if (perimeter != null)
|
||||
{
|
||||
var offsetEntity = perimeter.OffsetEntity(spacing, OffsetSide.Left) as Shape;
|
||||
var offsetEntity = perimeter.OffsetOutward(spacing);
|
||||
|
||||
if (offsetEntity != null)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
using ACadSharp;
|
||||
using OpenNest.Bending;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenNest.IO.Bending
|
||||
{
|
||||
public static class BendDetectorRegistry
|
||||
{
|
||||
private static readonly List<IBendDetector> detectors = new();
|
||||
|
||||
static BendDetectorRegistry()
|
||||
{
|
||||
Register(new SolidWorksBendDetector());
|
||||
}
|
||||
|
||||
public static void Register(IBendDetector detector)
|
||||
{
|
||||
detectors.Add(detector);
|
||||
}
|
||||
|
||||
public static IReadOnlyList<IBendDetector> Detectors => detectors;
|
||||
|
||||
public static IBendDetector GetByName(string name)
|
||||
{
|
||||
return detectors.FirstOrDefault(d => d.Name == name);
|
||||
}
|
||||
|
||||
public static List<Bend> AutoDetect(CadDocument document)
|
||||
{
|
||||
foreach (var detector in detectors)
|
||||
{
|
||||
var bends = detector.DetectBends(document);
|
||||
if (bends.Count > 0)
|
||||
return bends;
|
||||
}
|
||||
return new List<Bend>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using ACadSharp;
|
||||
using OpenNest.Bending;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.IO.Bending
|
||||
{
|
||||
public interface IBendDetector
|
||||
{
|
||||
string Name { get; }
|
||||
List<Bend> DetectBends(CadDocument document);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
using ACadSharp;
|
||||
using ACadSharp.Entities;
|
||||
using OpenNest.Bending;
|
||||
using OpenNest.Geometry;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace OpenNest.IO.Bending
|
||||
{
|
||||
public class SolidWorksBendDetector : IBendDetector
|
||||
{
|
||||
public string Name => "SolidWorks";
|
||||
|
||||
public double MaxBendRadius { get; set; } = 4.0;
|
||||
|
||||
private static readonly Regex BendNoteRegex = new Regex(
|
||||
@"(?<direction>UP|DOWN|DN)\s+(?<angle>\d+(\.\d+)?)[^A-Z\d]*R\s*(?<radius>\d+(\.\d+)?)",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
private static readonly Regex MTextFormatRegex = new Regex(
|
||||
@"\\[fHCTQWASpOoLlKk][^;]*;|\\P|[{}]|%%[dDpPcC]",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
public List<Bend> DetectBends(CadDocument document)
|
||||
{
|
||||
var bendLines = FindBendLines(document);
|
||||
var bendNotes = FindBendNotes(document);
|
||||
|
||||
if (bendLines.Count == 0)
|
||||
return new List<Bend>();
|
||||
|
||||
var bends = new List<Bend>();
|
||||
|
||||
foreach (var line in bendLines)
|
||||
{
|
||||
var start = new Vector(line.StartPoint.X, line.StartPoint.Y);
|
||||
var end = new Vector(line.EndPoint.X, line.EndPoint.Y);
|
||||
|
||||
var bend = new Bend
|
||||
{
|
||||
StartPoint = start,
|
||||
EndPoint = end,
|
||||
Direction = BendDirection.Unknown
|
||||
};
|
||||
|
||||
var note = FindClosestBendNote(line, bendNotes);
|
||||
if (note != null)
|
||||
{
|
||||
var noteText = StripMTextFormatting(note.Value);
|
||||
bend.Direction = GetBendDirection(noteText);
|
||||
bend.NoteText = noteText;
|
||||
ParseBendNote(noteText, bend);
|
||||
}
|
||||
|
||||
if (!bend.Radius.HasValue || bend.Radius.Value <= MaxBendRadius)
|
||||
bends.Add(bend);
|
||||
}
|
||||
|
||||
return bends;
|
||||
}
|
||||
|
||||
private List<ACadSharp.Entities.Line> FindBendLines(CadDocument document)
|
||||
{
|
||||
return document.Entities
|
||||
.OfType<ACadSharp.Entities.Line>()
|
||||
.Where(l => l.Layer?.Name == "BEND"
|
||||
&& (l.LineType?.Name?.Contains("CENTER") == true
|
||||
|| l.LineType?.Name == "CENTERX2"))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private List<MText> FindBendNotes(CadDocument document)
|
||||
{
|
||||
return document.Entities
|
||||
.OfType<MText>()
|
||||
.Where(t => GetBendDirection(t.Value) != BendDirection.Unknown)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static BendDirection GetBendDirection(string text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
return BendDirection.Unknown;
|
||||
|
||||
var upper = text.ToUpperInvariant();
|
||||
|
||||
if (upper.Contains("UP"))
|
||||
return BendDirection.Up;
|
||||
|
||||
if (upper.Contains("DOWN") || upper.Contains("DN"))
|
||||
return BendDirection.Down;
|
||||
|
||||
return BendDirection.Unknown;
|
||||
}
|
||||
|
||||
private static void ParseBendNote(string text, Bend bend)
|
||||
{
|
||||
var normalized = text.ToUpperInvariant().Replace("SHARP", "R0");
|
||||
var match = BendNoteRegex.Match(normalized);
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
if (double.TryParse(match.Groups["radius"].Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var radius))
|
||||
bend.Radius = radius;
|
||||
|
||||
if (double.TryParse(match.Groups["angle"].Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var angle))
|
||||
bend.Angle = angle;
|
||||
}
|
||||
}
|
||||
|
||||
private static string StripMTextFormatting(string text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
return text;
|
||||
|
||||
// Replace known DXF special characters
|
||||
var result = text
|
||||
.Replace("%%d", "°").Replace("%%D", "°")
|
||||
.Replace("%%p", "±").Replace("%%P", "±")
|
||||
.Replace("%%c", "⌀").Replace("%%C", "⌀");
|
||||
|
||||
// Strip MText formatting codes and braces
|
||||
result = MTextFormatRegex.Replace(result, " ");
|
||||
|
||||
// Collapse multiple spaces
|
||||
return Regex.Replace(result.Trim(), @"\s+", " ");
|
||||
}
|
||||
|
||||
private MText FindClosestBendNote(ACadSharp.Entities.Line bendLine, List<MText> notes)
|
||||
{
|
||||
if (notes.Count == 0) return null;
|
||||
|
||||
MText closest = null;
|
||||
var closestDist = double.MaxValue;
|
||||
|
||||
foreach (var note in notes)
|
||||
{
|
||||
var notePos = new Vector(note.InsertPoint.X, note.InsertPoint.Y);
|
||||
var lineStart = new Vector(bendLine.StartPoint.X, bendLine.StartPoint.Y);
|
||||
var lineEnd = new Vector(bendLine.EndPoint.X, bendLine.EndPoint.Y);
|
||||
|
||||
var geomLine = new OpenNest.Geometry.Line(lineStart, lineEnd);
|
||||
var perpPoint = geomLine.ClosestPointTo(notePos);
|
||||
var dist = notePos.DistanceTo(perpPoint);
|
||||
|
||||
var maxAcceptable = note.Height * 2.0;
|
||||
if (dist > maxAcceptable) continue;
|
||||
|
||||
if (dist < closestDist)
|
||||
{
|
||||
closestDist = dist;
|
||||
closest = note;
|
||||
}
|
||||
}
|
||||
|
||||
return closest;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using ACadSharp;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.IO
|
||||
{
|
||||
public class DxfImportResult
|
||||
{
|
||||
public List<Entity> Entities { get; set; } = new();
|
||||
public CadDocument Document { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -65,6 +65,24 @@ namespace OpenNest.IO
|
||||
return entities;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Imports a DXF file, returning both converted entities and the raw CadDocument
|
||||
/// for bend detection. The CadDocument is NOT disposed — caller can use it for
|
||||
/// additional analysis (e.g., MText extraction for bend notes).
|
||||
/// </summary>
|
||||
public DxfImportResult Import(string path)
|
||||
{
|
||||
using var reader = new DxfReader(path);
|
||||
var doc = reader.Read();
|
||||
var entities = GetGeometry(doc);
|
||||
|
||||
return new DxfImportResult
|
||||
{
|
||||
Entities = entities,
|
||||
Document = doc
|
||||
};
|
||||
}
|
||||
|
||||
public bool GetGeometry(Stream stream, out List<Entity> geometry)
|
||||
{
|
||||
var success = false;
|
||||
|
||||
@@ -50,6 +50,7 @@ namespace OpenNest.IO
|
||||
public ConstraintsDto Constraints { get; init; } = new();
|
||||
public MaterialDto Material { get; init; } = new();
|
||||
public SourceDto Source { get; init; } = new();
|
||||
public List<BendDto> Bends { get; init; } = new();
|
||||
}
|
||||
|
||||
public record PlateDto
|
||||
@@ -62,6 +63,7 @@ namespace OpenNest.IO
|
||||
public double PartSpacing { get; init; }
|
||||
public MaterialDto Material { get; init; } = new();
|
||||
public SpacingDto EdgeSpacing { get; init; } = new();
|
||||
public double GrainAngle { get; init; }
|
||||
public List<PartDto> Parts { get; init; } = new();
|
||||
public List<CutOffDto> CutOffs { get; init; } = new();
|
||||
}
|
||||
@@ -137,6 +139,18 @@ namespace OpenNest.IO
|
||||
public double Y { get; init; }
|
||||
}
|
||||
|
||||
public record BendDto
|
||||
{
|
||||
public double StartX { get; init; }
|
||||
public double StartY { get; init; }
|
||||
public double EndX { get; init; }
|
||||
public double EndY { get; init; }
|
||||
public string Direction { get; init; } = "Unknown";
|
||||
public double? Angle { get; init; }
|
||||
public double? Radius { get; init; }
|
||||
public string NoteText { get; init; } = "";
|
||||
}
|
||||
|
||||
public record BestFitSetDto
|
||||
{
|
||||
public double PlateWidth { get; init; }
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using OpenNest.Bending;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Engine.BestFit;
|
||||
using OpenNest.Geometry;
|
||||
@@ -91,6 +92,23 @@ namespace OpenNest.IO
|
||||
drawing.Source.Path = d.Source.Path;
|
||||
drawing.Source.Offset = new Vector(d.Source.Offset.X, d.Source.Offset.Y);
|
||||
|
||||
if (d.Bends != null)
|
||||
{
|
||||
foreach (var b in d.Bends)
|
||||
{
|
||||
drawing.Bends.Add(new Bend
|
||||
{
|
||||
StartPoint = new Vector(b.StartX, b.StartY),
|
||||
EndPoint = new Vector(b.EndX, b.EndY),
|
||||
Direction = Enum.TryParse<BendDirection>(b.Direction, true, out var dir)
|
||||
? dir : BendDirection.Unknown,
|
||||
Angle = b.Angle,
|
||||
Radius = b.Radius,
|
||||
NoteText = b.NoteText
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (programs.TryGetValue(d.Id, out var pgm))
|
||||
drawing.Program = pgm;
|
||||
|
||||
@@ -186,6 +204,7 @@ namespace OpenNest.IO
|
||||
plate.PartSpacing = p.PartSpacing;
|
||||
plate.Material = new Material(p.Material.Name, p.Material.Grade, p.Material.Density);
|
||||
plate.EdgeSpacing = new Spacing(p.EdgeSpacing.Left, p.EdgeSpacing.Bottom, p.EdgeSpacing.Right, p.EdgeSpacing.Top);
|
||||
plate.GrainAngle = p.GrainAngle;
|
||||
|
||||
foreach (var partDto in p.Parts)
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using OpenNest.Bending;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Engine.BestFit;
|
||||
using System;
|
||||
@@ -140,7 +141,18 @@ namespace OpenNest.IO
|
||||
{
|
||||
Path = d.Source.Path ?? "",
|
||||
Offset = new OffsetDto { X = d.Source.Offset.X, Y = d.Source.Offset.Y }
|
||||
}
|
||||
},
|
||||
Bends = d.Bends?.Select(b => new BendDto
|
||||
{
|
||||
StartX = b.StartPoint.X,
|
||||
StartY = b.StartPoint.Y,
|
||||
EndX = b.EndPoint.X,
|
||||
EndY = b.EndPoint.Y,
|
||||
Direction = b.Direction.ToString(),
|
||||
Angle = b.Angle,
|
||||
Radius = b.Radius,
|
||||
NoteText = b.NoteText ?? ""
|
||||
}).ToList() ?? new List<BendDto>()
|
||||
});
|
||||
}
|
||||
return list;
|
||||
@@ -200,7 +212,8 @@ namespace OpenNest.IO
|
||||
Bottom = plate.EdgeSpacing.Bottom
|
||||
},
|
||||
Parts = parts,
|
||||
CutOffs = cutoffs
|
||||
CutOffs = cutoffs,
|
||||
GrainAngle = plate.GrainAngle
|
||||
});
|
||||
}
|
||||
return list;
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
using ACadSharp;
|
||||
using ACadSharp.Entities;
|
||||
using ACadSharp.IO;
|
||||
using ACadSharp.Tables;
|
||||
using CSMath;
|
||||
using OpenNest.Bending;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
// Disambiguate Entity — both ACadSharp.Entities and OpenNest.Geometry define it
|
||||
using GeoEntity = OpenNest.Geometry.Entity;
|
||||
|
||||
namespace OpenNest.IO
|
||||
{
|
||||
public class SplitDxfWriter
|
||||
{
|
||||
private const double DefaultEtchLength = 1.0;
|
||||
|
||||
public double EtchLength { get; set; } = DefaultEtchLength;
|
||||
|
||||
public void Write(string path, Drawing drawing)
|
||||
{
|
||||
var doc = new CadDocument();
|
||||
|
||||
var cutLayer = new ACadSharp.Tables.Layer("CUT") { Color = new Color(7) };
|
||||
var bendLayer = new ACadSharp.Tables.Layer("BEND") { Color = new Color(2) };
|
||||
var etchLayer = new ACadSharp.Tables.Layer("ETCH") { Color = new Color(3) };
|
||||
doc.Layers.Add(cutLayer);
|
||||
doc.Layers.Add(bendLayer);
|
||||
doc.Layers.Add(etchLayer);
|
||||
|
||||
var centerLineType = new LineType("CENTERX2");
|
||||
doc.LineTypes.Add(centerLineType);
|
||||
|
||||
WriteProgramEntities(doc, drawing.Program, cutLayer);
|
||||
|
||||
if (drawing.Bends != null)
|
||||
{
|
||||
foreach (var bend in drawing.Bends)
|
||||
{
|
||||
WriteBendLine(doc, bend, bendLayer, centerLineType);
|
||||
WriteEtchLines(doc, bend, etchLayer);
|
||||
}
|
||||
}
|
||||
|
||||
using var stream = File.Create(path);
|
||||
using var writer = new DxfWriter(stream, doc, false);
|
||||
writer.Write();
|
||||
}
|
||||
|
||||
private static void WriteProgramEntities(CadDocument doc, CNC.Program program, ACadSharp.Tables.Layer layer)
|
||||
{
|
||||
var geometry = ConvertProgram.ToGeometry(program);
|
||||
WriteGeometryEntities(doc, geometry, layer);
|
||||
}
|
||||
|
||||
private static void WriteGeometryEntities(CadDocument doc, List<GeoEntity> geometry, ACadSharp.Tables.Layer layer)
|
||||
{
|
||||
foreach (var entity in geometry)
|
||||
{
|
||||
// Skip rapid moves
|
||||
if (entity.Layer == SpecialLayers.Rapid)
|
||||
continue;
|
||||
|
||||
switch (entity)
|
||||
{
|
||||
case OpenNest.Geometry.Line line:
|
||||
doc.Entities.Add(new ACadSharp.Entities.Line
|
||||
{
|
||||
StartPoint = new XYZ(line.StartPoint.X, line.StartPoint.Y, 0),
|
||||
EndPoint = new XYZ(line.EndPoint.X, line.EndPoint.Y, 0),
|
||||
Layer = layer
|
||||
});
|
||||
break;
|
||||
|
||||
case OpenNest.Geometry.Arc arc:
|
||||
var startAngle = arc.StartAngle;
|
||||
var endAngle = arc.EndAngle;
|
||||
if (arc.IsReversed)
|
||||
OpenNest.Math.Generic.Swap(ref startAngle, ref endAngle);
|
||||
|
||||
doc.Entities.Add(new ACadSharp.Entities.Arc
|
||||
{
|
||||
Center = new XYZ(arc.Center.X, arc.Center.Y, 0),
|
||||
Radius = arc.Radius,
|
||||
StartAngle = startAngle,
|
||||
EndAngle = endAngle,
|
||||
Layer = layer
|
||||
});
|
||||
break;
|
||||
|
||||
case OpenNest.Geometry.Circle circle:
|
||||
doc.Entities.Add(new ACadSharp.Entities.Circle
|
||||
{
|
||||
Center = new XYZ(circle.Center.X, circle.Center.Y, 0),
|
||||
Radius = circle.Radius,
|
||||
Layer = layer
|
||||
});
|
||||
break;
|
||||
|
||||
case OpenNest.Geometry.Shape shape:
|
||||
WriteGeometryEntities(doc, shape.Entities, layer);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteBendLine(CadDocument doc, Bend bend, ACadSharp.Tables.Layer layer, LineType lineType)
|
||||
{
|
||||
var line = new ACadSharp.Entities.Line
|
||||
{
|
||||
StartPoint = new XYZ(bend.StartPoint.X, bend.StartPoint.Y, 0),
|
||||
EndPoint = new XYZ(bend.EndPoint.X, bend.EndPoint.Y, 0),
|
||||
Layer = layer,
|
||||
LineType = lineType
|
||||
};
|
||||
doc.Entities.Add(line);
|
||||
|
||||
if (!string.IsNullOrEmpty(bend.NoteText))
|
||||
{
|
||||
var midX = (bend.StartPoint.X + bend.EndPoint.X) / 2;
|
||||
var midY = (bend.StartPoint.Y + bend.EndPoint.Y) / 2;
|
||||
|
||||
var mtext = new MText
|
||||
{
|
||||
InsertPoint = new XYZ(midX, midY + 0.5, 0),
|
||||
Value = bend.NoteText,
|
||||
Height = 0.1,
|
||||
Layer = layer
|
||||
};
|
||||
doc.Entities.Add(mtext);
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteEtchLines(CadDocument doc, Bend bend, ACadSharp.Tables.Layer layer)
|
||||
{
|
||||
if (bend.Direction != BendDirection.Up)
|
||||
return;
|
||||
|
||||
var start = bend.StartPoint;
|
||||
var end = bend.EndPoint;
|
||||
var length = bend.Length;
|
||||
|
||||
if (length < EtchLength * 3.0)
|
||||
{
|
||||
doc.Entities.Add(new ACadSharp.Entities.Line
|
||||
{
|
||||
StartPoint = new XYZ(start.X, start.Y, 0),
|
||||
EndPoint = new XYZ(end.X, end.Y, 0),
|
||||
Layer = layer
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
var angle = start.AngleTo(end);
|
||||
var dx = System.Math.Cos(angle) * EtchLength;
|
||||
var dy = System.Math.Sin(angle) * EtchLength;
|
||||
|
||||
doc.Entities.Add(new ACadSharp.Entities.Line
|
||||
{
|
||||
StartPoint = new XYZ(start.X, start.Y, 0),
|
||||
EndPoint = new XYZ(start.X + dx, start.Y + dy, 0),
|
||||
Layer = layer
|
||||
});
|
||||
|
||||
doc.Entities.Add(new ACadSharp.Entities.Line
|
||||
{
|
||||
StartPoint = new XYZ(end.X, end.Y, 0),
|
||||
EndPoint = new XYZ(end.X - dx, end.Y - dy, 0),
|
||||
Layer = layer
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
using OpenNest.Bending;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.Bending;
|
||||
|
||||
public class BendModelTests
|
||||
{
|
||||
[Fact]
|
||||
public void Bend_StoresStartAndEndPoints()
|
||||
{
|
||||
var bend = new Bend
|
||||
{
|
||||
StartPoint = new Vector(0, 5),
|
||||
EndPoint = new Vector(10, 5),
|
||||
Direction = BendDirection.Up,
|
||||
Angle = 90,
|
||||
Radius = 0.06,
|
||||
NoteText = "UP 90° R0.06"
|
||||
};
|
||||
|
||||
Assert.Equal(0, bend.StartPoint.X);
|
||||
Assert.Equal(10, bend.EndPoint.X);
|
||||
Assert.Equal(BendDirection.Up, bend.Direction);
|
||||
Assert.Equal(90, bend.Angle);
|
||||
Assert.Equal(0.06, bend.Radius);
|
||||
Assert.Equal("UP 90° R0.06", bend.NoteText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bend_ToLine_ReturnsGeometryLine()
|
||||
{
|
||||
var bend = new Bend
|
||||
{
|
||||
StartPoint = new Vector(0, 5),
|
||||
EndPoint = new Vector(10, 5)
|
||||
};
|
||||
|
||||
var line = bend.ToLine();
|
||||
|
||||
Assert.Equal(0, line.StartPoint.X);
|
||||
Assert.Equal(5, line.StartPoint.Y);
|
||||
Assert.Equal(10, line.EndPoint.X);
|
||||
Assert.Equal(5, line.EndPoint.Y);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bend_Length_ComputesCorrectly()
|
||||
{
|
||||
var bend = new Bend
|
||||
{
|
||||
StartPoint = new Vector(0, 0),
|
||||
EndPoint = new Vector(3, 4)
|
||||
};
|
||||
|
||||
Assert.Equal(5.0, bend.Length, 0.001);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bend_BendAngleRadians_ConvertsDegreesToRadians()
|
||||
{
|
||||
var bend = new Bend { Angle = 90 };
|
||||
|
||||
Assert.Equal(System.Math.PI / 2, bend.AngleRadians, 0.001);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bend_DefaultDirection_IsUnknown()
|
||||
{
|
||||
var bend = new Bend();
|
||||
|
||||
Assert.Equal(BendDirection.Unknown, bend.Direction);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bend_ToString_FormatsNicely()
|
||||
{
|
||||
var bend = new Bend
|
||||
{
|
||||
Direction = BendDirection.Up,
|
||||
Angle = 90,
|
||||
Radius = 0.06
|
||||
};
|
||||
|
||||
var str = bend.ToString();
|
||||
|
||||
Assert.Contains("Up", str);
|
||||
Assert.Contains("90", str);
|
||||
Assert.Contains("0.06", str);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using OpenNest.Bending;
|
||||
using OpenNest.IO.Bending;
|
||||
|
||||
namespace OpenNest.Tests.Bending;
|
||||
|
||||
public class SolidWorksBendDetectorTests
|
||||
{
|
||||
[Fact]
|
||||
public void SolidWorksDetector_IsRegistered()
|
||||
{
|
||||
var detector = BendDetectorRegistry.GetByName("SolidWorks");
|
||||
Assert.NotNull(detector);
|
||||
Assert.Equal("SolidWorks", detector.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Registry_ContainsSolidWorksDetector()
|
||||
{
|
||||
Assert.Contains(BendDetectorRegistry.Detectors,
|
||||
d => d.Name == "SolidWorks");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AutoDetect_EmptyDocument_ReturnsEmptyList()
|
||||
{
|
||||
var doc = new ACadSharp.CadDocument();
|
||||
var bends = BendDetectorRegistry.AutoDetect(doc);
|
||||
Assert.Empty(bends);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
using OpenNest.Bending;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenNest.Tests;
|
||||
|
||||
public class NestBendSerializationTests
|
||||
{
|
||||
[Fact]
|
||||
public void Bends_SurviveNestRoundtrip()
|
||||
{
|
||||
var drawing = TestHelpers.MakeSquareDrawing();
|
||||
drawing.Bends.Add(new Bend
|
||||
{
|
||||
StartPoint = new Vector(0, 5),
|
||||
EndPoint = new Vector(10, 5),
|
||||
Direction = BendDirection.Up,
|
||||
Angle = 90,
|
||||
Radius = 0.06,
|
||||
NoteText = "UP 90° R0.06"
|
||||
});
|
||||
drawing.Bends.Add(new Bend
|
||||
{
|
||||
StartPoint = new Vector(0, 3),
|
||||
EndPoint = new Vector(10, 3),
|
||||
Direction = BendDirection.Down,
|
||||
Angle = 45.5,
|
||||
Radius = 0.125,
|
||||
NoteText = "DOWN 45.5° R0.125"
|
||||
});
|
||||
|
||||
var nest = new Nest();
|
||||
nest.Drawings.Add(drawing);
|
||||
var plate = new Plate(60, 120);
|
||||
plate.GrainAngle = 0.5236;
|
||||
plate.Parts.Add(new Part(drawing, new Vector(0, 0)));
|
||||
nest.Plates.Add(plate);
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
var writer = new NestWriter(nest);
|
||||
writer.Write(ms);
|
||||
|
||||
ms.Position = 0;
|
||||
var reader = new NestReader(ms);
|
||||
var loaded = reader.Read();
|
||||
|
||||
var loadedDrawing = loaded.Drawings.First();
|
||||
Assert.Equal(2, loadedDrawing.Bends.Count);
|
||||
|
||||
var bend1 = loadedDrawing.Bends[0];
|
||||
Assert.Equal(0, bend1.StartPoint.X, 0.001);
|
||||
Assert.Equal(5, bend1.StartPoint.Y, 0.001);
|
||||
Assert.Equal(10, bend1.EndPoint.X, 0.001);
|
||||
Assert.Equal(5, bend1.EndPoint.Y, 0.001);
|
||||
Assert.Equal(BendDirection.Up, bend1.Direction);
|
||||
Assert.Equal(90, bend1.Angle);
|
||||
Assert.Equal(0.06, bend1.Radius);
|
||||
Assert.Equal("UP 90° R0.06", bend1.NoteText);
|
||||
|
||||
var bend2 = loadedDrawing.Bends[1];
|
||||
Assert.Equal(BendDirection.Down, bend2.Direction);
|
||||
Assert.Equal(45.5, bend2.Angle);
|
||||
|
||||
var loadedPlate = loaded.Plates[0];
|
||||
Assert.Equal(0.5236, loadedPlate.GrainAngle, 0.0001);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoBends_SurviveNestRoundtrip()
|
||||
{
|
||||
var drawing = TestHelpers.MakeSquareDrawing();
|
||||
|
||||
var nest = new Nest();
|
||||
nest.Drawings.Add(drawing);
|
||||
var plate = new Plate(60, 120);
|
||||
plate.Parts.Add(new Part(drawing, new Vector(0, 0)));
|
||||
nest.Plates.Add(plate);
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
var writer = new NestWriter(nest);
|
||||
writer.Write(ms);
|
||||
|
||||
ms.Position = 0;
|
||||
var reader = new NestReader(ms);
|
||||
var loaded = reader.Read();
|
||||
|
||||
Assert.Empty(loaded.Drawings.First().Bends);
|
||||
Assert.Equal(0, loaded.Plates[0].GrainAngle);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.Splitting;
|
||||
|
||||
public class DrawingSplitterTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper: creates a Drawing from a rectangular perimeter.
|
||||
/// </summary>
|
||||
private static Drawing MakeRectangleDrawing(string name, double width, double height)
|
||||
{
|
||||
var entities = new List<Entity>
|
||||
{
|
||||
new Line(new Vector(0, 0), new Vector(width, 0)),
|
||||
new Line(new Vector(width, 0), new Vector(width, height)),
|
||||
new Line(new Vector(width, height), new Vector(0, height)),
|
||||
new Line(new Vector(0, height), new Vector(0, 0))
|
||||
};
|
||||
var pgm = ConvertGeometry.ToProgram(entities);
|
||||
return new Drawing(name, pgm);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Split_Rectangle_Vertical_ProducesTwoPieces()
|
||||
{
|
||||
var drawing = MakeRectangleDrawing("RECT", 100, 50);
|
||||
var splitLines = new List<SplitLine> { new SplitLine(50.0, CutOffAxis.Vertical) };
|
||||
var parameters = new SplitParameters { Type = SplitType.Straight };
|
||||
|
||||
var results = DrawingSplitter.Split(drawing, splitLines, parameters);
|
||||
|
||||
Assert.Equal(2, results.Count);
|
||||
Assert.Equal("RECT-1", results[0].Name);
|
||||
Assert.Equal("RECT-2", results[1].Name);
|
||||
|
||||
// Each piece should have area close to half the original
|
||||
var totalArea = results.Sum(d => d.Area);
|
||||
Assert.Equal(drawing.Area, totalArea, 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Split_Rectangle_Horizontal_ProducesTwoPieces()
|
||||
{
|
||||
var drawing = MakeRectangleDrawing("RECT", 100, 60);
|
||||
var splitLines = new List<SplitLine> { new SplitLine(30.0, CutOffAxis.Horizontal) };
|
||||
var parameters = new SplitParameters { Type = SplitType.Straight };
|
||||
|
||||
var results = DrawingSplitter.Split(drawing, splitLines, parameters);
|
||||
|
||||
Assert.Equal(2, results.Count);
|
||||
Assert.Equal("RECT-1", results[0].Name);
|
||||
Assert.Equal("RECT-2", results[1].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Split_ThreePieces_NamesSequentially()
|
||||
{
|
||||
var drawing = MakeRectangleDrawing("PART", 150, 50);
|
||||
var splitLines = new List<SplitLine>
|
||||
{
|
||||
new SplitLine(50.0, CutOffAxis.Vertical),
|
||||
new SplitLine(100.0, CutOffAxis.Vertical)
|
||||
};
|
||||
var parameters = new SplitParameters { Type = SplitType.Straight };
|
||||
|
||||
var results = DrawingSplitter.Split(drawing, splitLines, parameters);
|
||||
|
||||
Assert.Equal(3, results.Count);
|
||||
Assert.Equal("PART-1", results[0].Name);
|
||||
Assert.Equal("PART-2", results[1].Name);
|
||||
Assert.Equal("PART-3", results[2].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Split_CopiesDrawingProperties()
|
||||
{
|
||||
var drawing = MakeRectangleDrawing("PART", 100, 50);
|
||||
drawing.Color = System.Drawing.Color.Red;
|
||||
drawing.Priority = 5;
|
||||
|
||||
var results = DrawingSplitter.Split(drawing,
|
||||
new List<SplitLine> { new SplitLine(50.0, CutOffAxis.Vertical) },
|
||||
new SplitParameters());
|
||||
|
||||
Assert.All(results, d =>
|
||||
{
|
||||
Assert.Equal(System.Drawing.Color.Red, d.Color);
|
||||
Assert.Equal(5, d.Priority);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Split_PiecesNormalizedToOrigin()
|
||||
{
|
||||
var drawing = MakeRectangleDrawing("PART", 100, 50);
|
||||
var results = DrawingSplitter.Split(drawing,
|
||||
new List<SplitLine> { new SplitLine(50.0, CutOffAxis.Vertical) },
|
||||
new SplitParameters());
|
||||
|
||||
// Each piece's program bounding box should start near (0,0)
|
||||
foreach (var d in results)
|
||||
{
|
||||
var bb = d.Program.BoundingBox();
|
||||
Assert.True(bb.X < 1.0, $"Piece {d.Name} not normalized: X={bb.X}");
|
||||
Assert.True(bb.Y < 1.0, $"Piece {d.Name} not normalized: Y={bb.Y}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Split_WithCutout_AssignsCutoutToCorrectPiece()
|
||||
{
|
||||
// Rectangle 100x50 with a small square cutout at (20,20)-(30,30)
|
||||
var perimeterEntities = new List<Entity>
|
||||
{
|
||||
new Line(new Vector(0, 0), new Vector(100, 0)),
|
||||
new Line(new Vector(100, 0), new Vector(100, 50)),
|
||||
new Line(new Vector(100, 50), new Vector(0, 50)),
|
||||
new Line(new Vector(0, 50), new Vector(0, 0))
|
||||
};
|
||||
var cutoutEntities = new List<Entity>
|
||||
{
|
||||
new Line(new Vector(20, 20), new Vector(30, 20)),
|
||||
new Line(new Vector(30, 20), new Vector(30, 30)),
|
||||
new Line(new Vector(30, 30), new Vector(20, 30)),
|
||||
new Line(new Vector(20, 30), new Vector(20, 20))
|
||||
};
|
||||
var allEntities = new List<Entity>();
|
||||
allEntities.AddRange(perimeterEntities);
|
||||
allEntities.AddRange(cutoutEntities);
|
||||
|
||||
var pgm = ConvertGeometry.ToProgram(allEntities);
|
||||
var drawing = new Drawing("HOLE", pgm);
|
||||
|
||||
// Split at X=50 — cutout is in the left half
|
||||
var results = DrawingSplitter.Split(drawing,
|
||||
new List<SplitLine> { new SplitLine(50.0, CutOffAxis.Vertical) },
|
||||
new SplitParameters());
|
||||
|
||||
Assert.Equal(2, results.Count);
|
||||
// Left piece should have smaller area (has the cutout)
|
||||
Assert.True(results[0].Area < results[1].Area,
|
||||
"Left piece should have less area due to cutout");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Split_GridSplit_ProducesFourPieces()
|
||||
{
|
||||
var drawing = MakeRectangleDrawing("GRID", 100, 100);
|
||||
var splitLines = new List<SplitLine>
|
||||
{
|
||||
new SplitLine(50.0, CutOffAxis.Vertical),
|
||||
new SplitLine(50.0, CutOffAxis.Horizontal)
|
||||
};
|
||||
var results = DrawingSplitter.Split(drawing, splitLines, new SplitParameters());
|
||||
|
||||
Assert.Equal(4, results.Count);
|
||||
Assert.Equal("GRID-1", results[0].Name);
|
||||
Assert.Equal("GRID-2", results[1].Name);
|
||||
Assert.Equal("GRID-3", results[2].Name);
|
||||
Assert.Equal("GRID-4", results[3].Name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Tests.Splitting;
|
||||
|
||||
public class EntitySplitTests
|
||||
{
|
||||
// --- SplitLine.ToLine ---
|
||||
|
||||
[Fact]
|
||||
public void ToLine_Vertical_ReturnsVerticalLine()
|
||||
{
|
||||
var sl = new SplitLine(50.0, CutOffAxis.Vertical);
|
||||
var line = sl.ToLine(0, 100);
|
||||
|
||||
Assert.Equal(50.0, line.StartPoint.X, 5);
|
||||
Assert.Equal(0.0, line.StartPoint.Y, 5);
|
||||
Assert.Equal(50.0, line.EndPoint.X, 5);
|
||||
Assert.Equal(100.0, line.EndPoint.Y, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToLine_Horizontal_ReturnsHorizontalLine()
|
||||
{
|
||||
var sl = new SplitLine(30.0, CutOffAxis.Horizontal);
|
||||
var line = sl.ToLine(10, 90);
|
||||
|
||||
Assert.Equal(10.0, line.StartPoint.X, 5);
|
||||
Assert.Equal(30.0, line.StartPoint.Y, 5);
|
||||
Assert.Equal(90.0, line.EndPoint.X, 5);
|
||||
Assert.Equal(30.0, line.EndPoint.Y, 5);
|
||||
}
|
||||
|
||||
// --- FindIntersection: Line crossing vertical split ---
|
||||
|
||||
[Fact]
|
||||
public void FindIntersection_LineCrossesVerticalSplit_ReturnsPoint()
|
||||
{
|
||||
// Diagonal line from (0,0) to (100,100), vertical split at x=50
|
||||
var line = new Line(0, 0, 100, 100);
|
||||
var sl = new SplitLine(50.0, CutOffAxis.Vertical);
|
||||
|
||||
var result = SplitLineIntersect.FindIntersection(line, sl);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(50.0, result.Value.X, 5);
|
||||
Assert.Equal(50.0, result.Value.Y, 5);
|
||||
}
|
||||
|
||||
// --- FindIntersection: Line NOT crossing ---
|
||||
|
||||
[Fact]
|
||||
public void FindIntersection_LineDoesNotCross_ReturnsNull()
|
||||
{
|
||||
// Line entirely to the left of split at x=50
|
||||
var line = new Line(0, 0, 40, 40);
|
||||
var sl = new SplitLine(50.0, CutOffAxis.Vertical);
|
||||
|
||||
var result = SplitLineIntersect.FindIntersection(line, sl);
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
// --- FindIntersection: Line parallel to split ---
|
||||
|
||||
[Fact]
|
||||
public void FindIntersection_LineParallelToSplit_ReturnsNull()
|
||||
{
|
||||
// Vertical line at x=50 — parallel to vertical split at x=50
|
||||
var line = new Line(50, 0, 50, 100);
|
||||
var sl = new SplitLine(50.0, CutOffAxis.Vertical);
|
||||
|
||||
var result = SplitLineIntersect.FindIntersection(line, sl);
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
// --- FindIntersection: Arc crossing vertical split ---
|
||||
|
||||
[Fact]
|
||||
public void FindIntersection_ArcCrossesVerticalSplit_ReturnsPoint()
|
||||
{
|
||||
// Arc centered at (60,50), radius 20, from PI to 0 (CCW).
|
||||
// CCW from PI wraps through 3PI/2 (bottom) then 0 (right).
|
||||
// At x=50: (50-60)^2 + (y-50)^2 = 400 => (y-50)^2 = 300
|
||||
// y = 50 - sqrt(300) ≈ 32.68 (bottom intersection, on the arc)
|
||||
// y = 50 + sqrt(300) ≈ 67.32 (top intersection, also on the arc)
|
||||
var arc = new Arc(60, 50, 20, System.Math.PI, 0, false);
|
||||
var sl = new SplitLine(50.0, CutOffAxis.Vertical);
|
||||
|
||||
var result = SplitLineIntersect.FindIntersection(arc, sl);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(50.0, result.Value.X, 1);
|
||||
// The first intersection found by the circle-line algorithm; either ~32.68 or ~67.32
|
||||
var y = result.Value.Y;
|
||||
var expectedLow = 50.0 - System.Math.Sqrt(300);
|
||||
var expectedHigh = 50.0 + System.Math.Sqrt(300);
|
||||
Assert.True(
|
||||
System.Math.Abs(y - expectedLow) < 0.1 || System.Math.Abs(y - expectedHigh) < 0.1,
|
||||
$"Expected Y near {expectedLow:F2} or {expectedHigh:F2}, got {y:F2}");
|
||||
}
|
||||
|
||||
// --- CrossesSplitLine ---
|
||||
|
||||
[Fact]
|
||||
public void CrossesSplitLine_LineStraddles_ReturnsTrue()
|
||||
{
|
||||
var line = new Line(40, 0, 60, 100);
|
||||
var sl = new SplitLine(50.0, CutOffAxis.Vertical);
|
||||
|
||||
Assert.True(SplitLineIntersect.CrossesSplitLine(line, sl));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CrossesSplitLine_LineEntirelyOnOneSide_ReturnsFalse()
|
||||
{
|
||||
var line = new Line(10, 0, 40, 100);
|
||||
var sl = new SplitLine(50.0, CutOffAxis.Vertical);
|
||||
|
||||
Assert.False(SplitLineIntersect.CrossesSplitLine(line, sl));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CrossesSplitLine_HorizontalSplit_Works()
|
||||
{
|
||||
var line = new Line(0, 10, 100, 60);
|
||||
var sl = new SplitLine(30.0, CutOffAxis.Horizontal);
|
||||
|
||||
Assert.True(SplitLineIntersect.CrossesSplitLine(line, sl));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CrossesSplitLine_LineTouchingButNotStraddling_ReturnsFalse()
|
||||
{
|
||||
// Line endpoint exactly at the split line — bbox right == 50, left < 50
|
||||
// But right must be > pos (strictly), so touching exactly returns false
|
||||
var line = new Line(10, 0, 50, 100);
|
||||
var sl = new SplitLine(50.0, CutOffAxis.Vertical);
|
||||
|
||||
Assert.False(SplitLineIntersect.CrossesSplitLine(line, sl));
|
||||
}
|
||||
|
||||
// --- SideOf ---
|
||||
|
||||
[Fact]
|
||||
public void SideOf_PointLeftOfVerticalSplit_ReturnsNegative()
|
||||
{
|
||||
var pt = new Vector(30, 50);
|
||||
var sl = new SplitLine(50.0, CutOffAxis.Vertical);
|
||||
|
||||
Assert.Equal(-1, SplitLineIntersect.SideOf(pt, sl));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SideOf_PointRightOfVerticalSplit_ReturnsPositive()
|
||||
{
|
||||
var pt = new Vector(70, 50);
|
||||
var sl = new SplitLine(50.0, CutOffAxis.Vertical);
|
||||
|
||||
Assert.Equal(1, SplitLineIntersect.SideOf(pt, sl));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SideOf_PointOnVerticalSplit_ReturnsZero()
|
||||
{
|
||||
var pt = new Vector(50, 50);
|
||||
var sl = new SplitLine(50.0, CutOffAxis.Vertical);
|
||||
|
||||
Assert.Equal(0, SplitLineIntersect.SideOf(pt, sl));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SideOf_PointBelowHorizontalSplit_ReturnsNegative()
|
||||
{
|
||||
var pt = new Vector(50, 10);
|
||||
var sl = new SplitLine(30.0, CutOffAxis.Horizontal);
|
||||
|
||||
Assert.Equal(-1, SplitLineIntersect.SideOf(pt, sl));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SideOf_PointAboveHorizontalSplit_ReturnsPositive()
|
||||
{
|
||||
var pt = new Vector(50, 60);
|
||||
var sl = new SplitLine(30.0, CutOffAxis.Horizontal);
|
||||
|
||||
Assert.Equal(1, SplitLineIntersect.SideOf(pt, sl));
|
||||
}
|
||||
|
||||
// --- FindIntersection: horizontal split ---
|
||||
|
||||
[Fact]
|
||||
public void FindIntersection_LineCrossesHorizontalSplit_ReturnsPoint()
|
||||
{
|
||||
// Diagonal line from (0,0) to (100,100), horizontal split at y=50
|
||||
var line = new Line(0, 0, 100, 100);
|
||||
var sl = new SplitLine(50.0, CutOffAxis.Horizontal);
|
||||
|
||||
var result = SplitLineIntersect.FindIntersection(line, sl);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(50.0, result.Value.X, 5);
|
||||
Assert.Equal(50.0, result.Value.Y, 5);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
using System.Linq;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.Splitting;
|
||||
|
||||
public class SplitFeatureTests
|
||||
{
|
||||
[Fact]
|
||||
public void WeldGapTabSplit_Vertical_TabsOnNegativeSide()
|
||||
{
|
||||
var feature = new WeldGapTabSplit();
|
||||
var line = new SplitLine(50.0, CutOffAxis.Vertical);
|
||||
var parameters = new SplitParameters
|
||||
{
|
||||
Type = SplitType.WeldGapTabs,
|
||||
TabWidth = 2.0,
|
||||
TabHeight = 0.25,
|
||||
TabCount = 2
|
||||
};
|
||||
|
||||
var result = feature.GenerateFeatures(line, 0.0, 100.0, parameters);
|
||||
|
||||
// Positive side (right): single straight line (no tabs)
|
||||
Assert.Single(result.PositiveSideEdge);
|
||||
Assert.IsType<Line>(result.PositiveSideEdge[0]);
|
||||
|
||||
// Negative side (left): has tab protrusions — more than 1 entity
|
||||
Assert.True(result.NegativeSideEdge.Count > 1);
|
||||
|
||||
// All entities should be lines
|
||||
Assert.All(result.NegativeSideEdge, e => Assert.IsType<Line>(e));
|
||||
|
||||
// First entity starts at extent start, last ends at extent end
|
||||
var first = (Line)result.NegativeSideEdge[0];
|
||||
var last = (Line)result.NegativeSideEdge[^1];
|
||||
Assert.Equal(0.0, first.StartPoint.Y, 6);
|
||||
Assert.Equal(100.0, last.EndPoint.Y, 6);
|
||||
|
||||
// Tabs protrude in the negative-X direction (left of split line)
|
||||
var tabEntities = result.NegativeSideEdge.Cast<Line>().ToList();
|
||||
var minX = tabEntities.Min(l => System.Math.Min(l.StartPoint.X, l.EndPoint.X));
|
||||
Assert.Equal(50.0 - 0.25, minX, 6); // tabHeight = 0.25
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WeldGapTabSplit_Name()
|
||||
{
|
||||
Assert.Equal("Weld-Gap Tabs", new WeldGapTabSplit().Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StraightSplit_Vertical_ProducesSingleLineEachSide()
|
||||
{
|
||||
var feature = new StraightSplit();
|
||||
var line = new SplitLine(50.0, CutOffAxis.Vertical);
|
||||
var parameters = new SplitParameters { Type = SplitType.Straight };
|
||||
|
||||
var result = feature.GenerateFeatures(line, 10.0, 90.0, parameters);
|
||||
|
||||
Assert.Single(result.NegativeSideEdge);
|
||||
var negLine = Assert.IsType<Line>(result.NegativeSideEdge[0]);
|
||||
Assert.Equal(50.0, negLine.StartPoint.X, 6);
|
||||
Assert.Equal(10.0, negLine.StartPoint.Y, 6);
|
||||
Assert.Equal(50.0, negLine.EndPoint.X, 6);
|
||||
Assert.Equal(90.0, negLine.EndPoint.Y, 6);
|
||||
|
||||
Assert.Single(result.PositiveSideEdge);
|
||||
var posLine = Assert.IsType<Line>(result.PositiveSideEdge[0]);
|
||||
Assert.Equal(50.0, posLine.StartPoint.X, 6);
|
||||
Assert.Equal(90.0, posLine.StartPoint.Y, 6);
|
||||
Assert.Equal(50.0, posLine.EndPoint.X, 6);
|
||||
Assert.Equal(10.0, posLine.EndPoint.Y, 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StraightSplit_Horizontal_ProducesSingleLineEachSide()
|
||||
{
|
||||
var feature = new StraightSplit();
|
||||
var line = new SplitLine(40.0, CutOffAxis.Horizontal);
|
||||
var parameters = new SplitParameters { Type = SplitType.Straight };
|
||||
|
||||
var result = feature.GenerateFeatures(line, 5.0, 95.0, parameters);
|
||||
|
||||
var negLine = Assert.IsType<Line>(result.NegativeSideEdge[0]);
|
||||
Assert.Equal(5.0, negLine.StartPoint.X, 6);
|
||||
Assert.Equal(40.0, negLine.StartPoint.Y, 6);
|
||||
Assert.Equal(95.0, negLine.EndPoint.X, 6);
|
||||
Assert.Equal(40.0, negLine.EndPoint.Y, 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StraightSplit_Name()
|
||||
{
|
||||
Assert.Equal("Straight", new StraightSplit().Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpikeGrooveSplit_Vertical_TwoPairs_SpikesOnPositiveSide()
|
||||
{
|
||||
var feature = new SpikeGrooveSplit();
|
||||
var line = new SplitLine(50.0, CutOffAxis.Vertical);
|
||||
var parameters = new SplitParameters
|
||||
{
|
||||
Type = SplitType.SpikeGroove,
|
||||
SpikeDepth = 1.0,
|
||||
SpikeAngle = 60.0,
|
||||
SpikePairCount = 2
|
||||
};
|
||||
|
||||
var result = feature.GenerateFeatures(line, 0.0, 100.0, parameters);
|
||||
|
||||
// Both sides should have multiple entities (straight segments + spike/groove geometry)
|
||||
Assert.True(result.NegativeSideEdge.Count > 1, "Negative side should have groove geometry");
|
||||
Assert.True(result.PositiveSideEdge.Count > 1, "Positive side should have spike geometry");
|
||||
|
||||
// All entities should be lines
|
||||
Assert.All(result.NegativeSideEdge, e => Assert.IsType<Line>(e));
|
||||
Assert.All(result.PositiveSideEdge, e => Assert.IsType<Line>(e));
|
||||
|
||||
// Spikes protrude in negative-X direction (into the negative side's territory)
|
||||
var posLines = result.PositiveSideEdge.Cast<Line>().ToList();
|
||||
var minX = posLines.Min(l => System.Math.Min(l.StartPoint.X, l.EndPoint.X));
|
||||
Assert.True(minX < 50.0, "Spikes should protrude past the split line");
|
||||
|
||||
// Grooves indent in the positive-X direction (into positive side's territory)
|
||||
var negLines = result.NegativeSideEdge.Cast<Line>().ToList();
|
||||
var maxX = negLines.Max(l => System.Math.Max(l.StartPoint.X, l.EndPoint.X));
|
||||
Assert.True(maxX <= 50.0, "Grooves should not protrude past the split line");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpikeGrooveSplit_Name()
|
||||
{
|
||||
Assert.Equal("Spike / V-Groove", new SpikeGrooveSplit().Name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.Splitting;
|
||||
|
||||
public class SplitIntegrationTest
|
||||
{
|
||||
[Fact]
|
||||
public void Split_SpikeGroove_NoContinuityGaps()
|
||||
{
|
||||
// Create a rectangle
|
||||
var entities = new List<Entity>
|
||||
{
|
||||
new Line(new Vector(0, 0), new Vector(100, 0)),
|
||||
new Line(new Vector(100, 0), new Vector(100, 50)),
|
||||
new Line(new Vector(100, 50), new Vector(0, 50)),
|
||||
new Line(new Vector(0, 50), new Vector(0, 0))
|
||||
};
|
||||
var pgm = ConvertGeometry.ToProgram(entities);
|
||||
var drawing = new Drawing("TEST", pgm);
|
||||
|
||||
var sl = new SplitLine(50.0, CutOffAxis.Vertical);
|
||||
sl.FeaturePositions.Add(12.5);
|
||||
sl.FeaturePositions.Add(37.5);
|
||||
|
||||
var parameters = new SplitParameters
|
||||
{
|
||||
Type = SplitType.SpikeGroove,
|
||||
GrooveDepth = 0.625,
|
||||
SpikeDepth = 0.75,
|
||||
SpikeWeldGap = 0.125,
|
||||
SpikeAngle = 45,
|
||||
SpikePairCount = 2
|
||||
};
|
||||
|
||||
var results = DrawingSplitter.Split(drawing, new List<SplitLine> { sl }, parameters);
|
||||
Assert.Equal(2, results.Count);
|
||||
|
||||
foreach (var piece in results)
|
||||
{
|
||||
// Get cut entities only (no rapids)
|
||||
var pieceEntities = ConvertProgram.ToGeometry(piece.Program)
|
||||
.Where(e => e.Layer != SpecialLayers.Rapid).ToList();
|
||||
|
||||
// Check that consecutive entity endpoints connect (no gaps)
|
||||
for (var i = 0; i < pieceEntities.Count - 1; i++)
|
||||
{
|
||||
var end = GetEndPoint(pieceEntities[i]);
|
||||
var start = GetStartPoint(pieceEntities[i + 1]);
|
||||
var gap = end.DistanceTo(start);
|
||||
Assert.True(gap < 0.01,
|
||||
$"Gap of {gap:F6} between entities {i} and {i + 1} in {piece.Name}");
|
||||
}
|
||||
|
||||
// Area should be non-zero
|
||||
Assert.True(piece.Area > 0, $"{piece.Name} has zero area");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Split_SpikeGroove_Horizontal_NoContinuityGaps()
|
||||
{
|
||||
var entities = new List<Entity>
|
||||
{
|
||||
new Line(new Vector(0, 0), new Vector(100, 0)),
|
||||
new Line(new Vector(100, 0), new Vector(100, 50)),
|
||||
new Line(new Vector(100, 50), new Vector(0, 50)),
|
||||
new Line(new Vector(0, 50), new Vector(0, 0))
|
||||
};
|
||||
var pgm = ConvertGeometry.ToProgram(entities);
|
||||
var drawing = new Drawing("TEST", pgm);
|
||||
|
||||
var sl = new SplitLine(25.0, CutOffAxis.Horizontal);
|
||||
sl.FeaturePositions.Add(25.0);
|
||||
sl.FeaturePositions.Add(75.0);
|
||||
|
||||
var parameters = new SplitParameters
|
||||
{
|
||||
Type = SplitType.SpikeGroove,
|
||||
GrooveDepth = 0.625,
|
||||
SpikeDepth = 0.75,
|
||||
SpikeWeldGap = 0.125,
|
||||
SpikeAngle = 45,
|
||||
SpikePairCount = 2
|
||||
};
|
||||
|
||||
var results = DrawingSplitter.Split(drawing, new List<SplitLine> { sl }, parameters);
|
||||
Assert.Equal(2, results.Count);
|
||||
|
||||
foreach (var piece in results)
|
||||
{
|
||||
var pieceEntities = ConvertProgram.ToGeometry(piece.Program)
|
||||
.Where(e => e.Layer != SpecialLayers.Rapid).ToList();
|
||||
|
||||
for (var i = 0; i < pieceEntities.Count - 1; i++)
|
||||
{
|
||||
var end = GetEndPoint(pieceEntities[i]);
|
||||
var start = GetStartPoint(pieceEntities[i + 1]);
|
||||
var gap = end.DistanceTo(start);
|
||||
Assert.True(gap < 0.01,
|
||||
$"Gap of {gap:F6} between entities {i} and {i + 1} in {piece.Name}");
|
||||
}
|
||||
|
||||
Assert.True(piece.Area > 0, $"{piece.Name} has zero area");
|
||||
}
|
||||
}
|
||||
|
||||
private static Vector GetStartPoint(Entity entity)
|
||||
{
|
||||
return entity switch
|
||||
{
|
||||
Line l => l.StartPoint,
|
||||
Arc a => a.StartPoint(),
|
||||
_ => new Vector(0, 0)
|
||||
};
|
||||
}
|
||||
|
||||
private static Vector GetEndPoint(Entity entity)
|
||||
{
|
||||
return entity switch
|
||||
{
|
||||
Line l => l.EndPoint,
|
||||
Arc a => a.EndPoint(),
|
||||
_ => new Vector(0, 0)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.Splitting;
|
||||
|
||||
public class SplitLineTests
|
||||
{
|
||||
[Fact]
|
||||
public void SplitLine_Vertical_StoresPositionAsX()
|
||||
{
|
||||
var line = new SplitLine(50.0, CutOffAxis.Vertical);
|
||||
Assert.Equal(50.0, line.Position);
|
||||
Assert.Equal(CutOffAxis.Vertical, line.Axis);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SplitLine_Horizontal_StoresPositionAsY()
|
||||
{
|
||||
var line = new SplitLine(30.0, CutOffAxis.Horizontal);
|
||||
Assert.Equal(30.0, line.Position);
|
||||
Assert.Equal(CutOffAxis.Horizontal, line.Axis);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SplitParameters_Defaults()
|
||||
{
|
||||
var p = new SplitParameters();
|
||||
Assert.Equal(SplitType.Straight, p.Type);
|
||||
Assert.Equal(3, p.TabCount);
|
||||
Assert.Equal(1.0, p.TabWidth);
|
||||
Assert.Equal(0.125, p.TabHeight);
|
||||
Assert.Equal(2, p.SpikePairCount);
|
||||
}
|
||||
}
|
||||
|
||||
public class AutoSplitCalculatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void FitToPlate_SingleAxis_CalculatesCorrectSplits()
|
||||
{
|
||||
var partBounds = new Box(0, 0, 100, 50);
|
||||
var lines = AutoSplitCalculator.FitToPlate(partBounds, 60, 60, 1.0, 0);
|
||||
|
||||
Assert.Single(lines);
|
||||
Assert.Equal(CutOffAxis.Vertical, lines[0].Axis);
|
||||
Assert.Equal(50.0, lines[0].Position, 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FitToPlate_BothAxes_GeneratesGrid()
|
||||
{
|
||||
var partBounds = new Box(0, 0, 200, 200);
|
||||
var lines = AutoSplitCalculator.FitToPlate(partBounds, 60, 60, 0, 0);
|
||||
|
||||
var verticals = lines.Where(l => l.Axis == CutOffAxis.Vertical).ToList();
|
||||
var horizontals = lines.Where(l => l.Axis == CutOffAxis.Horizontal).ToList();
|
||||
Assert.Equal(3, verticals.Count);
|
||||
Assert.Equal(3, horizontals.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FitToPlate_AlreadyFits_ReturnsEmpty()
|
||||
{
|
||||
var partBounds = new Box(0, 0, 50, 50);
|
||||
var lines = AutoSplitCalculator.FitToPlate(partBounds, 60, 60, 1.0, 0);
|
||||
Assert.Empty(lines);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SplitByCount_SingleAxis_EvenlySpaced()
|
||||
{
|
||||
var partBounds = new Box(0, 0, 100, 50);
|
||||
var lines = AutoSplitCalculator.SplitByCount(partBounds, horizontalPieces: 1, verticalPieces: 3);
|
||||
|
||||
Assert.Equal(2, lines.Count);
|
||||
Assert.All(lines, l => Assert.Equal(CutOffAxis.Vertical, l.Axis));
|
||||
Assert.Equal(33.333, lines[0].Position, 2);
|
||||
Assert.Equal(66.667, lines[1].Position, 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FitToPlate_AccountsForFeatureOverhang()
|
||||
{
|
||||
var partBounds = new Box(0, 0, 100, 50);
|
||||
var lines = AutoSplitCalculator.FitToPlate(partBounds, 60, 60, 1.0, 0.5);
|
||||
Assert.Single(lines);
|
||||
}
|
||||
}
|
||||
@@ -154,11 +154,9 @@ namespace OpenNest.Actions
|
||||
default: hDir = PushDirection.Left; vDir = PushDirection.Down; break;
|
||||
}
|
||||
|
||||
// Phase 1: BB-only push to get past irregular geometry quickly.
|
||||
Compactor.PushBoundingBox(movingParts, plateView.Plate, hDir);
|
||||
Compactor.PushBoundingBox(movingParts, plateView.Plate, vDir);
|
||||
|
||||
// Phase 2: Geometry push to settle against actual contours.
|
||||
Compactor.Push(movingParts, plateView.Plate, hDir);
|
||||
Compactor.Push(movingParts, plateView.Plate, vDir);
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace OpenNest.Controls
|
||||
{
|
||||
public class CollapsiblePanel : Panel
|
||||
{
|
||||
private readonly Panel headerPanel;
|
||||
private readonly Label headerLabel;
|
||||
private readonly Label chevronLabel;
|
||||
private readonly Panel contentPanel;
|
||||
private bool isExpanded;
|
||||
private int expandedHeight;
|
||||
|
||||
public CollapsiblePanel()
|
||||
{
|
||||
isExpanded = true;
|
||||
expandedHeight = 200;
|
||||
|
||||
headerPanel = new Panel
|
||||
{
|
||||
Dock = DockStyle.Top,
|
||||
Height = 28,
|
||||
BackColor = Color.FromArgb(240, 240, 240),
|
||||
Cursor = Cursors.Hand
|
||||
};
|
||||
|
||||
chevronLabel = new Label
|
||||
{
|
||||
Text = "▾",
|
||||
AutoSize = false,
|
||||
Size = new Size(20, 28),
|
||||
Dock = DockStyle.Left,
|
||||
TextAlign = ContentAlignment.MiddleCenter,
|
||||
Font = new Font("Segoe UI", 9f)
|
||||
};
|
||||
|
||||
headerLabel = new Label
|
||||
{
|
||||
Text = "Section",
|
||||
AutoSize = false,
|
||||
Dock = DockStyle.Fill,
|
||||
TextAlign = ContentAlignment.MiddleLeft,
|
||||
Font = new Font("Segoe UI", 9f, FontStyle.Bold)
|
||||
};
|
||||
|
||||
headerPanel.Controls.Add(headerLabel);
|
||||
headerPanel.Controls.Add(chevronLabel);
|
||||
headerPanel.Click += (s, e) => Toggle();
|
||||
headerLabel.Click += (s, e) => Toggle();
|
||||
chevronLabel.Click += (s, e) => Toggle();
|
||||
|
||||
contentPanel = new Panel
|
||||
{
|
||||
Dock = DockStyle.Fill
|
||||
};
|
||||
|
||||
Controls.Add(contentPanel);
|
||||
Controls.Add(headerPanel);
|
||||
}
|
||||
|
||||
public string HeaderText
|
||||
{
|
||||
get => headerLabel.Text;
|
||||
set => headerLabel.Text = value;
|
||||
}
|
||||
|
||||
public bool IsExpanded
|
||||
{
|
||||
get => isExpanded;
|
||||
set
|
||||
{
|
||||
isExpanded = value;
|
||||
UpdateLayout();
|
||||
}
|
||||
}
|
||||
|
||||
public int ExpandedHeight
|
||||
{
|
||||
get => expandedHeight;
|
||||
set
|
||||
{
|
||||
expandedHeight = value;
|
||||
if (isExpanded) Height = value;
|
||||
}
|
||||
}
|
||||
|
||||
public Panel ContentPanel => contentPanel;
|
||||
|
||||
public void Toggle()
|
||||
{
|
||||
isExpanded = !isExpanded;
|
||||
UpdateLayout();
|
||||
}
|
||||
|
||||
private void UpdateLayout()
|
||||
{
|
||||
contentPanel.Visible = isExpanded;
|
||||
chevronLabel.Text = isExpanded ? "▾" : "▸";
|
||||
Height = isExpanded ? expandedHeight : headerPanel.Height;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,6 +167,7 @@ namespace OpenNest.Controls
|
||||
origin.Y += (Size.Height - lastSize.Height) * 0.5f;
|
||||
|
||||
lastSize = Size;
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
public float LengthWorldToGui(double length)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Bending;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
@@ -10,8 +11,11 @@ namespace OpenNest.Controls
|
||||
public class EntityView : DrawControl
|
||||
{
|
||||
public List<Entity> Entities;
|
||||
public List<Bend> Bends = new List<Bend>();
|
||||
public int SelectedBendIndex = -1;
|
||||
|
||||
private Pen pen = new Pen(Color.FromArgb(70, 70, 70));
|
||||
private readonly Pen gridPen = new Pen(Color.FromArgb(70, 70, 70));
|
||||
private readonly Dictionary<int, Pen> penCache = new Dictionary<int, Pen>();
|
||||
|
||||
public EntityView()
|
||||
{
|
||||
@@ -37,13 +41,18 @@ namespace OpenNest.Controls
|
||||
base.OnPaint(e);
|
||||
|
||||
e.Graphics.SmoothingMode = SmoothingMode.HighSpeed;
|
||||
e.Graphics.DrawLine(pen, origin.X, 0, origin.X, Height);
|
||||
e.Graphics.DrawLine(pen, 0, origin.Y, Width, origin.Y);
|
||||
e.Graphics.DrawLine(gridPen, origin.X, 0, origin.X, Height);
|
||||
e.Graphics.DrawLine(gridPen, 0, origin.Y, Width, origin.Y);
|
||||
|
||||
e.Graphics.TranslateTransform(origin.X, origin.Y);
|
||||
|
||||
foreach (var entity in Entities)
|
||||
DrawEntity(e.Graphics, entity, Pens.White);
|
||||
{
|
||||
var pen = GetEntityPen(entity.Color);
|
||||
DrawEntity(e.Graphics, entity, pen);
|
||||
}
|
||||
|
||||
DrawBendLines(e.Graphics);
|
||||
|
||||
#if DRAW_OFFSET
|
||||
|
||||
@@ -100,6 +109,68 @@ namespace OpenNest.Controls
|
||||
ZoomToFit();
|
||||
}
|
||||
|
||||
private Pen GetEntityPen(Color color)
|
||||
{
|
||||
if (color.IsEmpty || color.A == 0)
|
||||
color = Color.White;
|
||||
|
||||
// Clamp dark colors to ensure visibility on dark background
|
||||
var brightness = (color.R * 299 + color.G * 587 + color.B * 114) / 1000;
|
||||
if (brightness < 80)
|
||||
color = Color.FromArgb(color.A,
|
||||
System.Math.Max(color.R, (byte)80),
|
||||
System.Math.Max(color.G, (byte)80),
|
||||
System.Math.Max(color.B, (byte)80));
|
||||
|
||||
var argb = color.ToArgb();
|
||||
if (!penCache.TryGetValue(argb, out var pen))
|
||||
{
|
||||
pen = new Pen(color);
|
||||
penCache[argb] = pen;
|
||||
}
|
||||
return pen;
|
||||
}
|
||||
|
||||
public void ClearPenCache()
|
||||
{
|
||||
foreach (var pen in penCache.Values)
|
||||
pen.Dispose();
|
||||
penCache.Clear();
|
||||
}
|
||||
|
||||
private void DrawBendLines(Graphics g)
|
||||
{
|
||||
if (Bends == null || Bends.Count == 0)
|
||||
return;
|
||||
|
||||
using var bendPen = new Pen(Color.Yellow, 1.5f)
|
||||
{
|
||||
DashStyle = DashStyle.Dash
|
||||
};
|
||||
using var selectedPen = new Pen(Color.Cyan, 2.5f)
|
||||
{
|
||||
DashStyle = DashStyle.Dash
|
||||
};
|
||||
|
||||
for (var i = 0; i < Bends.Count; i++)
|
||||
{
|
||||
var bend = Bends[i];
|
||||
var pt1 = PointWorldToGraph(bend.StartPoint);
|
||||
var pt2 = PointWorldToGraph(bend.EndPoint);
|
||||
g.DrawLine(i == SelectedBendIndex ? selectedPen : bendPen, pt1, pt2);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
ClearPenCache();
|
||||
gridPen.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
private void DrawEntity(Graphics g, Entity e, Pen pen)
|
||||
{
|
||||
if (!e.Layer.IsVisible || !e.IsVisible)
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
// OpenNest/Controls/FileListControl.cs
|
||||
using OpenNest.Bending;
|
||||
using OpenNest.Geometry;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace OpenNest.Controls
|
||||
{
|
||||
public class FileListItem
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Customer { get; set; }
|
||||
public int Quantity { get; set; } = 1;
|
||||
public string Path { get; set; }
|
||||
public List<Entity> Entities { get; set; } = new();
|
||||
public List<Bend> Bends { get; set; } = new();
|
||||
public Box Bounds { get; set; }
|
||||
public int EntityCount { get; set; }
|
||||
}
|
||||
|
||||
public class FileListControl : Control
|
||||
{
|
||||
private readonly List<FileListItem> items = new();
|
||||
private int selectedIndex = -1;
|
||||
private int hoveredIndex = -1;
|
||||
private int scrollOffset;
|
||||
private const int ItemHeight = 48;
|
||||
private const int AccentBarWidth = 3;
|
||||
|
||||
private readonly SolidBrush selectedBrush = new SolidBrush(Color.FromArgb(230, 238, 255));
|
||||
private readonly SolidBrush hoveredBrush = new SolidBrush(Color.FromArgb(242, 245, 250));
|
||||
private readonly SolidBrush accentBrush = new SolidBrush(Color.FromArgb(60, 120, 216));
|
||||
private readonly Pen separatorPen = new Pen(Color.FromArgb(230, 230, 230));
|
||||
private readonly SolidBrush emptyStateBrush = new SolidBrush(Color.FromArgb(160, 160, 160));
|
||||
|
||||
public event EventHandler<int> SelectedIndexChanged;
|
||||
public event EventHandler<FileListItem> ItemRightClicked;
|
||||
|
||||
public FileListControl()
|
||||
{
|
||||
SetStyle(
|
||||
ControlStyles.AllPaintingInWmPaint |
|
||||
ControlStyles.OptimizedDoubleBuffer |
|
||||
ControlStyles.UserPaint |
|
||||
ControlStyles.ResizeRedraw, true);
|
||||
|
||||
BackColor = Color.White;
|
||||
Font = new Font("Segoe UI", 9f);
|
||||
}
|
||||
|
||||
public IReadOnlyList<FileListItem> Items => items;
|
||||
public int SelectedIndex => selectedIndex;
|
||||
|
||||
public FileListItem SelectedItem =>
|
||||
selectedIndex >= 0 && selectedIndex < items.Count
|
||||
? items[selectedIndex]
|
||||
: null;
|
||||
|
||||
public void AddItem(FileListItem item)
|
||||
{
|
||||
items.Add(item);
|
||||
if (items.Count == 1)
|
||||
{
|
||||
selectedIndex = 0;
|
||||
SelectedIndexChanged?.Invoke(this, selectedIndex);
|
||||
}
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
public void RemoveAt(int index)
|
||||
{
|
||||
items.RemoveAt(index);
|
||||
if (selectedIndex >= items.Count)
|
||||
selectedIndex = items.Count - 1;
|
||||
Invalidate();
|
||||
SelectedIndexChanged?.Invoke(this, selectedIndex);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
items.Clear();
|
||||
selectedIndex = -1;
|
||||
scrollOffset = 0;
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
public void InsertItems(int index, IEnumerable<FileListItem> newItems)
|
||||
{
|
||||
items.InsertRange(index, newItems);
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
protected override void OnPaint(PaintEventArgs e)
|
||||
{
|
||||
base.OnPaint(e);
|
||||
var g = e.Graphics;
|
||||
g.SmoothingMode = SmoothingMode.HighQuality;
|
||||
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
|
||||
|
||||
if (items.Count == 0)
|
||||
{
|
||||
DrawEmptyState(g);
|
||||
return;
|
||||
}
|
||||
|
||||
var boldFont = new Font(Font, FontStyle.Bold);
|
||||
var smallFont = new Font("Segoe UI", 8f);
|
||||
var mutedBrush = new SolidBrush(Color.FromArgb(130, 130, 130));
|
||||
var textBrush = new SolidBrush(ForeColor);
|
||||
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
var y = i * ItemHeight - scrollOffset;
|
||||
if (y + ItemHeight < 0 || y > Height) continue;
|
||||
|
||||
var item = items[i];
|
||||
var rect = new Rectangle(0, y, Width, ItemHeight);
|
||||
|
||||
// Background
|
||||
if (i == selectedIndex)
|
||||
g.FillRectangle(selectedBrush, rect);
|
||||
else if (i == hoveredIndex)
|
||||
g.FillRectangle(hoveredBrush, rect);
|
||||
|
||||
// Accent bar
|
||||
if (i == selectedIndex)
|
||||
g.FillRectangle(accentBrush, 0, y, AccentBarWidth, ItemHeight);
|
||||
|
||||
// Name
|
||||
var nameRect = new Rectangle(AccentBarWidth + 8, y + 6, Width - 70, 20);
|
||||
TextRenderer.DrawText(g, item.Name, boldFont, nameRect, ForeColor, TextFormatFlags.Left | TextFormatFlags.EndEllipsis);
|
||||
|
||||
// Dimensions + entity count
|
||||
var bounds = item.Bounds;
|
||||
var dimText = bounds != null
|
||||
? $"{bounds.Width:0.#} x {bounds.Length:0.#} — {item.EntityCount} entities"
|
||||
: $"{item.EntityCount} entities";
|
||||
var dimRect = new Rectangle(AccentBarWidth + 8, y + 26, Width - 70, 16);
|
||||
TextRenderer.DrawText(g, dimText, smallFont, dimRect, Color.FromArgb(130, 130, 130), TextFormatFlags.Left);
|
||||
|
||||
// Quantity badge
|
||||
var qtyText = $"x{item.Quantity}";
|
||||
var qtyRect = new Rectangle(Width - 50, y + 12, 40, 24);
|
||||
TextRenderer.DrawText(g, qtyText, Font, qtyRect, Color.FromArgb(100, 100, 100), TextFormatFlags.Right | TextFormatFlags.VerticalCenter);
|
||||
|
||||
// Separator
|
||||
if (i < items.Count - 1)
|
||||
g.DrawLine(separatorPen, AccentBarWidth + 8, y + ItemHeight - 1, Width - 8, y + ItemHeight - 1);
|
||||
}
|
||||
|
||||
boldFont.Dispose();
|
||||
smallFont.Dispose();
|
||||
mutedBrush.Dispose();
|
||||
textBrush.Dispose();
|
||||
}
|
||||
|
||||
private void DrawEmptyState(Graphics g)
|
||||
{
|
||||
var text = "Drop DXF files here\nor click Add Files...";
|
||||
var size = g.MeasureString(text, Font);
|
||||
var x = (Width - size.Width) / 2;
|
||||
var y = (Height - size.Height) / 2;
|
||||
g.DrawString(text, Font, emptyStateBrush, x, y,
|
||||
new StringFormat { Alignment = StringAlignment.Center });
|
||||
}
|
||||
|
||||
protected override void OnMouseClick(MouseEventArgs e)
|
||||
{
|
||||
base.OnMouseClick(e);
|
||||
var index = GetIndexAt(e.Y);
|
||||
if (index < 0 || index >= items.Count) return;
|
||||
|
||||
if (e.Button == MouseButtons.Right)
|
||||
{
|
||||
selectedIndex = index;
|
||||
Invalidate();
|
||||
SelectedIndexChanged?.Invoke(this, selectedIndex);
|
||||
ItemRightClicked?.Invoke(this, items[index]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (index != selectedIndex)
|
||||
{
|
||||
selectedIndex = index;
|
||||
Invalidate();
|
||||
SelectedIndexChanged?.Invoke(this, selectedIndex);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnMouseMove(MouseEventArgs e)
|
||||
{
|
||||
base.OnMouseMove(e);
|
||||
var index = GetIndexAt(e.Y);
|
||||
if (index != hoveredIndex)
|
||||
{
|
||||
hoveredIndex = index;
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnMouseLeave(EventArgs e)
|
||||
{
|
||||
base.OnMouseLeave(e);
|
||||
hoveredIndex = -1;
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
protected override void OnMouseWheel(MouseEventArgs e)
|
||||
{
|
||||
base.OnMouseWheel(e);
|
||||
var maxScroll = System.Math.Max(0, items.Count * ItemHeight - Height);
|
||||
scrollOffset = System.Math.Max(0, System.Math.Min(maxScroll, scrollOffset - e.Delta));
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
private int GetIndexAt(int y) => (y + scrollOffset) / ItemHeight;
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
selectedBrush.Dispose();
|
||||
hoveredBrush.Dispose();
|
||||
accentBrush.Dispose();
|
||||
separatorPen.Dispose();
|
||||
emptyStateBrush.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
using OpenNest.Bending;
|
||||
using OpenNest.Geometry;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace OpenNest.Controls
|
||||
{
|
||||
public class FilterPanel : Panel
|
||||
{
|
||||
private readonly CollapsiblePanel layersPanel;
|
||||
private readonly CollapsiblePanel colorsPanel;
|
||||
private readonly CollapsiblePanel lineTypesPanel;
|
||||
private readonly CollapsiblePanel bendLinesPanel;
|
||||
|
||||
private readonly CheckedListBox layersList;
|
||||
private readonly CheckedListBox colorsList;
|
||||
private readonly CheckedListBox lineTypesList;
|
||||
private readonly ListBox bendLinesList;
|
||||
|
||||
private List<Entity> currentEntities;
|
||||
private List<Bend> currentBends;
|
||||
|
||||
public event EventHandler FilterChanged;
|
||||
public event EventHandler<int> BendLineSelected;
|
||||
public event EventHandler<int> BendLineRemoved;
|
||||
|
||||
public FilterPanel()
|
||||
{
|
||||
AutoScroll = true;
|
||||
BackColor = Color.White;
|
||||
|
||||
// Bend Lines
|
||||
bendLinesPanel = new CollapsiblePanel
|
||||
{
|
||||
HeaderText = "Bend Lines (0)",
|
||||
Dock = DockStyle.Top,
|
||||
ExpandedHeight = 120,
|
||||
IsExpanded = false
|
||||
};
|
||||
bendLinesList = new ListBox
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
BorderStyle = BorderStyle.None,
|
||||
Font = new Font("Segoe UI", 9f)
|
||||
};
|
||||
bendLinesList.SelectedIndexChanged += (s, e) =>
|
||||
BendLineSelected?.Invoke(this, bendLinesList.SelectedIndex);
|
||||
|
||||
var bendDeleteLink = new LinkLabel
|
||||
{
|
||||
Text = "Remove Selected",
|
||||
Dock = DockStyle.Bottom,
|
||||
Height = 20,
|
||||
Font = new Font("Segoe UI", 8f)
|
||||
};
|
||||
bendDeleteLink.LinkClicked += (s, e) =>
|
||||
{
|
||||
if (bendLinesList.SelectedIndex >= 0)
|
||||
BendLineRemoved?.Invoke(this, bendLinesList.SelectedIndex);
|
||||
};
|
||||
|
||||
bendLinesPanel.ContentPanel.Controls.Add(bendLinesList);
|
||||
bendLinesPanel.ContentPanel.Controls.Add(bendDeleteLink);
|
||||
|
||||
// Line Types
|
||||
lineTypesPanel = new CollapsiblePanel
|
||||
{
|
||||
HeaderText = "Line Types (0)",
|
||||
Dock = DockStyle.Top,
|
||||
ExpandedHeight = 100,
|
||||
IsExpanded = false
|
||||
};
|
||||
lineTypesList = CreateCheckedList();
|
||||
lineTypesPanel.ContentPanel.Controls.Add(lineTypesList);
|
||||
|
||||
// Colors
|
||||
colorsPanel = new CollapsiblePanel
|
||||
{
|
||||
HeaderText = "Colors (0)",
|
||||
Dock = DockStyle.Top,
|
||||
ExpandedHeight = 100,
|
||||
IsExpanded = false
|
||||
};
|
||||
colorsList = CreateCheckedList();
|
||||
colorsList.DrawMode = DrawMode.OwnerDrawFixed;
|
||||
colorsList.ItemHeight = 20;
|
||||
colorsList.DrawItem += ColorsList_DrawItem;
|
||||
colorsPanel.ContentPanel.Controls.Add(colorsList);
|
||||
|
||||
// Layers (always expanded)
|
||||
layersPanel = new CollapsiblePanel
|
||||
{
|
||||
HeaderText = "Layers",
|
||||
Dock = DockStyle.Top,
|
||||
ExpandedHeight = 160,
|
||||
IsExpanded = true
|
||||
};
|
||||
|
||||
var checkAllPanel = new Panel { Dock = DockStyle.Top, Height = 22 };
|
||||
var checkAll = new LinkLabel { Text = "All", AutoSize = true, Location = new Point(4, 2), Font = new Font("Segoe UI", 8f) };
|
||||
var uncheckAll = new LinkLabel { Text = "None", AutoSize = true, Location = new Point(30, 2), Font = new Font("Segoe UI", 8f) };
|
||||
checkAll.LinkClicked += (s, e) => SetAllChecked(layersList, true);
|
||||
uncheckAll.LinkClicked += (s, e) => SetAllChecked(layersList, false);
|
||||
checkAllPanel.Controls.AddRange(new Control[] { checkAll, uncheckAll });
|
||||
|
||||
layersList = CreateCheckedList();
|
||||
layersPanel.ContentPanel.Controls.Add(layersList);
|
||||
layersPanel.ContentPanel.Controls.Add(checkAllPanel);
|
||||
|
||||
// Add panels in reverse order (Dock.Top stacks top-down)
|
||||
Controls.Add(bendLinesPanel);
|
||||
Controls.Add(lineTypesPanel);
|
||||
Controls.Add(colorsPanel);
|
||||
Controls.Add(layersPanel);
|
||||
}
|
||||
|
||||
private CheckedListBox CreateCheckedList()
|
||||
{
|
||||
var list = new CheckedListBox
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
BorderStyle = BorderStyle.None,
|
||||
CheckOnClick = true,
|
||||
Font = new Font("Segoe UI", 9f)
|
||||
};
|
||||
list.ItemCheck += (s, e) =>
|
||||
BeginInvoke((Action)(() => FilterChanged?.Invoke(this, EventArgs.Empty)));
|
||||
return list;
|
||||
}
|
||||
|
||||
public void LoadItem(List<Entity> entities, List<Bend> bends)
|
||||
{
|
||||
currentEntities = entities;
|
||||
currentBends = bends;
|
||||
|
||||
// Layers
|
||||
layersList.Items.Clear();
|
||||
var layers = entities
|
||||
.Where(e => e.Layer != null)
|
||||
.Select(e => e.Layer.Name)
|
||||
.Distinct();
|
||||
foreach (var layer in layers)
|
||||
layersList.Items.Add(layer, true); // checked = visible
|
||||
|
||||
layersPanel.HeaderText = $"Layers ({layersList.Items.Count})";
|
||||
|
||||
// Colors
|
||||
colorsList.Items.Clear();
|
||||
var colors = entities
|
||||
.Select(e => e.Color.ToArgb())
|
||||
.Distinct()
|
||||
.Select(argb => new ColorItem(Color.FromArgb(argb)));
|
||||
foreach (var color in colors)
|
||||
colorsList.Items.Add(color, true); // checked = visible
|
||||
|
||||
colorsPanel.HeaderText = $"Colors ({colorsList.Items.Count})";
|
||||
|
||||
// Line Types
|
||||
lineTypesList.Items.Clear();
|
||||
var lineTypes = entities
|
||||
.Select(e => e.LineTypeName ?? "Continuous")
|
||||
.Distinct();
|
||||
foreach (var lt in lineTypes)
|
||||
lineTypesList.Items.Add(lt, true); // checked = visible
|
||||
|
||||
lineTypesPanel.HeaderText = $"Line Types ({lineTypesList.Items.Count})";
|
||||
|
||||
// Bend Lines
|
||||
bendLinesList.Items.Clear();
|
||||
if (bends != null)
|
||||
{
|
||||
foreach (var bend in bends)
|
||||
bendLinesList.Items.Add(bend.ToString());
|
||||
}
|
||||
|
||||
var bendCount = bends?.Count ?? 0;
|
||||
bendLinesPanel.HeaderText = $"Bend Lines ({bendCount})";
|
||||
bendLinesPanel.IsExpanded = bendCount > 0;
|
||||
}
|
||||
|
||||
public void ApplyFilters(List<Entity> entities)
|
||||
{
|
||||
var hiddenLayers = new HashSet<string>();
|
||||
for (var i = 0; i < layersList.Items.Count; i++)
|
||||
{
|
||||
if (!layersList.GetItemChecked(i))
|
||||
hiddenLayers.Add(layersList.Items[i].ToString());
|
||||
}
|
||||
|
||||
var hiddenColors = new HashSet<int>();
|
||||
for (var i = 0; i < colorsList.Items.Count; i++)
|
||||
{
|
||||
if (!colorsList.GetItemChecked(i))
|
||||
hiddenColors.Add(((ColorItem)colorsList.Items[i]).Argb);
|
||||
}
|
||||
|
||||
var hiddenLineTypes = new HashSet<string>();
|
||||
for (var i = 0; i < lineTypesList.Items.Count; i++)
|
||||
{
|
||||
if (!lineTypesList.GetItemChecked(i))
|
||||
hiddenLineTypes.Add(lineTypesList.Items[i].ToString());
|
||||
}
|
||||
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
var layerVisible = entity.Layer?.Name == null || !hiddenLayers.Contains(entity.Layer.Name);
|
||||
var colorVisible = !hiddenColors.Contains(entity.Color.ToArgb());
|
||||
var ltVisible = !hiddenLineTypes.Contains(entity.LineTypeName ?? "Continuous");
|
||||
|
||||
entity.IsVisible = layerVisible && colorVisible && ltVisible;
|
||||
if (entity.Layer != null)
|
||||
entity.Layer.IsVisible = !hiddenLayers.Contains(entity.Layer.Name);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetAllChecked(CheckedListBox list, bool isChecked)
|
||||
{
|
||||
for (var i = 0; i < list.Items.Count; i++)
|
||||
list.SetItemChecked(i, isChecked);
|
||||
}
|
||||
|
||||
private void ColorsList_DrawItem(object sender, DrawItemEventArgs e)
|
||||
{
|
||||
if (e.Index < 0) return;
|
||||
|
||||
e.DrawBackground();
|
||||
|
||||
var colorItem = (ColorItem)colorsList.Items[e.Index];
|
||||
var swatchRect = new Rectangle(e.Bounds.Left + 20, e.Bounds.Top + 2, 16, e.Bounds.Height - 4);
|
||||
|
||||
using (var brush = new SolidBrush(colorItem.Color))
|
||||
e.Graphics.FillRectangle(brush, swatchRect);
|
||||
e.Graphics.DrawRectangle(Pens.Gray, swatchRect);
|
||||
|
||||
e.DrawFocusRectangle();
|
||||
}
|
||||
}
|
||||
|
||||
public class ColorItem
|
||||
{
|
||||
public int Argb { get; }
|
||||
public Color Color { get; }
|
||||
|
||||
public ColorItem(Color color)
|
||||
{
|
||||
Color = color;
|
||||
Argb = color.ToArgb();
|
||||
}
|
||||
|
||||
public override string ToString() => $"#{Color.R:X2}{Color.G:X2}{Color.B:X2}";
|
||||
public override bool Equals(object obj) => obj is ColorItem other && Argb == other.Argb;
|
||||
public override int GetHashCode() => Argb;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using OpenNest.Actions;
|
||||
using OpenNest.Bending;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Collections;
|
||||
using OpenNest.Engine.Fill;
|
||||
@@ -134,6 +135,8 @@ namespace OpenNest.Controls
|
||||
|
||||
public bool DrawOffset { get; set; }
|
||||
|
||||
public bool ShowBendLines { get; set; }
|
||||
|
||||
public double OffsetTolerance { get; set; } = 0.001;
|
||||
|
||||
public bool FillParts { get; set; }
|
||||
@@ -578,6 +581,8 @@ namespace OpenNest.Controls
|
||||
continue;
|
||||
|
||||
part.Draw(g, (i + 1).ToString());
|
||||
DrawBendLines(g, part.BasePart);
|
||||
DrawGrainWarning(g, part.BasePart);
|
||||
}
|
||||
|
||||
// Draw preview parts — active (current strategy) takes precedence
|
||||
@@ -614,6 +619,74 @@ namespace OpenNest.Controls
|
||||
DrawRapids(g);
|
||||
}
|
||||
|
||||
private void DrawBendLines(Graphics g, Part part)
|
||||
{
|
||||
if (!ShowBendLines || part.BaseDrawing.Bends == null || part.BaseDrawing.Bends.Count == 0)
|
||||
return;
|
||||
|
||||
using var bendPen = new Pen(Color.Yellow, 1.5f)
|
||||
{
|
||||
DashStyle = System.Drawing.Drawing2D.DashStyle.Dash
|
||||
};
|
||||
|
||||
foreach (var bend in part.BaseDrawing.Bends)
|
||||
{
|
||||
var start = bend.StartPoint;
|
||||
var end = bend.EndPoint;
|
||||
|
||||
// Apply part rotation
|
||||
if (part.Rotation != 0)
|
||||
{
|
||||
start = start.Rotate(part.Rotation);
|
||||
end = end.Rotate(part.Rotation);
|
||||
}
|
||||
|
||||
// Apply part offset
|
||||
start = start + part.Location;
|
||||
end = end + part.Location;
|
||||
|
||||
var pt1 = PointWorldToGraph(start);
|
||||
var pt2 = PointWorldToGraph(end);
|
||||
|
||||
g.DrawLine(bendPen, pt1, pt2);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawGrainWarning(Graphics g, Part part)
|
||||
{
|
||||
if (!ShowBendLines || Plate == null || part.BaseDrawing.Bends == null || part.BaseDrawing.Bends.Count == 0)
|
||||
return;
|
||||
|
||||
var grainAngle = Plate.GrainAngle;
|
||||
var tolerance = Angle.ToRadians(5);
|
||||
|
||||
foreach (var bend in part.BaseDrawing.Bends)
|
||||
{
|
||||
var bendAngle = bend.LineAngle + part.Rotation;
|
||||
bendAngle = bendAngle % System.Math.PI;
|
||||
if (bendAngle < 0) bendAngle += System.Math.PI;
|
||||
|
||||
var grainNormalized = grainAngle % System.Math.PI;
|
||||
if (grainNormalized < 0) grainNormalized += System.Math.PI;
|
||||
|
||||
var diff = System.Math.Abs(bendAngle - grainNormalized);
|
||||
diff = System.Math.Min(diff, System.Math.PI - diff);
|
||||
|
||||
if (diff > tolerance)
|
||||
{
|
||||
var box = part.BaseDrawing.Program.BoundingBox();
|
||||
var location = part.Location;
|
||||
var pt1 = PointWorldToGraph(location);
|
||||
var pt2 = PointWorldToGraph(new Vector(
|
||||
location.X + box.Width, location.Y + box.Length));
|
||||
using var warnPen = new Pen(Color.FromArgb(180, 255, 140, 0), 2f);
|
||||
g.DrawRectangle(warnPen, pt1.X, pt2.Y,
|
||||
System.Math.Abs(pt2.X - pt1.X), System.Math.Abs(pt2.Y - pt1.Y));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawCutOffs(Graphics g)
|
||||
{
|
||||
if (Plate?.CutOffs == null || Plate.CutOffs.Count == 0)
|
||||
|
||||
+176
-223
@@ -1,298 +1,251 @@
|
||||
namespace OpenNest.Forms
|
||||
namespace OpenNest.Forms
|
||||
{
|
||||
partial class CadConverterForm
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle1 = new System.Windows.Forms.DataGridViewCellStyle();
|
||||
splitContainer1 = new System.Windows.Forms.SplitContainer();
|
||||
dataGridView1 = new System.Windows.Forms.DataGridView();
|
||||
splitContainer2 = new System.Windows.Forms.SplitContainer();
|
||||
tabControl1 = new System.Windows.Forms.TabControl();
|
||||
tabPage1 = new System.Windows.Forms.TabPage();
|
||||
checkedListBox1 = new System.Windows.Forms.CheckedListBox();
|
||||
tabPage2 = new System.Windows.Forms.TabPage();
|
||||
checkedListBox2 = new System.Windows.Forms.CheckedListBox();
|
||||
tabPage3 = new System.Windows.Forms.TabPage();
|
||||
checkedListBox3 = new System.Windows.Forms.CheckedListBox();
|
||||
sidebarPanel = new System.Windows.Forms.Panel();
|
||||
fileList = new OpenNest.Controls.FileListControl();
|
||||
filterPanel = new OpenNest.Controls.FilterPanel();
|
||||
splitterSidebar = new System.Windows.Forms.Splitter();
|
||||
entityView1 = new OpenNest.Controls.EntityView();
|
||||
detailBar = new System.Windows.Forms.FlowLayoutPanel();
|
||||
lblQty = new System.Windows.Forms.Label();
|
||||
numQuantity = new System.Windows.Forms.NumericUpDown();
|
||||
lblCust = new System.Windows.Forms.Label();
|
||||
txtCustomer = new System.Windows.Forms.TextBox();
|
||||
lblDimensions = new System.Windows.Forms.Label();
|
||||
lblEntityCount = new System.Windows.Forms.Label();
|
||||
btnSplit = new System.Windows.Forms.Button();
|
||||
lblDetect = new System.Windows.Forms.Label();
|
||||
cboBendDetector = new System.Windows.Forms.ComboBox();
|
||||
bottomPanel1 = new OpenNest.Controls.BottomPanel();
|
||||
cancelButton = new System.Windows.Forms.Button();
|
||||
acceptButton = new System.Windows.Forms.Button();
|
||||
((System.ComponentModel.ISupportInitialize)splitContainer1).BeginInit();
|
||||
splitContainer1.Panel1.SuspendLayout();
|
||||
splitContainer1.Panel2.SuspendLayout();
|
||||
splitContainer1.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)dataGridView1).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)splitContainer2).BeginInit();
|
||||
splitContainer2.Panel1.SuspendLayout();
|
||||
splitContainer2.Panel2.SuspendLayout();
|
||||
splitContainer2.SuspendLayout();
|
||||
tabControl1.SuspendLayout();
|
||||
tabPage1.SuspendLayout();
|
||||
tabPage2.SuspendLayout();
|
||||
tabPage3.SuspendLayout();
|
||||
|
||||
((System.ComponentModel.ISupportInitialize)numQuantity).BeginInit();
|
||||
sidebarPanel.SuspendLayout();
|
||||
bottomPanel1.SuspendLayout();
|
||||
SuspendLayout();
|
||||
|
||||
//
|
||||
// splitContainer1
|
||||
// sidebarPanel (Left dock — contains file list + filter panel)
|
||||
//
|
||||
splitContainer1.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
splitContainer1.Location = new System.Drawing.Point(0, 0);
|
||||
splitContainer1.Name = "splitContainer1";
|
||||
splitContainer1.Orientation = System.Windows.Forms.Orientation.Horizontal;
|
||||
sidebarPanel.Dock = System.Windows.Forms.DockStyle.Left;
|
||||
sidebarPanel.Name = "sidebarPanel";
|
||||
sidebarPanel.Size = new System.Drawing.Size(260, 670);
|
||||
sidebarPanel.Controls.Add(filterPanel);
|
||||
sidebarPanel.Controls.Add(fileList);
|
||||
|
||||
//
|
||||
// splitContainer1.Panel1
|
||||
// fileList (Top of sidebar)
|
||||
//
|
||||
splitContainer1.Panel1.Controls.Add(dataGridView1);
|
||||
fileList.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
fileList.AllowDrop = true;
|
||||
fileList.Name = "fileList";
|
||||
fileList.Size = new System.Drawing.Size(260, 300);
|
||||
|
||||
//
|
||||
// splitContainer1.Panel2
|
||||
// filterPanel (Fill remainder of sidebar)
|
||||
//
|
||||
splitContainer1.Panel2.Controls.Add(splitContainer2);
|
||||
splitContainer1.Size = new System.Drawing.Size(928, 643);
|
||||
splitContainer1.SplitterDistance = 302;
|
||||
splitContainer1.TabIndex = 0;
|
||||
filterPanel.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
filterPanel.Name = "filterPanel";
|
||||
filterPanel.Size = new System.Drawing.Size(260, 370);
|
||||
|
||||
//
|
||||
// dataGridView1
|
||||
// splitterSidebar (between sidebar and preview)
|
||||
//
|
||||
dataGridView1.AllowUserToAddRows = false;
|
||||
dataGridView1.AllowUserToResizeRows = false;
|
||||
dataGridView1.BackgroundColor = System.Drawing.Color.White;
|
||||
dataGridView1.BorderStyle = System.Windows.Forms.BorderStyle.None;
|
||||
dataGridView1.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
|
||||
dataGridViewCellStyle1.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft;
|
||||
dataGridViewCellStyle1.BackColor = System.Drawing.SystemColors.Window;
|
||||
dataGridViewCellStyle1.Font = new System.Drawing.Font("Microsoft Sans Serif", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 0);
|
||||
dataGridViewCellStyle1.ForeColor = System.Drawing.SystemColors.ControlText;
|
||||
dataGridViewCellStyle1.SelectionBackColor = System.Drawing.Color.FromArgb(255, 255, 192);
|
||||
dataGridViewCellStyle1.SelectionForeColor = System.Drawing.Color.Black;
|
||||
dataGridViewCellStyle1.WrapMode = System.Windows.Forms.DataGridViewTriState.False;
|
||||
dataGridView1.DefaultCellStyle = dataGridViewCellStyle1;
|
||||
dataGridView1.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
dataGridView1.GridColor = System.Drawing.Color.Gainsboro;
|
||||
dataGridView1.Location = new System.Drawing.Point(0, 0);
|
||||
dataGridView1.MultiSelect = false;
|
||||
dataGridView1.Name = "dataGridView1";
|
||||
dataGridView1.RowHeadersVisible = false;
|
||||
dataGridView1.RowTemplate.Height = 26;
|
||||
dataGridView1.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect;
|
||||
dataGridView1.Size = new System.Drawing.Size(928, 302);
|
||||
dataGridView1.TabIndex = 0;
|
||||
dataGridView1.DataBindingComplete += dataGridView1_DataBindingComplete;
|
||||
dataGridView1.SelectionChanged += dataGridView1_SelectionChanged;
|
||||
splitterSidebar.Location = new System.Drawing.Point(260, 0);
|
||||
splitterSidebar.Name = "splitterSidebar";
|
||||
splitterSidebar.Size = new System.Drawing.Size(3, 670);
|
||||
splitterSidebar.TabStop = false;
|
||||
|
||||
//
|
||||
// splitContainer2
|
||||
//
|
||||
splitContainer2.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
splitContainer2.Location = new System.Drawing.Point(0, 0);
|
||||
splitContainer2.Name = "splitContainer2";
|
||||
//
|
||||
// splitContainer2.Panel1
|
||||
//
|
||||
splitContainer2.Panel1.Controls.Add(tabControl1);
|
||||
//
|
||||
// splitContainer2.Panel2
|
||||
//
|
||||
splitContainer2.Panel2.Controls.Add(entityView1);
|
||||
splitContainer2.Size = new System.Drawing.Size(928, 337);
|
||||
splitContainer2.SplitterDistance = 309;
|
||||
splitContainer2.TabIndex = 0;
|
||||
//
|
||||
// tabControl1
|
||||
//
|
||||
tabControl1.Controls.Add(tabPage1);
|
||||
tabControl1.Controls.Add(tabPage2);
|
||||
tabControl1.Controls.Add(tabPage3);
|
||||
tabControl1.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
tabControl1.Location = new System.Drawing.Point(0, 0);
|
||||
tabControl1.Name = "tabControl1";
|
||||
tabControl1.SelectedIndex = 0;
|
||||
tabControl1.Size = new System.Drawing.Size(309, 337);
|
||||
tabControl1.TabIndex = 0;
|
||||
//
|
||||
// tabPage1
|
||||
//
|
||||
tabPage1.Controls.Add(checkedListBox1);
|
||||
tabPage1.Location = new System.Drawing.Point(4, 25);
|
||||
tabPage1.Name = "tabPage1";
|
||||
tabPage1.Padding = new System.Windows.Forms.Padding(3);
|
||||
tabPage1.Size = new System.Drawing.Size(301, 308);
|
||||
tabPage1.TabIndex = 0;
|
||||
tabPage1.Text = "Layers";
|
||||
tabPage1.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// checkedListBox1
|
||||
//
|
||||
checkedListBox1.BorderStyle = System.Windows.Forms.BorderStyle.None;
|
||||
checkedListBox1.CheckOnClick = true;
|
||||
checkedListBox1.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
checkedListBox1.FormattingEnabled = true;
|
||||
checkedListBox1.Location = new System.Drawing.Point(3, 3);
|
||||
checkedListBox1.Name = "checkedListBox1";
|
||||
checkedListBox1.Size = new System.Drawing.Size(295, 302);
|
||||
checkedListBox1.TabIndex = 0;
|
||||
checkedListBox1.SelectedIndexChanged += checkedListBox1_SelectedIndexChanged;
|
||||
//
|
||||
// tabPage2
|
||||
//
|
||||
tabPage2.Controls.Add(checkedListBox2);
|
||||
tabPage2.Location = new System.Drawing.Point(4, 24);
|
||||
tabPage2.Name = "tabPage2";
|
||||
tabPage2.Padding = new System.Windows.Forms.Padding(3);
|
||||
tabPage2.Size = new System.Drawing.Size(301, 309);
|
||||
tabPage2.TabIndex = 1;
|
||||
tabPage2.Text = "Colors";
|
||||
tabPage2.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// checkedListBox2
|
||||
//
|
||||
checkedListBox2.BorderStyle = System.Windows.Forms.BorderStyle.None;
|
||||
checkedListBox2.CheckOnClick = true;
|
||||
checkedListBox2.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
checkedListBox2.FormattingEnabled = true;
|
||||
checkedListBox2.Location = new System.Drawing.Point(3, 3);
|
||||
checkedListBox2.Name = "checkedListBox2";
|
||||
checkedListBox2.Size = new System.Drawing.Size(295, 303);
|
||||
checkedListBox2.TabIndex = 1;
|
||||
checkedListBox2.DrawItem += checkedListBox2_DrawItem;
|
||||
checkedListBox2.SelectedIndexChanged += checkedListBox2_SelectedIndexChanged;
|
||||
//
|
||||
// tabPage3
|
||||
//
|
||||
tabPage3.Controls.Add(checkedListBox3);
|
||||
tabPage3.Location = new System.Drawing.Point(4, 24);
|
||||
tabPage3.Name = "tabPage3";
|
||||
tabPage3.Padding = new System.Windows.Forms.Padding(3);
|
||||
tabPage3.Size = new System.Drawing.Size(301, 309);
|
||||
tabPage3.TabIndex = 2;
|
||||
tabPage3.Text = "Line Types";
|
||||
tabPage3.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// checkedListBox3
|
||||
//
|
||||
checkedListBox3.BorderStyle = System.Windows.Forms.BorderStyle.None;
|
||||
checkedListBox3.CheckOnClick = true;
|
||||
checkedListBox3.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
checkedListBox3.FormattingEnabled = true;
|
||||
checkedListBox3.Location = new System.Drawing.Point(3, 3);
|
||||
checkedListBox3.Name = "checkedListBox3";
|
||||
checkedListBox3.Size = new System.Drawing.Size(295, 303);
|
||||
checkedListBox3.TabIndex = 2;
|
||||
checkedListBox3.SelectedIndexChanged += checkedListBox3_SelectedIndexChanged;
|
||||
//
|
||||
// entityView1
|
||||
// entityView1 (Fill — main preview area)
|
||||
//
|
||||
entityView1.BackColor = System.Drawing.Color.FromArgb(33, 40, 48);
|
||||
entityView1.Cursor = System.Windows.Forms.Cursors.Cross;
|
||||
entityView1.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
entityView1.Location = new System.Drawing.Point(0, 0);
|
||||
entityView1.Name = "entityView1";
|
||||
entityView1.Size = new System.Drawing.Size(615, 337);
|
||||
entityView1.TabIndex = 0;
|
||||
entityView1.Text = "entityView1";
|
||||
entityView1.Size = new System.Drawing.Size(761, 634);
|
||||
|
||||
//
|
||||
// detailBar (Bottom of preview area)
|
||||
//
|
||||
detailBar.Dock = System.Windows.Forms.DockStyle.Bottom;
|
||||
detailBar.Name = "detailBar";
|
||||
detailBar.Size = new System.Drawing.Size(761, 36);
|
||||
detailBar.BackColor = System.Drawing.Color.FromArgb(245, 245, 245);
|
||||
detailBar.Padding = new System.Windows.Forms.Padding(4, 6, 4, 4);
|
||||
detailBar.WrapContents = false;
|
||||
|
||||
//
|
||||
// lblQty
|
||||
//
|
||||
lblQty.Text = "Qty:";
|
||||
lblQty.AutoSize = true;
|
||||
lblQty.Font = new System.Drawing.Font("Segoe UI", 9f);
|
||||
lblQty.Margin = new System.Windows.Forms.Padding(2, 3, 0, 0);
|
||||
|
||||
//
|
||||
// numQuantity
|
||||
//
|
||||
numQuantity.Size = new System.Drawing.Size(50, 24);
|
||||
numQuantity.Minimum = 1;
|
||||
numQuantity.Maximum = 9999;
|
||||
numQuantity.Value = 1;
|
||||
numQuantity.Font = new System.Drawing.Font("Segoe UI", 9f);
|
||||
numQuantity.Margin = new System.Windows.Forms.Padding(2, 0, 8, 0);
|
||||
|
||||
//
|
||||
// lblCust
|
||||
//
|
||||
lblCust.Text = "Customer:";
|
||||
lblCust.AutoSize = true;
|
||||
lblCust.Font = new System.Drawing.Font("Segoe UI", 9f);
|
||||
lblCust.Margin = new System.Windows.Forms.Padding(2, 3, 0, 0);
|
||||
|
||||
//
|
||||
// txtCustomer
|
||||
//
|
||||
txtCustomer.Size = new System.Drawing.Size(100, 24);
|
||||
txtCustomer.Font = new System.Drawing.Font("Segoe UI", 9f);
|
||||
txtCustomer.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
|
||||
txtCustomer.Margin = new System.Windows.Forms.Padding(2, 0, 8, 0);
|
||||
|
||||
//
|
||||
// lblDimensions
|
||||
//
|
||||
lblDimensions.AutoSize = true;
|
||||
lblDimensions.Font = new System.Drawing.Font("Segoe UI", 9f);
|
||||
lblDimensions.ForeColor = System.Drawing.Color.Gray;
|
||||
lblDimensions.Margin = new System.Windows.Forms.Padding(2, 3, 8, 0);
|
||||
|
||||
//
|
||||
// lblEntityCount
|
||||
//
|
||||
lblEntityCount.AutoSize = true;
|
||||
lblEntityCount.Font = new System.Drawing.Font("Segoe UI", 9f);
|
||||
lblEntityCount.ForeColor = System.Drawing.Color.Gray;
|
||||
lblEntityCount.Margin = new System.Windows.Forms.Padding(2, 3, 8, 0);
|
||||
|
||||
//
|
||||
// btnSplit
|
||||
//
|
||||
btnSplit.Text = "Split...";
|
||||
btnSplit.Size = new System.Drawing.Size(60, 24);
|
||||
btnSplit.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
|
||||
btnSplit.Font = new System.Drawing.Font("Segoe UI", 9f);
|
||||
btnSplit.Margin = new System.Windows.Forms.Padding(2, 0, 8, 0);
|
||||
|
||||
//
|
||||
// lblDetect
|
||||
//
|
||||
lblDetect.Text = "Bends:";
|
||||
lblDetect.AutoSize = true;
|
||||
lblDetect.Font = new System.Drawing.Font("Segoe UI", 9f);
|
||||
lblDetect.Margin = new System.Windows.Forms.Padding(2, 3, 0, 0);
|
||||
|
||||
//
|
||||
// cboBendDetector
|
||||
//
|
||||
cboBendDetector.Size = new System.Drawing.Size(90, 24);
|
||||
cboBendDetector.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
cboBendDetector.Font = new System.Drawing.Font("Segoe UI", 9f);
|
||||
cboBendDetector.Margin = new System.Windows.Forms.Padding(2, 0, 0, 0);
|
||||
|
||||
detailBar.Controls.AddRange(new System.Windows.Forms.Control[] {
|
||||
lblQty, numQuantity, lblCust, txtCustomer,
|
||||
lblDimensions, lblEntityCount, btnSplit,
|
||||
lblDetect, cboBendDetector
|
||||
});
|
||||
|
||||
//
|
||||
// bottomPanel1
|
||||
//
|
||||
bottomPanel1.Dock = System.Windows.Forms.DockStyle.Bottom;
|
||||
bottomPanel1.Name = "bottomPanel1";
|
||||
bottomPanel1.Size = new System.Drawing.Size(1024, 50);
|
||||
bottomPanel1.Controls.Add(cancelButton);
|
||||
bottomPanel1.Controls.Add(acceptButton);
|
||||
bottomPanel1.Dock = System.Windows.Forms.DockStyle.Bottom;
|
||||
bottomPanel1.Location = new System.Drawing.Point(0, 643);
|
||||
bottomPanel1.Name = "bottomPanel1";
|
||||
bottomPanel1.Size = new System.Drawing.Size(928, 50);
|
||||
bottomPanel1.TabIndex = 1;
|
||||
|
||||
//
|
||||
// cancelButton
|
||||
//
|
||||
cancelButton.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
|
||||
cancelButton.DialogResult = System.Windows.Forms.DialogResult.Cancel;
|
||||
cancelButton.Location = new System.Drawing.Point(826, 10);
|
||||
cancelButton.Name = "cancelButton";
|
||||
cancelButton.Location = new System.Drawing.Point(922, 10);
|
||||
cancelButton.Size = new System.Drawing.Size(90, 28);
|
||||
cancelButton.TabIndex = 1;
|
||||
cancelButton.Text = "Cancel";
|
||||
cancelButton.UseVisualStyleBackColor = true;
|
||||
cancelButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
|
||||
cancelButton.Font = new System.Drawing.Font("Segoe UI", 9f);
|
||||
|
||||
//
|
||||
// acceptButton
|
||||
//
|
||||
acceptButton.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
|
||||
acceptButton.DialogResult = System.Windows.Forms.DialogResult.OK;
|
||||
acceptButton.Location = new System.Drawing.Point(730, 10);
|
||||
acceptButton.Name = "acceptButton";
|
||||
acceptButton.Location = new System.Drawing.Point(826, 10);
|
||||
acceptButton.Size = new System.Drawing.Size(90, 28);
|
||||
acceptButton.TabIndex = 0;
|
||||
acceptButton.Text = "Accept";
|
||||
acceptButton.UseVisualStyleBackColor = true;
|
||||
acceptButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
|
||||
acceptButton.Font = new System.Drawing.Font("Segoe UI", 9f);
|
||||
|
||||
//
|
||||
// CadConverterForm
|
||||
// Add order: Fill last so it gets remaining space
|
||||
//
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
|
||||
ClientSize = new System.Drawing.Size(928, 693);
|
||||
Controls.Add(splitContainer1);
|
||||
ClientSize = new System.Drawing.Size(1024, 720);
|
||||
Controls.Add(entityView1);
|
||||
Controls.Add(detailBar);
|
||||
Controls.Add(splitterSidebar);
|
||||
Controls.Add(sidebarPanel);
|
||||
Controls.Add(bottomPanel1);
|
||||
Font = new System.Drawing.Font("Microsoft Sans Serif", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 0);
|
||||
Font = new System.Drawing.Font("Segoe UI", 9f);
|
||||
MinimizeBox = false;
|
||||
Name = "CadConverterForm";
|
||||
ShowIcon = false;
|
||||
ShowInTaskbar = false;
|
||||
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
Text = "CAD Converter";
|
||||
splitContainer1.Panel1.ResumeLayout(false);
|
||||
splitContainer1.Panel2.ResumeLayout(false);
|
||||
((System.ComponentModel.ISupportInitialize)splitContainer1).EndInit();
|
||||
splitContainer1.ResumeLayout(false);
|
||||
((System.ComponentModel.ISupportInitialize)dataGridView1).EndInit();
|
||||
splitContainer2.Panel1.ResumeLayout(false);
|
||||
splitContainer2.Panel2.ResumeLayout(false);
|
||||
((System.ComponentModel.ISupportInitialize)splitContainer2).EndInit();
|
||||
splitContainer2.ResumeLayout(false);
|
||||
tabControl1.ResumeLayout(false);
|
||||
tabPage1.ResumeLayout(false);
|
||||
tabPage2.ResumeLayout(false);
|
||||
tabPage3.ResumeLayout(false);
|
||||
AllowDrop = true;
|
||||
|
||||
((System.ComponentModel.ISupportInitialize)numQuantity).EndInit();
|
||||
sidebarPanel.ResumeLayout(false);
|
||||
bottomPanel1.ResumeLayout(false);
|
||||
ResumeLayout(false);
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.Panel sidebarPanel;
|
||||
private System.Windows.Forms.Splitter splitterSidebar;
|
||||
private Controls.FileListControl fileList;
|
||||
private Controls.FilterPanel filterPanel;
|
||||
private Controls.EntityView entityView1;
|
||||
private System.Windows.Forms.FlowLayoutPanel detailBar;
|
||||
private System.Windows.Forms.Label lblDimensions;
|
||||
private System.Windows.Forms.Label lblEntityCount;
|
||||
private System.Windows.Forms.NumericUpDown numQuantity;
|
||||
private System.Windows.Forms.TextBox txtCustomer;
|
||||
private System.Windows.Forms.Button btnSplit;
|
||||
private System.Windows.Forms.ComboBox cboBendDetector;
|
||||
private System.Windows.Forms.Label lblQty;
|
||||
private System.Windows.Forms.Label lblCust;
|
||||
private System.Windows.Forms.Label lblDetect;
|
||||
private Controls.BottomPanel bottomPanel1;
|
||||
private System.Windows.Forms.Button acceptButton;
|
||||
private System.Windows.Forms.Button cancelButton;
|
||||
private Controls.BottomPanel bottomPanel1;
|
||||
private System.Windows.Forms.SplitContainer splitContainer1;
|
||||
private System.Windows.Forms.DataGridView dataGridView1;
|
||||
private System.Windows.Forms.SplitContainer splitContainer2;
|
||||
private System.Windows.Forms.TabControl tabControl1;
|
||||
private System.Windows.Forms.TabPage tabPage1;
|
||||
private System.Windows.Forms.TabPage tabPage2;
|
||||
private Controls.EntityView entityView1;
|
||||
private System.Windows.Forms.CheckedListBox checkedListBox1;
|
||||
private System.Windows.Forms.TabPage tabPage3;
|
||||
private System.Windows.Forms.CheckedListBox checkedListBox2;
|
||||
private System.Windows.Forms.CheckedListBox checkedListBox3;
|
||||
}
|
||||
}
|
||||
+300
-219
@@ -1,10 +1,13 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Bending;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Controls;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.IO;
|
||||
using OpenNest.IO.Bending;
|
||||
using OpenNest.Properties;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -16,85 +19,294 @@ namespace OpenNest.Forms
|
||||
{
|
||||
public partial class CadConverterForm : Form
|
||||
{
|
||||
private static int colorIndex;
|
||||
|
||||
public CadConverterForm()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
Items = new BindingList<CadConverterItem>();
|
||||
dataGridView1.DataSource = Items;
|
||||
dataGridView1.DataError += dataGridView1_DataError;
|
||||
fileList.SelectedIndexChanged += OnFileSelected;
|
||||
filterPanel.FilterChanged += OnFilterChanged;
|
||||
filterPanel.BendLineSelected += OnBendLineSelected;
|
||||
filterPanel.BendLineRemoved += OnBendLineRemoved;
|
||||
btnSplit.Click += OnSplitClicked;
|
||||
numQuantity.ValueChanged += OnQuantityChanged;
|
||||
txtCustomer.TextChanged += OnCustomerChanged;
|
||||
cboBendDetector.SelectedIndexChanged += OnBendDetectorChanged;
|
||||
|
||||
// Populate bend detector dropdown
|
||||
cboBendDetector.Items.Add("Auto");
|
||||
foreach (var detector in BendDetectorRegistry.Detectors)
|
||||
cboBendDetector.Items.Add(detector.Name);
|
||||
cboBendDetector.SelectedIndex = 0;
|
||||
|
||||
// Drag & drop
|
||||
AllowDrop = true;
|
||||
DragEnter += OnDragEnter;
|
||||
DragDrop += OnDragDrop;
|
||||
}
|
||||
|
||||
private BindingList<CadConverterItem> Items { get; set; }
|
||||
private FileListItem CurrentItem => fileList.SelectedItem;
|
||||
|
||||
private void SetRotation(Shape shape, RotationType rotation)
|
||||
#region File Import
|
||||
|
||||
public void AddFile(string file) => AddFile(file, 0, null);
|
||||
|
||||
private void AddFile(string file, int detectorIndex, string detectorName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var dir = shape.ToPolygon(3).RotationDirection();
|
||||
var importer = new DxfImporter();
|
||||
importer.SplinePrecision = Settings.Default.ImportSplinePrecision;
|
||||
var result = importer.Import(file);
|
||||
|
||||
if (dir != rotation)
|
||||
shape.Reverse();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
if (result.Entities.Count == 0)
|
||||
return;
|
||||
|
||||
private void LoadItem(CadConverterItem item)
|
||||
// Compute bounds
|
||||
var bounds = result.Entities.GetBoundingBox();
|
||||
|
||||
// Detect bends (detectorIndex/Name captured on UI thread)
|
||||
var bends = new List<Bend>();
|
||||
if (result.Document != null)
|
||||
{
|
||||
bends = detectorIndex == 0
|
||||
? BendDetectorRegistry.AutoDetect(result.Document)
|
||||
: BendDetectorRegistry.GetByName(detectorName)
|
||||
?.DetectBends(result.Document)
|
||||
?? new List<Bend>();
|
||||
}
|
||||
|
||||
var item = new FileListItem
|
||||
{
|
||||
Name = Path.GetFileNameWithoutExtension(file),
|
||||
Entities = result.Entities,
|
||||
Path = file,
|
||||
Quantity = 1,
|
||||
Customer = string.Empty,
|
||||
Bends = bends,
|
||||
Bounds = bounds,
|
||||
EntityCount = result.Entities.Count
|
||||
};
|
||||
|
||||
if (InvokeRequired)
|
||||
BeginInvoke((Action)(() => fileList.AddItem(item)));
|
||||
else
|
||||
fileList.AddItem(item);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"Error importing \"{file}\": {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public void AddFiles(IEnumerable<string> files)
|
||||
{
|
||||
var fileArray = files.ToArray();
|
||||
// Capture UI state on main thread before entering parallel loop
|
||||
var detectorIndex = cboBendDetector.SelectedIndex;
|
||||
var detectorName = cboBendDetector.SelectedItem?.ToString();
|
||||
|
||||
System.Threading.Tasks.Task.Run(() =>
|
||||
{
|
||||
Parallel.ForEach(fileArray, file => AddFile(file, detectorIndex, detectorName));
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
private void OnFileSelected(object sender, int index)
|
||||
{
|
||||
var item = CurrentItem;
|
||||
if (item == null)
|
||||
{
|
||||
ClearDetailBar();
|
||||
return;
|
||||
}
|
||||
|
||||
LoadItem(item);
|
||||
}
|
||||
|
||||
private void LoadItem(FileListItem item)
|
||||
{
|
||||
entityView1.ClearPenCache();
|
||||
entityView1.Entities.Clear();
|
||||
entityView1.Entities.AddRange(item.Entities);
|
||||
entityView1.ZoomToFit();
|
||||
entityView1.Bends = item.Bends ?? new List<Bend>();
|
||||
|
||||
item.Entities.ForEach(e => e.IsVisible = true);
|
||||
if (item.Entities.Any(e => e.Layer != null))
|
||||
item.Entities.ForEach(e => e.Layer.IsVisible = true);
|
||||
|
||||
// Layers
|
||||
checkedListBox1.Items.Clear();
|
||||
filterPanel.LoadItem(item.Entities, item.Bends);
|
||||
|
||||
var layers = item.Entities
|
||||
.Where(e => e.Layer != null)
|
||||
.Select(e => e.Layer.Name)
|
||||
.ToList()
|
||||
.Distinct();
|
||||
numQuantity.Value = item.Quantity;
|
||||
txtCustomer.Text = item.Customer ?? "";
|
||||
|
||||
foreach (var layer in layers)
|
||||
checkedListBox1.Items.Add(layer, true);
|
||||
var bounds = item.Bounds;
|
||||
lblDimensions.Text = bounds != null
|
||||
? $"{bounds.Width:0.#} x {bounds.Length:0.#}"
|
||||
: "";
|
||||
lblEntityCount.Text = $"{item.EntityCount} entities";
|
||||
|
||||
// Colors
|
||||
checkedListBox2.Items.Clear();
|
||||
|
||||
var colors = item.Entities
|
||||
.Select(e => e.Color.ToArgb())
|
||||
.Distinct()
|
||||
.Select(argb => new ColorItem(Color.FromArgb(argb)));
|
||||
|
||||
foreach (var color in colors)
|
||||
checkedListBox2.Items.Add(color, false);
|
||||
|
||||
// Line Types
|
||||
checkedListBox3.Items.Clear();
|
||||
|
||||
var lineTypes = item.Entities
|
||||
.Select(e => e.LineTypeName ?? "Continuous")
|
||||
.Distinct();
|
||||
|
||||
foreach (var lineType in lineTypes)
|
||||
checkedListBox3.Items.Add(lineType, false);
|
||||
entityView1.ZoomToFit();
|
||||
}
|
||||
|
||||
private static int colorIndex;
|
||||
|
||||
private static Color GetNextColor()
|
||||
private void ClearDetailBar()
|
||||
{
|
||||
var color = ColorScheme.PartColors[colorIndex % ColorScheme.PartColors.Length];
|
||||
colorIndex++;
|
||||
return color;
|
||||
numQuantity.Value = 1;
|
||||
txtCustomer.Text = "";
|
||||
lblDimensions.Text = "";
|
||||
lblEntityCount.Text = "";
|
||||
entityView1.Entities.Clear();
|
||||
entityView1.Invalidate();
|
||||
}
|
||||
|
||||
private void OnFilterChanged(object sender, EventArgs e)
|
||||
{
|
||||
var item = CurrentItem;
|
||||
if (item == null) return;
|
||||
|
||||
filterPanel.ApplyFilters(item.Entities);
|
||||
entityView1.Invalidate();
|
||||
}
|
||||
|
||||
private void OnBendLineSelected(object sender, int index)
|
||||
{
|
||||
entityView1.SelectedBendIndex = index;
|
||||
entityView1.Invalidate();
|
||||
}
|
||||
|
||||
private void OnBendLineRemoved(object sender, int index)
|
||||
{
|
||||
var item = CurrentItem;
|
||||
if (item == null || index < 0 || index >= item.Bends.Count) return;
|
||||
|
||||
item.Bends.RemoveAt(index);
|
||||
entityView1.Bends = item.Bends;
|
||||
entityView1.SelectedBendIndex = -1;
|
||||
filterPanel.LoadItem(item.Entities, item.Bends);
|
||||
entityView1.Invalidate();
|
||||
}
|
||||
|
||||
private void OnQuantityChanged(object sender, EventArgs e)
|
||||
{
|
||||
var item = CurrentItem;
|
||||
if (item == null) return;
|
||||
|
||||
item.Quantity = (int)numQuantity.Value;
|
||||
fileList.Invalidate();
|
||||
}
|
||||
|
||||
private void OnCustomerChanged(object sender, EventArgs e)
|
||||
{
|
||||
var item = CurrentItem;
|
||||
if (item != null)
|
||||
item.Customer = txtCustomer.Text;
|
||||
}
|
||||
|
||||
private void OnBendDetectorChanged(object sender, EventArgs e)
|
||||
{
|
||||
// Re-run bend detection on current item if it has a document
|
||||
// For now, bend detection only runs at import time
|
||||
}
|
||||
|
||||
private void OnSplitClicked(object sender, EventArgs e)
|
||||
{
|
||||
var item = CurrentItem;
|
||||
if (item == null) return;
|
||||
|
||||
var entities = item.Entities.Where(en => en.Layer.IsVisible && en.IsVisible).ToList();
|
||||
if (entities.Count == 0) return;
|
||||
|
||||
var shape = new ShapeProfile(entities);
|
||||
SetRotation(shape.Perimeter, RotationType.CW);
|
||||
foreach (var cutout in shape.Cutouts)
|
||||
SetRotation(cutout, RotationType.CCW);
|
||||
|
||||
var drawEntities = new List<Entity>();
|
||||
drawEntities.AddRange(shape.Perimeter.Entities);
|
||||
shape.Cutouts.ForEach(c => drawEntities.AddRange(c.Entities));
|
||||
|
||||
var pgm = ConvertGeometry.ToProgram(drawEntities);
|
||||
if (pgm.Codes.Count > 0 && pgm[0].Type == CodeType.RapidMove)
|
||||
{
|
||||
var rapid = (RapidMove)pgm[0];
|
||||
pgm.Offset(-rapid.EndPoint);
|
||||
pgm.Codes.RemoveAt(0);
|
||||
}
|
||||
|
||||
var drawing = new Drawing(item.Name, pgm);
|
||||
|
||||
using var form = new SplitDrawingForm(drawing);
|
||||
if (form.ShowDialog(this) != DialogResult.OK || form.ResultDrawings?.Count <= 1)
|
||||
return;
|
||||
|
||||
// Write split DXF files and re-import
|
||||
var sourceDir = Path.GetDirectoryName(item.Path);
|
||||
var baseName = Path.GetFileNameWithoutExtension(item.Path);
|
||||
var writableDir = Directory.Exists(sourceDir) && IsDirectoryWritable(sourceDir)
|
||||
? sourceDir
|
||||
: Path.GetTempPath();
|
||||
|
||||
var index = fileList.SelectedIndex;
|
||||
var newItems = new List<string>();
|
||||
|
||||
var splitWriter = new SplitDxfWriter();
|
||||
|
||||
for (var i = 0; i < form.ResultDrawings.Count; i++)
|
||||
{
|
||||
var splitDrawing = form.ResultDrawings[i];
|
||||
|
||||
// Assign bends from the source item — spatial filtering is a future enhancement
|
||||
splitDrawing.Bends.AddRange(item.Bends);
|
||||
|
||||
var splitName = $"{baseName}_split{i + 1}.dxf";
|
||||
var splitPath = GetUniquePath(Path.Combine(writableDir, splitName));
|
||||
|
||||
splitWriter.Write(splitPath, splitDrawing);
|
||||
newItems.Add(splitPath);
|
||||
}
|
||||
|
||||
// Remove original and add split files
|
||||
fileList.RemoveAt(index);
|
||||
foreach (var path in newItems)
|
||||
AddFile(path);
|
||||
|
||||
if (writableDir != sourceDir)
|
||||
MessageBox.Show($"Split files written to: {writableDir}", "Split Output",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
|
||||
private void OnDragEnter(object sender, DragEventArgs e)
|
||||
{
|
||||
if (e.Data.GetDataPresent(DataFormats.FileDrop))
|
||||
e.Effect = DragDropEffects.Copy;
|
||||
}
|
||||
|
||||
private void OnDragDrop(object sender, DragEventArgs e)
|
||||
{
|
||||
if (e.Data.GetDataPresent(DataFormats.FileDrop))
|
||||
{
|
||||
var files = (string[])e.Data.GetData(DataFormats.FileDrop);
|
||||
var dxfFiles = files.Where(f =>
|
||||
f.EndsWith(".dxf", StringComparison.OrdinalIgnoreCase)).ToArray();
|
||||
if (dxfFiles.Length > 0)
|
||||
AddFiles(dxfFiles);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Output
|
||||
|
||||
public List<Drawing> GetDrawings()
|
||||
{
|
||||
var drawings = new List<Drawing>();
|
||||
|
||||
foreach (var item in Items)
|
||||
foreach (var item in fileList.Items)
|
||||
{
|
||||
var entities = item.Entities.Where(e => e.Layer.IsVisible && e.IsVisible).ToList();
|
||||
|
||||
@@ -107,6 +319,10 @@ namespace OpenNest.Forms
|
||||
drawing.Source.Path = item.Path;
|
||||
drawing.Quantity.Required = item.Quantity;
|
||||
|
||||
// Copy bends
|
||||
if (item.Bends != null)
|
||||
drawing.Bends.AddRange(item.Bends);
|
||||
|
||||
var shape = new ShapeProfile(entities);
|
||||
|
||||
SetRotation(shape.Perimeter, RotationType.CW);
|
||||
@@ -116,7 +332,6 @@ namespace OpenNest.Forms
|
||||
|
||||
entities = new List<Entity>();
|
||||
entities.AddRange(shape.Perimeter.Entities);
|
||||
|
||||
shape.Cutouts.ForEach(cutout => entities.AddRange(cutout.Entities));
|
||||
|
||||
var pgm = ConvertGeometry.ToProgram(entities);
|
||||
@@ -125,9 +340,7 @@ namespace OpenNest.Forms
|
||||
if (firstCode.Type == CodeType.RapidMove)
|
||||
{
|
||||
var rapid = (RapidMove)firstCode;
|
||||
|
||||
drawing.Source.Offset = rapid.EndPoint;
|
||||
|
||||
pgm.Offset(-rapid.EndPoint);
|
||||
pgm.Codes.RemoveAt(0);
|
||||
}
|
||||
@@ -141,189 +354,57 @@ namespace OpenNest.Forms
|
||||
return drawings;
|
||||
}
|
||||
|
||||
private CadConverterItem CurrentItem
|
||||
{
|
||||
get
|
||||
{
|
||||
return dataGridView1.SelectedRows.Count != 0
|
||||
? Items[dataGridView1.SelectedRows[0].Index]
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
public void AddFile(string file)
|
||||
{
|
||||
var importer = new DxfImporter();
|
||||
importer.SplinePrecision = Settings.Default.ImportSplinePrecision;
|
||||
|
||||
var entities = new List<Entity>();
|
||||
|
||||
if (!importer.GetGeometry(file, out entities))
|
||||
{
|
||||
MessageBox.Show("Failed to import file \"" + file + "\"");
|
||||
return;
|
||||
}
|
||||
|
||||
lock (Items)
|
||||
{
|
||||
Items.Add(new CadConverterItem
|
||||
{
|
||||
Name = Path.GetFileNameWithoutExtension(file),
|
||||
Entities = entities,
|
||||
Path = file,
|
||||
Quantity = 1,
|
||||
Customer = string.Empty
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void AddFiles(IEnumerable<string> files)
|
||||
{
|
||||
Parallel.ForEach(files, AddFile);
|
||||
}
|
||||
|
||||
private void dataGridView1_DataError(object sender, DataGridViewDataErrorEventArgs e)
|
||||
{
|
||||
MessageBox.Show(e.Exception.Message);
|
||||
}
|
||||
|
||||
private void dataGridView1_DataBindingComplete(object sender, DataGridViewBindingCompleteEventArgs e)
|
||||
{
|
||||
dataGridView1.AutoResizeColumns(DataGridViewAutoSizeColumnsMode.AllCells);
|
||||
}
|
||||
|
||||
private void dataGridView1_SelectionChanged(object sender, System.EventArgs e)
|
||||
{
|
||||
var currentItem = CurrentItem;
|
||||
|
||||
if (currentItem != null)
|
||||
LoadItem(currentItem);
|
||||
}
|
||||
|
||||
#region Colors
|
||||
|
||||
private static Color[] Colors = new Color[]
|
||||
{
|
||||
Color.FromArgb(160, 255, 255),
|
||||
Color.FromArgb(160, 255, 160),
|
||||
Color.FromArgb(160, 160, 255),
|
||||
Color.FromArgb(255, 255, 160),
|
||||
Color.FromArgb(255, 160, 255),
|
||||
Color.FromArgb(255, 160, 160),
|
||||
|
||||
Color.FromArgb(200, 255, 255),
|
||||
Color.FromArgb(200, 255, 200),
|
||||
Color.FromArgb(200, 200, 255),
|
||||
Color.FromArgb(255, 255, 200),
|
||||
Color.FromArgb(255, 200, 255),
|
||||
Color.FromArgb(255, 200, 200),
|
||||
};
|
||||
|
||||
#endregion
|
||||
|
||||
private void checkedListBox1_SelectedIndexChanged(object sender, System.EventArgs e)
|
||||
{
|
||||
var index = checkedListBox1.SelectedIndex;
|
||||
var layerName = checkedListBox1.Items[index].ToString();
|
||||
var isVisible = checkedListBox1.CheckedItems.Contains(layerName);
|
||||
#region Helpers
|
||||
|
||||
CurrentItem.Entities.ForEach(entity =>
|
||||
private static void SetRotation(Shape shape, RotationType rotation)
|
||||
{
|
||||
if (entity.Layer.Name == layerName)
|
||||
entity.Layer.IsVisible = isVisible;
|
||||
});
|
||||
|
||||
entityView1.Invalidate();
|
||||
try
|
||||
{
|
||||
var dir = shape.ToPolygon(3).RotationDirection();
|
||||
if (dir != rotation) shape.Reverse();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private void checkedListBox2_SelectedIndexChanged(object sender, System.EventArgs e)
|
||||
private static Color GetNextColor()
|
||||
{
|
||||
UpdateEntityVisibility();
|
||||
var color = ColorScheme.PartColors[colorIndex % ColorScheme.PartColors.Length];
|
||||
colorIndex++;
|
||||
return color;
|
||||
}
|
||||
|
||||
private void checkedListBox3_SelectedIndexChanged(object sender, System.EventArgs e)
|
||||
private static bool IsDirectoryWritable(string path)
|
||||
{
|
||||
UpdateEntityVisibility();
|
||||
try
|
||||
{
|
||||
var testFile = Path.Combine(path, $".writetest_{Guid.NewGuid()}");
|
||||
File.WriteAllText(testFile, "");
|
||||
File.Delete(testFile);
|
||||
return true;
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
private void UpdateEntityVisibility()
|
||||
private static string GetUniquePath(string path)
|
||||
{
|
||||
var item = CurrentItem;
|
||||
if (item == null) return;
|
||||
if (!File.Exists(path)) return path;
|
||||
|
||||
var checkedColors = new HashSet<int>();
|
||||
for (var i = 0; i < checkedListBox2.Items.Count; i++)
|
||||
var dir = Path.GetDirectoryName(path);
|
||||
var name = Path.GetFileNameWithoutExtension(path);
|
||||
var ext = Path.GetExtension(path);
|
||||
var counter = 2;
|
||||
|
||||
while (File.Exists(path))
|
||||
{
|
||||
if (checkedListBox2.GetItemChecked(i))
|
||||
checkedColors.Add(((ColorItem)checkedListBox2.Items[i]).Argb);
|
||||
path = Path.Combine(dir, $"{name}_{counter}{ext}");
|
||||
counter++;
|
||||
}
|
||||
|
||||
var checkedLineTypes = new HashSet<string>();
|
||||
for (var i = 0; i < checkedListBox3.Items.Count; i++)
|
||||
{
|
||||
if (checkedListBox3.GetItemChecked(i))
|
||||
checkedLineTypes.Add(checkedListBox3.Items[i].ToString());
|
||||
return path;
|
||||
}
|
||||
|
||||
item.Entities.ForEach(entity =>
|
||||
{
|
||||
entity.IsVisible = !checkedColors.Contains(entity.Color.ToArgb())
|
||||
&& !checkedLineTypes.Contains(entity.LineTypeName ?? "Continuous");
|
||||
});
|
||||
|
||||
entityView1.Invalidate();
|
||||
#endregion
|
||||
}
|
||||
|
||||
private void checkedListBox2_DrawItem(object sender, DrawItemEventArgs e)
|
||||
{
|
||||
if (e.Index < 0) return;
|
||||
|
||||
e.DrawBackground();
|
||||
|
||||
var colorItem = (ColorItem)checkedListBox2.Items[e.Index];
|
||||
var swatchRect = new Rectangle(e.Bounds.Left + 20, e.Bounds.Top + 2, 16, e.Bounds.Height - 4);
|
||||
|
||||
using (var brush = new SolidBrush(colorItem.Color))
|
||||
e.Graphics.FillRectangle(brush, swatchRect);
|
||||
|
||||
e.Graphics.DrawRectangle(Pens.Gray, swatchRect);
|
||||
|
||||
var textRect = new Rectangle(swatchRect.Right + 4, e.Bounds.Top, e.Bounds.Width - swatchRect.Right - 4, e.Bounds.Height);
|
||||
TextRenderer.DrawText(e.Graphics, colorItem.ToString(), e.Font, textRect, e.ForeColor, TextFormatFlags.VerticalCenter);
|
||||
|
||||
e.DrawFocusRectangle();
|
||||
}
|
||||
}
|
||||
|
||||
class CadConverterItem
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public string Customer { get; set; }
|
||||
|
||||
public int Quantity { get; set; }
|
||||
|
||||
[ReadOnly(true)]
|
||||
public string Path { get; set; }
|
||||
|
||||
[Browsable(false)]
|
||||
public List<Entity> Entities { get; set; }
|
||||
}
|
||||
|
||||
class ColorItem
|
||||
{
|
||||
public int Argb { get; }
|
||||
public Color Color { get; }
|
||||
|
||||
public ColorItem(Color color)
|
||||
{
|
||||
Color = color;
|
||||
Argb = color.ToArgb();
|
||||
}
|
||||
|
||||
public override string ToString() => $"RGB({Color.R}, {Color.G}, {Color.B})";
|
||||
public override bool Equals(object obj) => obj is ColorItem other && Argb == other.Argb;
|
||||
public override int GetHashCode() => Argb;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -479,6 +479,12 @@ namespace OpenNest.Forms
|
||||
PlateView.Invalidate();
|
||||
}
|
||||
|
||||
public void ToggleBendLines()
|
||||
{
|
||||
PlateView.ShowBendLines = !PlateView.ShowBendLines;
|
||||
PlateView.Invalidate();
|
||||
}
|
||||
|
||||
public void ToggleDrawOffset()
|
||||
{
|
||||
PlateView.DrawOffset = !PlateView.DrawOffset;
|
||||
|
||||
+737
@@ -0,0 +1,737 @@
|
||||
namespace OpenNest.Forms
|
||||
{
|
||||
partial class SplitDrawingForm
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
pnlSettings = new System.Windows.Forms.Panel();
|
||||
pnlButtons = new System.Windows.Forms.Panel();
|
||||
btnCancel = new System.Windows.Forms.Button();
|
||||
btnOK = new System.Windows.Forms.Button();
|
||||
grpSpikeParams = new System.Windows.Forms.GroupBox();
|
||||
nudSpikePairCount = new System.Windows.Forms.NumericUpDown();
|
||||
lblSpikePairCount = new System.Windows.Forms.Label();
|
||||
nudSpikeWeldGap = new System.Windows.Forms.NumericUpDown();
|
||||
lblSpikeWeldGap = new System.Windows.Forms.Label();
|
||||
nudGrooveDepth = new System.Windows.Forms.NumericUpDown();
|
||||
lblGrooveDepth = new System.Windows.Forms.Label();
|
||||
nudSpikeAngle = new System.Windows.Forms.NumericUpDown();
|
||||
lblSpikeAngle = new System.Windows.Forms.Label();
|
||||
grpTabParams = new System.Windows.Forms.GroupBox();
|
||||
nudTabCount = new System.Windows.Forms.NumericUpDown();
|
||||
lblTabCount = new System.Windows.Forms.Label();
|
||||
nudTabHeight = new System.Windows.Forms.NumericUpDown();
|
||||
lblTabHeight = new System.Windows.Forms.Label();
|
||||
nudTabWidth = new System.Windows.Forms.NumericUpDown();
|
||||
lblTabWidth = new System.Windows.Forms.Label();
|
||||
grpType = new System.Windows.Forms.GroupBox();
|
||||
radSpike = new System.Windows.Forms.RadioButton();
|
||||
radTabs = new System.Windows.Forms.RadioButton();
|
||||
radStraight = new System.Windows.Forms.RadioButton();
|
||||
grpByCount = new System.Windows.Forms.GroupBox();
|
||||
nudVerticalPieces = new System.Windows.Forms.NumericUpDown();
|
||||
lblVerticalPieces = new System.Windows.Forms.Label();
|
||||
nudHorizontalPieces = new System.Windows.Forms.NumericUpDown();
|
||||
lblHorizontalPieces = new System.Windows.Forms.Label();
|
||||
grpAutoFit = new System.Windows.Forms.GroupBox();
|
||||
cboSplitAxis = new System.Windows.Forms.ComboBox();
|
||||
lblSplitAxis = new System.Windows.Forms.Label();
|
||||
nudEdgeSpacing = new System.Windows.Forms.NumericUpDown();
|
||||
lblEdgeSpacing = new System.Windows.Forms.Label();
|
||||
nudPlateHeight = new System.Windows.Forms.NumericUpDown();
|
||||
lblPlateHeight = new System.Windows.Forms.Label();
|
||||
nudPlateWidth = new System.Windows.Forms.NumericUpDown();
|
||||
lblPlateWidth = new System.Windows.Forms.Label();
|
||||
grpMethod = new System.Windows.Forms.GroupBox();
|
||||
radByCount = new System.Windows.Forms.RadioButton();
|
||||
radFitToPlate = new System.Windows.Forms.RadioButton();
|
||||
radManual = new System.Windows.Forms.RadioButton();
|
||||
pnlPreview = new SplitPreview();
|
||||
toolStrip = new System.Windows.Forms.ToolStrip();
|
||||
btnAddLine = new System.Windows.Forms.ToolStripButton();
|
||||
btnDeleteLine = new System.Windows.Forms.ToolStripButton();
|
||||
statusStrip = new System.Windows.Forms.StatusStrip();
|
||||
lblStatus = new System.Windows.Forms.ToolStripStatusLabel();
|
||||
lblCursor = new System.Windows.Forms.ToolStripStatusLabel();
|
||||
pnlSettings.SuspendLayout();
|
||||
grpSpikeParams.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)nudSpikePairCount).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)nudSpikeWeldGap).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)nudGrooveDepth).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)nudSpikeAngle).BeginInit();
|
||||
grpTabParams.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)nudTabCount).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)nudTabHeight).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)nudTabWidth).BeginInit();
|
||||
grpType.SuspendLayout();
|
||||
grpByCount.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)nudVerticalPieces).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)nudHorizontalPieces).BeginInit();
|
||||
grpAutoFit.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)nudEdgeSpacing).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)nudPlateHeight).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)nudPlateWidth).BeginInit();
|
||||
grpMethod.SuspendLayout();
|
||||
toolStrip.SuspendLayout();
|
||||
statusStrip.SuspendLayout();
|
||||
SuspendLayout();
|
||||
//
|
||||
// pnlSettings
|
||||
//
|
||||
pnlSettings.AutoScroll = true;
|
||||
pnlSettings.Controls.Add(grpSpikeParams);
|
||||
pnlSettings.Controls.Add(grpTabParams);
|
||||
pnlSettings.Controls.Add(grpType);
|
||||
pnlSettings.Controls.Add(grpByCount);
|
||||
pnlSettings.Controls.Add(grpAutoFit);
|
||||
pnlSettings.Controls.Add(grpMethod);
|
||||
pnlSettings.Controls.Add(pnlButtons);
|
||||
pnlSettings.Dock = System.Windows.Forms.DockStyle.Right;
|
||||
pnlSettings.Location = new System.Drawing.Point(647, 25);
|
||||
pnlSettings.Name = "pnlSettings";
|
||||
pnlSettings.Padding = new System.Windows.Forms.Padding(6);
|
||||
pnlSettings.Size = new System.Drawing.Size(220, 611);
|
||||
pnlSettings.TabIndex = 2;
|
||||
//
|
||||
// pnlButtons
|
||||
//
|
||||
pnlButtons.Controls.Add(btnOK);
|
||||
pnlButtons.Controls.Add(btnCancel);
|
||||
pnlButtons.Dock = System.Windows.Forms.DockStyle.Bottom;
|
||||
pnlButtons.Name = "pnlButtons";
|
||||
pnlButtons.Size = new System.Drawing.Size(208, 40);
|
||||
pnlButtons.TabIndex = 8;
|
||||
//
|
||||
// btnCancel
|
||||
//
|
||||
btnCancel.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
|
||||
btnCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel;
|
||||
btnCancel.Location = new System.Drawing.Point(110, 6);
|
||||
btnCancel.Name = "btnCancel";
|
||||
btnCancel.Size = new System.Drawing.Size(80, 28);
|
||||
btnCancel.TabIndex = 7;
|
||||
btnCancel.Text = "Cancel";
|
||||
btnCancel.UseVisualStyleBackColor = true;
|
||||
btnCancel.Click += OnCancel;
|
||||
//
|
||||
// btnOK
|
||||
//
|
||||
btnOK.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
|
||||
btnOK.Location = new System.Drawing.Point(20, 6);
|
||||
btnOK.Name = "btnOK";
|
||||
btnOK.Size = new System.Drawing.Size(80, 28);
|
||||
btnOK.TabIndex = 6;
|
||||
btnOK.Text = "OK";
|
||||
btnOK.UseVisualStyleBackColor = true;
|
||||
btnOK.Click += OnOK;
|
||||
//
|
||||
// grpSpikeParams
|
||||
//
|
||||
grpSpikeParams.Controls.Add(nudSpikePairCount);
|
||||
grpSpikeParams.Controls.Add(lblSpikePairCount);
|
||||
grpSpikeParams.Controls.Add(nudSpikeWeldGap);
|
||||
grpSpikeParams.Controls.Add(lblSpikeWeldGap);
|
||||
grpSpikeParams.Controls.Add(nudGrooveDepth);
|
||||
grpSpikeParams.Controls.Add(lblGrooveDepth);
|
||||
grpSpikeParams.Controls.Add(nudSpikeAngle);
|
||||
grpSpikeParams.Controls.Add(lblSpikeAngle);
|
||||
grpSpikeParams.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
grpSpikeParams.Location = new System.Drawing.Point(6, 511);
|
||||
grpSpikeParams.Name = "grpSpikeParams";
|
||||
grpSpikeParams.Size = new System.Drawing.Size(191, 132);
|
||||
grpSpikeParams.TabIndex = 5;
|
||||
grpSpikeParams.TabStop = false;
|
||||
grpSpikeParams.Text = "Spike Parameters";
|
||||
grpSpikeParams.Visible = false;
|
||||
//
|
||||
// nudSpikePairCount
|
||||
//
|
||||
nudSpikePairCount.Location = new System.Drawing.Point(110, 101);
|
||||
nudSpikePairCount.Maximum = new decimal(new int[] { 50, 0, 0, 0 });
|
||||
nudSpikePairCount.Minimum = new decimal(new int[] { 1, 0, 0, 0 });
|
||||
nudSpikePairCount.Name = "nudSpikePairCount";
|
||||
nudSpikePairCount.Size = new System.Drawing.Size(88, 23);
|
||||
nudSpikePairCount.TabIndex = 4;
|
||||
nudSpikePairCount.Value = new decimal(new int[] { 2, 0, 0, 0 });
|
||||
nudSpikePairCount.ValueChanged += OnFeatureCountChanged;
|
||||
//
|
||||
// lblSpikePairCount
|
||||
//
|
||||
lblSpikePairCount.AutoSize = true;
|
||||
lblSpikePairCount.Location = new System.Drawing.Point(10, 103);
|
||||
lblSpikePairCount.Name = "lblSpikePairCount";
|
||||
lblSpikePairCount.Size = new System.Drawing.Size(66, 15);
|
||||
lblSpikePairCount.TabIndex = 5;
|
||||
lblSpikePairCount.Text = "Pair Count:";
|
||||
//
|
||||
// nudSpikeWeldGap
|
||||
//
|
||||
nudSpikeWeldGap.DecimalPlaces = 3;
|
||||
nudSpikeWeldGap.Location = new System.Drawing.Point(110, 47);
|
||||
nudSpikeWeldGap.Maximum = new decimal(new int[] { 10, 0, 0, 0 });
|
||||
nudSpikeWeldGap.Name = "nudSpikeWeldGap";
|
||||
nudSpikeWeldGap.Size = new System.Drawing.Size(88, 23);
|
||||
nudSpikeWeldGap.TabIndex = 2;
|
||||
nudSpikeWeldGap.Value = new decimal(new int[] { 125, 0, 0, 196608 });
|
||||
nudSpikeWeldGap.ValueChanged += OnSpikeParamChanged;
|
||||
//
|
||||
// lblSpikeWeldGap
|
||||
//
|
||||
lblSpikeWeldGap.AutoSize = true;
|
||||
lblSpikeWeldGap.Location = new System.Drawing.Point(10, 49);
|
||||
lblSpikeWeldGap.Name = "lblSpikeWeldGap";
|
||||
lblSpikeWeldGap.Size = new System.Drawing.Size(61, 15);
|
||||
lblSpikeWeldGap.TabIndex = 6;
|
||||
lblSpikeWeldGap.Text = "Weld Gap:";
|
||||
//
|
||||
// nudGrooveDepth
|
||||
//
|
||||
nudGrooveDepth.DecimalPlaces = 3;
|
||||
nudGrooveDepth.Location = new System.Drawing.Point(110, 20);
|
||||
nudGrooveDepth.Minimum = new decimal(new int[] { 1, 0, 0, 131072 });
|
||||
nudGrooveDepth.Name = "nudGrooveDepth";
|
||||
nudGrooveDepth.Size = new System.Drawing.Size(88, 23);
|
||||
nudGrooveDepth.TabIndex = 1;
|
||||
nudGrooveDepth.Value = new decimal(new int[] { 625, 0, 0, 196608 });
|
||||
nudGrooveDepth.ValueChanged += OnSpikeParamChanged;
|
||||
//
|
||||
// lblGrooveDepth
|
||||
//
|
||||
lblGrooveDepth.AutoSize = true;
|
||||
lblGrooveDepth.Location = new System.Drawing.Point(10, 22);
|
||||
lblGrooveDepth.Name = "lblGrooveDepth";
|
||||
lblGrooveDepth.Size = new System.Drawing.Size(83, 15);
|
||||
lblGrooveDepth.TabIndex = 7;
|
||||
lblGrooveDepth.Text = "Groove Depth:";
|
||||
//
|
||||
// nudSpikeAngle
|
||||
//
|
||||
nudSpikeAngle.DecimalPlaces = 1;
|
||||
nudSpikeAngle.Location = new System.Drawing.Point(110, 74);
|
||||
nudSpikeAngle.Maximum = new decimal(new int[] { 89, 0, 0, 0 });
|
||||
nudSpikeAngle.Minimum = new decimal(new int[] { 10, 0, 0, 0 });
|
||||
nudSpikeAngle.Name = "nudSpikeAngle";
|
||||
nudSpikeAngle.Size = new System.Drawing.Size(88, 23);
|
||||
nudSpikeAngle.TabIndex = 3;
|
||||
nudSpikeAngle.Value = new decimal(new int[] { 45, 0, 0, 0 });
|
||||
//
|
||||
// lblSpikeAngle
|
||||
//
|
||||
lblSpikeAngle.AutoSize = true;
|
||||
lblSpikeAngle.Location = new System.Drawing.Point(10, 76);
|
||||
lblSpikeAngle.Name = "lblSpikeAngle";
|
||||
lblSpikeAngle.Size = new System.Drawing.Size(72, 15);
|
||||
lblSpikeAngle.TabIndex = 8;
|
||||
lblSpikeAngle.Text = "Spike Angle:";
|
||||
//
|
||||
// grpTabParams
|
||||
//
|
||||
grpTabParams.Controls.Add(nudTabCount);
|
||||
grpTabParams.Controls.Add(lblTabCount);
|
||||
grpTabParams.Controls.Add(nudTabHeight);
|
||||
grpTabParams.Controls.Add(lblTabHeight);
|
||||
grpTabParams.Controls.Add(nudTabWidth);
|
||||
grpTabParams.Controls.Add(lblTabWidth);
|
||||
grpTabParams.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
grpTabParams.Location = new System.Drawing.Point(6, 406);
|
||||
grpTabParams.Name = "grpTabParams";
|
||||
grpTabParams.Size = new System.Drawing.Size(191, 105);
|
||||
grpTabParams.TabIndex = 4;
|
||||
grpTabParams.TabStop = false;
|
||||
grpTabParams.Text = "Tab Parameters";
|
||||
grpTabParams.Visible = false;
|
||||
//
|
||||
// nudTabCount
|
||||
//
|
||||
nudTabCount.Location = new System.Drawing.Point(110, 74);
|
||||
nudTabCount.Maximum = new decimal(new int[] { 50, 0, 0, 0 });
|
||||
nudTabCount.Minimum = new decimal(new int[] { 1, 0, 0, 0 });
|
||||
nudTabCount.Name = "nudTabCount";
|
||||
nudTabCount.Size = new System.Drawing.Size(88, 23);
|
||||
nudTabCount.TabIndex = 2;
|
||||
nudTabCount.Value = new decimal(new int[] { 2, 0, 0, 0 });
|
||||
nudTabCount.ValueChanged += OnFeatureCountChanged;
|
||||
//
|
||||
// lblTabCount
|
||||
//
|
||||
lblTabCount.AutoSize = true;
|
||||
lblTabCount.Location = new System.Drawing.Point(10, 76);
|
||||
lblTabCount.Name = "lblTabCount";
|
||||
lblTabCount.Size = new System.Drawing.Size(65, 15);
|
||||
lblTabCount.TabIndex = 3;
|
||||
lblTabCount.Text = "Tab Count:";
|
||||
//
|
||||
// nudTabHeight
|
||||
//
|
||||
nudTabHeight.DecimalPlaces = 2;
|
||||
nudTabHeight.Location = new System.Drawing.Point(110, 47);
|
||||
nudTabHeight.Minimum = new decimal(new int[] { 1, 0, 0, 131072 });
|
||||
nudTabHeight.Name = "nudTabHeight";
|
||||
nudTabHeight.Size = new System.Drawing.Size(88, 23);
|
||||
nudTabHeight.TabIndex = 1;
|
||||
nudTabHeight.Value = new decimal(new int[] { 1, 0, 0, 65536 });
|
||||
//
|
||||
// lblTabHeight
|
||||
//
|
||||
lblTabHeight.AutoSize = true;
|
||||
lblTabHeight.Location = new System.Drawing.Point(10, 49);
|
||||
lblTabHeight.Name = "lblTabHeight";
|
||||
lblTabHeight.Size = new System.Drawing.Size(61, 15);
|
||||
lblTabHeight.TabIndex = 4;
|
||||
lblTabHeight.Text = "Weld Gap:";
|
||||
//
|
||||
// nudTabWidth
|
||||
//
|
||||
nudTabWidth.DecimalPlaces = 2;
|
||||
nudTabWidth.Location = new System.Drawing.Point(110, 20);
|
||||
nudTabWidth.Maximum = new decimal(new int[] { 1000, 0, 0, 0 });
|
||||
nudTabWidth.Minimum = new decimal(new int[] { 1, 0, 0, 131072 });
|
||||
nudTabWidth.Name = "nudTabWidth";
|
||||
nudTabWidth.Size = new System.Drawing.Size(88, 23);
|
||||
nudTabWidth.TabIndex = 0;
|
||||
nudTabWidth.Value = new decimal(new int[] { 5, 0, 0, 65536 });
|
||||
//
|
||||
// lblTabWidth
|
||||
//
|
||||
lblTabWidth.AutoSize = true;
|
||||
lblTabWidth.Location = new System.Drawing.Point(10, 22);
|
||||
lblTabWidth.Name = "lblTabWidth";
|
||||
lblTabWidth.Size = new System.Drawing.Size(69, 15);
|
||||
lblTabWidth.TabIndex = 5;
|
||||
lblTabWidth.Text = "Tab Length:";
|
||||
//
|
||||
// grpType
|
||||
//
|
||||
grpType.Controls.Add(radSpike);
|
||||
grpType.Controls.Add(radTabs);
|
||||
grpType.Controls.Add(radStraight);
|
||||
grpType.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
grpType.Location = new System.Drawing.Point(6, 311);
|
||||
grpType.Name = "grpType";
|
||||
grpType.Size = new System.Drawing.Size(191, 95);
|
||||
grpType.TabIndex = 3;
|
||||
grpType.TabStop = false;
|
||||
grpType.Text = "Split Type";
|
||||
//
|
||||
// radSpike
|
||||
//
|
||||
radSpike.AutoSize = true;
|
||||
radSpike.Location = new System.Drawing.Point(10, 66);
|
||||
radSpike.Name = "radSpike";
|
||||
radSpike.Size = new System.Drawing.Size(96, 19);
|
||||
radSpike.TabIndex = 2;
|
||||
radSpike.Text = "Spike-Groove";
|
||||
radSpike.CheckedChanged += OnTypeChanged;
|
||||
//
|
||||
// radTabs
|
||||
//
|
||||
radTabs.AutoSize = true;
|
||||
radTabs.Location = new System.Drawing.Point(10, 43);
|
||||
radTabs.Name = "radTabs";
|
||||
radTabs.Size = new System.Drawing.Size(105, 19);
|
||||
radTabs.TabIndex = 1;
|
||||
radTabs.Text = "Weld-Gap Tabs";
|
||||
radTabs.CheckedChanged += OnTypeChanged;
|
||||
//
|
||||
// radStraight
|
||||
//
|
||||
radStraight.AutoSize = true;
|
||||
radStraight.Checked = true;
|
||||
radStraight.Location = new System.Drawing.Point(10, 20);
|
||||
radStraight.Name = "radStraight";
|
||||
radStraight.Size = new System.Drawing.Size(66, 19);
|
||||
radStraight.TabIndex = 0;
|
||||
radStraight.TabStop = true;
|
||||
radStraight.Text = "Straight";
|
||||
radStraight.CheckedChanged += OnTypeChanged;
|
||||
//
|
||||
// grpByCount
|
||||
//
|
||||
grpByCount.Controls.Add(nudVerticalPieces);
|
||||
grpByCount.Controls.Add(lblVerticalPieces);
|
||||
grpByCount.Controls.Add(nudHorizontalPieces);
|
||||
grpByCount.Controls.Add(lblHorizontalPieces);
|
||||
grpByCount.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
grpByCount.Location = new System.Drawing.Point(6, 233);
|
||||
grpByCount.Name = "grpByCount";
|
||||
grpByCount.Size = new System.Drawing.Size(191, 78);
|
||||
grpByCount.TabIndex = 2;
|
||||
grpByCount.TabStop = false;
|
||||
grpByCount.Text = "Split by Count";
|
||||
grpByCount.Visible = false;
|
||||
//
|
||||
// nudVerticalPieces
|
||||
//
|
||||
nudVerticalPieces.Location = new System.Drawing.Point(110, 47);
|
||||
nudVerticalPieces.Maximum = new decimal(new int[] { 20, 0, 0, 0 });
|
||||
nudVerticalPieces.Minimum = new decimal(new int[] { 1, 0, 0, 0 });
|
||||
nudVerticalPieces.Name = "nudVerticalPieces";
|
||||
nudVerticalPieces.Size = new System.Drawing.Size(88, 23);
|
||||
nudVerticalPieces.TabIndex = 1;
|
||||
nudVerticalPieces.Value = new decimal(new int[] { 1, 0, 0, 0 });
|
||||
nudVerticalPieces.ValueChanged += OnByCountValueChanged;
|
||||
//
|
||||
// lblVerticalPieces
|
||||
//
|
||||
lblVerticalPieces.AutoSize = true;
|
||||
lblVerticalPieces.Location = new System.Drawing.Point(10, 49);
|
||||
lblVerticalPieces.Name = "lblVerticalPieces";
|
||||
lblVerticalPieces.Size = new System.Drawing.Size(56, 15);
|
||||
lblVerticalPieces.TabIndex = 2;
|
||||
lblVerticalPieces.Text = "V. Pieces:";
|
||||
//
|
||||
// nudHorizontalPieces
|
||||
//
|
||||
nudHorizontalPieces.Location = new System.Drawing.Point(110, 20);
|
||||
nudHorizontalPieces.Maximum = new decimal(new int[] { 20, 0, 0, 0 });
|
||||
nudHorizontalPieces.Minimum = new decimal(new int[] { 1, 0, 0, 0 });
|
||||
nudHorizontalPieces.Name = "nudHorizontalPieces";
|
||||
nudHorizontalPieces.Size = new System.Drawing.Size(88, 23);
|
||||
nudHorizontalPieces.TabIndex = 0;
|
||||
nudHorizontalPieces.Value = new decimal(new int[] { 2, 0, 0, 0 });
|
||||
nudHorizontalPieces.ValueChanged += OnByCountValueChanged;
|
||||
//
|
||||
// lblHorizontalPieces
|
||||
//
|
||||
lblHorizontalPieces.AutoSize = true;
|
||||
lblHorizontalPieces.Location = new System.Drawing.Point(10, 22);
|
||||
lblHorizontalPieces.Name = "lblHorizontalPieces";
|
||||
lblHorizontalPieces.Size = new System.Drawing.Size(58, 15);
|
||||
lblHorizontalPieces.TabIndex = 3;
|
||||
lblHorizontalPieces.Text = "H. Pieces:";
|
||||
//
|
||||
// grpAutoFit
|
||||
//
|
||||
grpAutoFit.Controls.Add(cboSplitAxis);
|
||||
grpAutoFit.Controls.Add(lblSplitAxis);
|
||||
grpAutoFit.Controls.Add(nudEdgeSpacing);
|
||||
grpAutoFit.Controls.Add(lblEdgeSpacing);
|
||||
grpAutoFit.Controls.Add(nudPlateHeight);
|
||||
grpAutoFit.Controls.Add(lblPlateHeight);
|
||||
grpAutoFit.Controls.Add(nudPlateWidth);
|
||||
grpAutoFit.Controls.Add(lblPlateWidth);
|
||||
grpAutoFit.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
grpAutoFit.Location = new System.Drawing.Point(6, 101);
|
||||
grpAutoFit.Name = "grpAutoFit";
|
||||
grpAutoFit.Size = new System.Drawing.Size(191, 132);
|
||||
grpAutoFit.TabIndex = 1;
|
||||
grpAutoFit.TabStop = false;
|
||||
grpAutoFit.Text = "Auto-Fit Options";
|
||||
grpAutoFit.Visible = false;
|
||||
//
|
||||
// cboSplitAxis
|
||||
//
|
||||
cboSplitAxis.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
cboSplitAxis.Items.AddRange(new object[] { "Auto", "Vertical Only", "Horizontal Only" });
|
||||
cboSplitAxis.Location = new System.Drawing.Point(110, 100);
|
||||
cboSplitAxis.Name = "cboSplitAxis";
|
||||
cboSplitAxis.Size = new System.Drawing.Size(88, 23);
|
||||
cboSplitAxis.TabIndex = 3;
|
||||
cboSplitAxis.SelectedIndexChanged += OnAutoFitValueChanged;
|
||||
//
|
||||
// lblSplitAxis
|
||||
//
|
||||
lblSplitAxis.AutoSize = true;
|
||||
lblSplitAxis.Location = new System.Drawing.Point(10, 103);
|
||||
lblSplitAxis.Name = "lblSplitAxis";
|
||||
lblSplitAxis.Size = new System.Drawing.Size(57, 15);
|
||||
lblSplitAxis.TabIndex = 4;
|
||||
lblSplitAxis.Text = "Split Axis:";
|
||||
//
|
||||
// nudEdgeSpacing
|
||||
//
|
||||
nudEdgeSpacing.DecimalPlaces = 2;
|
||||
nudEdgeSpacing.Location = new System.Drawing.Point(110, 74);
|
||||
nudEdgeSpacing.Name = "nudEdgeSpacing";
|
||||
nudEdgeSpacing.Size = new System.Drawing.Size(88, 23);
|
||||
nudEdgeSpacing.TabIndex = 2;
|
||||
nudEdgeSpacing.Value = new decimal(new int[] { 5, 0, 0, 65536 });
|
||||
nudEdgeSpacing.ValueChanged += OnAutoFitValueChanged;
|
||||
//
|
||||
// lblEdgeSpacing
|
||||
//
|
||||
lblEdgeSpacing.AutoSize = true;
|
||||
lblEdgeSpacing.Location = new System.Drawing.Point(10, 76);
|
||||
lblEdgeSpacing.Name = "lblEdgeSpacing";
|
||||
lblEdgeSpacing.Size = new System.Drawing.Size(81, 15);
|
||||
lblEdgeSpacing.TabIndex = 5;
|
||||
lblEdgeSpacing.Text = "Edge Spacing:";
|
||||
//
|
||||
// nudPlateHeight
|
||||
//
|
||||
nudPlateHeight.DecimalPlaces = 2;
|
||||
nudPlateHeight.Location = new System.Drawing.Point(110, 47);
|
||||
nudPlateHeight.Maximum = new decimal(new int[] { 100000, 0, 0, 0 });
|
||||
nudPlateHeight.Minimum = new decimal(new int[] { 1, 0, 0, 0 });
|
||||
nudPlateHeight.Name = "nudPlateHeight";
|
||||
nudPlateHeight.Size = new System.Drawing.Size(88, 23);
|
||||
nudPlateHeight.TabIndex = 1;
|
||||
nudPlateHeight.Value = new decimal(new int[] { 120, 0, 0, 0 });
|
||||
nudPlateHeight.ValueChanged += OnAutoFitValueChanged;
|
||||
//
|
||||
// lblPlateHeight
|
||||
//
|
||||
lblPlateHeight.AutoSize = true;
|
||||
lblPlateHeight.Location = new System.Drawing.Point(10, 49);
|
||||
lblPlateHeight.Name = "lblPlateHeight";
|
||||
lblPlateHeight.Size = new System.Drawing.Size(76, 15);
|
||||
lblPlateHeight.TabIndex = 6;
|
||||
lblPlateHeight.Text = "Plate Length:";
|
||||
//
|
||||
// nudPlateWidth
|
||||
//
|
||||
nudPlateWidth.DecimalPlaces = 2;
|
||||
nudPlateWidth.Location = new System.Drawing.Point(110, 20);
|
||||
nudPlateWidth.Maximum = new decimal(new int[] { 100000, 0, 0, 0 });
|
||||
nudPlateWidth.Minimum = new decimal(new int[] { 1, 0, 0, 0 });
|
||||
nudPlateWidth.Name = "nudPlateWidth";
|
||||
nudPlateWidth.Size = new System.Drawing.Size(88, 23);
|
||||
nudPlateWidth.TabIndex = 0;
|
||||
nudPlateWidth.Value = new decimal(new int[] { 60, 0, 0, 0 });
|
||||
nudPlateWidth.ValueChanged += OnAutoFitValueChanged;
|
||||
//
|
||||
// lblPlateWidth
|
||||
//
|
||||
lblPlateWidth.AutoSize = true;
|
||||
lblPlateWidth.Location = new System.Drawing.Point(10, 22);
|
||||
lblPlateWidth.Name = "lblPlateWidth";
|
||||
lblPlateWidth.Size = new System.Drawing.Size(71, 15);
|
||||
lblPlateWidth.TabIndex = 7;
|
||||
lblPlateWidth.Text = "Plate Width:";
|
||||
//
|
||||
// grpMethod
|
||||
//
|
||||
grpMethod.Controls.Add(radByCount);
|
||||
grpMethod.Controls.Add(radFitToPlate);
|
||||
grpMethod.Controls.Add(radManual);
|
||||
grpMethod.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
grpMethod.Location = new System.Drawing.Point(6, 6);
|
||||
grpMethod.Name = "grpMethod";
|
||||
grpMethod.Size = new System.Drawing.Size(191, 95);
|
||||
grpMethod.TabIndex = 0;
|
||||
grpMethod.TabStop = false;
|
||||
grpMethod.Text = "Split Method";
|
||||
//
|
||||
// radByCount
|
||||
//
|
||||
radByCount.AutoSize = true;
|
||||
radByCount.Location = new System.Drawing.Point(10, 66);
|
||||
radByCount.Name = "radByCount";
|
||||
radByCount.Size = new System.Drawing.Size(100, 19);
|
||||
radByCount.TabIndex = 2;
|
||||
radByCount.Text = "Split by Count";
|
||||
radByCount.CheckedChanged += OnMethodChanged;
|
||||
//
|
||||
// radFitToPlate
|
||||
//
|
||||
radFitToPlate.AutoSize = true;
|
||||
radFitToPlate.Location = new System.Drawing.Point(10, 43);
|
||||
radFitToPlate.Name = "radFitToPlate";
|
||||
radFitToPlate.Size = new System.Drawing.Size(81, 19);
|
||||
radFitToPlate.TabIndex = 1;
|
||||
radFitToPlate.Text = "Fit to Plate";
|
||||
radFitToPlate.CheckedChanged += OnMethodChanged;
|
||||
//
|
||||
// radManual
|
||||
//
|
||||
radManual.AutoSize = true;
|
||||
radManual.Checked = true;
|
||||
radManual.Location = new System.Drawing.Point(10, 20);
|
||||
radManual.Name = "radManual";
|
||||
radManual.Size = new System.Drawing.Size(65, 19);
|
||||
radManual.TabIndex = 0;
|
||||
radManual.TabStop = true;
|
||||
radManual.Text = "Manual";
|
||||
radManual.CheckedChanged += OnMethodChanged;
|
||||
//
|
||||
// pnlPreview
|
||||
//
|
||||
pnlPreview.BackColor = System.Drawing.Color.FromArgb(33, 40, 48);
|
||||
pnlPreview.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
pnlPreview.DrawOverlays = null;
|
||||
pnlPreview.Location = new System.Drawing.Point(0, 25);
|
||||
pnlPreview.Name = "pnlPreview";
|
||||
pnlPreview.Size = new System.Drawing.Size(647, 611);
|
||||
pnlPreview.TabIndex = 3;
|
||||
pnlPreview.MouseDown += OnPreviewMouseDown;
|
||||
pnlPreview.MouseMove += OnPreviewMouseMove;
|
||||
pnlPreview.MouseUp += OnPreviewMouseUp;
|
||||
//
|
||||
// toolStrip
|
||||
//
|
||||
toolStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { btnAddLine, btnDeleteLine });
|
||||
toolStrip.Location = new System.Drawing.Point(0, 0);
|
||||
toolStrip.Name = "toolStrip";
|
||||
toolStrip.Size = new System.Drawing.Size(867, 25);
|
||||
toolStrip.TabIndex = 0;
|
||||
//
|
||||
// btnAddLine
|
||||
//
|
||||
btnAddLine.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
btnAddLine.Name = "btnAddLine";
|
||||
btnAddLine.Size = new System.Drawing.Size(84, 22);
|
||||
btnAddLine.Text = "Add Split Line";
|
||||
btnAddLine.Click += OnAddSplitLine;
|
||||
//
|
||||
// btnDeleteLine
|
||||
//
|
||||
btnDeleteLine.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
btnDeleteLine.Name = "btnDeleteLine";
|
||||
btnDeleteLine.Size = new System.Drawing.Size(69, 22);
|
||||
btnDeleteLine.Text = "Delete Line";
|
||||
btnDeleteLine.Click += OnDeleteSplitLine;
|
||||
//
|
||||
// statusStrip
|
||||
//
|
||||
statusStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { lblStatus, lblCursor });
|
||||
statusStrip.Location = new System.Drawing.Point(0, 636);
|
||||
statusStrip.Name = "statusStrip";
|
||||
statusStrip.Size = new System.Drawing.Size(867, 22);
|
||||
statusStrip.TabIndex = 1;
|
||||
//
|
||||
// lblStatus
|
||||
//
|
||||
lblStatus.Name = "lblStatus";
|
||||
lblStatus.Size = new System.Drawing.Size(756, 17);
|
||||
lblStatus.Spring = true;
|
||||
lblStatus.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
|
||||
//
|
||||
// lblCursor
|
||||
//
|
||||
lblCursor.Name = "lblCursor";
|
||||
lblCursor.Size = new System.Drawing.Size(96, 17);
|
||||
lblCursor.Text = "Cursor: 0.00, 0.00";
|
||||
lblCursor.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
|
||||
//
|
||||
// SplitDrawingForm
|
||||
//
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
|
||||
CancelButton = btnCancel;
|
||||
ClientSize = new System.Drawing.Size(867, 658);
|
||||
Controls.Add(pnlPreview);
|
||||
Controls.Add(pnlSettings);
|
||||
Controls.Add(statusStrip);
|
||||
Controls.Add(toolStrip);
|
||||
MinimumSize = new System.Drawing.Size(600, 450);
|
||||
Name = "SplitDrawingForm";
|
||||
ShowIcon = false;
|
||||
ShowInTaskbar = false;
|
||||
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
Text = "Split Drawing";
|
||||
pnlSettings.ResumeLayout(false);
|
||||
grpSpikeParams.ResumeLayout(false);
|
||||
grpSpikeParams.PerformLayout();
|
||||
((System.ComponentModel.ISupportInitialize)nudSpikePairCount).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)nudSpikeWeldGap).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)nudGrooveDepth).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)nudSpikeAngle).EndInit();
|
||||
grpTabParams.ResumeLayout(false);
|
||||
grpTabParams.PerformLayout();
|
||||
((System.ComponentModel.ISupportInitialize)nudTabCount).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)nudTabHeight).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)nudTabWidth).EndInit();
|
||||
grpType.ResumeLayout(false);
|
||||
grpType.PerformLayout();
|
||||
grpByCount.ResumeLayout(false);
|
||||
grpByCount.PerformLayout();
|
||||
((System.ComponentModel.ISupportInitialize)nudVerticalPieces).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)nudHorizontalPieces).EndInit();
|
||||
grpAutoFit.ResumeLayout(false);
|
||||
grpAutoFit.PerformLayout();
|
||||
((System.ComponentModel.ISupportInitialize)nudEdgeSpacing).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)nudPlateHeight).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)nudPlateWidth).EndInit();
|
||||
grpMethod.ResumeLayout(false);
|
||||
grpMethod.PerformLayout();
|
||||
toolStrip.ResumeLayout(false);
|
||||
toolStrip.PerformLayout();
|
||||
statusStrip.ResumeLayout(false);
|
||||
statusStrip.PerformLayout();
|
||||
ResumeLayout(false);
|
||||
PerformLayout();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private SplitPreview pnlPreview;
|
||||
private System.Windows.Forms.Panel pnlSettings;
|
||||
private System.Windows.Forms.ToolStrip toolStrip;
|
||||
private System.Windows.Forms.ToolStripButton btnAddLine;
|
||||
private System.Windows.Forms.ToolStripButton btnDeleteLine;
|
||||
private System.Windows.Forms.StatusStrip statusStrip;
|
||||
private System.Windows.Forms.ToolStripStatusLabel lblStatus;
|
||||
private System.Windows.Forms.ToolStripStatusLabel lblCursor;
|
||||
|
||||
private System.Windows.Forms.GroupBox grpMethod;
|
||||
private System.Windows.Forms.RadioButton radManual;
|
||||
private System.Windows.Forms.RadioButton radFitToPlate;
|
||||
private System.Windows.Forms.RadioButton radByCount;
|
||||
|
||||
private System.Windows.Forms.GroupBox grpAutoFit;
|
||||
private System.Windows.Forms.Label lblPlateWidth;
|
||||
private System.Windows.Forms.NumericUpDown nudPlateWidth;
|
||||
private System.Windows.Forms.Label lblPlateHeight;
|
||||
private System.Windows.Forms.NumericUpDown nudPlateHeight;
|
||||
private System.Windows.Forms.Label lblEdgeSpacing;
|
||||
private System.Windows.Forms.NumericUpDown nudEdgeSpacing;
|
||||
private System.Windows.Forms.Label lblSplitAxis;
|
||||
private System.Windows.Forms.ComboBox cboSplitAxis;
|
||||
|
||||
private System.Windows.Forms.GroupBox grpByCount;
|
||||
private System.Windows.Forms.Label lblHorizontalPieces;
|
||||
private System.Windows.Forms.NumericUpDown nudHorizontalPieces;
|
||||
private System.Windows.Forms.Label lblVerticalPieces;
|
||||
private System.Windows.Forms.NumericUpDown nudVerticalPieces;
|
||||
|
||||
private System.Windows.Forms.GroupBox grpType;
|
||||
private System.Windows.Forms.RadioButton radStraight;
|
||||
private System.Windows.Forms.RadioButton radTabs;
|
||||
private System.Windows.Forms.RadioButton radSpike;
|
||||
|
||||
private System.Windows.Forms.GroupBox grpTabParams;
|
||||
private System.Windows.Forms.Label lblTabWidth;
|
||||
private System.Windows.Forms.NumericUpDown nudTabWidth;
|
||||
private System.Windows.Forms.Label lblTabHeight;
|
||||
private System.Windows.Forms.NumericUpDown nudTabHeight;
|
||||
private System.Windows.Forms.Label lblTabCount;
|
||||
private System.Windows.Forms.NumericUpDown nudTabCount;
|
||||
|
||||
private System.Windows.Forms.GroupBox grpSpikeParams;
|
||||
private System.Windows.Forms.Label lblSpikeAngle;
|
||||
private System.Windows.Forms.NumericUpDown nudSpikeAngle;
|
||||
private System.Windows.Forms.Label lblSpikePairCount;
|
||||
private System.Windows.Forms.NumericUpDown nudSpikePairCount;
|
||||
private System.Windows.Forms.Label lblGrooveDepth;
|
||||
private System.Windows.Forms.NumericUpDown nudGrooveDepth;
|
||||
private System.Windows.Forms.Label lblSpikeWeldGap;
|
||||
private System.Windows.Forms.NumericUpDown nudSpikeWeldGap;
|
||||
|
||||
private System.Windows.Forms.Panel pnlButtons;
|
||||
private System.Windows.Forms.Button btnOK;
|
||||
private System.Windows.Forms.Button btnCancel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,600 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.Controls;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Forms;
|
||||
|
||||
public partial class SplitDrawingForm : Form
|
||||
{
|
||||
private readonly Drawing _drawing;
|
||||
private readonly List<Entity> _drawingEntities;
|
||||
private readonly Box _drawingBounds;
|
||||
private readonly List<SplitLine> _splitLines = new();
|
||||
private CutOffAxis _currentAxis = CutOffAxis.Vertical;
|
||||
private bool _placingLine;
|
||||
|
||||
// Feature handle drag state
|
||||
private int _dragLineIndex = -1;
|
||||
private int _dragFeatureIndex = -1;
|
||||
private int _hoverLineIndex = -1;
|
||||
private int _hoverFeatureIndex = -1;
|
||||
private const float HandleRadius = 5f;
|
||||
private const double SnapThreshold = 5.0;
|
||||
|
||||
public List<Drawing> ResultDrawings { get; private set; }
|
||||
|
||||
public SplitDrawingForm(Drawing drawing)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_drawing = drawing;
|
||||
_drawingEntities = ConvertProgram.ToGeometry(drawing.Program)
|
||||
.Where(e => e.Layer != SpecialLayers.Rapid).ToList();
|
||||
_drawingBounds = drawing.Program.BoundingBox();
|
||||
|
||||
foreach (var entity in _drawingEntities)
|
||||
entity.Layer.IsVisible = true;
|
||||
|
||||
pnlPreview.Entities = _drawingEntities;
|
||||
pnlPreview.DrawOverlays = PaintOverlays;
|
||||
|
||||
Text = $"Split Drawing: {drawing.Name}";
|
||||
UpdateUI();
|
||||
pnlPreview.ZoomToFit(true);
|
||||
}
|
||||
|
||||
// --- Split Method Selection ---
|
||||
|
||||
private void OnMethodChanged(object sender, EventArgs e)
|
||||
{
|
||||
grpAutoFit.Visible = radFitToPlate.Checked;
|
||||
grpByCount.Visible = radByCount.Checked;
|
||||
|
||||
if (radFitToPlate.Checked || radByCount.Checked)
|
||||
RecalculateAutoSplitLines();
|
||||
}
|
||||
|
||||
private void RecalculateAutoSplitLines()
|
||||
{
|
||||
_splitLines.Clear();
|
||||
|
||||
if (radFitToPlate.Checked)
|
||||
{
|
||||
var plateW = (double)nudPlateWidth.Value;
|
||||
var plateH = (double)nudPlateHeight.Value;
|
||||
var spacing = (double)nudEdgeSpacing.Value;
|
||||
var overhang = GetCurrentParameters().FeatureOverhang;
|
||||
var axisIndex = cboSplitAxis.SelectedIndex;
|
||||
|
||||
if (axisIndex == 1)
|
||||
{
|
||||
var usable = System.Math.Min(plateW, plateH) - 2 * spacing - overhang;
|
||||
if (usable > 0)
|
||||
{
|
||||
var splits = (int)System.Math.Ceiling(_drawingBounds.Width / usable) - 1;
|
||||
if (splits > 0)
|
||||
{
|
||||
var step = _drawingBounds.Width / (splits + 1);
|
||||
for (var i = 1; i <= splits; i++)
|
||||
_splitLines.Add(new SplitLine(_drawingBounds.X + step * i, CutOffAxis.Vertical));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (axisIndex == 2)
|
||||
{
|
||||
var usable = System.Math.Min(plateW, plateH) - 2 * spacing - overhang;
|
||||
if (usable > 0)
|
||||
{
|
||||
var splits = (int)System.Math.Ceiling(_drawingBounds.Length / usable) - 1;
|
||||
if (splits > 0)
|
||||
{
|
||||
var step = _drawingBounds.Length / (splits + 1);
|
||||
for (var i = 1; i <= splits; i++)
|
||||
_splitLines.Add(new SplitLine(_drawingBounds.Y + step * i, CutOffAxis.Horizontal));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_splitLines.AddRange(AutoSplitCalculator.FitToPlate(_drawingBounds, plateW, plateH, spacing, overhang));
|
||||
}
|
||||
}
|
||||
else if (radByCount.Checked)
|
||||
{
|
||||
var hPieces = (int)nudHorizontalPieces.Value;
|
||||
var vPieces = (int)nudVerticalPieces.Value;
|
||||
_splitLines.AddRange(AutoSplitCalculator.SplitByCount(_drawingBounds, hPieces, vPieces));
|
||||
}
|
||||
|
||||
InitializeAllFeaturePositions();
|
||||
UpdateUI();
|
||||
pnlPreview.Invalidate();
|
||||
}
|
||||
|
||||
private void OnAutoFitValueChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (radFitToPlate.Checked)
|
||||
RecalculateAutoSplitLines();
|
||||
}
|
||||
|
||||
private void OnByCountValueChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (radByCount.Checked)
|
||||
RecalculateAutoSplitLines();
|
||||
}
|
||||
|
||||
// --- Split Type Selection ---
|
||||
|
||||
private void OnTypeChanged(object sender, EventArgs e)
|
||||
{
|
||||
grpTabParams.Visible = radTabs.Checked;
|
||||
grpSpikeParams.Visible = radSpike.Checked;
|
||||
InitializeAllFeaturePositions();
|
||||
if (radFitToPlate.Checked)
|
||||
RecalculateAutoSplitLines();
|
||||
pnlPreview.Invalidate();
|
||||
}
|
||||
|
||||
private void OnSpikeParamChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (radFitToPlate.Checked)
|
||||
RecalculateAutoSplitLines();
|
||||
pnlPreview.Invalidate();
|
||||
}
|
||||
|
||||
private void OnFeatureCountChanged(object sender, EventArgs e)
|
||||
{
|
||||
InitializeAllFeaturePositions();
|
||||
pnlPreview.Invalidate();
|
||||
}
|
||||
|
||||
private SplitParameters GetCurrentParameters()
|
||||
{
|
||||
var p = new SplitParameters();
|
||||
if (radTabs.Checked)
|
||||
{
|
||||
p.Type = SplitType.WeldGapTabs;
|
||||
p.TabWidth = (double)nudTabWidth.Value;
|
||||
p.TabHeight = (double)nudTabHeight.Value;
|
||||
p.TabCount = (int)nudTabCount.Value;
|
||||
}
|
||||
else if (radSpike.Checked)
|
||||
{
|
||||
p.Type = SplitType.SpikeGroove;
|
||||
p.GrooveDepth = (double)nudGrooveDepth.Value;
|
||||
p.SpikeWeldGap = (double)nudSpikeWeldGap.Value;
|
||||
p.SpikeDepth = p.GrooveDepth + p.SpikeWeldGap;
|
||||
p.SpikeAngle = (double)nudSpikeAngle.Value;
|
||||
p.SpikePairCount = (int)nudSpikePairCount.Value;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
// --- Feature Position Management ---
|
||||
|
||||
private int GetFeatureCount()
|
||||
{
|
||||
if (radTabs.Checked) return (int)nudTabCount.Value;
|
||||
if (radSpike.Checked) return (int)nudSpikePairCount.Value;
|
||||
return 0;
|
||||
}
|
||||
|
||||
private void GetExtent(SplitLine sl, out double start, out double end)
|
||||
{
|
||||
if (sl.Axis == CutOffAxis.Vertical)
|
||||
{
|
||||
start = _drawingBounds.Bottom;
|
||||
end = _drawingBounds.Top;
|
||||
}
|
||||
else
|
||||
{
|
||||
start = _drawingBounds.Left;
|
||||
end = _drawingBounds.Right;
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeFeaturePositions(SplitLine sl)
|
||||
{
|
||||
var count = GetFeatureCount();
|
||||
GetExtent(sl, out var start, out var end);
|
||||
var extent = end - start;
|
||||
|
||||
sl.FeaturePositions.Clear();
|
||||
if (count <= 0 || extent <= 0) return;
|
||||
|
||||
if (radSpike.Checked)
|
||||
{
|
||||
var margin = extent * 0.15;
|
||||
if (count == 1)
|
||||
{
|
||||
sl.FeaturePositions.Add(start + extent / 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
var usable = extent - 2 * margin;
|
||||
for (var i = 0; i < count; i++)
|
||||
sl.FeaturePositions.Add(start + margin + usable * i / (count - 1));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var spacing = extent / (count + 1);
|
||||
for (var i = 0; i < count; i++)
|
||||
sl.FeaturePositions.Add(start + spacing * (i + 1));
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeAllFeaturePositions()
|
||||
{
|
||||
foreach (var sl in _splitLines)
|
||||
InitializeFeaturePositions(sl);
|
||||
}
|
||||
|
||||
// --- Mouse Interaction ---
|
||||
|
||||
private void OnPreviewMouseDown(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (e.Button != MouseButtons.Left) return;
|
||||
|
||||
var worldPt = pnlPreview.PointControlToWorld(e.Location);
|
||||
|
||||
// Check for feature handle hit
|
||||
var (lineIdx, featIdx) = HitTestFeatureHandle(worldPt);
|
||||
if (lineIdx >= 0)
|
||||
{
|
||||
_dragLineIndex = lineIdx;
|
||||
_dragFeatureIndex = featIdx;
|
||||
return;
|
||||
}
|
||||
|
||||
// Split line placement
|
||||
if (radManual.Checked && _placingLine)
|
||||
{
|
||||
var snapped = SnapToMidpoint(worldPt);
|
||||
var position = _currentAxis == CutOffAxis.Vertical ? snapped.X : snapped.Y;
|
||||
var sl = new SplitLine(position, _currentAxis);
|
||||
InitializeFeaturePositions(sl);
|
||||
_splitLines.Add(sl);
|
||||
UpdateUI();
|
||||
pnlPreview.Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPreviewMouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
var worldPt = pnlPreview.PointControlToWorld(e.Location);
|
||||
|
||||
if (_dragLineIndex >= 0)
|
||||
{
|
||||
var sl = _splitLines[_dragLineIndex];
|
||||
GetExtent(sl, out var start, out var end);
|
||||
var pos = sl.Axis == CutOffAxis.Vertical ? worldPt.Y : worldPt.X;
|
||||
pos = System.Math.Max(start, System.Math.Min(end, pos));
|
||||
sl.FeaturePositions[_dragFeatureIndex] = pos;
|
||||
pnlPreview.Invalidate();
|
||||
}
|
||||
else
|
||||
{
|
||||
var (lineIdx, featIdx) = HitTestFeatureHandle(worldPt);
|
||||
if (lineIdx != _hoverLineIndex || featIdx != _hoverFeatureIndex)
|
||||
{
|
||||
_hoverLineIndex = lineIdx;
|
||||
_hoverFeatureIndex = featIdx;
|
||||
pnlPreview.Cursor = _hoverLineIndex >= 0
|
||||
? (_splitLines[_hoverLineIndex].Axis == CutOffAxis.Vertical ? Cursors.SizeNS : Cursors.SizeWE)
|
||||
: Cursors.Cross;
|
||||
pnlPreview.Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
lblCursor.Text = $"Cursor: {worldPt.X:F2}, {worldPt.Y:F2}";
|
||||
}
|
||||
|
||||
private void OnPreviewMouseUp(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (e.Button == MouseButtons.Left && _dragLineIndex >= 0)
|
||||
{
|
||||
_dragLineIndex = -1;
|
||||
_dragFeatureIndex = -1;
|
||||
pnlPreview.Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
private (int lineIndex, int featureIndex) HitTestFeatureHandle(Vector worldPt)
|
||||
{
|
||||
if (radStraight.Checked) return (-1, -1);
|
||||
|
||||
var hitRadius = HandleRadius / pnlPreview.ViewScale;
|
||||
for (var li = 0; li < _splitLines.Count; li++)
|
||||
{
|
||||
var sl = _splitLines[li];
|
||||
for (var fi = 0; fi < sl.FeaturePositions.Count; fi++)
|
||||
{
|
||||
var center = GetFeatureHandleWorld(sl, fi);
|
||||
var dx = worldPt.X - center.X;
|
||||
var dy = worldPt.Y - center.Y;
|
||||
if (dx * dx + dy * dy <= hitRadius * hitRadius)
|
||||
return (li, fi);
|
||||
}
|
||||
}
|
||||
return (-1, -1);
|
||||
}
|
||||
|
||||
private Vector GetFeatureHandleWorld(SplitLine sl, int featureIndex)
|
||||
{
|
||||
var pos = sl.FeaturePositions[featureIndex];
|
||||
return sl.Axis == CutOffAxis.Vertical
|
||||
? new Vector(sl.Position, pos)
|
||||
: new Vector(pos, sl.Position);
|
||||
}
|
||||
|
||||
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
|
||||
{
|
||||
if (keyData == Keys.Space)
|
||||
{
|
||||
_currentAxis = _currentAxis == CutOffAxis.Vertical ? CutOffAxis.Horizontal : CutOffAxis.Vertical;
|
||||
return true;
|
||||
}
|
||||
if (keyData == Keys.Escape)
|
||||
{
|
||||
_placingLine = false;
|
||||
return true;
|
||||
}
|
||||
return base.ProcessCmdKey(ref msg, keyData);
|
||||
}
|
||||
|
||||
private Vector SnapToMidpoint(Vector pt)
|
||||
{
|
||||
var midX = _drawingBounds.Center.X;
|
||||
var midY = _drawingBounds.Center.Y;
|
||||
var threshold = SnapThreshold / pnlPreview.ViewScale;
|
||||
|
||||
if (_currentAxis == CutOffAxis.Vertical && System.Math.Abs(pt.X - midX) < threshold)
|
||||
return new Vector(midX, pt.Y);
|
||||
if (_currentAxis == CutOffAxis.Horizontal && System.Math.Abs(pt.Y - midY) < threshold)
|
||||
return new Vector(pt.X, midY);
|
||||
return pt;
|
||||
}
|
||||
|
||||
// --- Rendering (drawn on top of entities via SplitPreview) ---
|
||||
|
||||
private void PaintOverlays(Graphics g)
|
||||
{
|
||||
g.SmoothingMode = SmoothingMode.AntiAlias;
|
||||
|
||||
// Piece color overlays
|
||||
var regions = BuildPreviewRegions();
|
||||
for (var i = 0; i < regions.Count; i++)
|
||||
{
|
||||
var color = PieceColors[i % PieceColors.Length];
|
||||
using var brush = new SolidBrush(color);
|
||||
var r = regions[i];
|
||||
var tl = pnlPreview.PointWorldToGraph(r.Left, r.Top);
|
||||
var br = pnlPreview.PointWorldToGraph(r.Right, r.Bottom);
|
||||
g.FillRectangle(brush, System.Math.Min(tl.X, br.X), System.Math.Min(tl.Y, br.Y),
|
||||
System.Math.Abs(br.X - tl.X), System.Math.Abs(br.Y - tl.Y));
|
||||
}
|
||||
|
||||
// Split lines — trimmed at feature positions with feature contours
|
||||
var parameters = GetCurrentParameters();
|
||||
var feature = GetSplitFeature(parameters.Type);
|
||||
using var splitPen = new Pen(Color.FromArgb(255, 82, 82));
|
||||
splitPen.DashStyle = DashStyle.Dash;
|
||||
using var featurePen = new Pen(Color.FromArgb(200, 255, 82, 82), 1.5f);
|
||||
|
||||
foreach (var sl in _splitLines)
|
||||
{
|
||||
GetExtent(sl, out var extStart, out var extEnd);
|
||||
var isVert = sl.Axis == CutOffAxis.Vertical;
|
||||
var margin = 10.0;
|
||||
|
||||
if (sl.FeaturePositions.Count == 0 || radStraight.Checked)
|
||||
{
|
||||
// No features — draw one continuous line
|
||||
var p1 = isVert
|
||||
? pnlPreview.PointWorldToGraph(sl.Position, extStart - margin)
|
||||
: pnlPreview.PointWorldToGraph(extStart - margin, sl.Position);
|
||||
var p2 = isVert
|
||||
? pnlPreview.PointWorldToGraph(sl.Position, extEnd + margin)
|
||||
: pnlPreview.PointWorldToGraph(extEnd + margin, sl.Position);
|
||||
g.DrawLine(splitPen, p1, p2);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Generate feature geometry and draw contours
|
||||
var featureResult = feature.GenerateFeatures(sl, extStart, extEnd, parameters);
|
||||
DrawFeatureEdge(g, featurePen, featureResult.NegativeSideEdge, isVert);
|
||||
DrawFeatureEdge(g, featurePen, featureResult.PositiveSideEdge, isVert);
|
||||
|
||||
// Draw split line in segments between features
|
||||
var halfExt = GetFeatureHalfExtent(parameters);
|
||||
var sorted = new List<double>(sl.FeaturePositions);
|
||||
sorted.Sort();
|
||||
|
||||
var cursor = extStart - margin;
|
||||
foreach (var fc in sorted)
|
||||
{
|
||||
var gapStart = fc - halfExt;
|
||||
if (gapStart > cursor)
|
||||
{
|
||||
var p1 = isVert
|
||||
? pnlPreview.PointWorldToGraph(sl.Position, cursor)
|
||||
: pnlPreview.PointWorldToGraph(cursor, sl.Position);
|
||||
var p2 = isVert
|
||||
? pnlPreview.PointWorldToGraph(sl.Position, gapStart)
|
||||
: pnlPreview.PointWorldToGraph(gapStart, sl.Position);
|
||||
g.DrawLine(splitPen, p1, p2);
|
||||
}
|
||||
cursor = fc + halfExt;
|
||||
}
|
||||
|
||||
// Final segment after last feature
|
||||
var end = extEnd + margin;
|
||||
if (end > cursor)
|
||||
{
|
||||
var p1 = isVert
|
||||
? pnlPreview.PointWorldToGraph(sl.Position, cursor)
|
||||
: pnlPreview.PointWorldToGraph(cursor, sl.Position);
|
||||
var p2 = isVert
|
||||
? pnlPreview.PointWorldToGraph(sl.Position, end)
|
||||
: pnlPreview.PointWorldToGraph(end, sl.Position);
|
||||
g.DrawLine(splitPen, p1, p2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Feature position handles
|
||||
if (!radStraight.Checked)
|
||||
{
|
||||
for (var li = 0; li < _splitLines.Count; li++)
|
||||
{
|
||||
var sl = _splitLines[li];
|
||||
for (var fi = 0; fi < sl.FeaturePositions.Count; fi++)
|
||||
{
|
||||
var center = pnlPreview.PointWorldToGraph(GetFeatureHandleWorld(sl, fi));
|
||||
var isDrag = li == _dragLineIndex && fi == _dragFeatureIndex;
|
||||
var isHover = li == _hoverLineIndex && fi == _hoverFeatureIndex;
|
||||
var fillColor = isDrag ? Color.FromArgb(255, 82, 82)
|
||||
: isHover ? Color.FromArgb(255, 183, 77)
|
||||
: Color.White;
|
||||
using var fill = new SolidBrush(fillColor);
|
||||
using var border = new Pen(Color.FromArgb(80, 80, 80));
|
||||
g.FillEllipse(fill, center.X - HandleRadius, center.Y - HandleRadius,
|
||||
HandleRadius * 2, HandleRadius * 2);
|
||||
g.DrawEllipse(border, center.X - HandleRadius, center.Y - HandleRadius,
|
||||
HandleRadius * 2, HandleRadius * 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly Color[] PieceColors =
|
||||
{
|
||||
Color.FromArgb(40, 79, 195, 247),
|
||||
Color.FromArgb(40, 129, 199, 132),
|
||||
Color.FromArgb(40, 255, 183, 77),
|
||||
Color.FromArgb(40, 206, 147, 216),
|
||||
Color.FromArgb(40, 255, 138, 128),
|
||||
Color.FromArgb(40, 128, 222, 234)
|
||||
};
|
||||
|
||||
private List<Box> BuildPreviewRegions()
|
||||
{
|
||||
var verticals = _splitLines.Where(l => l.Axis == CutOffAxis.Vertical).OrderBy(l => l.Position).ToList();
|
||||
var horizontals = _splitLines.Where(l => l.Axis == CutOffAxis.Horizontal).OrderBy(l => l.Position).ToList();
|
||||
|
||||
var xEdges = new List<double> { _drawingBounds.Left };
|
||||
xEdges.AddRange(verticals.Select(v => v.Position));
|
||||
xEdges.Add(_drawingBounds.Right);
|
||||
|
||||
var yEdges = new List<double> { _drawingBounds.Bottom };
|
||||
yEdges.AddRange(horizontals.Select(h => h.Position));
|
||||
yEdges.Add(_drawingBounds.Top);
|
||||
|
||||
var regions = new List<Box>();
|
||||
for (var yi = 0; yi < yEdges.Count - 1; yi++)
|
||||
for (var xi = 0; xi < xEdges.Count - 1; xi++)
|
||||
regions.Add(new Box(xEdges[xi], yEdges[yi], xEdges[xi + 1] - xEdges[xi], yEdges[yi + 1] - yEdges[yi]));
|
||||
|
||||
return regions;
|
||||
}
|
||||
|
||||
// --- OK/Cancel ---
|
||||
|
||||
private void OnOK(object sender, EventArgs e)
|
||||
{
|
||||
if (_splitLines.Count == 0)
|
||||
{
|
||||
MessageBox.Show("No split lines defined.", "Split Drawing", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
ResultDrawings = DrawingSplitter.Split(_drawing, _splitLines, GetCurrentParameters());
|
||||
DialogResult = DialogResult.OK;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void OnCancel(object sender, EventArgs e)
|
||||
{
|
||||
DialogResult = DialogResult.Cancel;
|
||||
Close();
|
||||
}
|
||||
|
||||
// --- Toolbar ---
|
||||
|
||||
private void OnAddSplitLine(object sender, EventArgs e)
|
||||
{
|
||||
radManual.Checked = true;
|
||||
_placingLine = true;
|
||||
}
|
||||
|
||||
private void OnDeleteSplitLine(object sender, EventArgs e)
|
||||
{
|
||||
if (_splitLines.Count > 0)
|
||||
{
|
||||
_splitLines.RemoveAt(_splitLines.Count - 1);
|
||||
UpdateUI();
|
||||
pnlPreview.Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateUI()
|
||||
{
|
||||
var pieceCount = _splitLines.Count == 0 ? 1 : BuildPreviewRegions().Count;
|
||||
lblStatus.Text = $"Part: {_drawingBounds.Width:F2} x {_drawingBounds.Length:F2} | {_splitLines.Count} split lines | {pieceCount} pieces";
|
||||
}
|
||||
|
||||
// --- Feature rendering helpers ---
|
||||
|
||||
private static ISplitFeature GetSplitFeature(SplitType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
SplitType.WeldGapTabs => new WeldGapTabSplit(),
|
||||
SplitType.SpikeGroove => new SpikeGrooveSplit(),
|
||||
_ => new StraightSplit()
|
||||
};
|
||||
}
|
||||
|
||||
private static double GetFeatureHalfExtent(SplitParameters p)
|
||||
{
|
||||
return p.Type switch
|
||||
{
|
||||
SplitType.WeldGapTabs => p.TabWidth / 2,
|
||||
SplitType.SpikeGroove => p.GrooveDepth * System.Math.Tan(OpenNest.Math.Angle.ToRadians(p.SpikeAngle / 2)),
|
||||
_ => 0
|
||||
};
|
||||
}
|
||||
|
||||
private void DrawFeatureEdge(Graphics g, Pen pen, List<Geometry.Entity> entities, bool isVertical)
|
||||
{
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
if (entity is Geometry.Line line)
|
||||
{
|
||||
var p1 = pnlPreview.PointWorldToGraph(line.StartPoint.X, line.StartPoint.Y);
|
||||
var p2 = pnlPreview.PointWorldToGraph(line.EndPoint.X, line.EndPoint.Y);
|
||||
g.DrawLine(pen, p1, p2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- SplitPreview control ---
|
||||
|
||||
private class SplitPreview : EntityView
|
||||
{
|
||||
public Action<Graphics> DrawOverlays { get; set; }
|
||||
|
||||
protected override void OnPaint(PaintEventArgs e)
|
||||
{
|
||||
base.OnPaint(e);
|
||||
DrawOverlays?.Invoke(e.Graphics);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<metadata name="toolStrip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>17, 17</value>
|
||||
</metadata>
|
||||
<metadata name="statusStrip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>116, 17</value>
|
||||
</metadata>
|
||||
</root>
|
||||
@@ -182,7 +182,7 @@ namespace OpenNest
|
||||
|
||||
foreach (var shape in shapes)
|
||||
{
|
||||
var offsetEntity = shape.OffsetEntity(spacing, OffsetSide.Left) as Shape;
|
||||
var offsetEntity = shape.OffsetOutward(spacing);
|
||||
|
||||
if (offsetEntity == null)
|
||||
continue;
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\OpenNest.Core\OpenNest.Core.csproj" />
|
||||
<ProjectReference Include="..\..\OpenNest.IO\OpenNest.IO.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,209 +0,0 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using OpenNest;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
using Size = OpenNest.Geometry.Size;
|
||||
using OpenNest.IO;
|
||||
|
||||
var 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
|
||||
};
|
||||
|
||||
var templateDir = @"C:\Users\AJ\Desktop\Projects\OpenNest\docs\Templates";
|
||||
var outputDir = @"C:\Users\AJ\Desktop\Projects\OpenNest\docs\Templates\Nests";
|
||||
Directory.CreateDirectory(outputDir);
|
||||
|
||||
// BOM: (fileName, qty, thickness, material)
|
||||
var bom = new (string File, int Qty, double Thickness, string Material)[]
|
||||
{
|
||||
("PT01", 2, 0.250, "304SS"),
|
||||
("PT02", 5, 0.625, "304SS"),
|
||||
("PT03", 4, 0.250, "304SS"),
|
||||
("PT04", 4, 0.250, "304SS"),
|
||||
("PT05", 1, 0.250, "304SS"),
|
||||
("PT06", 21, 0.375, "304SS"),
|
||||
("PT07", 2, 0.250, "304SS"),
|
||||
("PT08", 22, 0.250, "304SS"),
|
||||
("PT11", 2, 0.1875, "304SS"),
|
||||
("PT12", 2, 0.1875, "304SS"),
|
||||
("PT13", 6, 0.1875, "304SS"),
|
||||
("PT15", 6, 0.1875, "304SS"),
|
||||
("PT16", 6, 0.1875, "304SS"),
|
||||
("PT18", 3, 0.250, "304SS"),
|
||||
("PT19", 3, 0.1875, "304SS"),
|
||||
("PT20", 2, 0.1196, "304SS"),
|
||||
("PT21", 6, 0.1196, "304SS"),
|
||||
("PT22", 2, 0.1196, "304SS"),
|
||||
("PT23", 1, 0.0598, "304SS"),
|
||||
("PT24", 1, 0.0598, "304SS"),
|
||||
("PT26", 4, 0.250, "304SS"),
|
||||
("PT27", 2, 0.250, "304SS"),
|
||||
("PT28", 4, 0.250, "304SS"),
|
||||
("PT29", 6, 0.250, "304SS"),
|
||||
("PT33", 2, 0.250, "304SS"),
|
||||
("PT34", 4, 0.250, "304SS"),
|
||||
("PT35", 3, 0.1875, "304SS"),
|
||||
("PT36", 4, 0.1875, "304SS"),
|
||||
("PT37", 4, 0.1875, "304SS"),
|
||||
("PT38", 4, 0.1875, "304SS"),
|
||||
("PT39", 2, 0.0598, "304SS"),
|
||||
("PT40", 4, 0.0598, "304SS"),
|
||||
("PT41", 1, 0.1875, "304SS"),
|
||||
("PT43", 1, 0.0598, "304SS"),
|
||||
("PT44", 1, 0.0598, "304SS"),
|
||||
("PT45", 1, 0.250, "304SS"),
|
||||
("PT46", 2, 0.250, "304SS"),
|
||||
("PT47", 4, 0.250, "304SS"),
|
||||
("PT48", 1, 0.250, "304SS"),
|
||||
("PT49", 1, 0.750, "PCS"),
|
||||
("PT50", 2, 0.375, "PCS"),
|
||||
("PT51", 1, 0.250, "304SS"),
|
||||
("PT52", 1, 0.1875, "304SS"),
|
||||
("PT53", 1, 0.1875, "304SS"),
|
||||
("PT54", 2, 0.250, "304SS"),
|
||||
("PT55", 1, 0.1196, "304SS"),
|
||||
("PT56", 1, 0.1196, "304SS"),
|
||||
("PT57", 1, 0.0598, "304SS"),
|
||||
("PT58", 1, 0.0598, "304SS"),
|
||||
};
|
||||
|
||||
// Group by material + thickness
|
||||
var groups = bom.GroupBy(b => (b.Material, b.Thickness)).OrderBy(g => g.Key.Material).ThenBy(g => g.Key.Thickness);
|
||||
|
||||
foreach (var group in groups)
|
||||
{
|
||||
var material = group.Key.Material;
|
||||
var thickness = group.Key.Thickness;
|
||||
var thicknessLabel = thickness switch
|
||||
{
|
||||
0.0598 => "16GA",
|
||||
0.1196 => "11GA",
|
||||
0.1875 => "3-16",
|
||||
0.250 => "1-4",
|
||||
0.375 => "3-8",
|
||||
0.625 => "5-8",
|
||||
0.750 => "3-4",
|
||||
_ => thickness.ToString("F4")
|
||||
};
|
||||
|
||||
var nestName = $"4526 A14 - {material} {thicknessLabel}";
|
||||
Console.WriteLine($"\n=== {nestName} ===");
|
||||
|
||||
var nest = new Nest();
|
||||
nest.Name = nestName;
|
||||
nest.PlateDefaults.Thickness = thickness;
|
||||
nest.PlateDefaults.Material = new Material { Name = material };
|
||||
nest.PlateDefaults.PartSpacing = 0.125;
|
||||
nest.PlateDefaults.EdgeSpacing = new Spacing(0.25, 0.25, 0.25, 0.25);
|
||||
|
||||
// Import DXFs for this group
|
||||
var importer = new DxfImporter();
|
||||
var colorIndex = 0;
|
||||
double maxMinDim = 0;
|
||||
double maxMaxDim = 0;
|
||||
|
||||
foreach (var item in group)
|
||||
{
|
||||
var dxfPath = Path.Combine(templateDir, $"4526 A14 {item.File}.dxf");
|
||||
if (!File.Exists(dxfPath))
|
||||
{
|
||||
Console.WriteLine($" WARNING: {dxfPath} not found, skipping");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!importer.GetGeometry(dxfPath, out var geometry) || geometry.Count == 0)
|
||||
{
|
||||
Console.WriteLine($" WARNING: no geometry in {item.File}, skipping");
|
||||
continue;
|
||||
}
|
||||
|
||||
var pgm = ConvertGeometry.ToProgram(geometry);
|
||||
if (pgm == null)
|
||||
{
|
||||
Console.WriteLine($" WARNING: failed to convert {item.File}, skipping");
|
||||
continue;
|
||||
}
|
||||
|
||||
var drawing = new Drawing(item.File, pgm);
|
||||
drawing.Quantity.Required = item.Qty;
|
||||
drawing.Material = new Material { Name = material };
|
||||
drawing.Color = partColors[colorIndex % partColors.Length];
|
||||
colorIndex++;
|
||||
nest.Drawings.Add(drawing);
|
||||
|
||||
var bbox = pgm.BoundingBox();
|
||||
var minDim = System.Math.Min(bbox.Width, bbox.Length);
|
||||
var maxDim = System.Math.Max(bbox.Width, bbox.Length);
|
||||
maxMinDim = System.Math.Max(maxMinDim, minDim);
|
||||
maxMaxDim = System.Math.Max(maxMaxDim, maxDim);
|
||||
|
||||
Console.WriteLine($" {item.File}: {bbox.Width:F2} x {bbox.Length:F2}, qty={item.Qty}");
|
||||
}
|
||||
|
||||
// Choose plate size based on largest part dimensions
|
||||
// Size(width, length) — width is the short side, length is the long side
|
||||
// Standard sizes: 48x96, 48x120, 60x120, 60x144, 72x144, 96x120
|
||||
double plateW, plateL;
|
||||
if (maxMinDim <= 47.5 && maxMaxDim <= 95.5)
|
||||
{
|
||||
plateW = 48; plateL = 96;
|
||||
}
|
||||
else if (maxMinDim <= 47.5 && maxMaxDim <= 119.5)
|
||||
{
|
||||
plateW = 48; plateL = 120;
|
||||
}
|
||||
else if (maxMinDim <= 59.5 && maxMaxDim <= 119.5)
|
||||
{
|
||||
plateW = 60; plateL = 120;
|
||||
}
|
||||
else if (maxMinDim <= 59.5 && maxMaxDim <= 143.5)
|
||||
{
|
||||
plateW = 60; plateL = 144;
|
||||
}
|
||||
else if (maxMinDim <= 71.5 && maxMaxDim <= 143.5)
|
||||
{
|
||||
plateW = 72; plateL = 144;
|
||||
}
|
||||
else if (maxMinDim <= 95.5 && maxMaxDim <= 119.5)
|
||||
{
|
||||
plateW = 96; plateL = 120;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: round up to nearest 12"
|
||||
plateW = System.Math.Ceiling((maxMinDim + 1) / 12.0) * 12;
|
||||
plateL = System.Math.Ceiling((maxMaxDim + 1) / 12.0) * 12;
|
||||
}
|
||||
|
||||
// Create one empty plate via PlateDefaults so it inherits settings
|
||||
nest.PlateDefaults.Size = new Size(plateW, plateL);
|
||||
var plate = nest.CreatePlate();
|
||||
plate.Quantity = 1;
|
||||
|
||||
Console.WriteLine($" Plate size: {plateW} x {plateL} (W x L)");
|
||||
Console.WriteLine($" Drawings: {nest.Drawings.Count}");
|
||||
|
||||
var outputPath = Path.Combine(outputDir, $"{nestName}.nest");
|
||||
var writer = new NestWriter(nest);
|
||||
if (writer.Write(outputPath))
|
||||
Console.WriteLine($" Saved: {outputPath}");
|
||||
else
|
||||
Console.WriteLine($" ERROR: failed to save {outputPath}");
|
||||
}
|
||||
|
||||
Console.WriteLine("\nDone!");
|
||||
Reference in New Issue
Block a user