mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-10 13:24:12 -04:00
fix(ui): settle shift-click cloned parts with repeated directional pushes
This commit is contained in:
1 parent
c1c4b18f17
commit
8628c69098
4 files changed
+127
-7
No files matched your search
@@ -290,6 +290,63 @@ namespace OpenNest.Engine.Fill
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Settles a copied placement against the plate in both axis orders, choosing
|
||||
/// the group closest to the quadrant's work-area corner. A coarse box pass
|
||||
/// runs only when no moving box starts inside an existing part's box.
|
||||
/// </summary>
|
||||
public static void SettlePlacement(
|
||||
List<Part> movingParts,
|
||||
Plate plate,
|
||||
PushDirection horizontal,
|
||||
PushDirection vertical,
|
||||
int maxIterations = 20
|
||||
)
|
||||
{
|
||||
if (movingParts.Count == 0)
|
||||
return;
|
||||
|
||||
var workArea = plate.WorkArea();
|
||||
var skipBoxes = movingParts.Any(moving =>
|
||||
plate.Parts.Any(obstacle => moving.BoundingBox.Intersects(obstacle.BoundingBox))
|
||||
);
|
||||
var bestScore = double.MaxValue;
|
||||
Vector[] best = null;
|
||||
|
||||
foreach (var first in new[] { horizontal, vertical })
|
||||
{
|
||||
var second = first == horizontal ? vertical : horizontal;
|
||||
var trial = movingParts.Select(p => (Part)p.Clone()).ToList();
|
||||
if (!skipBoxes)
|
||||
{
|
||||
PushBoundingBox(trial, plate, first);
|
||||
PushBoundingBox(trial, plate, second);
|
||||
}
|
||||
|
||||
for (var i = 0; i < maxIterations; i++)
|
||||
{
|
||||
var moved = Push(trial, plate, first) + Push(trial, plate, second);
|
||||
if (moved < 0.01)
|
||||
break;
|
||||
}
|
||||
|
||||
var bounds = trial.GetBoundingBox();
|
||||
var dx = horizontal == PushDirection.Left
|
||||
? bounds.Left - workArea.Left : workArea.Right - bounds.Right;
|
||||
var dy = vertical == PushDirection.Down
|
||||
? bounds.Bottom - workArea.Bottom : workArea.Top - bounds.Top;
|
||||
var score = dx * dx + dy * dy;
|
||||
if (score < bestScore)
|
||||
{
|
||||
bestScore = score;
|
||||
best = trial.Select(p => p.Location).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < movingParts.Count; i++)
|
||||
movingParts[i].Location = best[i];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Repeatedly pushes parts left then down until total movement per
|
||||
/// iteration falls below the given threshold.
|
||||
|
||||
@@ -527,6 +527,72 @@ namespace OpenNest.Tests.Fill
|
||||
Assert.True(part.BoundingBox.Bottom < 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SettlePlacement_RepeatsAfterVerticalMovementOpensHorizontalPath()
|
||||
{
|
||||
var plate = new Plate(100, 100);
|
||||
plate.Parts.Add(MakeRectPart(20, 20, 20, 30));
|
||||
var moving = MakeRectPart(60, 35, 10, 10);
|
||||
|
||||
Compactor.SettlePlacement(new List<Part> { moving }, plate,
|
||||
PushDirection.Left, PushDirection.Down);
|
||||
|
||||
Assert.Equal(0, moving.BoundingBox.Left, 6);
|
||||
Assert.Equal(0, moving.BoundingBox.Bottom, 6);
|
||||
Assert.False(moving.Intersects(plate.Parts[0], out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SettlePlacement_MovesCopiedGroupTogether()
|
||||
{
|
||||
var plate = new Plate(100, 100);
|
||||
var left = MakeRectPart(40, 40, 5, 5);
|
||||
var right = MakeRectPart(50, 40, 5, 5);
|
||||
var originalGap = right.Location.X - left.Location.X;
|
||||
|
||||
Compactor.SettlePlacement(new List<Part> { left, right }, plate,
|
||||
PushDirection.Left, PushDirection.Down);
|
||||
|
||||
Assert.Equal(0, left.BoundingBox.Left, 6);
|
||||
Assert.Equal(0, left.BoundingBox.Bottom, 6);
|
||||
Assert.Equal(originalGap, right.Location.X - left.Location.X, 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SettlePlacement_GeometryRepeatsWhenInitialBoxPassIsSkipped()
|
||||
{
|
||||
var plate = new Plate(100, 100);
|
||||
plate.Parts.Add(MakeRectPart(20, 20, 20, 30));
|
||||
// Its bounding box covers the start, but its material is above it.
|
||||
plate.Parts.Add(MakeTrianglePart(new Vector(0, 100),
|
||||
new Vector(100, 100), new Vector(100, 40)));
|
||||
var moving = MakeRectPart(60, 35, 10, 10);
|
||||
|
||||
Compactor.SettlePlacement(new List<Part> { moving }, plate,
|
||||
PushDirection.Left, PushDirection.Down);
|
||||
|
||||
Assert.Equal(0, moving.BoundingBox.Left, 6);
|
||||
Assert.Equal(0, moving.BoundingBox.Bottom, 6);
|
||||
Assert.All(plate.Parts, obstacle => Assert.False(moving.Intersects(obstacle, out _)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SettlePlacement_SkipsBoxPassWhenAlreadyInsideObstacleBounds()
|
||||
{
|
||||
// Triangle material sits below the diagonal; the free pocket at (25,25)
|
||||
// is within its bounding box. A box-only slide would falsely block it.
|
||||
var plate = new Plate(100, 100);
|
||||
plate.Parts.Add(MakeTrianglePart(new Vector(0, 0),
|
||||
new Vector(40, 0), new Vector(0, 40)));
|
||||
var moving = MakeRectPart(25, 25, 5, 5);
|
||||
var start = moving.Location;
|
||||
|
||||
Compactor.SettlePlacement(new List<Part> { moving }, plate,
|
||||
PushDirection.Left, PushDirection.Down, maxIterations: 0);
|
||||
|
||||
Assert.Equal(start, moving.Location);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PushBoundingBox_Left_MovesPartTowardEdge()
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
@@ -178,11 +178,7 @@ namespace OpenNest.Actions
|
||||
break;
|
||||
}
|
||||
|
||||
Compactor.PushBoundingBox(movingParts, plateView.Plate, hDir);
|
||||
Compactor.PushBoundingBox(movingParts, plateView.Plate, vDir);
|
||||
|
||||
Compactor.Push(movingParts, plateView.Plate, hDir);
|
||||
Compactor.Push(movingParts, plateView.Plate, vDir);
|
||||
Compactor.SettlePlacement(movingParts, plateView.Plate, hDir, vDir);
|
||||
|
||||
parts.ForEach(p => p.IsDirty = true);
|
||||
plateView.Invalidate();
|
||||
|
||||
@@ -15,8 +15,9 @@ Open/incomplete chains and ambiguous contacts conservatively block. This is not
|
||||
- CPU best-fit batches prepare contact topology once and use all vertices plus curve/line interior and curve/curve tangency events. The old leading-half vertex filter cannot establish the next blocker after a skipped touch.
|
||||
- GPU kernels retain nearest-hit reduction and return unsnapped contact witnesses. The shared CPU classifier accepts a blocking witness or replays the full query after a nonblocking witness, preserving tied/later blockers. Both batch APIs honor active buffer lengths and refresh mutated/reused segment arrays. The GPU distance adapter sends only exact cardinal directions to the axis-only slide interface; arbitrary directions and native curves use the shared CPU path.
|
||||
|
||||
## Regression coverage
|
||||
- Shift-click while cloning parts settles the copied group toward the plate quadrant. If its starting bounds overlap any placed part's bounds, it skips the coarse bounding-box pass and uses geometry directly; otherwise it tries coarse horizontal/vertical and vertical/horizontal orders. Each candidate then alternates geometry pushes until movement is negligible (at most 20 iterations), and the group nearest the quadrant's work-area corner wins. The coarse pass remains useful for avoiding sawtooth/rung traps when the starting boxes do not overlap.
|
||||
|
||||
## Regression coverage
|
||||
`SlideContactTests` exercises cardinal line, translated line, reused edge-array, arbitrary-vector, native-entity, and both CPU batch paths. Cases include winding reversal, nonzero origins, rotated hooks, holes, separating circles, positive-distance grazing followed by a blocker, full-circle arc seams, concave/straight junctions, thin rings, and circle/line interior contact.
|
||||
|
||||
`CompactorTests` covers the reported sequence (push left with spacing, then right/up/down), genuine zero-distance blocking, zero/nonzero-spacing later hooks, and inside-hole pushes through both direct and plate entry points. Physical spacing is measured from raw outlines rather than the inflated contours used by the solver.
|
||||
|
||||
Reference in new issue
Block a user