Compare commits

...

2 Commits

Author SHA1 Message Date
107eab90fa NoteDetectionRefactor plan 2025-07-31 10:29:56 +09:00
9c76b624bf note detection v2 2025-07-31 10:03:48 +09:00
3 changed files with 229 additions and 183 deletions

View File

@@ -513,56 +513,189 @@ namespace DwgExtractorManual.Models
private (Point3d minPoint, Point3d maxPoint)? FindNoteBox(
Transaction tran, DBText noteText, List<ObjectId> polylineIds, List<ObjectId> lineIds, HashSet<(Point3d minPoint, Point3d maxPoint)> usedBoxes)
{
var notePos = noteText.Position;
var noteHeight = noteText.Height;
double stableX;
if (noteText.HorizontalMode == TextHorizontalMode.TextLeft)
var allLineSegments = new List<Line>();
try
{
stableX = noteText.Position.X;
}
else
{
stableX = noteText.AlignmentPoint.X;
}
var notePos = noteText.Position;
var noteHeight = noteText.Height;
// 검색 라인 정의: 안정적인 X좌표를 기준으로 하되, 약간의 오차를 허용하는 너비
double searchY = notePos.Y - (noteHeight * 2);
double searchWidth = noteHeight * 15;
var searchLineStart = new Point3d(stableX - noteHeight * 2, 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}) with stableX={stableX:F2}");
// 1. Polyline과 교차 확인
foreach (var polylineId in polylineIds)
{
using (var polyline = tran.GetObject(polylineId, OpenMode.ForRead) as Polyline)
// Polylines를 Line 세그먼트로 분해하여 추가
foreach (var polylineId in polylineIds)
{
if (polyline == null) continue;
if (DoesLineIntersectPolyline(searchLineStart, searchLineEnd, polyline))
using (var polyline = tran.GetObject(polylineId, OpenMode.ForRead) as Polyline)
{
var box = GetPolylineBounds(polyline);
if (box.HasValue && IsValidNoteBox(box.Value, notePos, noteHeight) && !usedBoxes.Contains(box.Value))
if (polyline == null) continue;
var explodedEntities = new DBObjectCollection();
try
{
Debug.WriteLine($"[DEBUG] 교차하는 Polyline 박스 발견: {box.Value.minPoint} to {box.Value.maxPoint}");
return box;
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;
if (noteText.HorizontalMode == TextHorizontalMode.TextLeft)
{
stableX = noteText.Position.X;
}
else
{
stableX = 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})");
var intersectingLines = FindIntersectingLineSegments(allLineSegments, searchLineStart, searchLineEnd);
foreach (var startLine in intersectingLines)
{
var rectangle = TraceRectangleFromLineSegments(allLineSegments, startLine, notePos, noteHeight, usedBoxes);
if (rectangle.HasValue)
{
Debug.WriteLine($"[DEBUG] 교차하는 Line/Polyline 조합 사각형 발견: {rectangle.Value.minPoint} to {rectangle.Value.maxPoint}");
return rectangle;
}
}
return null;
}
finally
{
// 생성된 모든 Line 객체들을 Dispose
foreach (var line in allLineSegments)
{
line.Dispose();
}
}
}
private List<Line> FindIntersectingLineSegments(List<Line> allLineSegments, Point3d searchLineStart, Point3d searchLineEnd)
{
var intersectingLines = new List<Line>();
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<Line> allLineSegments, Line startLine, Point3d notePos, double noteHeight, HashSet<(Point3d minPoint, Point3d maxPoint)> usedBoxes)
{
try
{
double tolerance = noteHeight * 2.0;
var visitedLines = new HashSet<Line>();
var rectanglePoints = new List<Point3d>();
var currentPoint = startLine.StartPoint;
var nextPoint = startLine.EndPoint;
rectanglePoints.Add(currentPoint);
rectanglePoints.Add(nextPoint);
visitedLines.Add(startLine);
for (int step = 0; step < 5; step++)
{
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)
{
break;
}
}
if (rectanglePoints.Count >= 4)
{
var bounds = CalculateBounds(rectanglePoints);
if (bounds.HasValue && IsValidNoteBox(bounds.Value, notePos, noteHeight) && !usedBoxes.Contains(bounds.Value))
{
return bounds;
}
}
return null;
}
catch (System.Exception ex)
{
Debug.WriteLine($"[DEBUG] 통합 사각형 추적 중 오류: {ex.Message}");
return null;
}
}
private (Line line, string connectionType)? FindNextConnectedLineSegment(List<Line> allLineSegments, Point3d currentPoint, HashSet<Line> visitedLines, double tolerance)
{
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";
}
}
// 2. Line들과 교차하여 사각형 찾기
var intersectingLines = FindIntersectingLines(tran, lineIds, searchLineStart, searchLineEnd);
foreach (var startLineId in intersectingLines)
if (bestMatch != null)
{
var rectangle = TraceRectangleFromLine(tran, lineIds, startLineId, notePos, noteHeight);
if (rectangle.HasValue && !usedBoxes.Contains(rectangle.Value))
{
Debug.WriteLine($"[DEBUG] 교차하는 Line 사각형 발견: {rectangle.Value.minPoint} to {rectangle.Value.maxPoint}");
return rectangle;
}
return (bestMatch, bestConnectionType);
}
return null;
@@ -682,150 +815,7 @@ namespace DwgExtractorManual.Models
}
}
/// <summary>
/// 수평선과 교차하는 Line들을 찾습니다.
/// </summary>
private List<ObjectId> FindIntersectingLines(Transaction tran, List<ObjectId> lineIds, Point3d searchLineStart, Point3d searchLineEnd)
{
var intersectingLines = new List<ObjectId>();
foreach (var lineId in lineIds)
{
using (var line = tran.GetObject(lineId, OpenMode.ForRead) as Line)
{
if (line == null) continue;
if (DoLinesIntersect(searchLineStart, searchLineEnd, line.StartPoint, line.EndPoint))
{
intersectingLines.Add(lineId);
Debug.WriteLine($"[DEBUG] 교차 Line 발견: ({line.StartPoint.X}, {line.StartPoint.Y}) to ({line.EndPoint.X}, {line.EndPoint.Y})");
}
}
}
return intersectingLines;
}
/// <summary>
/// Line에서 시작하여 반시계방향으로 사각형을 추적합니다. (갭 허용)
/// </summary>
private (Point3d minPoint, Point3d maxPoint)? TraceRectangleFromLine(Transaction tran, List<ObjectId> lineIds, ObjectId startLineId, Point3d notePos, double noteHeight)
{
try
{
double tolerance = noteHeight * 2.0; // 갭 허용 오차를 Note 높이에 비례하여 설정
var visitedLines = new HashSet<ObjectId>();
var rectanglePoints = new List<Point3d>();
using (var startLine = tran.GetObject(startLineId, OpenMode.ForRead) as Line)
{
if (startLine == null) return null;
var currentPoint = startLine.StartPoint;
var nextPoint = startLine.EndPoint;
rectanglePoints.Add(currentPoint);
rectanglePoints.Add(nextPoint);
visitedLines.Add(startLineId);
Debug.WriteLine($"[DEBUG] 사각형 추적 시작 (갭 허용): ({currentPoint.X:F2}, {currentPoint.Y:F2}) -> ({nextPoint.X:F2}, {nextPoint.Y:F2})");
for (int step = 0; step < 5; step++) // 최대 5번의 연결 시도
{
var findResult = FindNextConnectedLine(tran, lineIds, nextPoint, visitedLines, tolerance);
if (findResult == null) break;
var nextLineId = findResult.Value.lineId;
var connectionType = findResult.Value.connectionType;
using (var nextLine = tran.GetObject(nextLineId, OpenMode.ForRead) as Line)
{
if (nextLine == null) break;
Point3d newNextPoint;
if (connectionType == "Start")
{
newNextPoint = nextLine.EndPoint;
}
else
{
newNextPoint = nextLine.StartPoint;
}
rectanglePoints.Add(newNextPoint);
visitedLines.Add(nextLineId);
nextPoint = newNextPoint;
Debug.WriteLine($"[DEBUG] 다음 Line 추가 (갭 허용): {nextLine.StartPoint} -> {nextLine.EndPoint}");
if (nextPoint.DistanceTo(currentPoint) < tolerance)
{
Debug.WriteLine("[DEBUG] 사각형 완성됨 (갭 허용)");
break;
}
}
}
if (rectanglePoints.Count >= 4)
{
var bounds = CalculateBounds(rectanglePoints);
if (bounds.HasValue && IsValidNoteBox(bounds.Value, notePos, noteHeight))
{
return bounds;
}
}
}
return null;
}
catch (System.Exception ex)
{
Debug.WriteLine($"[DEBUG] 사각형 추적 중 오류 (갭 허용): {ex.Message}");
return null;
}
}
/// <summary>
/// 현재 점에서 가장 가까운 연결된 다음 Line을 찾습니다. (갭 허용)
/// </summary>
private (ObjectId lineId, string connectionType)? FindNextConnectedLine(Transaction tran, List<ObjectId> lineIds, Point3d currentPoint, HashSet<ObjectId> visitedLines, double tolerance)
{
ObjectId? bestMatchId = null;
string? bestConnectionType = null;
double minDistance = tolerance;
foreach (var lineId in lineIds)
{
if (visitedLines.Contains(lineId)) continue;
using (var line = tran.GetObject(lineId, OpenMode.ForRead) as Line)
{
if (line == null) continue;
double distToStart = currentPoint.DistanceTo(line.StartPoint);
if (distToStart < minDistance)
{
minDistance = distToStart;
bestMatchId = lineId;
bestConnectionType = "Start";
}
double distToEnd = currentPoint.DistanceTo(line.EndPoint);
if (distToEnd < minDistance)
{
minDistance = distToEnd;
bestMatchId = lineId;
bestConnectionType = "End";
}
}
}
if (bestMatchId.HasValue)
{
return (bestMatchId.Value, bestConnectionType);
}
return null;
}
/// <summary>
/// 점들의 경계 상자를 계산합니다.

View File

@@ -340,6 +340,14 @@ namespace DwgExtractorManual.Models
worksheet.Cells[row, 5] = fileName ?? "";
worksheet.Cells[row, 6] = noteEntity.Text ?? "";
// "NOTE" 타입인 경우 행 배경색 변경
if (noteEntity.Type == "Note")
{
Excel.Range noteRowRange = worksheet.Range[worksheet.Cells[row, 1], worksheet.Cells[row, 6]];
noteRowRange.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightYellow);
noteRowRange.Font.Bold = true;
}
Debug.WriteLine($"[DEBUG] Excel 기록: Row {row}, Order {noteEntity.SortOrder}, Type {noteEntity.Type}, Pos({noteEntity.X:F1},{noteEntity.Y:F1}), Text: '{noteEntity.Text}'");
row++;
}

48
NoteDetectionRefactor.md Normal file
View File

@@ -0,0 +1,48 @@
Project: Refactor the NOTE Content Box Detection Algorithm
1. High-Level Goal:
The primary objective is to replace the current, fragile "horizontal search line" algorithm in
Models/DwgDataExtractor.cs with a more robust and accurate method that reliably finds the content box for
any "NOTE" text, regardless of its position or the composition of its bounding box.
2. Core Strategy: "Vertical Ray-Casting"
We will implement a new algorithm that emulates how a human would visually locate the content. This
involves a "gradual downward scan" (or vertical ray-cast) from the NOTE's position.
3. Implementation Plan (TODO List):
* Step 1: Unify All Geometry into Line Segments
* Create a single helper method, GetAllLineSegments, that processes all Line and Polyline entities from
the drawing.
* This method will decompose every Polyline into its constituent Line segments.
* It will return a single, unified List<Line> containing every potential boundary segment in the
drawing.
* Crucially: This method must ensure all temporary Line objects created during the process are properly
disposed of to prevent memory leaks.
* Step 2: Implement the Ray-Casting Logic in `FindNoteBox`
* The FindNoteBox method will be completely rewritten.
* It will first call GetAllLineSegments to get the unified geometry list.
* It will then perform the vertical ray-cast starting from the NOTE's X-coordinate and scanning
downwards.
* It will find all horizontal lines that intersect the ray and sort them by their Y-coordinate (from
top to bottom).
* It will identify the second line in this sorted list as the top edge of the content box (the first is
assumed to be the NOTE's own bounding box).
* Step 3: Implement Smart Box Tracing
* Create a new helper method, TraceBoxFromTopLine.
* This method will take the identified top line segment as its starting point.
* It will intelligently trace the remaining three sides of the rectangle by searching the unified list
of line segments for the nearest connecting corners.
* This tracing logic must be tolerant of small gaps between the endpoints of the lines forming the box.
* Step 4: Final Cleanup
* Once the new ray-casting algorithm is fully implemented and validated, all of the old, obsolete
methods related to the previous search-line approach must be deleted to keep the code clean. This
includes:
* FindIntersectingLineSegments
* TraceRectangleFromLineSegments
* FindNextConnectedLineSegment
* DoesLineIntersectPolyline
* GetPolylineBounds