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; } /// /// 모든 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의 정렬점을 기준으로 안정적인 검색 라인을 사용합니다. /// private (Point3d minPoint, Point3d maxPoint)? FindNoteBox( Transaction tran, DBText noteText, List polylineIds, List lineIds, HashSet<(Point3d minPoint, Point3d maxPoint)> usedBoxes) { var allLineSegments = GetAllLineSegments(tran, polylineIds, lineIds); try { var notePos = noteText.Position; var noteHeight = noteText.Height; // Note의 X 좌표 결정 (정렬 방식에 따라) double noteX; if (noteText.HorizontalMode == TextHorizontalMode.TextLeft) { noteX = noteText.Position.X; } else { noteX = noteText.AlignmentPoint.X; } Debug.WriteLine($"[DEBUG] 수직 Ray-Casting 시작: NOTE '{noteText.TextString}' at ({noteX:F2}, {notePos.Y:F2})"); // 수직 레이 캐스팅을 위한 하향 스캔 var horizontalLines = new List<(Line line, double y)>(); // 모든 라인 세그먼트를 검사하여 수직 레이와 교차하는 수평선들을 찾음 foreach (var line in allLineSegments) { // 수평선인지 확인 (Y 좌표가 거의 같음) if (Math.Abs(line.StartPoint.Y - line.EndPoint.Y) < noteHeight * 0.1) { 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 { // 생성된 모든 Line 객체들을 Dispose foreach (var line in allLineSegments) { line.Dispose(); } } } /// /// 식별된 상단선으로부터 지능적으로 박스의 나머지 세 변을 추적합니다. /// 작은 간격에 대해 관용적입니다. /// private (Point3d minPoint, Point3d maxPoint)? TraceBoxFromTopLine( List allLineSegments, Line topLine, Point3d notePos, double noteHeight, HashSet<(Point3d minPoint, Point3d maxPoint)> usedBoxes) { try { 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 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); // 왼쪽 수직선 찾기 Line leftLine = null; foreach (var line in allLineSegments) { 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; } } // 오른쪽 수직선 찾기 Line rightLine = null; foreach (var line in allLineSegments) { if (IsVerticalLine(line, noteHeight) && IsLineConnectedToPoint(line, topRight, tolerance)) { 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}"); return null; } } /// /// 선분이 수직선인지 확인합니다. /// private bool IsVerticalLine(Line line, double noteHeight) { return Math.Abs(line.StartPoint.X - line.EndPoint.X) < noteHeight * 0.1; } /// /// 선분이 수평선인지 확인합니다. /// private bool IsHorizontalLine(Line line, double noteHeight) { 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; } /// /// 두 선분이 교차하는지 확인합니다. /// 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; } } /// /// 박스가 유효한 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; } } /// /// 점들의 경계 상자를 계산합니다. /// 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...) } }