mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-14 20:41:56 -04:00
fix(expand): separate parts touching cutout walls
This commit is contained in:
1 parent
6c76617905
commit
dc4b07ddf5
3 files changed
+103
-55
No files matched your search
@@ -479,6 +479,63 @@ public class ExpanderTests
|
||||
Assert.Equal(4, moving.Location.X);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Separate_TouchingHoleWall_UsesOppositeDirectionToOpenGap()
|
||||
{
|
||||
var plate = MakePlate(40, 24, edge: 0);
|
||||
var host = new Part(new Drawing("host", RectangleWithHole(20, 20, 5, 5, 10, 10)), new Vector(0, 0));
|
||||
plate.Parts.Add(host);
|
||||
var insert = AddSquare(plate, 13, 9, 2);
|
||||
|
||||
var (converged, poses, violations) = Expander.Separate(
|
||||
new List<Part> { insert }, plate, spacing: 1
|
||||
);
|
||||
Assert.True(converged, $"contact still blocked: {violations.Count}");
|
||||
Assert.True(poses[0].X <= 12 + 1e-4, $"insert at x={poses[0].X}");
|
||||
Assert.True(poses[0].X >= 5 + 1 - 1e-4);
|
||||
Assert.Equal(13, insert.Location.X);
|
||||
Assert.Equal(0, host.Location.X);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(13, 9)]
|
||||
[InlineData(5, 9)]
|
||||
[InlineData(9, 13)]
|
||||
[InlineData(9, 5)]
|
||||
[InlineData(12.9, 9)] // Positive gap uses its ordinary nearest-boundary direction.
|
||||
public void Separate_FourHoleSides_ReachARealUnitGap(double x, double y)
|
||||
{
|
||||
var plate = MakePlate(40, 24, edge: 0);
|
||||
var host = new Part(new Drawing("host", RectangleWithHole(20, 20, 5, 5, 10, 10)), Vector.Zero);
|
||||
plate.Parts.Add(host);
|
||||
var insert = AddSquare(plate, x, y, 2);
|
||||
|
||||
var (converged, poses, violations) = Expander.Separate(new List<Part> { insert }, plate, 1);
|
||||
Assert.True(converged, $"{x},{y}: {violations.Count} violations");
|
||||
Assert.InRange(poses[0].X, 6 - 1e-4, 12 + 1e-4);
|
||||
Assert.InRange(poses[0].Y, 6 - 1e-4, 12 + 1e-4);
|
||||
Assert.Equal(x, insert.Location.X);
|
||||
Assert.Equal(y, insert.Location.Y);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Expand_TouchingHoleInsertWithSecondSelectedPart_StaysInsideCutout()
|
||||
{
|
||||
var plate = MakePlate(40, 24, edge: 0);
|
||||
var host = new Part(new Drawing("host", RectangleWithHole(20, 20, 5, 5, 10, 10)), Vector.Zero);
|
||||
plate.Parts.Add(host);
|
||||
var insert = AddSquare(plate, 13, 9, 2);
|
||||
var other = AddSquare(plate, 30, 9, 2);
|
||||
|
||||
var result = Expander.Expand(new List<Part> { insert, other }, plate,
|
||||
new Expander.Options { MaxSpacing = 1 });
|
||||
Assert.False(result.Cancelled);
|
||||
Assert.True(result.AchievedSpacing >= 0.99);
|
||||
Assert.InRange(insert.Location.X, 6 - 1e-4, 12 + 1e-4);
|
||||
Assert.False(MateriallyOverlaps(insert, host));
|
||||
Assert.Equal(0, host.Location.X);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Expand_WallsAndUnselectedPairs_StayExactlyAtClearance()
|
||||
{
|
||||
|
||||
@@ -317,63 +317,22 @@ namespace OpenNest.Engine.Fill
|
||||
// the B side travels the opposite way.
|
||||
var direction = moverIndex == pair.IndexA ? directionA : -directionA;
|
||||
|
||||
var edgeRoom = context.ClipToWorkArea(
|
||||
moverIndex,
|
||||
positions[moverIndex],
|
||||
direction,
|
||||
need
|
||||
);
|
||||
var room = context.ClipToMaterial(moverIndex, positions, direction, edgeRoom);
|
||||
|
||||
// Take the largest valid step up to `room`: partial moves let
|
||||
// a blocked mover advance again once its own blockers move
|
||||
// away in later iterations (a wave separates a chain).
|
||||
var applied = 0.0;
|
||||
var blockedByEdge = false;
|
||||
|
||||
if (room > 0)
|
||||
// A zero-distance hole contact can have an ambiguous nearest-
|
||||
// boundary fallback. Only if the normal candidate makes no
|
||||
// safe progress, retry its opposite through the same path,
|
||||
// floor and active-improvement checks. Never move the anchor.
|
||||
var attempt = ProposeMove(context, positions, pair, distance, direction, need, epsMove);
|
||||
if (attempt.Applied <= 0 && distance <= 1e-7)
|
||||
{
|
||||
var trial = positions[moverIndex] + direction * room;
|
||||
|
||||
if (context.MaintainsValidity(moverIndex, trial, positions, pair, distance))
|
||||
{
|
||||
positions[moverIndex] = trial;
|
||||
applied = room;
|
||||
}
|
||||
else
|
||||
{
|
||||
var lo = 0.0;
|
||||
var hi2 = room;
|
||||
for (var bisect = 0; bisect < 24 && hi2 - lo > 1e-6; bisect++)
|
||||
{
|
||||
var mid = (lo + hi2) / 2;
|
||||
if (
|
||||
context.MaintainsValidity(
|
||||
moverIndex,
|
||||
positions[moverIndex] + direction * mid,
|
||||
positions,
|
||||
pair,
|
||||
distance
|
||||
)
|
||||
)
|
||||
lo = mid;
|
||||
else
|
||||
hi2 = mid;
|
||||
}
|
||||
|
||||
if (lo > epsMove)
|
||||
{
|
||||
positions[moverIndex] = positions[moverIndex] + direction * lo;
|
||||
applied = lo;
|
||||
}
|
||||
}
|
||||
|
||||
blockedByEdge = applied < need - epsMove;
|
||||
moved += applied;
|
||||
var opposite = ProposeMove(context, positions, pair, distance, -direction, need, epsMove);
|
||||
if (opposite.Applied > attempt.Applied)
|
||||
attempt = opposite;
|
||||
}
|
||||
else
|
||||
var applied = attempt.Applied;
|
||||
if (applied > 0)
|
||||
{
|
||||
blockedByEdge = true;
|
||||
positions[moverIndex] = attempt.Proposal;
|
||||
moved += applied;
|
||||
}
|
||||
|
||||
// A pair fully separated to the user target (the internal margin
|
||||
@@ -432,6 +391,38 @@ namespace OpenNest.Engine.Fill
|
||||
return (violations.Count == 0, positions, violations);
|
||||
}
|
||||
|
||||
private static (double Applied, Vector Proposal, bool EdgeLimited, bool PartLimited) ProposeMove(
|
||||
SeparationContext context, List<Vector> positions, Pair pair,
|
||||
double distance, Vector direction, double need, double epsMove)
|
||||
{
|
||||
var mover = pair.Mover;
|
||||
var start = positions[mover];
|
||||
var edgeRoom = context.ClipToWorkArea(mover, start, direction, need);
|
||||
var room = context.ClipToMaterial(mover, positions, direction, edgeRoom);
|
||||
var edgeLimited = edgeRoom < need - epsMove;
|
||||
var partLimited = room < edgeRoom - epsMove;
|
||||
if (room <= epsMove)
|
||||
return (0, start, edgeLimited, partLimited);
|
||||
|
||||
var trial = start + direction * room;
|
||||
if (context.MaintainsValidity(mover, trial, positions, pair, distance))
|
||||
return (room, trial, edgeLimited, partLimited);
|
||||
|
||||
var lo = 0.0;
|
||||
var hi = room;
|
||||
for (var bisect = 0; bisect < 24 && hi - lo > 1e-6; bisect++)
|
||||
{
|
||||
var mid = (lo + hi) / 2;
|
||||
if (context.MaintainsValidity(mover, start + direction * mid, positions, pair, distance))
|
||||
lo = mid;
|
||||
else
|
||||
hi = mid;
|
||||
}
|
||||
return lo > epsMove
|
||||
? (lo, start + direction * lo, edgeLimited, true)
|
||||
: (0, start, edgeLimited, true);
|
||||
}
|
||||
|
||||
/// <summary>One constrained part↔part pair with its anchor mover.</summary>
|
||||
private sealed class Pair
|
||||
{
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
|
||||
Every selected/selected and selected/nonselected pair has an operation-entry clearance floor, measured against material with cutouts subtracted. A later move may reuse slack opened by an earlier move, but it cannot reduce a pair below its own starting gap. Existing overlaps are accepted for attempted separation; a remaining overlap is reported as a violation, not a successful positive gap. Temporarily blocked pairs are retried after another part moves, while a pass without positive movement stops the relaxation.
|
||||
|
||||
Probes run on scratch poses. Each trial translation is clipped against blocking material along its entire straight path, including cutout walls. Initially overlapping convex line profiles can take a certified direct exit; unsupported or ambiguous overlap paths stay put and remain reported. Tiny clamp keep-outs use a conservative native-bound motion limit when tolerant geometry cannot certify them. Cancellation before application leaves the live layout unchanged; `Separate` never changes it. The desktop command is synchronous and has no Cancel dialog or automatic save. A bounded search reports the spacing it found, not an optimum. See `docs/geometry/directional-slides.md` for directional contact behavior.
|
||||
Probes run on scratch poses. Each trial translation is clipped against blocking material along its entire straight path, including cutout walls. Initially overlapping convex line profiles can take a certified direct exit; unsupported or ambiguous overlap paths stay put and remain reported. When a part touches a cutout wall and the usual nearest-boundary direction makes no safe progress, the tool tries the opposite direction through the same checks. Tiny clamp keep-outs use a conservative native-bound motion limit when tolerant geometry cannot certify them. Cancellation before application leaves the live layout unchanged; `Separate` never changes it. The desktop command is synchronous and has no Cancel dialog or automatic save. A bounded search reports the spacing it found, not an optimum. See `docs/geometry/directional-slides.md` for directional contact behavior.
|
||||
Reference in new issue
Block a user