Files
OpenNest/OpenNest.Engine/Fill/FillExtents.cs
T
aj aec0523062 style: apply CSharpier formatting to all C# sources
Repo-wide sweep with the pinned CSharpier 1.3.0 tool. Whitespace and
line-wrapping only; OpenNest.Engine.Tests (109) and OpenNest.IO.Tests
pass after reformat, full solution builds 0 errors.

Added .csharpierignore so csproj/config XML keeps its existing layout
(CSharpier's XML wrapping churns attributes with zero benefit).

Formatting is now enforceable: dotnet csharpier check . passes.
2026-09-20 16:41:50 -04:00

377 lines
13 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using OpenNest.Engine.Strategies;
using OpenNest.Geometry;
using OpenNest.Math;
namespace OpenNest.Engine.Fill
{
public class FillExtents
{
private const int MaxIterations = 10;
private readonly Box workArea;
private readonly double partSpacing;
private readonly double halfSpacing;
public FillExtents(Box workArea, double partSpacing)
{
this.workArea = workArea;
this.partSpacing = partSpacing;
halfSpacing = partSpacing / 2;
}
public List<Part> Fill(
Drawing drawing,
double rotationAngle = 0,
CancellationToken token = default,
Action<List<Part>, string> reportProgress = null
)
{
var pair = BuildPair(drawing, rotationAngle);
if (pair == null)
return new List<Part>();
var column = BuildColumn(pair.Value);
if (column.Count == 0)
return new List<Part>();
reportProgress?.Invoke(column, $"Extents: initial column {column.Count} parts");
var adjusted = AdjustColumn(pair.Value, column, token);
// The iterative pair adjustment can shift parts enough to cause
// genuine overlap. Fall back to the unadjusted column when this happens.
if (HasOverlappingParts(adjusted))
{
Debug.WriteLine("[FillExtents] Adjusted column has overlaps, using unadjusted");
adjusted = column;
}
reportProgress?.Invoke(adjusted, $"Extents: column {adjusted.Count} parts");
var result = RepeatColumns(adjusted, token);
reportProgress?.Invoke(result, $"Extents: {result.Count} parts total");
return result;
}
// --- Step 1: Pair Construction ---
private PartPair? BuildPair(Drawing drawing, double rotationAngle)
{
var part1 = Part.CreateAtOrigin(drawing, rotationAngle);
var part2 = Part.CreateAtOrigin(drawing, rotationAngle + System.Math.PI);
// Check that each part fits in the work area individually.
if (
part1.BoundingBox.Width > workArea.Width + Tolerance.Epsilon
|| part1.BoundingBox.Length > workArea.Length + Tolerance.Epsilon
)
return null;
// Slide part2 toward part1 from the right using geometry-aware distance.
var boundary1 = new PartBoundary(part1, halfSpacing);
var boundary2 = new PartBoundary(part2, halfSpacing);
// Position part2 to the right of part1 at bounding box width distance.
var startOffset = part1.BoundingBox.Length + part2.BoundingBox.Length + partSpacing;
part2.Offset(startOffset, 0);
part2.UpdateBounds();
// Slide part2 left toward part1.
var movingLines = boundary2.GetLines(part2.Location, PushDirection.Left);
var stationaryLines = boundary1.GetLines(part1.Location, PushDirection.Right);
var dist = SpatialQuery.DirectionalDistance(
movingLines,
stationaryLines,
PushDirection.Left
);
if (dist < double.MaxValue && dist > 0)
{
part2.Offset(-dist, 0);
part2.UpdateBounds();
}
var pair = AnchorToWorkArea(part1, part2);
if (pair == null)
return null;
// Verify pair fits in work area.
if (
pair.Value.Bbox.Width > workArea.Width + Tolerance.Epsilon
|| pair.Value.Bbox.Length > workArea.Length + Tolerance.Epsilon
)
return null;
return pair;
}
// --- Step 2: Build Column (tile vertically) ---
private List<Part> BuildColumn(PartPair pair)
{
var column = new List<Part> { (Part)pair.Part1.Clone(), (Part)pair.Part2.Clone() };
// Find geometry-aware copy distance for the pair vertically.
var boundary1 = new PartBoundary(pair.Part1, halfSpacing);
var boundary2 = new PartBoundary(pair.Part2, halfSpacing);
// Compute vertical copy distance using bounding boxes as starting point,
// then slide down to find true geometry distance.
var pairHeight = pair.Bbox.Width;
var testOffset = new Vector(0, pairHeight);
// Create test parts for slide distance measurement.
var testPart1 = pair.Part1.CloneAtOffset(testOffset);
var testPart2 = pair.Part2.CloneAtOffset(testOffset);
// Find minimum distance from test pair sliding down toward original pair.
var copyDistance = FindVerticalCopyDistance(
pair.Part1,
pair.Part2,
testPart1,
testPart2,
boundary1,
boundary2,
pairHeight
);
if (copyDistance <= 0)
return column;
var count = 1;
while (true)
{
var nextBottom = pair.Bbox.Bottom + copyDistance * count;
if (nextBottom + pairHeight > workArea.Top + Tolerance.Epsilon)
break;
var offset = new Vector(0, copyDistance * count);
column.Add(pair.Part1.CloneAtOffset(offset));
column.Add(pair.Part2.CloneAtOffset(offset));
count++;
}
return column;
}
private double FindVerticalCopyDistance(
Part origPart1,
Part origPart2,
Part testPart1,
Part testPart2,
PartBoundary boundary1,
PartBoundary boundary2,
double pairHeight
)
{
// Check all 4 combinations: test parts sliding down toward original parts.
var slidePairs = new[]
{
(
moving: boundary1,
movingLoc: testPart1.Location,
stationary: boundary1,
stationaryLoc: origPart1.Location
),
(
moving: boundary1,
movingLoc: testPart1.Location,
stationary: boundary2,
stationaryLoc: origPart2.Location
),
(
moving: boundary2,
movingLoc: testPart2.Location,
stationary: boundary1,
stationaryLoc: origPart1.Location
),
(
moving: boundary2,
movingLoc: testPart2.Location,
stationary: boundary2,
stationaryLoc: origPart2.Location
),
};
var minSlide = double.MaxValue;
foreach (var (moving, movingLoc, stationary, stationaryLoc) in slidePairs)
{
var d = SlideDistance(
moving,
movingLoc,
stationary,
stationaryLoc,
PushDirection.Down
);
if (d < minSlide)
minSlide = d;
}
if (minSlide >= double.MaxValue || minSlide < 0)
return pairHeight + partSpacing;
// Match FillLinear.ComputeCopyDistance: copyDist = startOffset - slide,
// clamped so it never goes below pairHeight + partSpacing to prevent
// bounding-box overlap from spurious slide values.
var copyDist = pairHeight - minSlide;
return System.Math.Max(copyDist, pairHeight + partSpacing);
}
private static double SlideDistance(
PartBoundary movingBoundary,
Vector movingLocation,
PartBoundary stationaryBoundary,
Vector stationaryLocation,
PushDirection direction
)
{
var opposite = SpatialQuery.OppositeDirection(direction);
var movingEdges = movingBoundary.GetEdges(direction);
var stationaryEdges = stationaryBoundary.GetEdges(opposite);
return SpatialQuery.DirectionalDistance(
movingEdges,
movingLocation,
stationaryEdges,
stationaryLocation,
direction
);
}
// --- Step 3: Iterative Adjustment ---
private List<Part> AdjustColumn(PartPair pair, List<Part> column, CancellationToken token)
{
var originalPairWidth = pair.Bbox.Length;
for (var iteration = 0; iteration < MaxIterations; iteration++)
{
if (token.IsCancellationRequested)
break;
// Measure current gap.
var topEdge = double.MinValue;
foreach (var p in column)
if (p.BoundingBox.Top > topEdge)
topEdge = p.BoundingBox.Top;
var gap = workArea.Top - topEdge;
if (gap <= Tolerance.Epsilon)
break;
var pairCount = column.Count / 2;
if (pairCount <= 0)
break;
var adjustment = gap / pairCount;
if (adjustment <= Tolerance.Epsilon)
break;
// Try adjusting the pair and rebuilding the column.
var adjusted = TryAdjustPair(pair, adjustment, originalPairWidth);
if (adjusted == null)
break;
var newColumn = BuildColumn(adjusted.Value);
if (newColumn.Count == 0)
break;
column = newColumn;
pair = adjusted.Value;
}
return column;
}
private PartPair? TryAdjustPair(PartPair pair, double adjustment, double originalPairWidth)
{
// Try shifting part2 up first.
var result = TryShiftDirection(pair, adjustment, originalPairWidth);
if (result != null)
return result;
// Up made the pair wider — try down instead.
return TryShiftDirection(pair, -adjustment, originalPairWidth);
}
private PartPair? TryShiftDirection(
PartPair pair,
double verticalShift,
double originalPairWidth
)
{
// Clone parts so we don't mutate the originals.
var p1 = (Part)pair.Part1.Clone();
var p2 = (Part)pair.Part2.Clone();
// Separate: shift part2 right so bounding boxes don't touch.
p2.Offset(partSpacing, 0);
p2.UpdateBounds();
// Apply the vertical shift.
p2.Offset(0, verticalShift);
p2.UpdateBounds();
// Compact part2 left toward part1.
var moving = new List<Part> { p2 };
var obstacles = new List<Part> { p1 };
Compactor.Push(moving, obstacles, workArea, partSpacing, PushDirection.Left);
// Check if the pair got wider.
var newBbox = PairBbox(p1, p2);
if (newBbox.Length > originalPairWidth + Tolerance.Epsilon)
return null;
return AnchorToWorkArea(p1, p2);
}
// --- Step 4: Horizontal Repetition ---
private List<Part> RepeatColumns(List<Part> column, CancellationToken token)
{
if (column.Count == 0)
return column;
var pattern = new Pattern();
pattern.Parts.AddRange(column);
pattern.UpdateBounds();
var linear = new FillLinear(workArea, partSpacing);
return linear.Fill(pattern, NestDirection.Horizontal);
}
// --- Helpers ---
private PartPair? AnchorToWorkArea(Part part1, Part part2)
{
var bbox = PairBbox(part1, part2);
var anchor = new Vector(workArea.X - bbox.Left, workArea.Y - bbox.Bottom);
part1.Offset(anchor);
part2.Offset(anchor);
part1.UpdateBounds();
part2.UpdateBounds();
bbox = PairBbox(part1, part2);
return new PartPair(part1, part2, bbox);
}
private static Box PairBbox(Part part1, Part part2) =>
((IEnumerable<IBoundable>)new IBoundable[] { part1, part2 }).GetBoundingBox();
private static bool HasOverlappingParts(List<Part> parts) =>
FillHelpers.HasOverlappingParts(parts);
private readonly record struct PartPair(Part Part1, Part Part2, Box Bbox);
}
}