36 Commits
Author SHA1 Message Date
ajandClaude Opus 4.6 4cc8b8f9b7 chore: remove one-off NestBuilder tool
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 23:09:35 -04:00
ajandClaude Opus 4.6 1f159d5dcc fix: prevent PushBoundingBox from making obstacles invisible to geometry push
PushBoundingBox left exactly partSpacing between bounding boxes, but the
geometry push inflates offset lines by halfSpacing + chordTolerance per
side (totaling partSpacing + 2*chordTolerance). The 0.002 overlap caused
RayEdgeDistance to return MaxValue for negative t, making the nearest
obstacle invisible and allowing the part to push through it.

Subtract 2*ChordTolerance from the BB push distance so the gap is wide
enough for the offset geometry lines to remain non-overlapping.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 23:07:46 -04:00
ajandClaude Opus 4.6 f626fbe063 fix: auto-select first part and refresh quantity in CadConverter file list
Fire SelectedIndexChanged when the first item is added so the preview
loads automatically. Invalidate the file list after quantity changes so
the badge repaints immediately.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 23:01:51 -04:00
ajandClaude Opus 4.6 d5b5ab57e3 fix: assign colors to SpecialLayers and ConvertProgram entities
ConvertProgram.ToGeometry() created entities without setting Color,
defaulting to Color.Empty (alpha=0). After ededc7b switched from
Pens.White to per-entity colors, these rendered fully transparent.

- Add explicit colors to all SpecialLayers (Cut=White, Rapid=Gray, etc.)
- Set entity Color from layer in ConvertProgram for lines, arcs, circles
- Add GetEntityPen fallback: treat Empty/alpha-0 as White
- Add bend line rendering and selection in EntityView/CadConverterForm
- Fix SolidWorksBendDetector MText formatting strip for bend notes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 21:57:50 -04:00
ajandClaude Opus 4.6 6916f5ecca fix: replace SplitContainer with Panel+Splitter layout in CadConverterForm
SplitContainer wasn't sizing children correctly. Switched to the same
Panel(Dock.Left) + Splitter + Fill pattern used by SplitDrawingForm,
with explicit Size on all controls.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 21:30:48 -04:00
ajandClaude Sonnet 4.6 e1bcb7498f feat: add SplitDxfWriter for split DXF output with bend data
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 20:43:40 -04:00
ajandClaude Opus 4.6 a7f8972722 feat: add bend line rendering and grain warning in PlateView
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 20:39:29 -04:00
ajandClaude Opus 4.6 6d1a3f5e2c feat: rebuild CadConverterForm with sidebar+preview layout
Replace the old DataGridView+TabControl layout with a sidebar containing
FileListControl and FilterPanel on the left, and EntityView with a detail
bar on the right. Adds drag-and-drop support, thread-safe parallel file
import, bend detection integration, and split-to-DXF workflow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 20:36:20 -04:00
ajandClaude Sonnet 4.6 52eca5f5c2 feat: add FilterPanel with layers, colors, linetypes, bend lines
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 20:32:27 -04:00
aj 3bce45be5f feat: add FileListControl owner-drawn file list 2026-03-24 20:30:32 -04:00
ajandClaude Sonnet 4.6 3f0a4c57b5 feat: add CollapsiblePanel reusable control
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 20:28:49 -04:00
ajandClaude Sonnet 4.6 ededc7b6b4 feat: draw entities in actual DXF colors with pen caching
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 20:27:46 -04:00
ajandClaude Sonnet 4.6 5f74afeda1 feat: add IBendDetector interface, SolidWorks implementation, and registry
Introduces a pluggable bend detection system in OpenNest.IO.Bending:
- IBendDetector takes CadDocument directly to preserve MText/layer/linetype info
- SolidWorksBendDetector finds lines on BEND layer with CENTER linetype and matches nearby MText notes
- BendDetectorRegistry auto-registers SolidWorks detector and supports AutoDetect

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 20:26:27 -04:00
ajandClaude Sonnet 4.6 574a8f2c38 feat: add DxfImporter.Import returning entities + CadDocument
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 20:23:53 -04:00
ajandClaude Sonnet 4.6 dd2892a9fe feat: serialize/deserialize bends in nest file format
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 20:22:55 -04:00
ajandClaude Sonnet 4.6 7056f8816f feat: add Bends property to Drawing, GrainAngle to Plate
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 20:19:53 -04:00
ajandClaude Sonnet 4.6 c2a470f79c feat: add Bend domain model and BendDirection enum to OpenNest.Core
Introduces OpenNest.Core/Bending/ with Bend and BendDirection types as
the foundation for bend line detection. Includes 6 passing unit tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 20:18:52 -04:00
ajandClaude Opus 4.6 39f8a79cfd feat: replace Clipper2 with direct entity splitting in DrawingSplitter
Replace polygon boolean clipping with direct entity splitting using
bounding box filtering and exact intersection math. Eliminates Clipper2
precision drift that caused contour gaps (0.0035") breaking area
calculation and ShapeBuilder chaining.

Also fixes SpikeGrooveSplit: spike depth is now grooveDepth + weldGap
(spike protrudes past groove), both V-shapes use same angle formula,
and weldGap no longer double-subtracted from tip depth.

SplitDrawingForm: fix parameter mapping (GrooveDepth direct from nud,
not inflated), remove redundant Spike Depth display, add feature
contour preview and trimmed split lines at feature positions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 18:19:47 -04:00
ajandClaude Opus 4.6 df18b72881 feat: add SplitLineIntersect helper for entity-splitline intersection
Add ToLine() to SplitLine and create SplitLineIntersect static class with
FindIntersection, CrossesSplitLine, and SideOf methods for testing entity
intersections against split lines. These helpers support the upcoming
Clipper2-free DrawingSplitter rewrite.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 18:04:21 -04:00
ajandClaude Opus 4.6 cd8adc97d6 feat: overhaul SplitDrawingForm — EntityView, draggable feature handles, UI fixes
- Replace raw Panel with EntityView (via SplitPreview subclass) for proper
  zoom-to-point, middle-button pan, and double-buffered rendering
- Add draggable handles for tab/spike positions along split lines; positions
  flow through to WeldGapTabSplit and SpikeGrooveSplit via SplitLine.FeaturePositions
- Fix OK/Cancel buttons hidden off-screen by putting them in a bottom-docked panel
- Fix DrawControl not invalidating on resize
- Swap plate Width/Length label order, default edge spacing to 0.5
- Rename tab labels: Tab Width→Tab Length, Tab Height→Weld Gap, default count 2
- Spike depth now calculated (read-only), groove depth means positioning depth
  beyond spike tip (default 0.125), converted to total depth internally
- Set entity layers visible so EntityView renders them

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 14:26:43 -04:00
ajandClaude Opus 4.6 ba7aa39941 feat: add groove depth and weld gap options to spike-groove split
- SpikeParameters: added GrooveDepth (how deep groove cuts into
  receiving part) and SpikeWeldGap (gap between spike tip and groove)
- SpikeGrooveSplit: groove uses its own depth (wider/deeper than spike),
  spike tip stops short by weld gap amount
- UI: added Groove Depth and Weld Gap fields to spike parameters panel
- Changed default pair count to 2 (one near each end)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 13:30:39 -04:00
aj 5d93ddb2c4 fix: swap plate dimension labels — Length first, Width second 2026-03-24 13:21:11 -04:00
aj 15b2043048 fix: rename Plate Height label to Plate Length 2026-03-24 13:20:09 -04:00
ajandClaude Opus 4.6 aa8b6f3d9e fix: use plate size correctly when split axis is forced
When user selects Vertical Only or Horizontal Only, use the smaller
plate dimension as the constraint for that axis. Previously it filtered
results from FitToPlate which produced no lines when the part already
fit in the plate width but not height.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 13:18:57 -04:00
ajandClaude Opus 4.6 3686d074e6 feat: add split axis selector to auto-fit options
Adds a "Split Axis" dropdown (Auto / Vertical Only / Horizontal Only)
to the Fit to Plate options so users can control which direction the
part is split when weld direction matters.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 13:14:54 -04:00
ajandClaude Opus 4.6 8f1a3fb6b7 fix: filter rapid-move entities from SplitDrawingForm preview
Rapid-layer entities (connecting lines between cutouts and perimeter)
were being rendered in the split preview. Filter them out, matching
the same pattern used in DrawingSplitter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 13:07:37 -04:00
aj 60ce297d6a feat: add split drawing feature for oversized parts 2026-03-24 12:47:39 -04:00
aj addd7acc3c docs: add split drawing feature to architecture documentation 2026-03-24 12:21:45 -04:00
ajandClaude Sonnet 4.6 d91ffccfa3 feat: add Split button to CadConverterForm to open SplitDrawingForm
Adds a Split button column to the DXF converter grid. Clicking it
builds a temporary Drawing from the item's visible entities and opens
SplitDrawingForm; if the user confirms, the split results replace the
original item in GetDrawings().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 12:20:22 -04:00
ajandClaude Opus 4.6 adb8ed12d7 feat: add SplitDrawingForm UI dialog
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 12:18:31 -04:00
ajandClaude Opus 4.6 4acd8b8bad feat: add DrawingSplitter core split pipeline
Implements the main drawing splitting algorithm that orchestrates splitting
a Drawing into multiple pieces along split lines using Clipper2 polygon
clipping. After clipping, recovers original arcs by matching clipped edges
back to perimeter entities, stitches in feature edges from ISplitFeature
where polygon edges lie on split lines, and normalizes each piece's origin.

Key fix from plan: filters rapid-layer entities before ShapeProfile
construction so cutouts are properly separated from perimeters.

Includes 7 integration tests covering vertical/horizontal splits, three-way
splits, property copying, origin normalization, cutout assignment, and
grid (cross) splits.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 12:13:37 -04:00
ajandClaude Sonnet 4.6 d7b095cf2d feat: add AutoSplitCalculator for fit-to-plate and split-by-count
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 12:03:18 -04:00
ajandClaude Sonnet 4.6 499e0425b5 feat: add SpikeGrooveSplit implementation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 12:01:40 -04:00
ajandClaude Sonnet 4.6 c2c3e23024 feat: add WeldGapTabSplit implementation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 11:59:45 -04:00
ajandClaude Sonnet 4.6 5afb311ac7 feat: add ISplitFeature interface and StraightSplit implementation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 11:58:00 -04:00
ajandClaude Sonnet 4.6 765a862440 feat: add SplitLine and SplitParameters models
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 11:56:00 -04:00
54 changed files with 5208 additions and 713 deletions
+2 -1
View File
@@ -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. - **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`. - **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. - **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. - **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) ### 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) ### OpenNest (WinForms WinExe, depends on Core + Engine + IO)
The UI application with MDI interface. 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`. - **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`. - **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. - **Post-processing**: `IPostProcessor` plugin interface loaded from DLLs in a `Posts/` directory at runtime.
+37
View File
@@ -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}";
}
}
}
+9
View File
@@ -0,0 +1,9 @@
namespace OpenNest.Bending
{
public enum BendDirection
{
Unknown,
Up,
Down
}
}
+7 -4
View File
@@ -59,9 +59,11 @@ namespace OpenNest.Converters
if (mode == Mode.Incremental) if (mode == Mode.Incremental)
pt += curpos; pt += curpos;
var layer = ConvertLayer(linearMove.Layer);
var line = new Line(curpos, pt) var line = new Line(curpos, pt)
{ {
Layer = ConvertLayer(linearMove.Layer) Layer = layer,
Color = layer.Color
}; };
geometry.Add(line); geometry.Add(line);
curpos = pt; curpos = pt;
@@ -76,7 +78,8 @@ namespace OpenNest.Converters
var line = new Line(curpos, pt) var line = new Line(curpos, pt)
{ {
Layer = SpecialLayers.Rapid Layer = SpecialLayers.Rapid,
Color = SpecialLayers.Rapid.Color
}; };
geometry.Add(line); geometry.Add(line);
curpos = pt; curpos = pt;
@@ -103,9 +106,9 @@ namespace OpenNest.Converters
var layer = ConvertLayer(arcMove.Layer); var layer = ConvertLayer(arcMove.Layer);
if (startAngle.IsEqualTo(endAngle)) if (startAngle.IsEqualTo(endAngle))
geometry.Add(new Circle(center, radius) { Layer = layer }); geometry.Add(new Circle(center, radius) { Layer = layer, Color = layer.Color });
else 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; curpos = endpt;
} }
+5 -1
View File
@@ -1,6 +1,8 @@
using OpenNest.CNC; using OpenNest.Bending;
using OpenNest.CNC;
using OpenNest.Converters; using OpenNest.Converters;
using OpenNest.Geometry; using OpenNest.Geometry;
using System.Collections.Generic;
using System.Drawing; using System.Drawing;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
@@ -62,6 +64,8 @@ namespace OpenNest
public SourceInfo Source { get; set; } public SourceInfo Source { get; set; }
public List<Bend> Bends { get; set; } = new List<Bend>();
public double Area { get; protected set; } public double Area { get; protected set; }
public void UpdateArea() public void UpdateArea()
+9 -1
View File
@@ -219,6 +219,14 @@ namespace OpenNest.Geometry
} }
internal static bool Intersects(Line line1, Line line2, out Vector pt) 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 a1 = line1.EndPoint.Y - line1.StartPoint.Y;
var b1 = line1.StartPoint.X - line1.EndPoint.X; var b1 = line1.StartPoint.X - line1.EndPoint.X;
@@ -240,7 +248,7 @@ namespace OpenNest.Geometry
var y = (a1 * c2 - a2 * c1) / d; var y = (a1 * c2 - a2 * c1) / d;
pt = new Vector(x, y); 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) internal static bool Intersects(Line line, Shape shape, out List<Vector> pts)
+41 -1
View File
@@ -534,7 +534,7 @@ namespace OpenNest.Geometry
{ {
Vector intersection; Vector intersection;
if (Intersect.Intersects(offsetLine, lastOffsetLine, out intersection)) if (Intersect.IntersectsUnbounded(offsetLine, lastOffsetLine, out intersection))
{ {
offsetLine.StartPoint = intersection; offsetLine.StartPoint = intersection;
lastOffsetLine.EndPoint = intersection; lastOffsetLine.EndPoint = intersection;
@@ -558,6 +558,46 @@ namespace OpenNest.Geometry
throw new NotImplementedException(); 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> /// <summary>
/// Gets the closest point on the shape to the given point. /// Gets the closest point on the shape to the given point.
/// </summary> /// </summary>
+3 -3
View File
@@ -49,7 +49,7 @@ namespace OpenNest
{ {
// Add chord tolerance to compensate for inscribed polygon chords // Add chord tolerance to compensate for inscribed polygon chords
// being inside the actual offset arcs. // 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) if (offsetEntity == null)
continue; continue;
@@ -71,7 +71,7 @@ namespace OpenNest
foreach (var shape in shapes) foreach (var shape in shapes)
{ {
var offsetEntity = shape.OffsetEntity(spacing + chordTolerance, OffsetSide.Left) as Shape; var offsetEntity = shape.OffsetOutward(spacing + chordTolerance);
if (offsetEntity == null) if (offsetEntity == null)
continue; continue;
@@ -109,7 +109,7 @@ namespace OpenNest
foreach (var shape in shapes) foreach (var shape in shapes)
{ {
var offsetEntity = shape.OffsetEntity(spacing + chordTolerance, OffsetSide.Left) as Shape; var offsetEntity = shape.OffsetOutward(spacing + chordTolerance);
if (offsetEntity == null) if (offsetEntity == null)
continue; continue;
+5
View File
@@ -88,6 +88,11 @@ namespace OpenNest
/// </summary> /// </summary>
public Material Material { get; set; } public Material Material { get; set; }
/// <summary>
/// Material grain direction in radians. 0 = horizontal.
/// </summary>
public double GrainAngle { get; set; }
/// <summary> /// <summary>
/// The parts that the plate contains. /// The parts that the plate contains.
/// </summary> /// </summary>
+9 -8
View File
@@ -1,21 +1,22 @@
using OpenNest.Geometry; using System.Drawing;
using OpenNest.Geometry;
namespace OpenNest namespace OpenNest
{ {
public static class SpecialLayers 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;
}
}
+505
View File
@@ -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()
};
}
}
+22
View File
@@ -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);
}
+112
View File
@@ -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));
}
}
+39
View File
@@ -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
};
}
+22
View File
@@ -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);
}
}
+1 -1
View File
@@ -157,7 +157,7 @@ namespace OpenNest.Engine.Fill
continue; continue;
var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction); 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 < 0) d = 0;
if (d < distance) if (d < distance)
distance = d; distance = d;
+1 -1
View File
@@ -33,7 +33,7 @@ namespace OpenNest.Engine.Fill
if (perimeter != null) if (perimeter != null)
{ {
var offsetEntity = perimeter.OffsetEntity(spacing, OffsetSide.Left) as Shape; var offsetEntity = perimeter.OffsetOutward(spacing);
if (offsetEntity != null) 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>();
}
}
}
+12
View File
@@ -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;
}
}
}
+12
View File
@@ -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; }
}
}
+18
View File
@@ -65,6 +65,24 @@ namespace OpenNest.IO
return entities; 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) public bool GetGeometry(Stream stream, out List<Entity> geometry)
{ {
var success = false; var success = false;
+14
View File
@@ -50,6 +50,7 @@ namespace OpenNest.IO
public ConstraintsDto Constraints { get; init; } = new(); public ConstraintsDto Constraints { get; init; } = new();
public MaterialDto Material { get; init; } = new(); public MaterialDto Material { get; init; } = new();
public SourceDto Source { get; init; } = new(); public SourceDto Source { get; init; } = new();
public List<BendDto> Bends { get; init; } = new();
} }
public record PlateDto public record PlateDto
@@ -62,6 +63,7 @@ namespace OpenNest.IO
public double PartSpacing { get; init; } public double PartSpacing { get; init; }
public MaterialDto Material { get; init; } = new(); public MaterialDto Material { get; init; } = new();
public SpacingDto EdgeSpacing { get; init; } = new(); public SpacingDto EdgeSpacing { get; init; } = new();
public double GrainAngle { get; init; }
public List<PartDto> Parts { get; init; } = new(); public List<PartDto> Parts { get; init; } = new();
public List<CutOffDto> CutOffs { get; init; } = new(); public List<CutOffDto> CutOffs { get; init; } = new();
} }
@@ -137,6 +139,18 @@ namespace OpenNest.IO
public double Y { get; init; } 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 record BestFitSetDto
{ {
public double PlateWidth { get; init; } public double PlateWidth { get; init; }
+19
View File
@@ -1,3 +1,4 @@
using OpenNest.Bending;
using OpenNest.CNC; using OpenNest.CNC;
using OpenNest.Engine.BestFit; using OpenNest.Engine.BestFit;
using OpenNest.Geometry; using OpenNest.Geometry;
@@ -91,6 +92,23 @@ namespace OpenNest.IO
drawing.Source.Path = d.Source.Path; drawing.Source.Path = d.Source.Path;
drawing.Source.Offset = new Vector(d.Source.Offset.X, d.Source.Offset.Y); 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)) if (programs.TryGetValue(d.Id, out var pgm))
drawing.Program = pgm; drawing.Program = pgm;
@@ -186,6 +204,7 @@ namespace OpenNest.IO
plate.PartSpacing = p.PartSpacing; plate.PartSpacing = p.PartSpacing;
plate.Material = new Material(p.Material.Name, p.Material.Grade, p.Material.Density); 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.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) foreach (var partDto in p.Parts)
{ {
+15 -2
View File
@@ -1,3 +1,4 @@
using OpenNest.Bending;
using OpenNest.CNC; using OpenNest.CNC;
using OpenNest.Engine.BestFit; using OpenNest.Engine.BestFit;
using System; using System;
@@ -140,7 +141,18 @@ namespace OpenNest.IO
{ {
Path = d.Source.Path ?? "", Path = d.Source.Path ?? "",
Offset = new OffsetDto { X = d.Source.Offset.X, Y = d.Source.Offset.Y } 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; return list;
@@ -200,7 +212,8 @@ namespace OpenNest.IO
Bottom = plate.EdgeSpacing.Bottom Bottom = plate.EdgeSpacing.Bottom
}, },
Parts = parts, Parts = parts,
CutOffs = cutoffs CutOffs = cutoffs,
GrainAngle = plate.GrainAngle
}); });
} }
return list; return list;
+177
View File
@@ -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
});
}
}
}
}
+90
View File
@@ -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);
}
}
-2
View File
@@ -154,11 +154,9 @@ namespace OpenNest.Actions
default: hDir = PushDirection.Left; vDir = PushDirection.Down; break; 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, hDir);
Compactor.PushBoundingBox(movingParts, plateView.Plate, vDir); 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, hDir);
Compactor.Push(movingParts, plateView.Plate, vDir); Compactor.Push(movingParts, plateView.Plate, vDir);
+104
View File
@@ -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;
}
}
}
+1
View File
@@ -167,6 +167,7 @@ namespace OpenNest.Controls
origin.Y += (Size.Height - lastSize.Height) * 0.5f; origin.Y += (Size.Height - lastSize.Height) * 0.5f;
lastSize = Size; lastSize = Size;
Invalidate();
} }
public float LengthWorldToGui(double length) public float LengthWorldToGui(double length)
+76 -5
View File
@@ -1,4 +1,5 @@
using OpenNest.Geometry; using OpenNest.Bending;
using OpenNest.Geometry;
using OpenNest.Math; using OpenNest.Math;
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing; using System.Drawing;
@@ -10,8 +11,11 @@ namespace OpenNest.Controls
public class EntityView : DrawControl public class EntityView : DrawControl
{ {
public List<Entity> Entities; 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() public EntityView()
{ {
@@ -37,13 +41,18 @@ namespace OpenNest.Controls
base.OnPaint(e); base.OnPaint(e);
e.Graphics.SmoothingMode = SmoothingMode.HighSpeed; e.Graphics.SmoothingMode = SmoothingMode.HighSpeed;
e.Graphics.DrawLine(pen, origin.X, 0, origin.X, Height); e.Graphics.DrawLine(gridPen, origin.X, 0, origin.X, Height);
e.Graphics.DrawLine(pen, 0, origin.Y, Width, origin.Y); e.Graphics.DrawLine(gridPen, 0, origin.Y, Width, origin.Y);
e.Graphics.TranslateTransform(origin.X, origin.Y); e.Graphics.TranslateTransform(origin.X, origin.Y);
foreach (var entity in Entities) 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 #if DRAW_OFFSET
@@ -100,6 +109,68 @@ namespace OpenNest.Controls
ZoomToFit(); 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) private void DrawEntity(Graphics g, Entity e, Pen pen)
{ {
if (!e.Layer.IsVisible || !e.IsVisible) if (!e.Layer.IsVisible || !e.IsVisible)
+235
View File
@@ -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);
}
}
}
+257
View File
@@ -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;
}
}
+73
View File
@@ -1,4 +1,5 @@
using OpenNest.Actions; using OpenNest.Actions;
using OpenNest.Bending;
using OpenNest.CNC; using OpenNest.CNC;
using OpenNest.Collections; using OpenNest.Collections;
using OpenNest.Engine.Fill; using OpenNest.Engine.Fill;
@@ -134,6 +135,8 @@ namespace OpenNest.Controls
public bool DrawOffset { get; set; } public bool DrawOffset { get; set; }
public bool ShowBendLines { get; set; }
public double OffsetTolerance { get; set; } = 0.001; public double OffsetTolerance { get; set; } = 0.001;
public bool FillParts { get; set; } public bool FillParts { get; set; }
@@ -578,6 +581,8 @@ namespace OpenNest.Controls
continue; continue;
part.Draw(g, (i + 1).ToString()); part.Draw(g, (i + 1).ToString());
DrawBendLines(g, part.BasePart);
DrawGrainWarning(g, part.BasePart);
} }
// Draw preview parts — active (current strategy) takes precedence // Draw preview parts — active (current strategy) takes precedence
@@ -614,6 +619,74 @@ namespace OpenNest.Controls
DrawRapids(g); 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) private void DrawCutOffs(Graphics g)
{ {
if (Plate?.CutOffs == null || Plate.CutOffs.Count == 0) if (Plate?.CutOffs == null || Plate.CutOffs.Count == 0)
+176 -223
View File
@@ -1,298 +1,251 @@
namespace OpenNest.Forms namespace OpenNest.Forms
{ {
partial class CadConverterForm partial class CadConverterForm
{ {
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null; 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) protected override void Dispose(bool disposing)
{ {
if (disposing && (components != null)) if (disposing && (components != null))
{
components.Dispose(); components.Dispose();
}
base.Dispose(disposing); base.Dispose(disposing);
} }
#region Windows Form Designer generated code #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() private void InitializeComponent()
{ {
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle1 = new System.Windows.Forms.DataGridViewCellStyle(); sidebarPanel = new System.Windows.Forms.Panel();
splitContainer1 = new System.Windows.Forms.SplitContainer(); fileList = new OpenNest.Controls.FileListControl();
dataGridView1 = new System.Windows.Forms.DataGridView(); filterPanel = new OpenNest.Controls.FilterPanel();
splitContainer2 = new System.Windows.Forms.SplitContainer(); splitterSidebar = new System.Windows.Forms.Splitter();
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();
entityView1 = new OpenNest.Controls.EntityView(); 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(); bottomPanel1 = new OpenNest.Controls.BottomPanel();
cancelButton = new System.Windows.Forms.Button(); cancelButton = new System.Windows.Forms.Button();
acceptButton = new System.Windows.Forms.Button(); acceptButton = new System.Windows.Forms.Button();
((System.ComponentModel.ISupportInitialize)splitContainer1).BeginInit();
splitContainer1.Panel1.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)numQuantity).BeginInit();
splitContainer1.Panel2.SuspendLayout(); sidebarPanel.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();
bottomPanel1.SuspendLayout(); bottomPanel1.SuspendLayout();
SuspendLayout(); SuspendLayout();
// //
// splitContainer1 // sidebarPanel (Left dock — contains file list + filter panel)
// //
splitContainer1.Dock = System.Windows.Forms.DockStyle.Fill; sidebarPanel.Dock = System.Windows.Forms.DockStyle.Left;
splitContainer1.Location = new System.Drawing.Point(0, 0); sidebarPanel.Name = "sidebarPanel";
splitContainer1.Name = "splitContainer1"; sidebarPanel.Size = new System.Drawing.Size(260, 670);
splitContainer1.Orientation = System.Windows.Forms.Orientation.Horizontal; 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); filterPanel.Dock = System.Windows.Forms.DockStyle.Fill;
splitContainer1.Size = new System.Drawing.Size(928, 643); filterPanel.Name = "filterPanel";
splitContainer1.SplitterDistance = 302; filterPanel.Size = new System.Drawing.Size(260, 370);
splitContainer1.TabIndex = 0;
// //
// dataGridView1 // splitterSidebar (between sidebar and preview)
// //
dataGridView1.AllowUserToAddRows = false; splitterSidebar.Location = new System.Drawing.Point(260, 0);
dataGridView1.AllowUserToResizeRows = false; splitterSidebar.Name = "splitterSidebar";
dataGridView1.BackgroundColor = System.Drawing.Color.White; splitterSidebar.Size = new System.Drawing.Size(3, 670);
dataGridView1.BorderStyle = System.Windows.Forms.BorderStyle.None; splitterSidebar.TabStop = false;
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;
// //
// splitContainer2 // entityView1 (Fill — main preview area)
//
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.BackColor = System.Drawing.Color.FromArgb(33, 40, 48); entityView1.BackColor = System.Drawing.Color.FromArgb(33, 40, 48);
entityView1.Cursor = System.Windows.Forms.Cursors.Cross; entityView1.Cursor = System.Windows.Forms.Cursors.Cross;
entityView1.Dock = System.Windows.Forms.DockStyle.Fill; entityView1.Dock = System.Windows.Forms.DockStyle.Fill;
entityView1.Location = new System.Drawing.Point(0, 0);
entityView1.Name = "entityView1"; entityView1.Name = "entityView1";
entityView1.Size = new System.Drawing.Size(615, 337); entityView1.Size = new System.Drawing.Size(761, 634);
entityView1.TabIndex = 0;
entityView1.Text = "entityView1"; //
// 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
// //
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(cancelButton);
bottomPanel1.Controls.Add(acceptButton); 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
// //
cancelButton.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right; cancelButton.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
cancelButton.DialogResult = System.Windows.Forms.DialogResult.Cancel; cancelButton.DialogResult = System.Windows.Forms.DialogResult.Cancel;
cancelButton.Location = new System.Drawing.Point(826, 10); cancelButton.Location = new System.Drawing.Point(922, 10);
cancelButton.Name = "cancelButton";
cancelButton.Size = new System.Drawing.Size(90, 28); cancelButton.Size = new System.Drawing.Size(90, 28);
cancelButton.TabIndex = 1;
cancelButton.Text = "Cancel"; cancelButton.Text = "Cancel";
cancelButton.UseVisualStyleBackColor = true; cancelButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
cancelButton.Font = new System.Drawing.Font("Segoe UI", 9f);
// //
// acceptButton // acceptButton
// //
acceptButton.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right; acceptButton.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
acceptButton.DialogResult = System.Windows.Forms.DialogResult.OK; acceptButton.DialogResult = System.Windows.Forms.DialogResult.OK;
acceptButton.Location = new System.Drawing.Point(730, 10); acceptButton.Location = new System.Drawing.Point(826, 10);
acceptButton.Name = "acceptButton";
acceptButton.Size = new System.Drawing.Size(90, 28); acceptButton.Size = new System.Drawing.Size(90, 28);
acceptButton.TabIndex = 0;
acceptButton.Text = "Accept"; acceptButton.Text = "Accept";
acceptButton.UseVisualStyleBackColor = true; acceptButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
acceptButton.Font = new System.Drawing.Font("Segoe UI", 9f);
// //
// CadConverterForm // CadConverterForm
// Add order: Fill last so it gets remaining space
// //
AutoScaleMode = System.Windows.Forms.AutoScaleMode.None; AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
ClientSize = new System.Drawing.Size(928, 693); ClientSize = new System.Drawing.Size(1024, 720);
Controls.Add(splitContainer1); Controls.Add(entityView1);
Controls.Add(detailBar);
Controls.Add(splitterSidebar);
Controls.Add(sidebarPanel);
Controls.Add(bottomPanel1); 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; MinimizeBox = false;
Name = "CadConverterForm";
ShowIcon = false; ShowIcon = false;
ShowInTaskbar = false; ShowInTaskbar = false;
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
Text = "CAD Converter"; Text = "CAD Converter";
splitContainer1.Panel1.ResumeLayout(false); AllowDrop = true;
splitContainer1.Panel2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)splitContainer1).EndInit(); ((System.ComponentModel.ISupportInitialize)numQuantity).EndInit();
splitContainer1.ResumeLayout(false); sidebarPanel.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);
bottomPanel1.ResumeLayout(false); bottomPanel1.ResumeLayout(false);
ResumeLayout(false); ResumeLayout(false);
} }
#endregion #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 acceptButton;
private System.Windows.Forms.Button cancelButton; 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
View File
@@ -1,10 +1,13 @@
using OpenNest.CNC; using OpenNest.Bending;
using OpenNest.CNC;
using OpenNest.Controls;
using OpenNest.Converters; using OpenNest.Converters;
using OpenNest.Geometry; using OpenNest.Geometry;
using OpenNest.IO; using OpenNest.IO;
using OpenNest.IO.Bending;
using OpenNest.Properties; using OpenNest.Properties;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing; using System.Drawing;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@@ -16,85 +19,294 @@ namespace OpenNest.Forms
{ {
public partial class CadConverterForm : Form public partial class CadConverterForm : Form
{ {
private static int colorIndex;
public CadConverterForm() public CadConverterForm()
{ {
InitializeComponent(); InitializeComponent();
Items = new BindingList<CadConverterItem>(); fileList.SelectedIndexChanged += OnFileSelected;
dataGridView1.DataSource = Items; filterPanel.FilterChanged += OnFilterChanged;
dataGridView1.DataError += dataGridView1_DataError; 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 try
{ {
var dir = shape.ToPolygon(3).RotationDirection(); var importer = new DxfImporter();
importer.SplinePrecision = Settings.Default.ImportSplinePrecision;
var result = importer.Import(file);
if (dir != rotation) if (result.Entities.Count == 0)
shape.Reverse(); return;
}
catch { }
}
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.Clear();
entityView1.Entities.AddRange(item.Entities); entityView1.Entities.AddRange(item.Entities);
entityView1.ZoomToFit(); entityView1.Bends = item.Bends ?? new List<Bend>();
item.Entities.ForEach(e => e.IsVisible = true); item.Entities.ForEach(e => e.IsVisible = true);
if (item.Entities.Any(e => e.Layer != null))
item.Entities.ForEach(e => e.Layer.IsVisible = true);
// Layers filterPanel.LoadItem(item.Entities, item.Bends);
checkedListBox1.Items.Clear();
var layers = item.Entities numQuantity.Value = item.Quantity;
.Where(e => e.Layer != null) txtCustomer.Text = item.Customer ?? "";
.Select(e => e.Layer.Name)
.ToList()
.Distinct();
foreach (var layer in layers) var bounds = item.Bounds;
checkedListBox1.Items.Add(layer, true); lblDimensions.Text = bounds != null
? $"{bounds.Width:0.#} x {bounds.Length:0.#}"
: "";
lblEntityCount.Text = $"{item.EntityCount} entities";
// Colors entityView1.ZoomToFit();
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);
} }
private static int colorIndex; private void ClearDetailBar()
private static Color GetNextColor()
{ {
var color = ColorScheme.PartColors[colorIndex % ColorScheme.PartColors.Length]; numQuantity.Value = 1;
colorIndex++; txtCustomer.Text = "";
return color; 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() public List<Drawing> GetDrawings()
{ {
var drawings = new List<Drawing>(); 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(); 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.Source.Path = item.Path;
drawing.Quantity.Required = item.Quantity; drawing.Quantity.Required = item.Quantity;
// Copy bends
if (item.Bends != null)
drawing.Bends.AddRange(item.Bends);
var shape = new ShapeProfile(entities); var shape = new ShapeProfile(entities);
SetRotation(shape.Perimeter, RotationType.CW); SetRotation(shape.Perimeter, RotationType.CW);
@@ -116,7 +332,6 @@ namespace OpenNest.Forms
entities = new List<Entity>(); entities = new List<Entity>();
entities.AddRange(shape.Perimeter.Entities); entities.AddRange(shape.Perimeter.Entities);
shape.Cutouts.ForEach(cutout => entities.AddRange(cutout.Entities)); shape.Cutouts.ForEach(cutout => entities.AddRange(cutout.Entities));
var pgm = ConvertGeometry.ToProgram(entities); var pgm = ConvertGeometry.ToProgram(entities);
@@ -125,9 +340,7 @@ namespace OpenNest.Forms
if (firstCode.Type == CodeType.RapidMove) if (firstCode.Type == CodeType.RapidMove)
{ {
var rapid = (RapidMove)firstCode; var rapid = (RapidMove)firstCode;
drawing.Source.Offset = rapid.EndPoint; drawing.Source.Offset = rapid.EndPoint;
pgm.Offset(-rapid.EndPoint); pgm.Offset(-rapid.EndPoint);
pgm.Codes.RemoveAt(0); pgm.Codes.RemoveAt(0);
} }
@@ -141,189 +354,57 @@ namespace OpenNest.Forms
return drawings; 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 #endregion
private void checkedListBox1_SelectedIndexChanged(object sender, System.EventArgs e) #region Helpers
{
var index = checkedListBox1.SelectedIndex;
var layerName = checkedListBox1.Items[index].ToString();
var isVisible = checkedListBox1.CheckedItems.Contains(layerName);
CurrentItem.Entities.ForEach(entity => private static void SetRotation(Shape shape, RotationType rotation)
{ {
if (entity.Layer.Name == layerName) try
entity.Layer.IsVisible = isVisible; {
}); var dir = shape.ToPolygon(3).RotationDirection();
if (dir != rotation) shape.Reverse();
entityView1.Invalidate(); }
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
}
private void UpdateEntityVisibility()
{ {
var item = CurrentItem; var testFile = Path.Combine(path, $".writetest_{Guid.NewGuid()}");
if (item == null) return; File.WriteAllText(testFile, "");
File.Delete(testFile);
return true;
}
catch { return false; }
}
var checkedColors = new HashSet<int>(); private static string GetUniquePath(string path)
for (var i = 0; i < checkedListBox2.Items.Count; i++)
{ {
if (checkedListBox2.GetItemChecked(i)) if (!File.Exists(path)) return path;
checkedColors.Add(((ColorItem)checkedListBox2.Items[i]).Argb);
}
var checkedLineTypes = new HashSet<string>(); var dir = Path.GetDirectoryName(path);
for (var i = 0; i < checkedListBox3.Items.Count; i++) var name = Path.GetFileNameWithoutExtension(path);
var ext = Path.GetExtension(path);
var counter = 2;
while (File.Exists(path))
{ {
if (checkedListBox3.GetItemChecked(i)) path = Path.Combine(dir, $"{name}_{counter}{ext}");
checkedLineTypes.Add(checkedListBox3.Items[i].ToString()); counter++;
} }
item.Entities.ForEach(entity => return path;
{
entity.IsVisible = !checkedColors.Contains(entity.Color.ToArgb())
&& !checkedLineTypes.Contains(entity.LineTypeName ?? "Continuous");
});
entityView1.Invalidate();
} }
private void checkedListBox2_DrawItem(object sender, DrawItemEventArgs e) #endregion
{
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;
}
}
+6
View File
@@ -479,6 +479,12 @@ namespace OpenNest.Forms
PlateView.Invalidate(); PlateView.Invalidate();
} }
public void ToggleBendLines()
{
PlateView.ShowBendLines = !PlateView.ShowBendLines;
PlateView.Invalidate();
}
public void ToggleDrawOffset() public void ToggleDrawOffset()
{ {
PlateView.DrawOffset = !PlateView.DrawOffset; PlateView.DrawOffset = !PlateView.DrawOffset;
+737
View File
@@ -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;
}
}
+600
View File
@@ -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);
}
}
}
+126
View File
@@ -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>
+1 -1
View File
@@ -182,7 +182,7 @@ namespace OpenNest
foreach (var shape in shapes) foreach (var shape in shapes)
{ {
var offsetEntity = shape.OffsetEntity(spacing, OffsetSide.Left) as Shape; var offsetEntity = shape.OffsetOutward(spacing);
if (offsetEntity == null) if (offsetEntity == null)
continue; continue;
-10
View File
@@ -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>
-209
View File
@@ -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!");