From 3abb3c07cef6a94f2e84527a3d96a475d6117b79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=8A=B9=EC=9A=B0=EC=9A=B0?= Date: Thu, 31 Jul 2025 11:23:46 +0900 Subject: [PATCH] raycasting note content box detect --- Models/DwgDataExtractor.cs | 416 +++++++++++++++++++++---------------- 1 file changed, 234 insertions(+), 182 deletions(-) diff --git a/Models/DwgDataExtractor.cs b/Models/DwgDataExtractor.cs index 33d3744..8d18d33 100644 --- a/Models/DwgDataExtractor.cs +++ b/Models/DwgDataExtractor.cs @@ -506,6 +506,59 @@ namespace DwgExtractorManual.Models return noteTextIds; } + /// + /// 모든 Line과 Polyline 엔터티를 통합된 Line 세그먼트 리스트로 변환합니다. + /// Polyline은 구성 Line 세그먼트로 분해됩니다. + /// + private List GetAllLineSegments(Transaction tran, List polylineIds, List lineIds) + { + var allLineSegments = new List(); + + // Polylines를 Line 세그먼트로 분해하여 추가 + foreach (var polylineId in polylineIds) + { + using (var polyline = tran.GetObject(polylineId, OpenMode.ForRead) as Polyline) + { + if (polyline == null) continue; + var explodedEntities = new DBObjectCollection(); + try + { + polyline.Explode(explodedEntities); + foreach (DBObject obj in explodedEntities) + { + if (obj is Line line) + { + allLineSegments.Add(line.Clone() as Line); + } + obj.Dispose(); + } + } + catch (System.Exception ex) + { + Debug.WriteLine($"[DEBUG] Polyline 분해 중 오류: {ex.Message}"); + } + finally + { + explodedEntities.Dispose(); + } + } + } + + // 기존 Line들을 추가 + foreach (var lineId in lineIds) + { + using (var line = tran.GetObject(lineId, OpenMode.ForRead) as Line) + { + if (line != null) + { + allLineSegments.Add(line.Clone() as Line); + } + } + } + + return allLineSegments; + } + /// /// Note 텍스트 아래에 있는 콘텐츠 박스를 찾습니다. /// Note의 정렬점을 기준으로 안정적인 검색 라인을 사용합니다. @@ -513,83 +566,84 @@ namespace DwgExtractorManual.Models private (Point3d minPoint, Point3d maxPoint)? FindNoteBox( Transaction tran, DBText noteText, List polylineIds, List lineIds, HashSet<(Point3d minPoint, Point3d maxPoint)> usedBoxes) { - var allLineSegments = new List(); + var allLineSegments = GetAllLineSegments(tran, polylineIds, lineIds); try { var notePos = noteText.Position; var noteHeight = noteText.Height; - // Polylines를 Line 세그먼트로 분해하여 추가 - foreach (var polylineId in polylineIds) - { - using (var polyline = tran.GetObject(polylineId, OpenMode.ForRead) as Polyline) - { - if (polyline == null) continue; - var explodedEntities = new DBObjectCollection(); - try - { - polyline.Explode(explodedEntities); - foreach (DBObject obj in explodedEntities) - { - if (obj is Line line) - { - allLineSegments.Add(line.Clone() as Line); - } - obj.Dispose(); - } - } - catch (System.Exception ex) - { - Debug.WriteLine($"[DEBUG] Polyline 분해 중 오류: {ex.Message}"); - } - finally - { - explodedEntities.Dispose(); - } - } - } - - // 기존 Line들을 추가 - foreach (var lineId in lineIds) - { - using (var line = tran.GetObject(lineId, OpenMode.ForRead) as Line) - { - if (line != null) - { - allLineSegments.Add(line.Clone() as Line); - } - } - } - - double stableX; + // Note의 X 좌표 결정 (정렬 방식에 따라) + double noteX; if (noteText.HorizontalMode == TextHorizontalMode.TextLeft) { - stableX = noteText.Position.X; + noteX = noteText.Position.X; } else { - stableX = noteText.AlignmentPoint.X; + noteX = noteText.AlignmentPoint.X; } - double searchY = notePos.Y - (noteHeight * 2); - double searchWidth = noteHeight * 15; - var searchLineStart = new Point3d(stableX - noteHeight * 6, searchY, 0); // 왼쪽으로 확장 (중간값) - var searchLineEnd = new Point3d(stableX + searchWidth, searchY, 0); - var searchLineEnd = new Point3d(stableX + searchWidth, searchY, 0); - - Debug.WriteLine($"[DEBUG] 확장된 통합 검색 라인: ({searchLineStart.X:F2}, {searchLineStart.Y:F2}) to ({searchLineEnd.X:F2}, {searchLineEnd.Y:F2}) for NOTE at ({notePos.X:F2}, {notePos.Y:F2})"); + Debug.WriteLine($"[DEBUG] 수직 Ray-Casting 시작: NOTE '{noteText.TextString}' at ({noteX:F2}, {notePos.Y:F2})"); - var intersectingLines = FindIntersectingLineSegments(allLineSegments, searchLineStart, searchLineEnd); - foreach (var startLine in intersectingLines) + // 수직 레이 캐스팅을 위한 하향 스캔 + var horizontalLines = new List<(Line line, double y)>(); + + // 모든 라인 세그먼트를 검사하여 수직 레이와 교차하는 수평선들을 찾음 + foreach (var line in allLineSegments) { - var rectangle = TraceRectangleFromLineSegments(allLineSegments, startLine, notePos, noteHeight, usedBoxes); - if (rectangle.HasValue) + // 수평선인지 확인 (Y 좌표가 거의 같음) + if (Math.Abs(line.StartPoint.Y - line.EndPoint.Y) < noteHeight * 0.1) { - Debug.WriteLine($"[DEBUG] 교차하는 Line/Polyline 조합 사각형 발견: {rectangle.Value.minPoint} to {rectangle.Value.maxPoint}"); - return rectangle; + double lineY = (line.StartPoint.Y + line.EndPoint.Y) / 2; + double lineMinX = Math.Min(line.StartPoint.X, line.EndPoint.X); + double lineMaxX = Math.Max(line.StartPoint.X, line.EndPoint.X); + + // 수직 레이가 이 수평선과 교차하는지 확인 + if (noteX >= lineMinX && noteX <= lineMaxX && lineY < notePos.Y) + { + horizontalLines.Add((line, lineY)); + Debug.WriteLine($"[DEBUG] 수평선 발견: Y={lineY:F2}, X범위=({lineMinX:F2}~{lineMaxX:F2})"); + } } } + // Y 좌표로 정렬 (위에서 아래로) + var sortedHorizontalLines = horizontalLines + .OrderByDescending(hl => hl.y) + .ToList(); + + Debug.WriteLine($"[DEBUG] 정렬된 수평선 개수: {sortedHorizontalLines.Count}"); + + // 두 번째 수평선을 콘텐츠 박스의 상단으로 식별 (첫 번째는 NOTE 자체의 경계로 가정) + if (sortedHorizontalLines.Count >= 2) + { + var topLine = sortedHorizontalLines[1].line; + Debug.WriteLine($"[DEBUG] 콘텐츠 박스 상단선 선택 (2번째): Y={sortedHorizontalLines[1].y:F2}"); + + // 이 상단선으로부터 박스를 추적 + var rectangle = TraceBoxFromTopLine(allLineSegments, topLine, notePos, noteHeight, usedBoxes); + if (rectangle.HasValue) + { + Debug.WriteLine($"[DEBUG] Ray-Casting으로 박스 발견 (2번째 선): {rectangle.Value.minPoint} to {rectangle.Value.maxPoint}"); + return rectangle; + } + + // 2번째 선으로 실패시 3번째 선 시도 (shadow effect polyline 고려) + if (sortedHorizontalLines.Count >= 3) + { + var fallbackTopLine = sortedHorizontalLines[2].line; + Debug.WriteLine($"[DEBUG] 대안 콘텐츠 박스 상단선 선택 (3번째): Y={sortedHorizontalLines[2].y:F2}"); + + var fallbackRectangle = TraceBoxFromTopLine(allLineSegments, fallbackTopLine, notePos, noteHeight, usedBoxes); + if (fallbackRectangle.HasValue) + { + Debug.WriteLine($"[DEBUG] Ray-Casting으로 박스 발견 (3번째 선): {fallbackRectangle.Value.minPoint} to {fallbackRectangle.Value.maxPoint}"); + return fallbackRectangle; + } + } + } + + Debug.WriteLine($"[DEBUG] Ray-Casting으로 적절한 박스를 찾지 못함"); return null; } finally @@ -602,133 +656,158 @@ namespace DwgExtractorManual.Models } } - private List FindIntersectingLineSegments(List allLineSegments, Point3d searchLineStart, Point3d searchLineEnd) - { - var intersectingLines = new List(); - foreach (var line in allLineSegments) - { - if (DoLinesIntersect(searchLineStart, searchLineEnd, line.StartPoint, line.EndPoint)) - { - intersectingLines.Add(line); - } - } - return intersectingLines; - } - - private (Point3d minPoint, Point3d maxPoint)? TraceRectangleFromLineSegments(List allLineSegments, Line startLine, Point3d notePos, double noteHeight, HashSet<(Point3d minPoint, Point3d maxPoint)> usedBoxes) + /// + /// 식별된 상단선으로부터 지능적으로 박스의 나머지 세 변을 추적합니다. + /// 작은 간격에 대해 관용적입니다. + /// + private (Point3d minPoint, Point3d maxPoint)? TraceBoxFromTopLine( + List allLineSegments, Line topLine, Point3d notePos, double noteHeight, HashSet<(Point3d minPoint, Point3d maxPoint)> usedBoxes) { try { - double tolerance = noteHeight * 2.0; - var visitedLines = new HashSet(); - var rectanglePoints = new List(); + double tolerance = noteHeight * 0.5; // 간격 허용 오차 + + Debug.WriteLine($"[DEBUG] 박스 추적 시작: 상단선 ({topLine.StartPoint.X:F2},{topLine.StartPoint.Y:F2}) to ({topLine.EndPoint.X:F2},{topLine.EndPoint.Y:F2})"); - var currentPoint = startLine.StartPoint; - var nextPoint = startLine.EndPoint; - rectanglePoints.Add(currentPoint); - rectanglePoints.Add(nextPoint); - visitedLines.Add(startLine); + // 상단선의 끝점들 + var topLeft = new Point3d(Math.Min(topLine.StartPoint.X, topLine.EndPoint.X), topLine.StartPoint.Y, 0); + var topRight = new Point3d(Math.Max(topLine.StartPoint.X, topLine.EndPoint.X), topLine.StartPoint.Y, 0); - for (int step = 0; step < 5; step++) + // 왼쪽 수직선 찾기 + Line leftLine = null; + foreach (var line in allLineSegments) { - var findResult = FindNextConnectedLineSegment(allLineSegments, nextPoint, visitedLines, tolerance); - if (findResult == null) break; - - var nextLine = findResult.Value.line; - var connectionType = findResult.Value.connectionType; - - Point3d newNextPoint = (connectionType == "Start") ? nextLine.EndPoint : nextLine.StartPoint; - - rectanglePoints.Add(newNextPoint); - visitedLines.Add(nextLine); - nextPoint = newNextPoint; - - if (nextPoint.DistanceTo(currentPoint) < tolerance) + if (IsVerticalLine(line, noteHeight) && + IsLineConnectedToPoint(line, topLeft, tolerance)) { + leftLine = line; + Debug.WriteLine($"[DEBUG] 왼쪽 수직선 발견: ({line.StartPoint.X:F2},{line.StartPoint.Y:F2}) to ({line.EndPoint.X:F2},{line.EndPoint.Y:F2})"); break; } } - if (rectanglePoints.Count >= 4) + // 오른쪽 수직선 찾기 + Line rightLine = null; + foreach (var line in allLineSegments) { - var bounds = CalculateBounds(rectanglePoints); - if (bounds.HasValue && IsValidNoteBox(bounds.Value, notePos, noteHeight) && !usedBoxes.Contains(bounds.Value)) + if (IsVerticalLine(line, noteHeight) && + IsLineConnectedToPoint(line, topRight, tolerance)) { - return bounds; + rightLine = line; + Debug.WriteLine($"[DEBUG] 오른쪽 수직선 발견: ({line.StartPoint.X:F2},{line.StartPoint.Y:F2}) to ({line.EndPoint.X:F2},{line.EndPoint.Y:F2})"); + break; } } + + if (leftLine == null || rightLine == null) + { + Debug.WriteLine($"[DEBUG] 수직선을 찾을 수 없음: left={leftLine != null}, right={rightLine != null}"); + return null; + } + + // 하단 끝점들 계산 + var bottomLeft = GetFarEndPoint(leftLine, topLeft); + var bottomRight = GetFarEndPoint(rightLine, topRight); + + // 하단선 찾기 + Line bottomLine = null; + double expectedBottomY = Math.Min(bottomLeft.Y, bottomRight.Y); + + foreach (var line in allLineSegments) + { + if (IsHorizontalLine(line, noteHeight)) + { + double lineY = (line.StartPoint.Y + line.EndPoint.Y) / 2; + if (Math.Abs(lineY - expectedBottomY) < tolerance) + { + // 하단선이 왼쪽과 오른쪽 끝점을 연결하는지 확인 + var lineLeft = new Point3d(Math.Min(line.StartPoint.X, line.EndPoint.X), lineY, 0); + var lineRight = new Point3d(Math.Max(line.StartPoint.X, line.EndPoint.X), lineY, 0); + + if (Math.Abs(lineLeft.X - bottomLeft.X) < tolerance && + Math.Abs(lineRight.X - bottomRight.X) < tolerance) + { + bottomLine = line; + Debug.WriteLine($"[DEBUG] 하단선 발견: ({line.StartPoint.X:F2},{line.StartPoint.Y:F2}) to ({line.EndPoint.X:F2},{line.EndPoint.Y:F2})"); + break; + } + } + } + } + + if (bottomLine == null) + { + Debug.WriteLine($"[DEBUG] 하단선을 찾을 수 없음"); + return null; + } + + // 최종 박스 경계 계산 + var minPoint = new Point3d( + Math.Min(topLeft.X, bottomLeft.X), + Math.Min(topLeft.Y, bottomLeft.Y), + 0 + ); + var maxPoint = new Point3d( + Math.Max(topRight.X, bottomRight.X), + Math.Max(topLeft.Y, bottomLeft.Y), + 0 + ); + + var result = (minPoint, maxPoint); + + // 유효성 검사 + if (IsValidNoteBox(result, notePos, noteHeight) && !usedBoxes.Contains(result)) + { + Debug.WriteLine($"[DEBUG] 유효한 박스 추적 완료: ({minPoint.X:F2},{minPoint.Y:F2}) to ({maxPoint.X:F2},{maxPoint.Y:F2})"); + return result; + } + + Debug.WriteLine($"[DEBUG] 추적된 박스가 유효하지 않음"); return null; } catch (System.Exception ex) { - Debug.WriteLine($"[DEBUG] 통합 사각형 추적 중 오류: {ex.Message}"); + Debug.WriteLine($"[DEBUG] 박스 추적 중 오류: {ex.Message}"); return null; } } - private (Line line, string connectionType)? FindNextConnectedLineSegment(List allLineSegments, Point3d currentPoint, HashSet visitedLines, double tolerance) + /// + /// 선분이 수직선인지 확인합니다. + /// + private bool IsVerticalLine(Line line, double noteHeight) { - Line bestMatch = null; - string bestConnectionType = null; - double minDistance = tolerance; - - foreach (var line in allLineSegments) - { - if (visitedLines.Contains(line)) continue; - - double distToStart = currentPoint.DistanceTo(line.StartPoint); - if (distToStart < minDistance) - { - minDistance = distToStart; - bestMatch = line; - bestConnectionType = "Start"; - } - - double distToEnd = currentPoint.DistanceTo(line.EndPoint); - if (distToEnd < minDistance) - { - minDistance = distToEnd; - bestMatch = line; - bestConnectionType = "End"; - } - } - - if (bestMatch != null) - { - return (bestMatch, bestConnectionType); - } - - return null; + return Math.Abs(line.StartPoint.X - line.EndPoint.X) < noteHeight * 0.1; } - /// - /// 수평선이 Polyline과 교차하는지 확인합니다. + /// 선분이 수평선인지 확인합니다. /// - private bool DoesLineIntersectPolyline(Point3d lineStart, Point3d lineEnd, Polyline polyline) + private bool IsHorizontalLine(Line line, double noteHeight) { - try - { - for (int i = 0; i < polyline.NumberOfVertices; i++) - { - int nextIndex = (i + 1) % polyline.NumberOfVertices; - var segStart = polyline.GetPoint3dAt(i); - var segEnd = polyline.GetPoint3dAt(nextIndex); - - // 수평선과 폴리라인 세그먼트의 교차점 확인 - if (DoLinesIntersect(lineStart, lineEnd, segStart, segEnd)) - { - return true; - } - } - return false; - } - catch - { - return false; - } + return Math.Abs(line.StartPoint.Y - line.EndPoint.Y) < noteHeight * 0.1; } + /// + /// 선분이 지정된 점에 연결되어 있는지 확인합니다. + /// + private bool IsLineConnectedToPoint(Line line, Point3d point, double tolerance) + { + return point.DistanceTo(line.StartPoint) < tolerance || point.DistanceTo(line.EndPoint) < tolerance; + } + + /// + /// 선분에서 지정된 점으로부터 가장 먼 끝점을 반환합니다. + /// + private Point3d GetFarEndPoint(Line line, Point3d referencePoint) + { + double distToStart = referencePoint.DistanceTo(line.StartPoint); + double distToEnd = referencePoint.DistanceTo(line.EndPoint); + + return distToStart > distToEnd ? line.StartPoint : line.EndPoint; + } + + /// /// 두 선분이 교차하는지 확인합니다. /// @@ -755,33 +834,6 @@ namespace DwgExtractorManual.Models } } - /// - /// Polyline의 경계 상자를 계산합니다. - /// - private (Point3d minPoint, Point3d maxPoint)? GetPolylineBounds(Polyline polyline) - { - try - { - if (polyline.NumberOfVertices < 3) return null; - - var vertices = new List(); - for (int i = 0; i < polyline.NumberOfVertices; i++) - { - vertices.Add(polyline.GetPoint3dAt(i)); - } - - double minX = vertices.Min(v => v.X); - double maxX = vertices.Max(v => v.X); - double minY = vertices.Min(v => v.Y); - double maxY = vertices.Max(v => v.Y); - - return (new Point3d(minX, minY, 0), new Point3d(maxX, maxY, 0)); - } - catch - { - return null; - } - } /// /// 박스가 유효한 Note 박스인지 확인합니다.