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>
This commit is contained in:
2026-03-24 18:19:47 -04:00
parent df18b72881
commit 39f8a79cfd
5 changed files with 531 additions and 230 deletions
+10 -38
View File
@@ -41,8 +41,6 @@ namespace OpenNest.Forms
lblGrooveDepth = new System.Windows.Forms.Label();
nudSpikeAngle = new System.Windows.Forms.NumericUpDown();
lblSpikeAngle = new System.Windows.Forms.Label();
nudSpikeDepth = new System.Windows.Forms.NumericUpDown();
lblSpikeDepth = new System.Windows.Forms.Label();
grpTabParams = new System.Windows.Forms.GroupBox();
nudTabCount = new System.Windows.Forms.NumericUpDown();
lblTabCount = new System.Windows.Forms.Label();
@@ -85,7 +83,6 @@ namespace OpenNest.Forms
((System.ComponentModel.ISupportInitialize)nudSpikeWeldGap).BeginInit();
((System.ComponentModel.ISupportInitialize)nudGrooveDepth).BeginInit();
((System.ComponentModel.ISupportInitialize)nudSpikeAngle).BeginInit();
((System.ComponentModel.ISupportInitialize)nudSpikeDepth).BeginInit();
grpTabParams.SuspendLayout();
((System.ComponentModel.ISupportInitialize)nudTabCount).BeginInit();
((System.ComponentModel.ISupportInitialize)nudTabHeight).BeginInit();
@@ -162,12 +159,10 @@ namespace OpenNest.Forms
grpSpikeParams.Controls.Add(lblGrooveDepth);
grpSpikeParams.Controls.Add(nudSpikeAngle);
grpSpikeParams.Controls.Add(lblSpikeAngle);
grpSpikeParams.Controls.Add(nudSpikeDepth);
grpSpikeParams.Controls.Add(lblSpikeDepth);
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, 159);
grpSpikeParams.Size = new System.Drawing.Size(191, 132);
grpSpikeParams.TabIndex = 5;
grpSpikeParams.TabStop = false;
grpSpikeParams.Text = "Spike Parameters";
@@ -175,7 +170,7 @@ namespace OpenNest.Forms
//
// nudSpikePairCount
//
nudSpikePairCount.Location = new System.Drawing.Point(110, 128);
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";
@@ -187,7 +182,7 @@ namespace OpenNest.Forms
// lblSpikePairCount
//
lblSpikePairCount.AutoSize = true;
lblSpikePairCount.Location = new System.Drawing.Point(10, 130);
lblSpikePairCount.Location = new System.Drawing.Point(10, 103);
lblSpikePairCount.Name = "lblSpikePairCount";
lblSpikePairCount.Size = new System.Drawing.Size(66, 15);
lblSpikePairCount.TabIndex = 5;
@@ -196,7 +191,7 @@ namespace OpenNest.Forms
// nudSpikeWeldGap
//
nudSpikeWeldGap.DecimalPlaces = 3;
nudSpikeWeldGap.Location = new System.Drawing.Point(110, 74);
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);
@@ -207,7 +202,7 @@ namespace OpenNest.Forms
// lblSpikeWeldGap
//
lblSpikeWeldGap.AutoSize = true;
lblSpikeWeldGap.Location = new System.Drawing.Point(10, 76);
lblSpikeWeldGap.Location = new System.Drawing.Point(10, 49);
lblSpikeWeldGap.Name = "lblSpikeWeldGap";
lblSpikeWeldGap.Size = new System.Drawing.Size(61, 15);
lblSpikeWeldGap.TabIndex = 6;
@@ -216,18 +211,18 @@ namespace OpenNest.Forms
// nudGrooveDepth
//
nudGrooveDepth.DecimalPlaces = 3;
nudGrooveDepth.Location = new System.Drawing.Point(110, 47);
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[] { 125, 0, 0, 196608 });
nudGrooveDepth.Value = new decimal(new int[] { 625, 0, 0, 196608 });
nudGrooveDepth.ValueChanged += OnSpikeParamChanged;
//
// lblGrooveDepth
//
lblGrooveDepth.AutoSize = true;
lblGrooveDepth.Location = new System.Drawing.Point(10, 49);
lblGrooveDepth.Location = new System.Drawing.Point(10, 22);
lblGrooveDepth.Name = "lblGrooveDepth";
lblGrooveDepth.Size = new System.Drawing.Size(83, 15);
lblGrooveDepth.TabIndex = 7;
@@ -236,7 +231,7 @@ namespace OpenNest.Forms
// nudSpikeAngle
//
nudSpikeAngle.DecimalPlaces = 1;
nudSpikeAngle.Location = new System.Drawing.Point(110, 101);
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";
@@ -247,32 +242,12 @@ namespace OpenNest.Forms
// lblSpikeAngle
//
lblSpikeAngle.AutoSize = true;
lblSpikeAngle.Location = new System.Drawing.Point(10, 103);
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:";
//
// nudSpikeDepth
//
nudSpikeDepth.DecimalPlaces = 2;
nudSpikeDepth.Enabled = false;
nudSpikeDepth.Location = new System.Drawing.Point(110, 20);
nudSpikeDepth.Minimum = new decimal(new int[] { 1, 0, 0, 131072 });
nudSpikeDepth.Name = "nudSpikeDepth";
nudSpikeDepth.Size = new System.Drawing.Size(88, 23);
nudSpikeDepth.TabIndex = 0;
nudSpikeDepth.Value = new decimal(new int[] { 25, 0, 0, 131072 });
//
// lblSpikeDepth
//
lblSpikeDepth.AutoSize = true;
lblSpikeDepth.Location = new System.Drawing.Point(10, 22);
lblSpikeDepth.Name = "lblSpikeDepth";
lblSpikeDepth.Size = new System.Drawing.Size(73, 15);
lblSpikeDepth.TabIndex = 9;
lblSpikeDepth.Text = "Spike Depth:";
//
// grpTabParams
//
grpTabParams.Controls.Add(nudTabCount);
@@ -674,7 +649,6 @@ namespace OpenNest.Forms
((System.ComponentModel.ISupportInitialize)nudSpikeWeldGap).EndInit();
((System.ComponentModel.ISupportInitialize)nudGrooveDepth).EndInit();
((System.ComponentModel.ISupportInitialize)nudSpikeAngle).EndInit();
((System.ComponentModel.ISupportInitialize)nudSpikeDepth).EndInit();
grpTabParams.ResumeLayout(false);
grpTabParams.PerformLayout();
((System.ComponentModel.ISupportInitialize)nudTabCount).EndInit();
@@ -747,8 +721,6 @@ namespace OpenNest.Forms
private System.Windows.Forms.NumericUpDown nudTabCount;
private System.Windows.Forms.GroupBox grpSpikeParams;
private System.Windows.Forms.Label lblSpikeDepth;
private System.Windows.Forms.NumericUpDown nudSpikeDepth;
private System.Windows.Forms.Label lblSpikeAngle;
private System.Windows.Forms.NumericUpDown nudSpikeAngle;
private System.Windows.Forms.Label lblSpikePairCount;
+94 -18
View File
@@ -141,16 +141,8 @@ public partial class SplitDrawingForm : Form
pnlPreview.Invalidate();
}
private void UpdateSpikeDepth()
{
var grooveDepth = (double)nudGrooveDepth.Value;
var weldGap = (double)nudSpikeWeldGap.Value;
nudSpikeDepth.Value = (decimal)(grooveDepth + weldGap);
}
private void OnSpikeParamChanged(object sender, EventArgs e)
{
UpdateSpikeDepth();
if (radFitToPlate.Checked)
RecalculateAutoSplitLines();
pnlPreview.Invalidate();
@@ -175,9 +167,9 @@ public partial class SplitDrawingForm : Form
else if (radSpike.Checked)
{
p.Type = SplitType.SpikeGroove;
p.SpikeDepth = (double)nudSpikeDepth.Value;
p.GrooveDepth = p.SpikeDepth + (double)nudGrooveDepth.Value;
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;
}
@@ -389,23 +381,72 @@ public partial class SplitDrawingForm : Form
System.Math.Abs(br.X - tl.X), System.Math.Abs(br.Y - tl.Y));
}
// Split lines
// 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)
{
PointF p1, p2;
if (sl.Axis == CutOffAxis.Vertical)
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)
{
p1 = pnlPreview.PointWorldToGraph(sl.Position, _drawingBounds.Bottom - 10);
p2 = pnlPreview.PointWorldToGraph(sl.Position, _drawingBounds.Top + 10);
// 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
{
p1 = pnlPreview.PointWorldToGraph(_drawingBounds.Left - 10, sl.Position);
p2 = pnlPreview.PointWorldToGraph(_drawingBounds.Right + 10, sl.Position);
// 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);
}
}
g.DrawLine(splitPen, p1, p2);
}
// Feature position handles
@@ -509,6 +550,41 @@ public partial class SplitDrawingForm : Form
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