using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using OpenNest.Geometry;
namespace OpenNest.Engine.Fill
{
public static partial class Expander
{
///
/// The lattice a selection repeats on. Parts of one drawing at one
/// rotation form a motif group; every group sits on the same two
/// translations and , so a fill of
/// interlocked pairs is one pattern with a two-part motif.
///
internal sealed class RepeatingPattern
{
public Vector A;
/// Second translation; for a single row
/// or for .
public Vector B;
///
/// True for rows that share the pitch but stack at
/// their own spacing, such as staggered rows topped by straight ones.
/// Each part is its own lattice point.
///
public bool Rows;
///
/// Per selected part, the lattice point of its motif copy. Parts of
/// one copy share it, so stretching the lattice keeps every motif rigid.
///
public Vector[] LatticePoints;
// Lattice coordinates may miss a whole number by this fraction of a
// pitch, which absorbs the four-decimal rounding of saved positions.
private const double CoordinateTolerance = 0.02;
// Rotations closer than this are the same motif orientation.
private const double RotationTolerance = 1e-6;
///
/// The pattern the parts repeat on, or null when they do not form one.
/// A pattern must over-determine its lattice: a row needs three parts
/// of one motif group and a grid four, since any two or three points
/// trivially fit some lattice. Every group needs at least two parts.
/// A single motif group that fits no one lattice may still form
/// .
///
public static RepeatingPattern Detect(IReadOnlyList parts)
{
if (parts == null || parts.Count < 3 || parts.Any(p => p.BaseDrawing == null || p.BaseDrawing.IsCutOff))
return null;
var centers = parts.Select(p => p.BoundingBox.Center).ToArray();
var groups = Group(parts);
if (groups.Any(g => g.Count < 2))
return null;
return DetectLattice(groups, centers)
?? DetectRows(parts, groups, centers, horizontal: true)
?? DetectRows(parts, groups, centers, horizontal: false);
}
private static RepeatingPattern DetectLattice(List> groups, Vector[] centers)
{
if (!TryBasis(groups, centers, out var a, out var b))
return null;
var rank = b.X == 0 && b.Y == 0 ? 1 : 2;
if (groups.Max(g => g.Count) < rank + 2)
return null;
// Each group's copies pair with the first group's copies through
// its part nearest that group's origin: the reduced motif offset,
// so an interlocked partner moves with its own copy.
var origin = centers[groups[0][0]];
var points = new Vector[centers.Length];
foreach (var group in groups)
{
var groupOrigin = centers[group.OrderBy(i => centers[i].DistanceTo(origin)).First()];
foreach (var index in group)
{
if (!TryCoordinates(centers[index] - groupOrigin, a, b, out var u, out var v))
return null;
points[index] = origin + a * u + b * v;
}
}
return new RepeatingPattern { A = a, B = b, LatticePoints = points };
}
///
/// Rows of one motif along X (or Y): at least two rows, each of at
/// least two parts and one of at least three, whose steps along the
/// row are all whole multiples of one shared pitch. Rows may stack at
/// any spacing and offset, so stretching moves each row rigidly.
///
private static RepeatingPattern DetectRows(
IReadOnlyList parts, List> groups, Vector[] centers, bool horizontal)
{
if (groups.Count != 1)
return null;
double Along(Vector v) => horizontal ? v.X : v.Y;
double Across(Vector v) => horizontal ? v.Y : v.X;
// Parts of one row share their cross coordinate to within a
// sliver of the part's own size.
var box = parts[0].BoundingBox;
var rowTolerance = CoordinateTolerance * System.Math.Min(box.Length, box.Width);
var rows = new List>();
foreach (var center in centers.OrderBy(Across))
{
if (rows.Count == 0 || Across(center) - Across(rows[^1][0]) > rowTolerance)
rows.Add(new List());
rows[^1].Add(center);
}
if (rows.Count < 2 || rows.Any(r => r.Count < 2) || rows.Max(r => r.Count) < 3)
return null;
var steps = rows
.SelectMany(r =>
{
var along = r.Select(Along).OrderBy(x => x).ToList();
return along.Skip(1).Select((x, i) => x - along[i]);
})
.ToList();
var pitch = steps.Min();
if (pitch <= rowTolerance)
return null;
if (steps.Any(step => System.Math.Abs(step / pitch - System.Math.Round(step / pitch)) > CoordinateTolerance))
return null;
return new RepeatingPattern
{
A = horizontal ? new Vector(pitch, 0) : new Vector(0, pitch),
B = Vector.Zero,
Rows = true,
LatticePoints = centers.ToArray(),
};
}
private static List> Group(IReadOnlyList parts)
{
var groups = new List>();
for (var i = 0; i < parts.Count; i++)
{
var group = groups.FirstOrDefault(g =>
ReferenceEquals(parts[g[0]].BaseDrawing, parts[i].BaseDrawing)
&& SameRotation(parts[g[0]].Rotation, parts[i].Rotation));
if (group == null)
groups.Add(group = new List());
group.Add(i);
}
return groups;
}
private static bool SameRotation(double a, double b)
{
var difference = OpenNest.Math.Angle.NormalizeRad(a - b);
return difference < RotationTolerance || OpenNest.Math.Angle.TwoPI - difference < RotationTolerance;
}
///
/// The shortest translation between two parts of one group, then the
/// shortest one not parallel to it. Coincident parts are no pattern.
///
private static bool TryBasis(List> groups, Vector[] centers, out Vector a, out Vector b)
{
a = Vector.Zero;
b = Vector.Zero;
var shortest = double.MaxValue;
foreach (var d in Translations(groups, centers))
{
var length = Length(d);
if (length < 1e-6)
return false;
if (length < shortest)
{
shortest = length;
a = d;
}
}
var second = double.MaxValue;
foreach (var d in Translations(groups, centers))
{
var length = Length(d);
if (System.Math.Abs(Cross(a, d)) <= 1e-3 * shortest * length)
continue;
if (length < second)
{
second = length;
b = d;
}
}
return true;
}
private static IEnumerable Translations(List> groups, Vector[] centers)
{
foreach (var group in groups)
for (var i = 0; i < group.Count; i++)
for (var j = i + 1; j < group.Count; j++)
yield return centers[group[j]] - centers[group[i]];
}
private static bool TryCoordinates(Vector d, Vector a, Vector b, out double u, out double v)
{
if (b.X == 0 && b.Y == 0)
{
// A row: on the line through a, at a whole multiple of it.
var lengthA = Length(a);
var exact = (d.X * a.X + d.Y * a.Y) / (lengthA * lengthA);
u = System.Math.Round(exact);
v = 0;
var offRow = System.Math.Abs(Cross(a, d)) / lengthA;
return offRow <= CoordinateTolerance * lengthA
&& System.Math.Abs(exact - u) <= CoordinateTolerance;
}
var det = Cross(a, b);
var uExact = Cross(d, b) / det;
var vExact = Cross(a, d) / det;
u = System.Math.Round(uExact);
v = System.Math.Round(vExact);
return System.Math.Abs(uExact - u) <= CoordinateTolerance
&& System.Math.Abs(vExact - v) <= CoordinateTolerance;
}
private static double Cross(Vector a, Vector b) => a.X * b.Y - a.Y * b.X;
private static double Length(Vector v) => System.Math.Sqrt(v.X * v.X + v.Y * v.Y);
}
///
/// Spreads a selection that repeats on a lattice by stretching the
/// lattice along X, then Y, as far as the work area and every clearance
/// floor allow, so the pattern survives instead of being pushed apart
/// pair by pair. Returns null when the selection is no pattern, overlaps
/// or starts outside the work area; the pair search handles those.
///
private static Result StretchPattern(
SeparationContext context,
List entry,
List selected,
double sMin,
Options opt,
CancellationToken token
)
{
if (sMin <= 0 || context.WorkAreaViolations(entry).Count > 0)
return null;
var pattern = RepeatingPattern.Detect(selected);
if (pattern == null)
return null;
// The tightest selected pair sets the floor: no selected gap closes
// below it, and gaps to other parts keep the usual floors at it.
var floor = double.MaxValue;
foreach (var pair in context.Pairs)
if (context.IsMover(pair.IndexB))
floor = System.Math.Min(floor, pair.EntryFloor);
if (!(floor > 0) || floor == double.MaxValue)
return null;
var stretch = new PatternStretch(context, entry, selected, pattern.LatticePoints, floor);
if (!stretch.Spread(horizontal: true, opt.Tolerance, token)
|| !stretch.Spread(horizontal: false, opt.Tolerance, token))
return new Result { Canceled = true };
var positions = stretch.Positions();
if (!context.MeetsEntryFloors(positions, entry, floor))
throw new InvalidOperationException("Pattern stretch violates an entry clearance floor.");
double measured;
try
{
measured = MinimumPairClearance(context, positions, token);
}
catch (OperationCanceledException) when (token.IsCancellationRequested)
{
return new Result { Canceled = true };
}
if (token.IsCancellationRequested)
return new Result { Canceled = true };
Apply(selected, entry, positions);
return new Result
{
AchievedSpacing = System.Math.Max(0, measured),
KeptPattern = true,
PatternMoved = stretch.ScaleX > 1 || stretch.ScaleY > 1,
};
}
///
/// Per-axis scale of a pattern's lattice points about their center, plus
/// the shift that keeps the stretched selection inside the work area.
/// A part moves by the displacement of its lattice point.
///
private sealed class PatternStretch
{
private readonly SeparationContext context;
private readonly List entry;
private readonly Box[] bounds;
private readonly Vector[] lattice;
private readonly Vector center;
private readonly double floor;
public double ScaleX = 1, ScaleY = 1;
private double shiftX, shiftY;
public PatternStretch(
SeparationContext context, List entry, List selected,
Vector[] lattice, double floor)
{
this.context = context;
this.entry = entry;
this.lattice = lattice;
this.floor = floor;
bounds = selected.Select(p => p.BoundingBox).ToArray();
center = new Vector(
(lattice.Min(p => p.X) + lattice.Max(p => p.X)) / 2,
(lattice.Min(p => p.Y) + lattice.Max(p => p.Y)) / 2);
}
public List Positions() => Positions(ScaleX, shiftX, ScaleY, shiftY);
private List Positions(double scaleX, double offsetX, double scaleY, double offsetY)
{
var positions = new List(entry);
for (var i = 0; i < lattice.Length; i++)
positions[i] = new Vector(
entry[i].X + (scaleX - 1) * (lattice[i].X - center.X) + offsetX,
entry[i].Y + (scaleY - 1) * (lattice[i].Y - center.Y) + offsetY);
return positions;
}
///
/// True when no selected part passes through a fixed part, cut-off or
/// clamp on its straight way from the entry layout, so a pattern never
/// spreads to the far side of material it started beside.
///
private bool Unobstructed(List positions)
{
if (!context.HasWalls)
return true;
for (var i = 0; i < lattice.Length; i++)
{
var move = positions[i] - entry[i];
var distance = System.Math.Sqrt(move.X * move.X + move.Y * move.Y);
if (distance <= 1e-9)
continue;
var room = context.ClipToMaterial(i, entry, move / distance, distance, wallsOnly: true);
if (room < distance - 1e-6)
return false;
}
return true;
}
private enum Placement { Center, Low, High }
///
/// Finds the largest scale along one axis whose stretched selection
/// fits the work area and keeps every floor, trying the selection
/// centered, then held at its low and high edges. Returns false when
/// canceled.
///
public bool Spread(bool horizontal, double tolerance, CancellationToken token)
{
double Of(Vector v) => horizontal ? v.X : v.Y;
double LowOf(Box box) => horizontal ? box.Left : box.Bottom;
double HighOf(Box box) => horizontal ? box.Right : box.Top;
var mid = Of(center);
var reach = lattice.Max(Of) - lattice.Min(Of);
if (reach <= tolerance)
return true; // one row or column along this axis
var area = context.WorkArea;
var areaLow = LowOf(area);
var areaHigh = HighOf(area);
var entryLow = bounds.Min(LowOf);
var entryHigh = bounds.Max(HighOf);
// Stretched extent with no shift; null when it is wider than the area.
(double Low, double High)? Extent(double scale)
{
var low = double.MaxValue;
var high = double.MinValue;
for (var i = 0; i < bounds.Length; i++)
{
var move = (scale - 1) * (Of(lattice[i]) - mid);
low = System.Math.Min(low, LowOf(bounds[i]) + move);
high = System.Math.Max(high, HighOf(bounds[i]) + move);
}
return high - low <= areaHigh - areaLow ? (low, high) : null;
}
double ShiftFor(double scale, Placement placement)
{
var (low, high) = Extent(scale).Value;
var shift = placement switch
{
Placement.Low => entryLow - low,
Placement.High => entryHigh - high,
_ => (entryLow + entryHigh - low - high) / 2,
};
return System.Math.Clamp(shift, areaLow - low, areaHigh - high);
}
bool Valid(double scale, Placement placement)
{
var shift = ShiftFor(scale, placement);
var positions = horizontal
? Positions(scale, shift, ScaleY, shiftY)
: Positions(ScaleX, shiftX, scale, shift);
return context.MeetsEntryFloors(positions, entry, floor) && Unobstructed(positions);
}
// The stretched extent is at least (scale - 1) * reach, so this
// upper bound never fits; the extent is convex in the scale.
var step = tolerance / reach;
var fits = 1.0;
var wide = 1 + (areaHigh - areaLow) / reach;
while (wide - fits > step)
{
var probe = (fits + wide) / 2;
if (Extent(probe) != null)
fits = probe;
else
wide = probe;
}
if (fits <= 1 + step)
return true;
var bestScale = 1.0;
var bestPlacement = Placement.Center;
foreach (var placement in new[] { Placement.Center, Placement.Low, Placement.High })
{
if (token.IsCancellationRequested)
return false;
if (Valid(fits, placement))
{
bestScale = fits;
bestPlacement = placement;
break;
}
// Scale 1 is the entry layout, which meets every floor.
var good = 1.0;
var bad = fits;
while (bad - good > step)
{
if (token.IsCancellationRequested)
return false;
var probe = (good + bad) / 2;
if (Valid(probe, placement))
good = probe;
else
bad = probe;
}
if (good > bestScale)
{
bestScale = good;
bestPlacement = placement;
}
}
if (bestScale <= 1)
return true;
var bestShift = ShiftFor(bestScale, bestPlacement);
if (horizontal)
{
ScaleX = bestScale;
shiftX = bestShift;
}
else
{
ScaleY = bestScale;
shiftY = bestShift;
}
return true;
}
}
}
}