fix: simplify Shape.OffsetOutward winding normalization and sync designer

OffsetOutward now normalizes to CW winding before offsetting instead of
trial-and-error with bounding box comparison. CadConverterForm designer
regenerated with new entityView1 properties.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-29 21:01:46 -04:00
parent 3a0267c041
commit fdb4a2373a
2 changed files with 82 additions and 64 deletions
+22 -27
View File
@@ -579,43 +579,38 @@ namespace OpenNest.Geometry
} }
/// <summary> /// <summary>
/// Offsets the shape outward by the given distance, detecting winding direction /// Offsets the shape outward by the given distance.
/// to choose the correct offset side. Falls back to the opposite side if the /// Normalizes to CW winding before offsetting Left (which is outward for CW),
/// bounding box shrinks (indicating the offset went inward). /// making the method independent of the original contour winding direction.
/// </summary> /// </summary>
public Shape OffsetOutward(double distance) public Shape OffsetOutward(double distance)
{ {
var poly = ToPolygon(); 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 (poly == null || poly.Vertices.Count < 3
|| poly.RotationDirection() == RotationType.CW)
return OffsetEntity(distance, OffsetSide.Left) as Shape;
if (result == null) // Shape is CCW — reverse to CW so Left offset goes outward.
return null; var copy = new Shape();
UpdateBounds(); for (var i = Entities.Count - 1; i >= 0; i--)
var originalBB = BoundingBox;
result.UpdateBounds();
var offsetBB = result.BoundingBox;
if (offsetBB.Width < originalBB.Width || offsetBB.Length < originalBB.Length)
{ {
Trace.TraceWarning( switch (Entities[i])
"Shape.OffsetOutward: offset shrank bounding box " + {
$"(original={originalBB.Width:F3}x{originalBB.Length:F3}, " + case Line l:
$"offset={offsetBB.Width:F3}x{offsetBB.Length:F3}). " + copy.Entities.Add(new Line(l.EndPoint, l.StartPoint) { Layer = l.Layer });
"Retrying with opposite side."); break;
case Arc a:
var opposite = side == OffsetSide.Left ? OffsetSide.Right : OffsetSide.Left; copy.Entities.Add(new Arc(a.Center, a.Radius, a.EndAngle, a.StartAngle, !a.IsReversed) { Layer = a.Layer });
var retry = OffsetEntity(distance, opposite) as Shape; break;
case Circle c:
if (retry != null) copy.Entities.Add(new Circle(c.Center, c.Radius) { Layer = c.Layer });
result = retry; break;
}
} }
return result; return copy.OffsetEntity(distance, OffsetSide.Left) as Shape;
} }
/// <summary> /// <summary>
+38 -15
View File
@@ -116,8 +116,15 @@ namespace OpenNest.Forms
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.IsPickingBendLine = false;
entityView1.Location = new System.Drawing.Point(0, 0); entityView1.Location = new System.Drawing.Point(0, 0);
entityView1.Name = "entityView1"; entityView1.Name = "entityView1";
entityView1.OriginalEntities = null;
entityView1.ShowEntityLabels = false;
entityView1.SimplifierHighlight = null;
entityView1.SimplifierPreview = null;
entityView1.SimplifierToleranceLeft = null;
entityView1.SimplifierToleranceRight = null;
entityView1.Size = new System.Drawing.Size(759, 634); entityView1.Size = new System.Drawing.Size(759, 634);
entityView1.TabIndex = 0; entityView1.TabIndex = 0;
// //
@@ -215,52 +222,68 @@ namespace OpenNest.Forms
// //
btnSplit.FlatStyle = System.Windows.Forms.FlatStyle.Flat; btnSplit.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
btnSplit.Font = new System.Drawing.Font("Segoe UI", 9F); btnSplit.Font = new System.Drawing.Font("Segoe UI", 9F);
btnSplit.Location = new System.Drawing.Point(291, 6); btnSplit.Location = new System.Drawing.Point(289, 6);
btnSplit.Margin = new System.Windows.Forms.Padding(2, 0, 8, 0); btnSplit.Margin = new System.Windows.Forms.Padding(0, 0, 10, 0);
btnSplit.Name = "btnSplit"; btnSplit.Name = "btnSplit";
btnSplit.Size = new System.Drawing.Size(60, 24); btnSplit.Size = new System.Drawing.Size(60, 27);
btnSplit.TabIndex = 6; btnSplit.TabIndex = 6;
btnSplit.Text = "Split..."; btnSplit.Text = "Split...";
// //
// btnSimplify // btnSimplify
// //
btnSimplify.AutoSize = true;
btnSimplify.FlatStyle = System.Windows.Forms.FlatStyle.Flat; btnSimplify.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
btnSimplify.Font = new System.Drawing.Font("Segoe UI", 9F); btnSimplify.Font = new System.Drawing.Font("Segoe UI", 9F);
btnSimplify.Location = new System.Drawing.Point(359, 6);
btnSimplify.Margin = new System.Windows.Forms.Padding(0, 0, 10, 0);
btnSimplify.Name = "btnSimplify";
btnSimplify.Size = new System.Drawing.Size(75, 27);
btnSimplify.TabIndex = 7;
btnSimplify.Text = "Simplify..."; btnSimplify.Text = "Simplify...";
btnSimplify.AutoSize = true; btnSimplify.Click += OnSimplifyClick;
btnSimplify.Margin = new System.Windows.Forms.Padding(4, 0, 0, 0);
btnSimplify.Click += new System.EventHandler(this.OnSimplifyClick);
// //
// btnExportDxf // btnExportDxf
// //
btnExportDxf.AutoSize = true;
btnExportDxf.FlatStyle = System.Windows.Forms.FlatStyle.Flat; btnExportDxf.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
btnExportDxf.Font = new System.Drawing.Font("Segoe UI", 9F); btnExportDxf.Font = new System.Drawing.Font("Segoe UI", 9F);
btnExportDxf.Location = new System.Drawing.Point(444, 6);
btnExportDxf.Margin = new System.Windows.Forms.Padding(0, 0, 10, 0);
btnExportDxf.Name = "btnExportDxf";
btnExportDxf.Size = new System.Drawing.Size(76, 27);
btnExportDxf.TabIndex = 8;
btnExportDxf.Text = "Export DXF"; btnExportDxf.Text = "Export DXF";
btnExportDxf.AutoSize = true; btnExportDxf.Click += OnExportDxfClick;
btnExportDxf.Margin = new System.Windows.Forms.Padding(4, 0, 0, 0);
btnExportDxf.Click += new System.EventHandler(this.OnExportDxfClick);
// //
// chkShowOriginal // chkShowOriginal
// //
chkShowOriginal.AutoSize = true; chkShowOriginal.AutoSize = true;
chkShowOriginal.Font = new System.Drawing.Font("Segoe UI", 9F); chkShowOriginal.Font = new System.Drawing.Font("Segoe UI", 9F);
chkShowOriginal.Text = "Original"; chkShowOriginal.Location = new System.Drawing.Point(536, 9);
chkShowOriginal.Margin = new System.Windows.Forms.Padding(6, 3, 0, 0); chkShowOriginal.Margin = new System.Windows.Forms.Padding(6, 3, 0, 0);
chkShowOriginal.CheckedChanged += new System.EventHandler(this.OnShowOriginalChanged); chkShowOriginal.Name = "chkShowOriginal";
chkShowOriginal.Size = new System.Drawing.Size(68, 19);
chkShowOriginal.TabIndex = 9;
chkShowOriginal.Text = "Original";
chkShowOriginal.CheckedChanged += OnShowOriginalChanged;
// //
// chkLabels // chkLabels
// //
chkLabels.AutoSize = true; chkLabels.AutoSize = true;
chkLabels.Font = new System.Drawing.Font("Segoe UI", 9F); chkLabels.Font = new System.Drawing.Font("Segoe UI", 9F);
chkLabels.Text = "Labels"; chkLabels.Location = new System.Drawing.Point(610, 9);
chkLabels.Margin = new System.Windows.Forms.Padding(6, 3, 0, 0); chkLabels.Margin = new System.Windows.Forms.Padding(6, 3, 0, 0);
chkLabels.CheckedChanged += new System.EventHandler(this.OnLabelsChanged); chkLabels.Name = "chkLabels";
chkLabels.Size = new System.Drawing.Size(59, 19);
chkLabels.TabIndex = 10;
chkLabels.Text = "Labels";
chkLabels.CheckedChanged += OnLabelsChanged;
// //
// lblDetect // lblDetect
// //
lblDetect.AutoSize = true; lblDetect.AutoSize = true;
lblDetect.Font = new System.Drawing.Font("Segoe UI", 9F); lblDetect.Font = new System.Drawing.Font("Segoe UI", 9F);
lblDetect.Location = new System.Drawing.Point(361, 9); lblDetect.Location = new System.Drawing.Point(671, 9);
lblDetect.Margin = new System.Windows.Forms.Padding(2, 3, 0, 0); lblDetect.Margin = new System.Windows.Forms.Padding(2, 3, 0, 0);
lblDetect.Name = "lblDetect"; lblDetect.Name = "lblDetect";
lblDetect.Size = new System.Drawing.Size(42, 15); lblDetect.Size = new System.Drawing.Size(42, 15);
@@ -271,7 +294,7 @@ namespace OpenNest.Forms
// //
cboBendDetector.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; cboBendDetector.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
cboBendDetector.Font = new System.Drawing.Font("Segoe UI", 9F); cboBendDetector.Font = new System.Drawing.Font("Segoe UI", 9F);
cboBendDetector.Location = new System.Drawing.Point(405, 6); cboBendDetector.Location = new System.Drawing.Point(715, 6);
cboBendDetector.Margin = new System.Windows.Forms.Padding(2, 0, 0, 0); cboBendDetector.Margin = new System.Windows.Forms.Padding(2, 0, 0, 0);
cboBendDetector.Name = "cboBendDetector"; cboBendDetector.Name = "cboBendDetector";
cboBendDetector.Size = new System.Drawing.Size(90, 23); cboBendDetector.Size = new System.Drawing.Size(90, 23);