using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; using Teigha.DatabaseServices; using Teigha.Geometry; using Teigha.Runtime; namespace DwgExtractorManual.Models { /// /// DWG ���Ͽ��� �ؽ�Ʈ ��ƼƼ�� �����ϴ� Ŭ���� /// internal class DwgDataExtractor { private readonly FieldMapper fieldMapper; public DwgDataExtractor(FieldMapper fieldMapper) { this.fieldMapper = fieldMapper ?? throw new ArgumentNullException(nameof(fieldMapper)); } /// /// DWG ���Ͽ��� �����͸� �����Ͽ� ExcelRowData ����Ʈ�� ��ȯ /// public DwgExtractionResult ExtractFromDwgFile(string filePath, IProgress? progress = null, CancellationToken cancellationToken = default) { var result = new DwgExtractionResult(); if (!File.Exists(filePath)) { Debug.WriteLine($"? ������ �������� �ʽ��ϴ�: {filePath}"); return result; } try { progress?.Report(0); cancellationToken.ThrowIfCancellationRequested(); using (var database = new Database(false, true)) { database.ReadDwgFile(filePath, FileOpenMode.OpenForReadAndWriteNoShare, false, null); progress?.Report(10); using (var tran = database.TransactionManager.StartTransaction()) { var bt = tran.GetObject(database.BlockTableId, OpenMode.ForRead) as BlockTable; using (var btr = tran.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForRead) as BlockTableRecord) { int totalEntities = btr.Cast().Count(); int processedCount = 0; var fileName = Path.GetFileNameWithoutExtension(database.Filename); if (string.IsNullOrEmpty(fileName)) { fileName = "Unknown_File"; } foreach (ObjectId entId in btr) { cancellationToken.ThrowIfCancellationRequested(); using (var ent = tran.GetObject(entId, OpenMode.ForRead) as Entity) { string layerName = GetLayerName(ent.LayerId, tran, database); ProcessEntity(ent, tran, database, layerName, fileName, result); } processedCount++; double currentProgress = 10.0 + (double)processedCount / totalEntities * 80.0; progress?.Report(Math.Min(currentProgress, 90.0)); } } tran.Commit(); } } progress?.Report(100); return result; } catch (OperationCanceledException) { Debug.WriteLine("? �۾��� ��ҵǾ����ϴ�."); progress?.Report(0); return result; } catch (Teigha.Runtime.Exception ex) { progress?.Report(0); Debug.WriteLine($"? DWG ���� ó�� �� Teigha ���� �߻�: {ex.Message}"); return result; } catch (System.Exception ex) { progress?.Report(0); Debug.WriteLine($"? �Ϲ� ���� �߻�: {ex.Message}"); return result; } } /// /// DWG ���Ͽ��� �ؽ�Ʈ ��ƼƼ���� �����Ͽ� Height ������ �Բ� ��ȯ�մϴ�. /// public List ExtractTextEntitiesWithHeight(string filePath) { var attRefEntities = new List(); var otherTextEntities = new List(); try { using (var database = new Database(false, true)) { database.ReadDwgFile(filePath, FileOpenMode.OpenForReadAndWriteNoShare, false, null); using (var tran = database.TransactionManager.StartTransaction()) { var bt = tran.GetObject(database.BlockTableId, OpenMode.ForRead) as BlockTable; using (var btr = tran.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForRead) as BlockTableRecord) { foreach (ObjectId entId in btr) { using (var ent = tran.GetObject(entId, OpenMode.ForRead) as Entity) { string layerName = GetLayerName(ent.LayerId, tran, database); // AttributeReference ó�� if (ent is BlockReference blr) { foreach (ObjectId attId in blr.AttributeCollection) { using (var attRef = tran.GetObject(attId, OpenMode.ForRead) as AttributeReference) { if (attRef != null) { var textString = attRef.TextString == null ? "" : attRef.TextString; attRefEntities.Add(new TextEntityInfo { Height = attRef.Height, Type = "AttRef", Layer = layerName, Tag = attRef.Tag, Text = textString, }); } } } } // DBText ó�� else if (ent is DBText dbText) { otherTextEntities.Add(new TextEntityInfo { Height = dbText.Height, Type = "DBText", Layer = layerName, Tag = "", Text = dbText.TextString }); } // MText ó�� else if (ent is MText mText) { otherTextEntities.Add(new TextEntityInfo { Height = mText.Height, Type = "MText", Layer = layerName, Tag = "", Text = mText.Contents }); } } } } tran.Commit(); } } } catch (System.Exception ex) { Debug.WriteLine($"? �ؽ�Ʈ ��ƼƼ ���� �� ���� ({Path.GetFileName(filePath)}): {ex.Message}"); } var sortedAttRefEntities = attRefEntities.OrderByDescending(e => e.Height).ToList(); var sortedOtherTextEntities = otherTextEntities.OrderByDescending(e => e.Height).ToList(); sortedAttRefEntities.AddRange(sortedOtherTextEntities); return sortedAttRefEntities; } private void ProcessEntity(Entity ent, Transaction tran, Database database, string layerName, string fileName, DwgExtractionResult result) { // AttributeDefinition ���� if (ent is AttributeDefinition attDef) { var titleBlockRow = new TitleBlockRowData { Type = attDef.GetType().Name, Name = attDef.BlockName, Tag = attDef.Tag, Prompt = attDef.Prompt, Value = attDef.TextString, Path = database.Filename, FileName = Path.GetFileName(database.Filename) }; result.TitleBlockRows.Add(titleBlockRow); // ���� ������ ���� AddMappingData(fileName, attDef.Tag, attDef.TextString, result); } // BlockReference �� �� ���� AttributeReference ���� else if (ent is BlockReference blr) { foreach (ObjectId attId in blr.AttributeCollection) { using (var attRef = tran.GetObject(attId, OpenMode.ForRead) as AttributeReference) { if (attRef != null && attRef.TextString.Trim() != "") { var titleBlockRow = new TitleBlockRowData { Type = attRef.GetType().Name, Name = blr.Name, Tag = attRef.Tag, Prompt = GetPromptFromAttributeReference(tran, blr, attRef.Tag), Value = attRef.TextString, Path = database.Filename, FileName = Path.GetFileName(database.Filename) }; result.TitleBlockRows.Add(titleBlockRow); // ���� ������ ���� var aiLabel = fieldMapper.ExpresswayToAilabel(attRef.Tag); if (aiLabel != null) { AddMappingData(fileName, attRef.Tag, attRef.TextString, result); } } } } } // DBText ��ƼƼ ���� (���� ��Ʈ) else if (ent is DBText dbText) { var textEntityRow = new TextEntityRowData { Type = "DBText", Layer = layerName, Text = dbText.TextString, Path = database.Filename, FileName = Path.GetFileName(database.Filename) }; result.TextEntityRows.Add(textEntityRow); } // MText ��ƼƼ ���� (���� ��Ʈ) else if (ent is MText mText) { var textEntityRow = new TextEntityRowData { Type = "MText", Layer = layerName, Text = mText.Contents, Path = database.Filename, FileName = Path.GetFileName(database.Filename) }; result.TextEntityRows.Add(textEntityRow); } } private void AddMappingData(string fileName, string tag, string attValue, DwgExtractionResult result) { var aiLabel = fieldMapper.ExpresswayToAilabel(tag); var mapKey = fieldMapper.AilabelToDocAiKey(aiLabel); if (!string.IsNullOrEmpty(aiLabel)) { var finalMapKey = mapKey ?? aiLabel; result.AddMappingData(fileName, finalMapKey, aiLabel, tag, attValue, ""); } else { var finalMapKey = mapKey ?? tag; if (!string.IsNullOrEmpty(finalMapKey)) { result.AddMappingData(fileName, finalMapKey, tag, tag, attValue, ""); } } } private string? GetPromptFromAttributeReference(Transaction tr, BlockReference blockref, string tag) { string? prompt = null; BlockTableRecord? blockDef = tr.GetObject(blockref.BlockTableRecord, OpenMode.ForRead) as BlockTableRecord; if (blockDef == null) return null; foreach (ObjectId objId in blockDef) { AttributeDefinition? attDef = tr.GetObject(objId, OpenMode.ForRead) as AttributeDefinition; if (attDef != null) { if (attDef.Tag.Equals(tag, System.StringComparison.OrdinalIgnoreCase)) { prompt = attDef.Prompt; break; } } } return prompt; } private string GetLayerName(ObjectId layerId, Transaction transaction, Database database) { try { using (var layerTableRecord = transaction.GetObject(layerId, OpenMode.ForRead) as LayerTableRecord) { return layerTableRecord?.Name ?? ""; } } catch (System.Exception ex) { Debug.WriteLine($"Layer �̸� �������� ����: {ex.Message}"); return ""; } } /// /// 도면에서 Note와 관련된 텍스트들을 추출합니다. /// public List ExtractNotesFromDrawing(string filePath) { var noteEntities = new List(); try { using (var database = new Database(false, true)) { database.ReadDwgFile(filePath, FileOpenMode.OpenForReadAndWriteNoShare, false, null); using (var tran = database.TransactionManager.StartTransaction()) { var bt = tran.GetObject(database.BlockTableId, OpenMode.ForRead) as BlockTable; using (var btr = tran.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForRead) as BlockTableRecord) { var allEntities = btr.Cast().ToList(); var dbTextIds = new List(); var polylineIds = new List(); var lineIds = new List(); // 먼저 모든 관련 엔터티들의 ObjectId를 수집 foreach (ObjectId entId in allEntities) { using (var ent = tran.GetObject(entId, OpenMode.ForRead) as Entity) { if (ent is DBText) { dbTextIds.Add(entId); } else if (ent is Polyline) { polylineIds.Add(entId); } else if (ent is Line) { lineIds.Add(entId); } } } Debug.WriteLine($"[DEBUG] 수집된 엔터티: DBText={dbTextIds.Count}, Polyline={polylineIds.Count}, Line={lineIds.Count}"); // Note 텍스트들을 찾기 var noteTextIds = FindNoteTexts(tran, dbTextIds); Debug.WriteLine($"[DEBUG] 발견된 Note 텍스트: {noteTextIds.Count}개"); // Note 그룹들을 저장할 리스트 (각 그룹은 NOTE 헤더 + 내용들) var noteGroups = new List>(); // 이미 사용된 박스들을 추적 (중복 할당 방지) var usedBoxes = new HashSet<(Point3d minPoint, Point3d maxPoint)>(); // 각 Note에 대해 처리 foreach (var noteTextId in noteTextIds) { using (var noteText = tran.GetObject(noteTextId, OpenMode.ForRead) as DBText) { Debug.WriteLine($"[DEBUG] Note 처리 중: '{noteText.TextString}' at {noteText.Position}"); // 이 Note에 대한 그룹 생성 var currentNoteGroup = new List(); // Note 우측아래에 있는 박스 찾기 (이미 사용된 박스 제외) var noteBox = FindNoteBox(tran, noteText, polylineIds, lineIds, usedBoxes); if (noteBox.HasValue) { Debug.WriteLine($"[DEBUG] Note 박스 발견: {noteBox.Value.minPoint} to {noteBox.Value.maxPoint}"); // 사용된 박스로 등록 usedBoxes.Add(noteBox.Value); // 박스 내부의 텍스트들 찾기 var boxTextIds = FindTextsInNoteBox(tran, noteText, noteBox.Value, dbTextIds); Debug.WriteLine($"[DEBUG] 박스 내 텍스트: {boxTextIds.Count}개"); // Note 자체를 그룹의 첫 번째로 추가 currentNoteGroup.Add(new NoteEntityInfo { Type = "Note", Layer = GetLayerName(noteText.LayerId, tran, database), Text = noteText.TextString, Path = database.Filename, FileName = Path.GetFileName(database.Filename), X = noteText.Position.X, Y = noteText.Position.Y, SortOrder = 0 // Note는 항상 먼저 }); // 박스 내 텍스트들을 좌표별로 정렬하여 그룹에 추가 var sortedBoxTexts = GetSortedNoteContents(tran, boxTextIds, database); currentNoteGroup.AddRange(sortedBoxTexts); } else { Debug.WriteLine($"[DEBUG] Note '{noteText.TextString}'에 대한 박스를 찾을 수 없음"); // 박스가 없어도 Note 헤더는 추가 currentNoteGroup.Add(new NoteEntityInfo { Type = "Note", Layer = GetLayerName(noteText.LayerId, tran, database), Text = noteText.TextString, Path = database.Filename, FileName = Path.GetFileName(database.Filename), X = noteText.Position.X, Y = noteText.Position.Y, SortOrder = 0 }); } noteGroups.Add(currentNoteGroup); } } // Note 그룹들을 Y 좌표별로 정렬 (위에서 아래로) var sortedNoteGroups = noteGroups .OrderByDescending(group => group[0].Y) // 각 그룹의 첫 번째 항목(NOTE 헤더)의 Y 좌표로 정렬 .ToList(); // 정렬된 그룹들을 하나의 리스트로 합치기 foreach (var group in sortedNoteGroups) { noteEntities.AddRange(group); } } tran.Commit(); } } } catch (System.Exception ex) { Debug.WriteLine($"❌ Note 추출 중 오류: {ex.Message}"); } Debug.WriteLine($"[DEBUG] 최종 Note 엔티티 정렬 완료: {noteEntities.Count}개"); return noteEntities; } /// /// DBText 중에서 "Note"가 포함된 텍스트들을 찾습니다. /// private List FindNoteTexts(Transaction tran, List dbTextIds) { var noteTextIds = new List(); foreach (var textId in dbTextIds) { using (var dbText = tran.GetObject(textId, OpenMode.ForRead) as DBText) { if (dbText == null) continue; string textContent = dbText.TextString?.Trim() ?? ""; // 대소문자 구분없이 "Note"가 포함되어 있는지 확인 if (textContent.IndexOf("Note", StringComparison.OrdinalIgnoreCase) >= 0) { noteTextIds.Add(textId); Debug.WriteLine($"[DEBUG] Note 텍스트 발견: '{textContent}' at {dbText.Position}, Color: {dbText.Color}"); } } } return noteTextIds; } /// /// Note 텍스트 아래에 있는 콘텐츠 박스를 찾습니다. /// Note의 정렬점을 기준으로 안정적인 검색 라인을 사용합니다. /// private (Point3d minPoint, Point3d maxPoint)? FindNoteBox( Transaction tran, DBText noteText, List polylineIds, List lineIds, HashSet<(Point3d minPoint, Point3d maxPoint)> usedBoxes) { var notePos = noteText.Position; var noteHeight = noteText.Height; double stableX; if (noteText.HorizontalMode == TextHorizontalMode.TextLeft) { stableX = noteText.Position.X; } else { stableX = noteText.AlignmentPoint.X; } // 검색 라인 정의: 안정적인 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) { if (polyline == null) continue; if (DoesLineIntersectPolyline(searchLineStart, searchLineEnd, polyline)) { var box = GetPolylineBounds(polyline); if (box.HasValue && IsValidNoteBox(box.Value, notePos, noteHeight) && !usedBoxes.Contains(box.Value)) { Debug.WriteLine($"[DEBUG] 교차하는 Polyline 박스 발견: {box.Value.minPoint} to {box.Value.maxPoint}"); return box; } } } } // 2. Line들과 교차하여 사각형 찾기 var intersectingLines = FindIntersectingLines(tran, lineIds, searchLineStart, searchLineEnd); foreach (var startLineId in intersectingLines) { 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 null; } /// /// 수평선이 Polyline과 교차하는지 확인합니다. /// private bool DoesLineIntersectPolyline(Point3d lineStart, Point3d lineEnd, Polyline polyline) { 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; } } /// /// 두 선분이 교차하는지 확인합니다. /// private bool DoLinesIntersect(Point3d line1Start, Point3d line1End, Point3d line2Start, Point3d line2End) { try { double x1 = line1Start.X, y1 = line1Start.Y; double x2 = line1End.X, y2 = line1End.Y; double x3 = line2Start.X, y3 = line2Start.Y; double x4 = line2End.X, y4 = line2End.Y; double denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4); if (Math.Abs(denom) < 1e-10) return false; // 평행선 double t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom; double u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denom; return t >= 0 && t <= 1 && u >= 0 && u <= 1; } catch { return false; } } /// /// 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 박스인지 확인합니다. /// private bool IsValidNoteBox((Point3d minPoint, Point3d maxPoint) box, Point3d notePos, double noteHeight) { try { // 박스가 Note 아래쪽에 있는지 확인 if (box.maxPoint.Y >= notePos.Y) return false; // 박스 크기가 적절한지 확인 double boxWidth = box.maxPoint.X - box.minPoint.X; double boxHeight = box.maxPoint.Y - box.minPoint.Y; // 너무 작거나 큰 박스는 제외 if (boxWidth < noteHeight || boxHeight < noteHeight) return false; if (boxWidth > noteHeight * 100 || boxHeight > noteHeight * 100) return false; // Note 위치와 적절한 거리에 있는지 확인 double distanceX = Math.Abs(box.minPoint.X - notePos.X); double distanceY = Math.Abs(box.maxPoint.Y - notePos.Y); if (distanceX > noteHeight * 50 || distanceY > noteHeight * 10) return false; return true; } catch { return false; } } /// /// 수평선과 교차하는 Line들을 찾습니다. /// private List FindIntersectingLines(Transaction tran, List lineIds, Point3d searchLineStart, Point3d searchLineEnd) { var intersectingLines = new List(); 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; } /// /// Line에서 시작하여 반시계방향으로 사각형을 추적합니다. (갭 허용) /// private (Point3d minPoint, Point3d maxPoint)? TraceRectangleFromLine(Transaction tran, List lineIds, ObjectId startLineId, Point3d notePos, double noteHeight) { try { double tolerance = noteHeight * 2.0; // 갭 허용 오차를 Note 높이에 비례하여 설정 var visitedLines = new HashSet(); var rectanglePoints = new List(); 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; } } /// /// 현재 점에서 가장 가까운 연결된 다음 Line을 찾습니다. (갭 허용) /// private (ObjectId lineId, string connectionType)? FindNextConnectedLine(Transaction tran, List lineIds, Point3d currentPoint, HashSet 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; } /// /// 점들의 경계 상자를 계산합니다. /// private (Point3d minPoint, Point3d maxPoint)? CalculateBounds(List points) { try { if (points.Count < 3) return null; double minX = points.Min(p => p.X); double maxX = points.Max(p => p.X); double minY = points.Min(p => p.Y); double maxY = points.Max(p => p.Y); return (new Point3d(minX, minY, 0), new Point3d(maxX, maxY, 0)); } catch { return null; } } /// /// Note 박스 내부의 텍스트들을 찾습니다. /// private List FindTextsInNoteBox( Transaction tran, DBText noteText, (Point3d minPoint, Point3d maxPoint) noteBox, List allTextIds) { var boxTextIds = new List(); var noteHeight = noteText.Height; foreach (var textId in allTextIds) { // Note 자신은 제외 if (textId == noteText.ObjectId) continue; using (var dbText = tran.GetObject(textId, OpenMode.ForRead) as DBText) { if (dbText == null) continue; var textPos = dbText.Position; // 박스 내부에 있는지 확인 if (textPos.X >= noteBox.minPoint.X && textPos.X <= noteBox.maxPoint.X && textPos.Y >= noteBox.minPoint.Y && textPos.Y <= noteBox.maxPoint.Y) { // Note의 height보다 작거나 같은지 확인 if (dbText.Height <= noteHeight) { boxTextIds.Add(textId); Debug.WriteLine($"[DEBUG] 박스 내 텍스트 발견: '{dbText.TextString}' at {textPos}"); } } } } return boxTextIds; } /// /// Note 박스 내의 텍스트들을 좌표에 따라 정렬합니다 (위에서 아래로, 왼쪽에서 오른쪽으로). /// private List GetSortedNoteContents(Transaction tran, List boxTextIds, Database database) { var noteContents = new List(); // 먼저 모든 텍스트와 좌표 정보를 수집 var textInfoList = new List<(ObjectId id, double x, double y, string text, string layer)>(); foreach (var boxTextId in boxTextIds) { using (var boxText = tran.GetObject(boxTextId, OpenMode.ForRead) as DBText) { if (boxText != null) { var pos = boxText.Position; textInfoList.Add((boxTextId, pos.X, pos.Y, boxText.TextString, GetLayerName(boxText.LayerId, tran, database))); } } } // Y 좌표로 먼저 정렬 (위에서 아래로 = Y 큰 값부터), 그다음 X 좌표로 정렬 (왼쪽에서 오른쪽으로 = X 작은 값부터) // CAD에서는 Y축이 위로 갈수록 커지므로, 텍스트를 위에서 아래로 읽으려면 Y가 큰 것부터 처리 (큰 Y = 위쪽) var sortedTexts = textInfoList .OrderByDescending(t => Math.Round(t.y, 1)) // Y 좌표로 내림차순 (위에서 아래로: 큰 Y부터) .ThenBy(t => Math.Round(t.x, 1)) // X 좌표로 오름차순 (왼쪽에서 오른쪽으로: 작은 X부터) .ToList(); Debug.WriteLine($"[DEBUG] Note 내용 정렬 결과:"); int sortOrder = 1; // Note 자체는 0이므로 내용은 1부터 시작 foreach (var textInfo in sortedTexts) { Debug.WriteLine($"[DEBUG] 순서 {sortOrder}: '{textInfo.text}' at ({textInfo.x:F1}, {textInfo.y:F1})"); noteContents.Add(new NoteEntityInfo { Type = "NoteContent", Layer = textInfo.layer, Text = textInfo.text, Path = database.Filename, FileName = Path.GetFileName(database.Filename), X = textInfo.x, Y = textInfo.y, SortOrder = sortOrder }); sortOrder++; } return noteContents; } } /// /// DWG ���� ����� ��� Ŭ���� /// public class DwgExtractionResult { public List TitleBlockRows { get; set; } = new List(); public List TextEntityRows { get; set; } = new List(); public Dictionary> FileToMapkeyToLabelTagValuePdf { get; set; } = new Dictionary>(); public void AddMappingData(string fileName, string mapKey, string aiLabel, string dwgTag, string dwgValue, string pdfValue) { if (!FileToMapkeyToLabelTagValuePdf.ContainsKey(fileName)) { FileToMapkeyToLabelTagValuePdf[fileName] = new Dictionary(); } FileToMapkeyToLabelTagValuePdf[fileName][mapKey] = (aiLabel, dwgTag, dwgValue, pdfValue); } } /// /// Title Block �� ������ /// public class TitleBlockRowData { public string Type { get; set; } = ""; public string Name { get; set; } = ""; public string Tag { get; set; } = ""; public string Prompt { get; set; } = ""; public string Value { get; set; } = ""; public string Path { get; set; } = ""; public string FileName { get; set; } = ""; } /// /// Text Entity �� ������ /// public class TextEntityRowData { public string Type { get; set; } = ""; public string Layer { get; set; } = ""; public string Text { get; set; } = ""; public string Path { get; set; } = ""; public string FileName { get; set; } = ""; } /// /// �ؽ�Ʈ ��ƼƼ ������ ��� Ŭ���� /// public class TextEntityInfo { public double Height { get; set; } public string Type { get; set; } = ""; public string Layer { get; set; } = ""; public string Tag { get; set; } = ""; public string Text { get; set; } = ""; } /// /// Note 엔티티 정보 클래스 /// public class NoteEntityInfo { public string Type { get; set; } = ""; public string Layer { get; set; } = ""; public string Text { get; set; } = ""; public string Path { get; set; } = ""; public string FileName { get; set; } = ""; public double X { get; set; } = 0; // X 좌표 public double Y { get; set; } = 0; // Y 좌표 public int SortOrder { get; set; } = 0; // 정렬 순서 (Note=0, NoteContent=1,2,3...) } }