diff --git a/OpenNest.IO/TitleBlockDetector.cs b/OpenNest.IO/TitleBlockDetector.cs index 9093bd4..7b881a3 100644 --- a/OpenNest.IO/TitleBlockDetector.cs +++ b/OpenNest.IO/TitleBlockDetector.cs @@ -19,6 +19,8 @@ namespace OpenNest.IO var flagged = new HashSet(); DetectByLayerName(entities, flagged); DetectBorder(entities, flagged); + if (document != null) + DetectTitleBlockRegion(entities, document, flagged); return flagged; } @@ -126,5 +128,185 @@ namespace OpenNest.IO flagged.Add(line.Id); } } + + private static void DetectTitleBlockRegion(List entities, CadDocument document, HashSet flagged) + { + var textPositions = ExtractTextPositions(document); + if (textPositions.Count < 3) return; + + var unflagged = entities.Where(e => !flagged.Contains(e.Id)).ToList(); + if (unflagged.Count == 0) return; + + var bounds = entities.GetBoundingBox(); + if (bounds == null || bounds.Area() < OpenNest.Math.Tolerance.Epsilon) return; + + var bestRegion = FindBestTitleBlockRegion(bounds, textPositions, unflagged); + if (bestRegion == null) return; + + var initiallyInside = unflagged.Where(e => { + var c = EntityCenter(e); + return c.HasValue && RegionContains(bestRegion, c.Value); + }).ToList(); + + var expandedBounds = initiallyInside.Count > 0 ? initiallyInside.GetBoundingBox() : null; + + foreach (var entity in unflagged) + { + var center = EntityCenter(entity); + if (!center.HasValue) continue; + if (RegionContains(bestRegion, center.Value) + || (expandedBounds != null && RegionContains(expandedBounds, center.Value))) + flagged.Add(entity.Id); + } + } + + private static List ExtractTextPositions(CadDocument document) + { + var positions = new List(); + foreach (var entity in document.Entities) + { + switch (entity) + { + case ACadSharp.Entities.MText mtext: + positions.Add(new Vector(mtext.InsertPoint.X, mtext.InsertPoint.Y)); + break; + case ACadSharp.Entities.TextEntity text: + var pt = text.HorizontalAlignment != 0 || text.VerticalAlignment != 0 + ? text.AlignmentPoint : text.InsertPoint; + positions.Add(new Vector(pt.X, pt.Y)); + break; + } + } + return positions; + } + + private static Box FindBestTitleBlockRegion(Box bounds, List textPositions, List entities) + { + var candidates = GenerateCandidateRegions(bounds); + Box bestRegion = null; + var bestScore = 0.0; + + var openLines = FindOpenLines(entities); + + foreach (var region in candidates) + { + var textCount = textPositions.Count(p => RegionContains(region, p)); + if (textCount < 3) continue; + + var openLineCount = openLines.Count(l => RegionContains(region, l.MidPoint)); + + var area = region.Area(); + if (area < OpenNest.Math.Tolerance.Epsilon) continue; + + var score = (double)textCount + openLineCount * 0.5; + + var regionCenterX = (region.Left + region.Right) / 2; + var regionCenterY = (region.Bottom + region.Top) / 2; + if (regionCenterX > bounds.Center.X) score *= 1.3; + if (regionCenterY < bounds.Center.Y) score *= 1.3; + + if (score > bestScore) + { + bestScore = score; + bestRegion = region; + } + } + + return bestRegion; + } + + private static List GenerateCandidateRegions(Box bounds) + { + var regions = new List(); + var fractions = new[] { 0.25, 0.333, 0.5 }; + + foreach (var fx in fractions) + { + foreach (var fy in fractions) + { + var w = bounds.Length * fx; + var h = bounds.Width * fy; + + regions.Add(new Box(bounds.Right - w, bounds.Bottom, w, h)); + regions.Add(new Box(bounds.Left, bounds.Bottom, w, h)); + regions.Add(new Box(bounds.Right - w, bounds.Top - h, w, h)); + regions.Add(new Box(bounds.Left, bounds.Top - h, w, h)); + } + } + + foreach (var fy in fractions) + { + var h = bounds.Width * fy; + regions.Add(new Box(bounds.Left, bounds.Bottom, bounds.Length, h)); + } + + foreach (var fx in fractions) + { + var w = bounds.Length * fx; + regions.Add(new Box(bounds.Right - w, bounds.Bottom, w, bounds.Width)); + } + + return regions; + } + + private static List FindOpenLines(List entities) + { + var endpointUsers = new Dictionary(); + + foreach (var entity in entities) + { + foreach (var ep in GetEntityEndpoints(entity)) + { + var key = QuantizePoint(ep); + endpointUsers[key] = endpointUsers.GetValueOrDefault(key) + 1; + } + } + + var openLines = new List(); + foreach (var line in entities.OfType()) + { + var startKey = QuantizePoint(line.StartPoint); + var endKey = QuantizePoint(line.EndPoint); + + if (endpointUsers.GetValueOrDefault(startKey) <= 1 || endpointUsers.GetValueOrDefault(endKey) <= 1) + openLines.Add(line); + } + + return openLines; + } + + private static List GetEntityEndpoints(Entity entity) + { + return entity switch + { + Line line => new List { line.StartPoint, line.EndPoint }, + Arc arc => new List { arc.StartPoint(), arc.EndPoint() }, + _ => new List() + }; + } + + private static long QuantizePoint(Vector pt) + { + var qx = (long)(pt.X * 1000); + var qy = (long)(pt.Y * 1000); + return qx * 100000000L + qy; + } + + private static Vector? EntityCenter(Entity entity) + { + return entity switch + { + Line line => line.MidPoint, + Arc arc => arc.Center, + Circle circle => circle.Center, + _ => null + }; + } + + private static bool RegionContains(Box box, Vector pt) + { + return pt.X >= box.Left && pt.X <= box.Right + && pt.Y >= box.Bottom && pt.Y <= box.Top; + } } } diff --git a/OpenNest.Tests/IO/TitleBlockDetectorTests.cs b/OpenNest.Tests/IO/TitleBlockDetectorTests.cs index c0eed17..e807f31 100644 --- a/OpenNest.Tests/IO/TitleBlockDetectorTests.cs +++ b/OpenNest.Tests/IO/TitleBlockDetectorTests.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using CSMath; using OpenNest.Geometry; using OpenNest.IO; using Xunit; @@ -148,5 +149,97 @@ namespace OpenNest.Tests.IO Assert.Contains(entities[0].Id, result); } + + [Fact] + public void DetectTitleBlock_FlagsEntitiesInTextDenseCorner() + { + var partLine1 = new Line(5, 70, 25, 120) { Layer = new Layer("0") }; + var partLine2 = new Line(25, 120, 45, 70) { Layer = new Layer("0") }; + var partLine3 = new Line(45, 70, 5, 70) { Layer = new Layer("0") }; + + var tbLines = new List(); + for (var x = 50; x <= 85; x += 5) + tbLines.Add(new Line(x, 0, x, 30) { Layer = new Layer("0") }); + for (var y = 0; y <= 30; y += 5) + tbLines.Add(new Line(50, y, 85, y) { Layer = new Layer("0") }); + + var entities = new List { partLine1, partLine2, partLine3 }; + entities.AddRange(tbLines); + + var doc = BuildDocWithTexts( + (60, 5, "TITLE: Test Part"), + (60, 10, "DWG NO: 12345"), + (60, 15, "SCALE: 1:1"), + (60, 20, "REV: A"), + (60, 25, "MATERIAL: STEEL")); + + var result = TitleBlockDetector.Detect(entities, doc); + + foreach (var tb in tbLines) + Assert.Contains(tb.Id, result); + Assert.DoesNotContain(partLine1.Id, result); + Assert.DoesNotContain(partLine2.Id, result); + Assert.DoesNotContain(partLine3.Id, result); + } + + [Fact] + public void DetectTitleBlock_NoFalsePositivesWithoutText() + { + var entities = new List + { + 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 DetectTitleBlock_BottomEdgeStrip() + { + var partLine = new Line(20, 40, 80, 40) { Layer = new Layer("0") }; + + var tbLines = new List(); + for (var x = 0; x <= 100; x += 10) + tbLines.Add(new Line(x, 0, x, 20) { Layer = new Layer("0") }); + for (var y = 0; y <= 20; y += 5) + tbLines.Add(new Line(0, y, 100, y) { Layer = new Layer("0") }); + + var entities = new List { partLine }; + entities.AddRange(tbLines); + + var doc = BuildDocWithTexts( + (10, 5, "TITLE"), + (30, 5, "DWG NO"), + (50, 5, "SCALE"), + (70, 5, "REV"), + (90, 5, "DATE")); + + var result = TitleBlockDetector.Detect(entities, doc); + + foreach (var tb in tbLines) + Assert.Contains(tb.Id, result); + Assert.DoesNotContain(partLine.Id, result); + } + + private static ACadSharp.CadDocument BuildDocWithTexts( + params (double x, double y, string value)[] texts) + { + var doc = new ACadSharp.CadDocument(); + foreach (var (x, y, value) in texts) + { + var mtext = new ACadSharp.Entities.MText + { + InsertPoint = new XYZ(x, y, 0), + Value = value, + Height = 2.0 + }; + doc.Entities.Add(mtext); + } + return doc; + } } }