feat(io): add border detection with angular tolerance and zone markers (phase 2)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@ namespace OpenNest.IO
|
|||||||
{
|
{
|
||||||
var flagged = new HashSet<Guid>();
|
var flagged = new HashSet<Guid>();
|
||||||
DetectByLayerName(entities, flagged);
|
DetectByLayerName(entities, flagged);
|
||||||
|
DetectBorder(entities, flagged);
|
||||||
return flagged;
|
return flagged;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,5 +30,101 @@ namespace OpenNest.IO
|
|||||||
flagged.Add(entity.Id);
|
flagged.Add(entity.Id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void DetectBorder(List<Entity> entities, HashSet<Guid> flagged)
|
||||||
|
{
|
||||||
|
var lines = entities.OfType<Line>().Where(l => !flagged.Contains(l.Id)).ToList();
|
||||||
|
if (lines.Count < 4) return;
|
||||||
|
|
||||||
|
var bounds = entities.GetBoundingBox();
|
||||||
|
if (bounds == null || bounds.Area() < OpenNest.Math.Tolerance.Epsilon) return;
|
||||||
|
|
||||||
|
var borderCount = 0;
|
||||||
|
foreach (var line in lines)
|
||||||
|
{
|
||||||
|
if (IsBorderLine(line, bounds))
|
||||||
|
{
|
||||||
|
flagged.Add(line.Id);
|
||||||
|
borderCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (borderCount >= 2)
|
||||||
|
DetectZoneMarkers(lines, bounds, flagged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsBorderLine(Line line, Box bounds)
|
||||||
|
{
|
||||||
|
var dx = line.EndPoint.X - line.StartPoint.X;
|
||||||
|
var dy = line.EndPoint.Y - line.StartPoint.Y;
|
||||||
|
var length = System.Math.Sqrt(dx * dx + dy * dy);
|
||||||
|
var angleRad = System.Math.Atan2(System.Math.Abs(dy), System.Math.Abs(dx));
|
||||||
|
var angularTolerance = OpenNest.Math.Angle.ToRadians(2.0);
|
||||||
|
var positionTolerance = System.Math.Max(bounds.Length, bounds.Width) * 0.01;
|
||||||
|
|
||||||
|
var isHorizontal = angleRad < angularTolerance;
|
||||||
|
var isVertical = System.Math.Abs(angleRad - System.Math.PI / 2) < angularTolerance;
|
||||||
|
|
||||||
|
if (!isHorizontal && !isVertical) return false;
|
||||||
|
|
||||||
|
var minSpan = isHorizontal ? bounds.Length * 0.8 : bounds.Width * 0.8;
|
||||||
|
if (length < minSpan) return false;
|
||||||
|
|
||||||
|
if (isHorizontal)
|
||||||
|
{
|
||||||
|
var midY = (line.StartPoint.Y + line.EndPoint.Y) / 2;
|
||||||
|
return System.Math.Abs(midY - bounds.Bottom) < positionTolerance
|
||||||
|
|| System.Math.Abs(midY - bounds.Top) < positionTolerance;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var midX = (line.StartPoint.X + line.EndPoint.X) / 2;
|
||||||
|
return System.Math.Abs(midX - bounds.Left) < positionTolerance
|
||||||
|
|| System.Math.Abs(midX - bounds.Right) < positionTolerance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DetectZoneMarkers(List<Line> lines, Box bounds, HashSet<Guid> flagged)
|
||||||
|
{
|
||||||
|
var positionTolerance = System.Math.Max(bounds.Length, bounds.Width) * 0.01;
|
||||||
|
var maxTickLength = System.Math.Max(bounds.Length, bounds.Width) * 0.05;
|
||||||
|
var angularTolerance = OpenNest.Math.Angle.ToRadians(2.0);
|
||||||
|
|
||||||
|
foreach (var line in lines)
|
||||||
|
{
|
||||||
|
if (flagged.Contains(line.Id)) continue;
|
||||||
|
|
||||||
|
var dx = line.EndPoint.X - line.StartPoint.X;
|
||||||
|
var dy = line.EndPoint.Y - line.StartPoint.Y;
|
||||||
|
var length = System.Math.Sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
|
if (length > maxTickLength || length < OpenNest.Math.Tolerance.Epsilon) continue;
|
||||||
|
|
||||||
|
var angleRad = System.Math.Atan2(System.Math.Abs(dy), System.Math.Abs(dx));
|
||||||
|
var isVertical = System.Math.Abs(angleRad - System.Math.PI / 2) < angularTolerance;
|
||||||
|
var isHorizontal = angleRad < angularTolerance;
|
||||||
|
|
||||||
|
if (!isVertical && !isHorizontal) continue;
|
||||||
|
|
||||||
|
var touchesEdge = false;
|
||||||
|
if (isVertical)
|
||||||
|
{
|
||||||
|
var minY = System.Math.Min(line.StartPoint.Y, line.EndPoint.Y);
|
||||||
|
var maxY = System.Math.Max(line.StartPoint.Y, line.EndPoint.Y);
|
||||||
|
touchesEdge = System.Math.Abs(minY - bounds.Bottom) < positionTolerance
|
||||||
|
|| System.Math.Abs(maxY - bounds.Top) < positionTolerance;
|
||||||
|
}
|
||||||
|
else if (isHorizontal)
|
||||||
|
{
|
||||||
|
var minX = System.Math.Min(line.StartPoint.X, line.EndPoint.X);
|
||||||
|
var maxX = System.Math.Max(line.StartPoint.X, line.EndPoint.X);
|
||||||
|
touchesEdge = System.Math.Abs(minX - bounds.Left) < positionTolerance
|
||||||
|
|| System.Math.Abs(maxX - bounds.Right) < positionTolerance;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (touchesEdge)
|
||||||
|
flagged.Add(line.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,5 +66,87 @@ namespace OpenNest.Tests.IO
|
|||||||
|
|
||||||
Assert.Contains(line.Id, result);
|
Assert.Contains(line.Id, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DetectBorder_FlagsLinesOnBoundingBoxEdges()
|
||||||
|
{
|
||||||
|
var entities = new List<Entity>
|
||||||
|
{
|
||||||
|
new Line(0, 0, 86, 0) { Layer = new Layer("0") },
|
||||||
|
new Line(86, 0, 86, 134) { Layer = new Layer("0") },
|
||||||
|
new Line(86, 134, 0, 134) { Layer = new Layer("0") },
|
||||||
|
new Line(0, 134, 0, 0) { Layer = new Layer("0") },
|
||||||
|
new Line(30, 40, 50, 90) { Layer = new Layer("0") },
|
||||||
|
new Line(50, 90, 70, 40) { Layer = new Layer("0") },
|
||||||
|
new Line(70, 40, 30, 40) { Layer = new Layer("0") },
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = TitleBlockDetector.Detect(entities, null);
|
||||||
|
|
||||||
|
Assert.Contains(entities[0].Id, result);
|
||||||
|
Assert.Contains(entities[1].Id, result);
|
||||||
|
Assert.Contains(entities[2].Id, result);
|
||||||
|
Assert.Contains(entities[3].Id, result);
|
||||||
|
Assert.DoesNotContain(entities[4].Id, result);
|
||||||
|
Assert.DoesNotContain(entities[5].Id, result);
|
||||||
|
Assert.DoesNotContain(entities[6].Id, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DetectBorder_FlagsZoneMarkerTicks()
|
||||||
|
{
|
||||||
|
var entities = new List<Entity>
|
||||||
|
{
|
||||||
|
new Line(0, 0, 100, 0) { Layer = new Layer("0") },
|
||||||
|
new Line(100, 0, 100, 80) { Layer = new Layer("0") },
|
||||||
|
new Line(100, 80, 0, 80) { Layer = new Layer("0") },
|
||||||
|
new Line(0, 80, 0, 0) { Layer = new Layer("0") },
|
||||||
|
new Line(25, 80, 25, 77) { Layer = new Layer("0") },
|
||||||
|
new Line(50, 80, 50, 77) { Layer = new Layer("0") },
|
||||||
|
new Line(75, 80, 75, 77) { Layer = new Layer("0") },
|
||||||
|
new Line(40, 30, 60, 30) { Layer = new Layer("0") },
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = TitleBlockDetector.Detect(entities, null);
|
||||||
|
|
||||||
|
Assert.Contains(entities[4].Id, result);
|
||||||
|
Assert.Contains(entities[5].Id, result);
|
||||||
|
Assert.Contains(entities[6].Id, result);
|
||||||
|
Assert.DoesNotContain(entities[7].Id, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DetectBorder_IgnoresWhenNoBorderPresent()
|
||||||
|
{
|
||||||
|
var entities = new List<Entity>
|
||||||
|
{
|
||||||
|
new Line(30, 40, 50, 90) { Layer = new Layer("0") },
|
||||||
|
new Line(50, 90, 70, 40) { Layer = new Layer("0") },
|
||||||
|
new Line(70, 40, 30, 40) { Layer = new Layer("0") },
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = TitleBlockDetector.Detect(entities, null);
|
||||||
|
|
||||||
|
Assert.Empty(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DetectBorder_ToleratesSlightRotation()
|
||||||
|
{
|
||||||
|
var angleRad = OpenNest.Math.Angle.ToRadians(0.5);
|
||||||
|
var endY = 86 * System.Math.Sin(angleRad);
|
||||||
|
var entities = new List<Entity>
|
||||||
|
{
|
||||||
|
new Line(0, 0, 86, endY) { Layer = new Layer("0") },
|
||||||
|
new Line(86, endY, 86, 134) { Layer = new Layer("0") },
|
||||||
|
new Line(86, 134, 0, 134) { Layer = new Layer("0") },
|
||||||
|
new Line(0, 134, 0, 0) { Layer = new Layer("0") },
|
||||||
|
new Line(30, 40, 50, 90) { Layer = new Layer("0") },
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = TitleBlockDetector.Detect(entities, null);
|
||||||
|
|
||||||
|
Assert.Contains(entities[0].Id, result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user