Add tested caller-stock StockLadder baseline with strict geometry validation

This commit is contained in:
aj
2026-09-19 11:24:36 -04:00
parent 9b69c67572
commit ea4bd836cd
11 changed files with 759 additions and 11 deletions
@@ -76,14 +76,131 @@ internal static class NestJobPlacementValidator
var contours = ShapeBuilder.GetShapes(cutEntities);
if (contours.Count == 0) throw new ArgumentException("Geometry must contain a closed contour.");
var closedEntities = new List<Entity>();
var marks = new List<Shape>();
foreach (var contour in contours)
ValidateContour(contour);
{
if (contour.IsClosed())
{
ValidateContour(contour);
closedEntities.AddRange(contour.Entities);
}
else marks.Add(contour);
}
if (closedEntities.Count == 0)
throw new ArgumentException("Geometry must contain a closed outer contour.");
var profile = new ShapeProfile(cutEntities);
// ShapeProfile selects the outer profile, but does not validate containment and
// treats open chains as cutouts. Only validated closed contours may define material.
var profile = new ShapeProfile(closedEntities);
foreach (var cutout in profile.Cutouts)
ValidateInternalChain(cutout, profile.Perimeter, new List<Shape>());
foreach (var mark in marks)
ValidateMark(mark, profile.Perimeter, profile.Cutouts);
profile.NormalizeWinding();
return new ShapeTopology(profile.Perimeter, profile.Cutouts);
}
private static void ValidateMark(Shape mark, Shape perimeter, List<Shape> holes)
{
const double chordTolerance = 0.00001;
var boundaries = new List<Shape> { perimeter };
boundaries.AddRange(holes);
var polygons = boundaries.ConvertAll(s => s.ToPolygonWithTolerance(chordTolerance));
foreach (var entity in mark.Entities)
{
if (entity.Length <= Epsilon || entity is not (Line or Arc))
throw new ArgumentException("Unsupported or degenerate internal mark.");
var parameters = new List<double> { 0, 1 };
foreach (var boundary in boundaries)
{
entity.Intersects(boundary, out var intersections);
foreach (var point in intersections)
AddParameter(point);
// Include endpoints of coincident edges (parallel intersections may be empty).
foreach (var point in boundary.Entities.CollectPoints())
if (entity.ClosestPointTo(point).DistanceTo(point) <= Epsilon)
AddParameter(point);
}
parameters.Sort();
for (var index = 0; index < parameters.Count; index++)
{
Check(PointAt(parameters[index]));
if (index > 0) Check(PointAt((parameters[index - 1] + parameters[index]) / 2));
}
void AddParameter(Vector point)
{
if (!point.IsValid()) throw new ArgumentException("Indeterminate mark intersection.");
var value = entity is Line line
? line.StartPoint.DistanceTo(point) / line.Length
: Angle.NormalizeRad(((Arc)entity).IsReversed
? ((Arc)entity).StartAngle - ((Arc)entity).Center.AngleTo(point)
: ((Arc)entity).Center.AngleTo(point) - ((Arc)entity).StartAngle) / ((Arc)entity).SweepAngle();
if (value >= 0 && value <= 1) parameters.Add(value);
}
Vector PointAt(double value)
{
if (entity is Line line) return line.StartPoint + (line.EndPoint - line.StartPoint) * value;
var arc = (Arc)entity;
var angle = arc.StartAngle + (arc.IsReversed ? -1 : 1) * arc.SweepAngle() * value;
return arc.Center + new Vector(System.Math.Cos(angle), System.Math.Sin(angle)) * arc.Radius;
}
void Check(Vector point)
{
for (var index = 0; index < boundaries.Count; index++)
{
// Exact analytic boundary contact is allowed; near-boundary uncertainty is not.
var onBoundary = false;
foreach (var edge in boundaries[index].Entities)
if (edge.ClosestPointTo(point).DistanceTo(point) <= Epsilon) onBoundary = true;
if (onBoundary) continue;
foreach (var edge in polygons[index].ToLines())
if (edge.ClosestPointTo(point).DistanceTo(point) <= 2 * chordTolerance)
throw new ArgumentException("Internal mark is too close to a material boundary.");
var inside = StrictlyInside(polygons[index], point);
if (index == 0 ? !inside : inside)
throw new ArgumentException("Open geometry leaves the closed material region.");
}
}
}
}
private static void ValidateInternalChain(Shape chain, Shape perimeter, List<Shape> holes)
{
// A connected analytic entity cannot leave material without crossing its boundary.
// Reject contact too: conservative, rather than guessing at tangent/collinear cuts.
// The witness point is farther than the polygonization error from every boundary.
const double chordTolerance = 0.00001;
var boundaries = new List<Shape> { perimeter };
boundaries.AddRange(holes);
var polygons = boundaries.ConvertAll(s => s.ToPolygonWithTolerance(chordTolerance));
foreach (var entity in chain.Entities)
{
if (entity.Length <= Epsilon)
throw new ArgumentException("Geometry contains a zero-length internal edge.");
var point = entity switch
{
Line line => line.StartPoint,
Arc arc => arc.StartPoint(),
Circle circle => circle.Center.Offset(circle.Radius, 0),
_ => throw new ArgumentException("Unsupported internal geometry.")
};
if (!StrictlyInside(polygons[0], point))
throw new ArgumentException("Open or disconnected geometry lies outside the closed perimeter.");
for (var index = 0; index < boundaries.Count; index++)
{
if (index > 0 && polygons[index].ContainsPoint(point))
throw new ArgumentException("Internal geometry lies in a cutout.");
foreach (var edge in polygons[index].ToLines())
if (edge.ClosestPointTo(point).DistanceTo(point) <= 2 * chordTolerance)
throw new ArgumentException("Internal geometry is too close to a material boundary.");
if (entity.Intersects(boundaries[index]))
throw new ArgumentException("Internal geometry crosses or touches a material boundary.");
}
}
}
private static void ValidateContour(Shape contour)
{
if (!contour.IsClosed())