using System; using System.Collections.Generic; using System.Linq; using ACadSharp; using OpenNest.Geometry; namespace OpenNest.IO { public static class TitleBlockDetector { private static readonly HashSet TitleBlockLayerNames = new(StringComparer.OrdinalIgnoreCase) { "TITLE", "TITLEBLOCK", "TITLE_BLOCK", "BORDER", "FRAME", "TB", "INFO", "SHEET", "ANNOTATION" }; public static HashSet Detect(List entities, CadDocument document) { var flagged = new HashSet(); DetectByLayerName(entities, flagged); DetectBorder(entities, flagged); if (document != null) DetectTitleBlockRegion(entities, document, flagged); return flagged; } private static void DetectByLayerName(List entities, HashSet flagged) { foreach (var entity in entities) { if (entity.Layer?.Name != null && TitleBlockLayerNames.Contains(entity.Layer.Name)) flagged.Add(entity.Id); } } private static void DetectBorder(List entities, HashSet flagged) { var lines = entities.OfType().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 lines, Box bounds, HashSet 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); } } 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; } } }