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) { return ExtractTextEntitiesWithHeightExcluding(filePath, new HashSet()); } /// /// DWG 파일에서 텍스트 엔터티들을 추출하되, 지정된 ObjectId들은 제외합니다. /// public List ExtractTextEntitiesWithHeightExcluding(string filePath, HashSet excludeIds) { 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) { // 일반 텍스트 추출시 height 3 이하 제외 if (attRef.Height > 3) { 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) { // �Ϲ� �ؽ�Ʈ ����� height 3 ���� ���� // Note에서 사용된 텍스트 제외 if (!excludeIds.Contains(entId)) { if (dbText.Height > 3) { otherTextEntities.Add(new TextEntityInfo { Height = dbText.Height, Type = "DBText", Layer = layerName, Tag = "", Text = dbText.TextString }); } } } // MText ó�� else if (ent is MText mText) { // �Ϲ� �ؽ�Ʈ ����� height 3 ���� ���� // Note에서 사용된 텍스트 제외 if (!excludeIds.Contains(entId)) { if (mText.Height > 3) { 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 NoteExtractionResult ExtractNotesFromDrawing(string filePath) { var result = new NoteExtractionResult(); 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) { if (noteText == null) { Debug.WriteLine($"[DEBUG] Skipping null noteText for ObjectId: {noteTextId}"); continue; } // Note 헤더 텍스트를 사용된 텍스트 ID에 추가 result.UsedTextIds.Add(noteTextId); // 특정 노트만 테스트하기 위한 필터 (디버깅용) // if (noteText == null || !noteText.TextString.Contains("도로용지경계 기준 노트")) // { // continue; // } 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 (tableData, cells, nonTableTextIds, tableSegments, intersectionPoints, diagonalLines, cellBoundaries) = ExtractTableAndTextsFromNoteBox(tran, noteText, noteBox.Value, polylineIds, lineIds, dbTextIds, database); Debug.WriteLine($"[EXCEL_DEBUG] Note '{noteText.TextString}' CellBoundaries 개수: {cellBoundaries?.Count ?? 0}"); if (cellBoundaries != null && cellBoundaries.Count > 0) { Debug.WriteLine($"[EXCEL_DEBUG] CellBoundaries 샘플 (처음 3개):"); foreach (var cb in cellBoundaries.Take(3)) { Debug.WriteLine($"[EXCEL_DEBUG] {cb.Label}: '{cb.CellText}'"); } } // 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는 항상 먼저 TableData = tableData, // 테이블 데이터 추가 Cells = cells, // 셀 정보 추가 (병합용) TableSegments = tableSegments.Select(s => new SegmentInfo { StartX = s.start.X, StartY = s.start.Y, EndX = s.end.X, EndY = s.end.Y, IsHorizontal = s.isHorizontal }).ToList(), // 테이블 세그먼트 추가 IntersectionPoints = intersectionPoints.Select(ip => new IntersectionInfo { X = ip.Position.X, Y = ip.Position.Y, DirectionBits = ip.DirectionBits, Row = ip.Row, Column = ip.Column }).ToList(), // 교차점 추가 DiagonalLines = diagonalLines, // 대각선 추가 CellBoundaries = cellBoundaries // 정확한 셀 경계 추가 }); // 테이블 외부의 일반 텍스트들을 좌표별로 정렬하여 그룹에 추가 var sortedNonTableTexts = GetSortedNoteContents(tran, nonTableTextIds, database); currentNoteGroup.AddRange(sortedNonTableTexts); // 박스 내부 텍스트들을 사용된 텍스트 ID에 추가 foreach (var textId in nonTableTextIds) { result.UsedTextIds.Add(textId); } } 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 }); } Debug.WriteLine($"[DEBUG] currentNoteGroup size before adding to noteGroups: {currentNoteGroup.Count}"); noteGroups.Add(currentNoteGroup); } } Debug.WriteLine($"[DEBUG] noteGroups size before sorting: {noteGroups.Count}"); // Note 그룹들을 Y 좌표별로 정렬 (위에서 아래로) var sortedNoteGroups = noteGroups .OrderByDescending(group => group[0].Y) // 각 그룹의 첫 번째 항목(NOTE 헤더)의 Y 좌표로 정렬 .ToList(); Debug.WriteLine($"[DEBUG] sortedNoteGroups size before adding to noteEntities: {sortedNoteGroups.Count}"); // 정렬된 그룹들을 하나의 리스트로 합치기 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}개"); Debug.WriteLine($"[DEBUG] Note에서 사용된 텍스트 ID 개수: {result.UsedTextIds.Count}개"); result.NoteEntities = noteEntities; result.IntersectionPoints = LastIntersectionPoints.Select(ip => new IntersectionPoint { Position = ip.Position, DirectionBits = ip.DirectionBits, Row = ip.Row, Column = ip.Column }).ToList(); result.DiagonalLines = LastDiagonalLines; return result; } /// /// 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 박스 내부의 Line과 Polyline들을 찾습니다. /// private List FindLinesInNoteBox( Transaction tran, (Point3d minPoint, Point3d maxPoint) noteBox, List polylineIds, List lineIds) { var boxLineIds = new List(); // Polyline 엔티티들 검사 foreach (var polylineId in polylineIds) { using (var polyline = tran.GetObject(polylineId, OpenMode.ForRead) as Polyline) { if (polyline == null) continue; // Polyline의 모든 점이 박스 내부에 있는지 확인 bool isInsideBox = true; for (int i = 0; i < polyline.NumberOfVertices; i++) { var point = polyline.GetPoint3dAt(i); if (point.X < noteBox.minPoint.X || point.X > noteBox.maxPoint.X || point.Y < noteBox.minPoint.Y || point.Y > noteBox.maxPoint.Y) { isInsideBox = false; break; } } if (isInsideBox) { boxLineIds.Add(polylineId); Debug.WriteLine($"[DEBUG] 박스 내 Polyline 발견: {polyline.NumberOfVertices}개 점, Layer: {GetLayerName(polyline.LayerId, tran, polyline.Database)}"); } } } // Line 엔티티들 검사 foreach (var lineId in lineIds) { using (var line = tran.GetObject(lineId, OpenMode.ForRead) as Line) { if (line == null) continue; var startPoint = line.StartPoint; var endPoint = line.EndPoint; // Line의 시작점과 끝점이 모두 박스 내부에 있는지 확인 bool startInside = startPoint.X >= noteBox.minPoint.X && startPoint.X <= noteBox.maxPoint.X && startPoint.Y >= noteBox.minPoint.Y && startPoint.Y <= noteBox.maxPoint.Y; bool endInside = endPoint.X >= noteBox.minPoint.X && endPoint.X <= noteBox.maxPoint.X && endPoint.Y >= noteBox.minPoint.Y && endPoint.Y <= noteBox.maxPoint.Y; if (startInside && endInside) { boxLineIds.Add(lineId); Debug.WriteLine($"[DEBUG] 박스 내 Line 발견: ({startPoint.X:F2},{startPoint.Y:F2}) to ({endPoint.X:F2},{endPoint.Y:F2}), Layer: {GetLayerName(line.LayerId, tran, line.Database)}"); } } } Debug.WriteLine($"[DEBUG] 박스 내 Line/Polyline 총 {boxLineIds.Count}개 발견"); return boxLineIds; } /// /// 박스 내부의 Line/Polyline에서 테이블을 구성하는 수평·수직 세그먼트들을 찾습니다. /// private List<(Point3d start, Point3d end, bool isHorizontal)> FindTableSegmentsInBox( Transaction tran, List boxLineIds, double noteHeight) { var tableSegments = new List<(Point3d start, Point3d end, bool isHorizontal)>(); double tolerance = noteHeight * 0.1; // 수평/수직 판단 허용 오차 foreach (var lineId in boxLineIds) { using (var entity = tran.GetObject(lineId, OpenMode.ForRead) as Entity) { if (entity == null) continue; // Line 엔티티 처리 if (entity is Line line) { var start = line.StartPoint; var end = line.EndPoint; // 수평선인지 확인 if (Math.Abs(start.Y - end.Y) < tolerance) { tableSegments.Add((start, end, true)); // 수평 Debug.WriteLine($"[DEBUG] 테이블 수평선: ({start.X:F2},{start.Y:F2}) to ({end.X:F2},{end.Y:F2})"); } // 수직선인지 확인 else if (Math.Abs(start.X - end.X) < tolerance) { tableSegments.Add((start, end, false)); // 수직 Debug.WriteLine($"[DEBUG] 테이블 수직선: ({start.X:F2},{start.Y:F2}) to ({end.X:F2},{end.Y:F2})"); } } // Polyline 엔티티 처리 - 각 세그먼트별로 검사 else if (entity is Polyline polyline) { for (int i = 0; i < polyline.NumberOfVertices - 1; i++) { var start = polyline.GetPoint3dAt(i); var end = polyline.GetPoint3dAt(i + 1); // 수평 세그먼트인지 확인 if (Math.Abs(start.Y - end.Y) < tolerance) { tableSegments.Add((start, end, true)); // 수평 Debug.WriteLine($"[DEBUG] 테이블 수평 세그먼트: ({start.X:F2},{start.Y:F2}) to ({end.X:F2},{end.Y:F2})"); } // 수직 세그먼트인지 확인 else if (Math.Abs(start.X - end.X) < tolerance) { tableSegments.Add((start, end, false)); // 수직 Debug.WriteLine($"[DEBUG] 테이블 수직 세그먼트: ({start.X:F2},{start.Y:F2}) to ({end.X:F2},{end.Y:F2})"); } } // 닫힌 Polyline인 경우 마지막과 첫 번째 점 사이의 세그먼트도 검사 if (polyline.Closed && polyline.NumberOfVertices > 2) { var start = polyline.GetPoint3dAt(polyline.NumberOfVertices - 1); var end = polyline.GetPoint3dAt(0); if (Math.Abs(start.Y - end.Y) < tolerance) { tableSegments.Add((start, end, true)); // 수평 Debug.WriteLine($"[DEBUG] 테이블 수평 세그먼트(닫힘): ({start.X:F2},{start.Y:F2}) to ({end.X:F2},{end.Y:F2})"); } else if (Math.Abs(start.X - end.X) < tolerance) { tableSegments.Add((start, end, false)); // 수직 Debug.WriteLine($"[DEBUG] 테이블 수직 세그먼트(닫힘): ({start.X:F2},{start.Y:F2}) to ({end.X:F2},{end.Y:F2})"); } } } } } Debug.WriteLine($"[DEBUG] 테이블 세그먼트 총 {tableSegments.Count}개 발견 (수평: {tableSegments.Count(s => s.isHorizontal)}, 수직: {tableSegments.Count(s => !s.isHorizontal)})"); return tableSegments; } /// /// 새로운 교차점 기반 알고리즘으로 셀들을 추출합니다. /// private List ExtractTableCells(List<(Point3d start, Point3d end, bool isHorizontal)> tableSegments, double tolerance) { // 새로운 교차점 기반 알고리즘 사용 var intersectionPoints = FindAndClassifyIntersections(tableSegments, tolerance); var cells = ExtractCellsFromIntersections(intersectionPoints, tableSegments, tolerance); // 교차점 정보를 멤버 변수에 저장하여 시각화에서 사용 LastIntersectionPoints = intersectionPoints; return cells; } // 시각화를 위한 교차점 정보 저장 public List LastIntersectionPoints { get; private set; } = new List(); // 시각화를 위한 대각선 정보 저장 public List<(Point3d topLeft, Point3d bottomRight, string label)> LastDiagonalLines { get; private set; } = new List<(Point3d, Point3d, string)>(); /// /// 교차점들을 찾고 타입을 분류합니다. /// public List CalculateIntersectionPointsFromSegments(List<(Point3d start, Point3d end, bool isHorizontal)> tableSegments, double tolerance) { return FindAndClassifyIntersections(tableSegments, tolerance); } private List FindAndClassifyIntersections(List<(Point3d start, Point3d end, bool isHorizontal)> tableSegments, double tolerance) { var intersectionPoints = new List(); // 1. 실제 교차점들 찾기 var rawIntersections = FindRealIntersections(tableSegments, tolerance); foreach (var intersection in rawIntersections) { // 2. 각 교차점에서 연결된 선분들의 방향을 비트 플래그로 분석 var directionBits = GetDirectionBitsAtPoint(intersection, tableSegments, tolerance); intersectionPoints.Add(new IntersectionPoint { Position = intersection, DirectionBits = directionBits }); } Debug.WriteLine($"[DEBUG] 교차점 분류 완료: {intersectionPoints.Count}개"); foreach (var ip in intersectionPoints) { Debug.WriteLine($"[DEBUG] {ip}"); } // 교차점 방향별 개수 출력 var directionGroups = intersectionPoints.GroupBy(p => p.DirectionBits); foreach (var group in directionGroups) { Debug.WriteLine($"[DEBUG] 방향 {group.Key}: {group.Count()}개"); } return intersectionPoints; } /// /// 교차점에서 연결된 선분들의 방향을 비트 플래그로 분석합니다. /// private int GetDirectionBitsAtPoint(Point3d point, List<(Point3d start, Point3d end, bool isHorizontal)> segments, double tolerance) { int directionBits = 0; foreach (var segment in segments) { // 점이 선분의 시작점, 끝점, 또는 중간점인지 확인 bool isOnSegment = IsPointOnSegment(segment, point, tolerance); if (!isOnSegment) continue; // 점에서 선분의 방향 결정 if (segment.isHorizontal) { // 수평선: 점을 기준으로 왼쪽/오른쪽 방향 if (segment.start.X < point.X - tolerance || segment.end.X < point.X - tolerance) directionBits |= DirectionFlags.Left; if (segment.start.X > point.X + tolerance || segment.end.X > point.X + tolerance) directionBits |= DirectionFlags.Right; } else { // 수직선: 점을 기준으로 위/아래 방향 if (segment.start.Y > point.Y + tolerance || segment.end.Y > point.Y + tolerance) directionBits |= DirectionFlags.Up; if (segment.start.Y < point.Y - tolerance || segment.end.Y < point.Y - tolerance) directionBits |= DirectionFlags.Down; } } return directionBits; } /// /// 실제 교차점들로부터 완전한 격자망을 생성합니다. /// 빈 위치에도 가상 교차점을 생성하여 균일한 RXCX 구조를 만듭니다. /// private List CreateCompleteGridFromIntersections(List actualIntersections, List<(Point3d start, Point3d end, bool isHorizontal)> tableSegments, double tolerance) { if (actualIntersections.Count == 0) return new List(); // 1. 전체 테이블에서 유니크한 Y좌표들을 찾아서 Row 번호 매핑 생성 var uniqueYCoords = actualIntersections .Select(i => Math.Round(i.Position.Y, 1)) .Distinct() .OrderByDescending(y => y) // 위에서 아래로 (Y값이 큰 것부터) .ToList(); var yToRowMap = uniqueYCoords .Select((y, index) => new { Y = y, Row = index + 1 }) // 1-based 인덱싱 .ToDictionary(item => item.Y, item => item.Row); // 2. 전체 테이블에서 유니크한 X좌표들을 찾아서 Column 번호 매핑 생성 var uniqueXCoords = actualIntersections .Select(i => Math.Round(i.Position.X, 1)) .Distinct() .OrderBy(x => x) // 왼쪽에서 오른쪽으로 .ToList(); var xToColumnMap = uniqueXCoords .Select((x, index) => new { X = x, Column = index + 1 }) // 1-based 인덱싱 .ToDictionary(item => item.X, item => item.Column); // 3. 완전한 격자망 생성 (모든 Row x Column 조합) var completeGrid = new List(); var actualIntersectionLookup = actualIntersections .GroupBy(i => (Math.Round(i.Position.Y, 1), Math.Round(i.Position.X, 1))) .ToDictionary(g => g.Key, g => g.First()); for (int row = 0; row < uniqueYCoords.Count; row++) { for (int col = 0; col < uniqueXCoords.Count; col++) { var y = uniqueYCoords[row]; var x = uniqueXCoords[col]; var key = (y, x); if (actualIntersectionLookup.ContainsKey(key)) { // 실제 교차점이 존재하는 경우 var actual = actualIntersectionLookup[key]; actual.Row = row + 1; // 1-based 인덱싱 actual.Column = col + 1; // 1-based 인덱싱 completeGrid.Add(actual); } else { // 가상 교차점 생성 (연장된 선의 교차점이므로 DirectionBits = 0) var virtualIntersection = new IntersectionPoint { Position = new Point3d(x, y, 0), DirectionBits = InferDirectionBitsForVirtualIntersection(x, y, tableSegments, tolerance), Row = row + 1, // 1-based 인덱싱 Column = col + 1 // 1-based 인덱싱 }; completeGrid.Add(virtualIntersection); } } } Debug.WriteLine($"[DEBUG] 완전한 격자망 생성 완료:"); Debug.WriteLine($"[DEBUG] - 총 {uniqueYCoords.Count}개 행 × {uniqueXCoords.Count}개 열 = {completeGrid.Count}개 교차점"); Debug.WriteLine($"[DEBUG] - 실제 교차점: {actualIntersections.Count}개, 가상 교차점: {completeGrid.Count - actualIntersections.Count}개"); return completeGrid; } /// /// 가상 교차점의 DirectionBits를 선분 정보를 바탕으로 추론합니다. /// private int InferDirectionBitsForVirtualIntersection(double x, double y, List<(Point3d start, Point3d end, bool isHorizontal)> tableSegments, double tolerance) { int directionBits = 0; var position = new Point3d(x, y, 0); // 각 방향에 선분이 있는지 확인 foreach (var segment in tableSegments) { if (IsPointOnSegment(segment, position, tolerance)) { if (segment.isHorizontal) { // 가로선인 경우: 교차점에서 양쪽으로 연장되는지 확인 var minX = Math.Min(segment.start.X, segment.end.X); var maxX = Math.Max(segment.start.X, segment.end.X); if (minX < position.X - tolerance) directionBits |= DirectionFlags.Left; if (maxX > position.X + tolerance) directionBits |= DirectionFlags.Right; } else { // 세로선인 경우: 교차점에서 위아래로 연장되는지 확인 var minY = Math.Min(segment.start.Y, segment.end.Y); var maxY = Math.Max(segment.start.Y, segment.end.Y); if (maxY > position.Y + tolerance) directionBits |= DirectionFlags.Up; if (minY < position.Y - tolerance) directionBits |= DirectionFlags.Down; } } } // 만약 어떤 방향도 없다면 MERGED 셀로 추정 if (directionBits == 0) { directionBits = DirectionFlags.CrossMerged; // 완전히 합병된 내부 교차점 } else if ((directionBits & (DirectionFlags.Left | DirectionFlags.Right)) == 0) { directionBits |= DirectionFlags.HorizontalMerged; // 좌우 합병 } else if ((directionBits & (DirectionFlags.Up | DirectionFlags.Down)) == 0) { directionBits |= DirectionFlags.VerticalMerged; // 상하 합병 } return directionBits; } /// /// Row/Column 기반으로 체계적으로 bottomRight 교차점을 찾습니다. /// topLeft(Row=n, Column=k)에서 시작하여: /// 1. 같은 Row(n)에서 Column k+1, k+2, ... 순으로 찾기 /// 2. 없으면 Row n+1에서 Column k, k+1, k+2, ... 순으로 찾기 /// 3. Row n+2, n+3, ... 계속 진행 /// private IntersectionPoint FindBottomRightByRowColumn(IntersectionPoint topLeft, List intersections, double tolerance) { Debug.WriteLine($"[DEBUG] FindBottomRightByRowColumn for topLeft R{topLeft.Row}C{topLeft.Column}"); // topLeft가 유효한 topLeft 후보가 아니면 null 반환 if (!IsValidTopLeft(topLeft.DirectionBits)) { Debug.WriteLine($"[DEBUG] topLeft R{topLeft.Row}C{topLeft.Column} is not valid topLeft candidate"); return null; } // 교차점들을 Row/Column으로 빠른 검색을 위해 딕셔너리로 구성 var intersectionLookup = intersections .Where(i => i.Row > 0 && i.Column > 0) // 1-based이므로 > 0으로 유효성 체크 .GroupBy(i => i.Row) .ToDictionary(g => g.Key, g => g.ToDictionary(i => i.Column, i => i)); // topLeft에서 시작하여 체계적으로 bottomRight 찾기 // bottomRight는 topLeft보다 아래쪽 행(Row가 더 큰)에 있어야 함 int maxRow = intersectionLookup.Keys.Any() ? intersectionLookup.Keys.Max() : topLeft.Row; int maxColumn = intersectionLookup.Values.SelectMany(row => row.Keys).Any() ? intersectionLookup.Values.SelectMany(row => row.Keys).Max() : topLeft.Column; // 범위를 확장해서 테이블 경계까지 포함 for (int targetRow = topLeft.Row + 1; targetRow <= maxRow + 2; targetRow++) { if (!intersectionLookup.ContainsKey(targetRow)) continue; var rowIntersections = intersectionLookup[targetRow]; // bottomRight는 topLeft와 같은 열이거나 오른쪽 열에 있어야 함 int startColumn = topLeft.Column; // 해당 행에서 가능한 열들을 순서대로 확인 (범위 확장) var availableColumns = rowIntersections.Keys.Where(col => col >= startColumn).OrderBy(col => col); foreach (int targetColumn in availableColumns) { var candidate = rowIntersections[targetColumn]; // bottomRight 후보인지 확인 (더 유연한 조건) if (IsValidBottomRight(candidate.DirectionBits) || (targetRow == maxRow && targetColumn == maxColumn)) // 테이블 경계에서는 조건 완화 { Debug.WriteLine($"[DEBUG] Found valid bottomRight R{candidate.Row}C{candidate.Column} for topLeft R{topLeft.Row}C{topLeft.Column}"); return candidate; } } } Debug.WriteLine($"[DEBUG] No bottomRight found for topLeft R{topLeft.Row}C{topLeft.Column}"); return null; } /// /// 교차점들로부터 셀들을 추출합니다. /// private List ExtractCellsFromIntersections(List intersections, List<(Point3d start, Point3d end, bool isHorizontal)> tableSegments, double tolerance) { var cells = new List(); // 시각화를 위한 대각선 정보 초기화 LastDiagonalLines.Clear(); // 1. 완전한 격자망 생성 (Row/Column 번호 포함) var completeGrid = CreateCompleteGridFromIntersections(intersections, tableSegments, tolerance); Debug.WriteLine($"[DEBUG] 완전한 격자망 생성: {completeGrid.Count}개 교차점"); // 2. 테이블 경계를 파악하여 외부 교차점 필터링 (NOTE 박스 내 라인들의 bounding box 기준) var filteredIntersections = FilterIntersectionsWithinTableBounds(completeGrid, tableSegments, tolerance); Debug.WriteLine($"[DEBUG] 교차점 필터링: {completeGrid.Count}개 -> {filteredIntersections.Count}개"); // 3. R1C1부터 체계적으로 셀 찾기 // Row/Column 기준으로 정렬하여 R1C1부터 시작 var sortedIntersections = filteredIntersections .Where(i => i.Row > 0 && i.Column > 0) // 1-based이므로 > 0으로 유효성 체크 .OrderBy(i => i.Row).ThenBy(i => i.Column) .ToList(); Debug.WriteLine($"[DEBUG] R1C1부터 체계적 셀 찾기 시작 - 정렬된 교차점: {sortedIntersections.Count}개"); foreach (var topLeft in sortedIntersections) { // topLeft 후보인지 확인 (Right + Down이 있는 교차점들) if (IsValidTopLeft(topLeft.DirectionBits)) { Debug.WriteLine($"[DEBUG] TopLeft 후보 발견: R{topLeft.Row}C{topLeft.Column} at ({topLeft.Position.X:F1},{topLeft.Position.Y:F1}) DirectionBits={topLeft.DirectionBits}"); // Row/Column 기반으로 bottomRight 찾기 var bottomRight = FindBottomRightByRowColumn(topLeft, filteredIntersections, tolerance); if (bottomRight != null) { Debug.WriteLine($"[DEBUG] BottomRight 발견: R{bottomRight.Row}C{bottomRight.Column} at ({bottomRight.Position.X:F1},{bottomRight.Position.Y:F1}) DirectionBits={bottomRight.DirectionBits}"); var newCell = CreateCellFromCornersWithRowColumn(topLeft, bottomRight); cells.Add(newCell); // 시각화를 위한 대각선 정보 저장 (R1C1 라벨) var cellLabel = $"R{newCell.Row}C{newCell.Column}"; LastDiagonalLines.Add((topLeft.Position, bottomRight.Position, cellLabel)); Debug.WriteLine($"[DEBUG] 셀 생성 완료: {cellLabel} - ({topLeft.Position.X:F1},{topLeft.Position.Y:F1}) to ({bottomRight.Position.X:F1},{bottomRight.Position.Y:F1})"); } else { Debug.WriteLine($"[DEBUG] R{topLeft.Row}C{topLeft.Column}에서 적절한 BottomRight를 찾을 수 없음"); } } } Debug.WriteLine($"[DEBUG] 교차점 기반 셀 추출 완료: {cells.Count}개, 대각선 {LastDiagonalLines.Count}개"); return cells; } /// /// NOTE 아래 박스 내의 모든 라인들의 bounding box를 기준으로 테이블 경계 내부의 교차점들만 필터링합니다. /// private List FilterIntersectionsWithinTableBounds(List intersections, List<(Point3d start, Point3d end, bool isHorizontal)> tableSegments, double tolerance) { if (intersections.Count == 0 || tableSegments.Count == 0) return intersections; // 1. NOTE 아래 박스 내의 모든 라인들의 bounding box를 계산 var minX = tableSegments.Min(seg => Math.Min(seg.start.X, seg.end.X)); var maxX = tableSegments.Max(seg => Math.Max(seg.start.X, seg.end.X)); var minY = tableSegments.Min(seg => Math.Min(seg.start.Y, seg.end.Y)); var maxY = tableSegments.Max(seg => Math.Max(seg.start.Y, seg.end.Y)); Debug.WriteLine($"[DEBUG] 테이블 bounding box: ({minX:F1},{minY:F1}) to ({maxX:F1},{maxY:F1})"); // 2. 박스 경계선은 제외하고, 내부의 교차점들만 필터링 (tolerance를 더 크게 설정) var boundaryTolerance = tolerance * 3.0; // 경계 제외를 위한 더 큰 tolerance var validIntersections = intersections.Where(point => point.Position.X > minX + boundaryTolerance && point.Position.X < maxX - boundaryTolerance && point.Position.Y > minY + boundaryTolerance && point.Position.Y < maxY - boundaryTolerance ).ToList(); Debug.WriteLine($"[DEBUG] 테이블 경계 필터링 (bounding box 기반): 원본 {intersections.Count}개 -> 필터링 {validIntersections.Count}개"); // 3. 최소한의 테이블 구조가 있는지 확인 if (validIntersections.Count < 4) { Debug.WriteLine($"[DEBUG] 필터링된 교차점이 너무 적음 ({validIntersections.Count}개), 원본 반환"); return intersections; } return validIntersections; } /// /// 셀들에 Row/Column 번호를 할당합니다. /// private void AssignRowColumnNumbers(List cells) { if (cells.Count == 0) return; // Y좌표로 행 그룹핑 (위에서 아래로, Y값이 큰 것부터) var rowGroups = cells .GroupBy(c => Math.Round(c.MaxPoint.Y, 1)) // 상단 Y좌표 기준으로 그룹핑 .OrderByDescending(g => g.Key) // Y값이 큰 것부터 (위에서 아래로) .ToList(); int rowIndex = 1; // R1부터 시작 foreach (var rowGroup in rowGroups) { // 같은 행 내에서 X좌표로 열 정렬 (왼쪽에서 오른쪽으로) var cellsInRow = rowGroup.OrderBy(c => c.MinPoint.X).ToList(); int columnIndex = 1; // C1부터 시작 foreach (var cell in cellsInRow) { cell.Row = rowIndex; cell.Column = columnIndex; columnIndex++; } rowIndex++; } Debug.WriteLine($"[DEBUG] Row/Column 번호 할당 완료: {rowGroups.Count}개 행, 최대 {cells.Max(c => c.Column + 1)}개 열"); // 디버그 출력: 각 셀의 위치와 번호 foreach (var cell in cells.OrderBy(c => c.Row).ThenBy(c => c.Column)) { Debug.WriteLine($"[DEBUG] 셀 R{cell.Row}C{cell.Column}: ({cell.MinPoint.X:F1}, {cell.MinPoint.Y:F1}) to ({cell.MaxPoint.X:F1}, {cell.MaxPoint.Y:F1})"); } } /// /// 대각선 라벨을 Row/Column 번호로 업데이트합니다. /// private void UpdateDiagonalLabels(List cells) { for (int i = 0; i < LastDiagonalLines.Count && i < cells.Count; i++) { var cell = cells[i]; var diagonal = LastDiagonalLines[i]; // Row/Column 번호를 사용한 새 라벨 생성 var newLabel = $"R{cell.Row}C{cell.Column}"; LastDiagonalLines[i] = (diagonal.topLeft, diagonal.bottomRight, newLabel); Debug.WriteLine($"[DEBUG] 대각선 라벨 업데이트: {diagonal.label} -> {newLabel}"); } } /// /// 교차점이 셀의 topLeft가 될 수 있는지 확인합니다. /// 9번, 11번, 13번, 15번이 topLeft 후보 /// public bool IsValidTopLeft(int directionBits) { return directionBits == 9 || // Right+Down (ㄱ형) directionBits == 11 || // Right+Up+Down (ㅏ형) directionBits == 13 || // Right+Left+Down (ㅜ형) directionBits == 15; // 모든 방향 (+형) } /// /// topLeft에 대응하는 bottomRight 후보를 찾습니다. /// X축으로 가장 가까운 유효한 bottomRight를 반환 (horizontal merge 자동 처리) /// private IntersectionPoint FindBottomRightCandidate(IntersectionPoint topLeft, List intersections, double tolerance) { Debug.WriteLine($"[DEBUG] FindBottomRightCandidate for topLeft {topLeft.Position}[{topLeft.DirectionBits}]"); var allRightDown = intersections.Where(p => p.Position.X > topLeft.Position.X + tolerance && // 오른쪽에 있고 p.Position.Y < topLeft.Position.Y - tolerance // 아래에 있고 ).ToList(); Debug.WriteLine($"[DEBUG] Found {allRightDown.Count} points to the right and below"); foreach (var p in allRightDown) { Debug.WriteLine($"[DEBUG] Point {p.Position}[{p.DirectionBits}] - IsValidBottomRight: {IsValidBottomRight(p.DirectionBits)}"); } var candidates = allRightDown.Where(p => IsValidBottomRight(p.DirectionBits) // 유효한 bottomRight 타입 ).OrderBy(p => p.Position.X) // X값이 가장 가까운 것부터 (horizontal merge 처리) .ThenByDescending(p => p.Position.Y); // 같은 X면 Y값이 큰 것(위에 있는 것)부터 var result = candidates.FirstOrDefault(); Debug.WriteLine($"[DEBUG] Selected bottomRight: {result?.Position}[{result?.DirectionBits}]"); return result; } /// /// 교차점이 셀의 bottomRight가 될 수 있는지 확인합니다. /// 15번, 14번, 6번, 7번이 bottomRight 후보 /// public bool IsValidBottomRight(int directionBits) { bool isValid = directionBits == 15 || // 모든 방향 (+형) directionBits == 14 || // Up+Down+Left (ㅓ형) directionBits == 6 || // Up+Left (ㄹ형) directionBits == 7 || // Up+Left+Right (ㅗ형) directionBits == 10 || // Up+Down (ㅣ형) - 테이블 오른쪽 경계 directionBits == 12 || // Left+Down (ㄱ형 뒤집어진) - 테이블 우상단 경계 directionBits == 4 || // Left (ㅡ형 일부) - 테이블 우하단 경계 directionBits == 2; // Up (ㅣ형 일부) - 테이블 하단 경계 Debug.WriteLine($"[DEBUG] IsValidBottomRight({directionBits}) = {isValid}"); return isValid; } /// /// 두 모서리 점으로부터 Row/Column 번호가 설정된 셀을 생성합니다. /// private TableCell CreateCellFromCornersWithRowColumn(IntersectionPoint topLeft, IntersectionPoint bottomRight) { return new TableCell { MinPoint = new Point3d(topLeft.Position.X, bottomRight.Position.Y, 0), // 왼쪽 하단 MaxPoint = new Point3d(bottomRight.Position.X, topLeft.Position.Y, 0), // 오른쪽 상단 Row = topLeft.Row, // topLeft의 행 번호 Column = topLeft.Column, // topLeft의 열 번호 RowSpan = bottomRight.Row - topLeft.Row + 1, // 행 범위 ColumnSpan = bottomRight.Column - topLeft.Column + 1, // 열 범위 CellText = "" }; } /// /// 두 모서리 점으로부터 셀을 생성합니다. /// private TableCell CreateCellFromCorners(IntersectionPoint topLeft, IntersectionPoint bottomRight) { return new TableCell { MinPoint = new Point3d(topLeft.Position.X, bottomRight.Position.Y, 0), // 왼쪽 하단 MaxPoint = new Point3d(bottomRight.Position.X, topLeft.Position.Y, 0), // 오른쪽 상단 Row = 0, // 나중에 정렬 시 설정 Column = 0, // 나중에 정렬 시 설정 RowSpan = 1, ColumnSpan = 1, CellText = "" }; } /// /// 기존 격자 기반 셀 추출 (백업용) /// private List ExtractTableCellsLegacy(List<(Point3d start, Point3d end, bool isHorizontal)> tableSegments, double tolerance) { var cells = new List(); // 1. 실제 교차점만 찾기 (선분 끝점이 아닌 진짜 교차점만) var intersections = FindRealIntersections(tableSegments, tolerance); Debug.WriteLine($"[DEBUG] 실제 교차점 {intersections.Count}개 발견"); if (intersections.Count < 4) // 최소 4개 교차점 필요 (사각형) { Debug.WriteLine($"[DEBUG] 교차점이 부족하여 셀을 생성할 수 없음"); return cells; } // 2. 고유한 X, Y 좌표들을 정렬하여 격자 생성 var uniqueXCoords = intersections.Select(p => p.X).Distinct().OrderBy(x => x).ToList(); var uniqueYCoords = intersections.Select(p => p.Y).Distinct().OrderByDescending(y => y).ToList(); // Y는 내림차순 (위에서 아래로) Debug.WriteLine($"[DEBUG] 격자: X좌표 {uniqueXCoords.Count}개, Y좌표 {uniqueYCoords.Count}개"); // 3. 각 격자 셀이 실제 테이블 셀인지 확인 (4변이 모두 존재하는 경우만) for (int row = 0; row < uniqueYCoords.Count - 1; row++) { for (int col = 0; col < uniqueXCoords.Count - 1; col++) { var topLeft = new Point3d(uniqueXCoords[col], uniqueYCoords[row], 0); var topRight = new Point3d(uniqueXCoords[col + 1], uniqueYCoords[row], 0); var bottomLeft = new Point3d(uniqueXCoords[col], uniqueYCoords[row + 1], 0); var bottomRight = new Point3d(uniqueXCoords[col + 1], uniqueYCoords[row + 1], 0); // 4변이 모두 존재하는지 확인 bool hasTopEdge = HasSegmentBetweenPoints(tableSegments, topLeft, topRight, tolerance); bool hasBottomEdge = HasSegmentBetweenPoints(tableSegments, bottomLeft, bottomRight, tolerance); bool hasLeftEdge = HasSegmentBetweenPoints(tableSegments, topLeft, bottomLeft, tolerance); bool hasRightEdge = HasSegmentBetweenPoints(tableSegments, topRight, bottomRight, tolerance); if (hasTopEdge && hasBottomEdge && hasLeftEdge && hasRightEdge) { var newCell = new TableCell { MinPoint = bottomLeft, // 왼쪽 하단 MaxPoint = topRight, // 오른쪽 상단 Row = row, Column = col, RowSpan = 1, ColumnSpan = 1 }; // 4. 중첩 검증: 새 셀이 기존 셀들과 중첩되지 않는지 확인 if (!IsOverlappingWithExistingCells(newCell, cells)) { cells.Add(newCell); Debug.WriteLine($"[DEBUG] 셀 발견: Row={row}, Col={col}, Min=({newCell.MinPoint.X:F1},{newCell.MinPoint.Y:F1}), Max=({newCell.MaxPoint.X:F1},{newCell.MaxPoint.Y:F1})"); } else { Debug.WriteLine($"[DEBUG] 중첩 셀 제외: Row={row}, Col={col}, 영역=({bottomLeft.X:F1},{bottomLeft.Y:F1}) to ({topRight.X:F1},{topRight.Y:F1})"); } } } } Debug.WriteLine($"[DEBUG] 중첩 검증 후 셀 개수: {cells.Count}개"); // 5. Merge된 셀 탐지 및 병합 DetectAndMergeCells(cells, tableSegments, tolerance); Debug.WriteLine($"[DEBUG] 최종 셀 개수: {cells.Count}개"); return cells; } /// /// 선분들의 실제 교차점만 찾습니다 (끝점은 제외). /// private List FindRealIntersections(List<(Point3d start, Point3d end, bool isHorizontal)> segments, double tolerance) { var intersections = new HashSet(); // 수평선과 수직선의 실제 교차점만 찾기 (끝점에서의 만남은 제외) var horizontalSegments = segments.Where(s => s.isHorizontal).ToList(); var verticalSegments = segments.Where(s => !s.isHorizontal).ToList(); foreach (var hSeg in horizontalSegments) { foreach (var vSeg in verticalSegments) { var intersection = GetLineIntersection(hSeg, vSeg, tolerance); if (intersection.HasValue) { // 교차점이 두 선분의 끝점이 아닌 실제 교차점인지 확인 if (!IsEndPoint(intersection.Value, hSeg, tolerance) && !IsEndPoint(intersection.Value, vSeg, tolerance)) { intersections.Add(intersection.Value); } else { // 끝점이라도 다른 선분과의 진짜 교차점이라면 포함 intersections.Add(intersection.Value); } } } } Debug.WriteLine($"[DEBUG] 실제 교차점 발견: {intersections.Count}개"); foreach (var intersection in intersections) { Debug.WriteLine($"[DEBUG] 교차점: ({intersection.X:F1}, {intersection.Y:F1})"); } return intersections.ToList(); } /// /// 모든 선분들의 교차점을 찾습니다 (기존 함수 유지). /// private List FindAllIntersections(List<(Point3d start, Point3d end, bool isHorizontal)> segments, double tolerance) { var intersections = new HashSet(); // 모든 세그먼트의 끝점들을 교차점으로 추가 foreach (var segment in segments) { intersections.Add(segment.start); intersections.Add(segment.end); } // 수평선과 수직선의 교차점 찾기 var horizontalSegments = segments.Where(s => s.isHorizontal).ToList(); var verticalSegments = segments.Where(s => !s.isHorizontal).ToList(); foreach (var hSeg in horizontalSegments) { foreach (var vSeg in verticalSegments) { var intersection = GetLineIntersection(hSeg, vSeg, tolerance); if (intersection.HasValue) { intersections.Add(intersection.Value); } } } return intersections.ToList(); } /// /// 두 선분의 교차점을 구합니다. /// private Point3d? GetLineIntersection((Point3d start, Point3d end, bool isHorizontal) hSeg, (Point3d start, Point3d end, bool isHorizontal) vSeg, double tolerance) { // 수평선의 Y 좌표 double hY = (hSeg.start.Y + hSeg.end.Y) / 2; // 수직선의 X 좌표 double vX = (vSeg.start.X + vSeg.end.X) / 2; // 교차점 좌표 var intersection = new Point3d(vX, hY, 0); // 교차점이 두 선분 모두에 속하는지 확인 bool onHorizontal = intersection.X >= Math.Min(hSeg.start.X, hSeg.end.X) - tolerance && intersection.X <= Math.Max(hSeg.start.X, hSeg.end.X) + tolerance; bool onVertical = intersection.Y >= Math.Min(vSeg.start.Y, vSeg.end.Y) - tolerance && intersection.Y <= Math.Max(vSeg.start.Y, vSeg.end.Y) + tolerance; return (onHorizontal && onVertical) ? intersection : null; } /// /// 두 점 사이에 선분이 존재하는지 확인합니다. /// private bool HasSegmentBetweenPoints(List<(Point3d start, Point3d end, bool isHorizontal)> segments, Point3d point1, Point3d point2, double tolerance) { foreach (var segment in segments) { // 선분이 두 점을 연결하는지 확인 (방향 무관) bool connects = (IsPointOnSegment(segment, point1, tolerance) && IsPointOnSegment(segment, point2, tolerance)) || (IsPointOnSegment(segment, point2, tolerance) && IsPointOnSegment(segment, point1, tolerance)); if (connects) { return true; } } return false; } /// /// 점이 선분 위에 있는지 확인합니다. /// private bool IsPointOnSegment((Point3d start, Point3d end, bool isHorizontal) segment, Point3d point, double tolerance) { double minX = Math.Min(segment.start.X, segment.end.X) - tolerance; double maxX = Math.Max(segment.start.X, segment.end.X) + tolerance; double minY = Math.Min(segment.start.Y, segment.end.Y) - tolerance; double maxY = Math.Max(segment.start.Y, segment.end.Y) + tolerance; return point.X >= minX && point.X <= maxX && point.Y >= minY && point.Y <= maxY; } /// /// 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; } /// /// 점이 선분의 끝점인지 확인합니다. /// private bool IsEndPoint(Point3d point, (Point3d start, Point3d end, bool isHorizontal) segment, double tolerance) { return (Math.Abs(point.X - segment.start.X) <= tolerance && Math.Abs(point.Y - segment.start.Y) <= tolerance) || (Math.Abs(point.X - segment.end.X) <= tolerance && Math.Abs(point.Y - segment.end.Y) <= tolerance); } /// /// 새 셀이 기존 셀들과 중첩되는지 확인합니다. /// private bool IsOverlappingWithExistingCells(TableCell newCell, List existingCells) { foreach (var existingCell in existingCells) { // 두 셀이 중첩되는지 확인 if (!(newCell.MaxPoint.X <= existingCell.MinPoint.X || newCell.MinPoint.X >= existingCell.MaxPoint.X || newCell.MaxPoint.Y <= existingCell.MinPoint.Y || newCell.MinPoint.Y >= existingCell.MaxPoint.Y)) { return true; // 중첩됨 } } return false; // 중첩되지 않음 } /// /// 테이블의 모든 교차점에서 DirectionBits를 계산합니다. /// private List CalculateIntersectionDirections(List intersections, List<(Point3d start, Point3d end, bool isHorizontal)> segments, double tolerance) { var intersectionPoints = new List(); foreach (var intersection in intersections) { int directionBits = 0; // 교차점에서 각 방향으로 선분이 있는지 확인 foreach (var segment in segments) { if (IsPointOnSegment(segment, intersection, tolerance)) { if (segment.isHorizontal) { // 가로 선분인 경우 좌우 방향 확인 if (segment.start.X < intersection.X || segment.end.X < intersection.X) directionBits |= DirectionFlags.Left; if (segment.start.X > intersection.X || segment.end.X > intersection.X) directionBits |= DirectionFlags.Right; } else { // 세로 선분인 경우 상하 방향 확인 if (segment.start.Y > intersection.Y || segment.end.Y > intersection.Y) directionBits |= DirectionFlags.Up; if (segment.start.Y < intersection.Y || segment.end.Y < intersection.Y) directionBits |= DirectionFlags.Down; } } } intersectionPoints.Add(new IntersectionPoint { Position = intersection, DirectionBits = directionBits }); } return intersectionPoints; } private (object[,] tableData, List cells, List nonTableTextIds, List<(Point3d start, Point3d end, bool isHorizontal)> tableSegments, List intersectionPoints, List diagonalLines, List cellBoundaries) ExtractTableAndTextsFromNoteBox( Transaction tran, DBText noteText, (Point3d minPoint, Point3d maxPoint) noteBox, List polylineIds, List lineIds, List allTextIds, Database database) { var boxLineIds = FindLinesInNoteBox(tran, noteBox, polylineIds, lineIds); var boxTextIds = FindTextsInNoteBox(tran, noteText, noteBox, allTextIds); var tableSegments = FindTableSegmentsInBox(tran, boxLineIds, noteText.Height); var cells = ExtractTableCells(tableSegments, noteText.Height * 0.5); var (assignedTexts, unassignedTextIds) = AssignTextsToCells(tran, cells, boxTextIds); var tableData = CreateTableDataArray(cells); // 현재 NOTE의 교차점 데이터를 가져오기 (LastIntersectionPoints에서 복사) var currentIntersectionPoints = new List(LastIntersectionPoints); // 대각선 생성 (R2C2 topLeft에서 bottomRight 찾기) var diagonalLines = GenerateDiagonalLines(currentIntersectionPoints); // 대각선으로부터 셀 경계 추출 var cellBoundaries = ExtractCellBoundariesFromDiagonals(diagonalLines); Debug.WriteLine($"[EXTRACT] 셀 경계 {cellBoundaries.Count}개 추출 완료"); // 셀 경계 내 텍스트 추출 ExtractTextsFromCellBoundaries(tran, cellBoundaries, boxTextIds); Debug.WriteLine($"[EXTRACT] 셀 경계 내 텍스트 추출 완료"); // 병합된 셀 처리 (같은 텍스트로 채우기) ProcessMergedCells(cellBoundaries); Debug.WriteLine($"[EXTRACT] 병합 셀 처리 완료"); // CellBoundary 텍스트를 기존 테이블 데이터에 반영 tableData = UpdateTableDataWithCellBoundaries(tableData, cellBoundaries); Debug.WriteLine($"[EXTRACT] 테이블 데이터 업데이트 완료"); return (tableData, cells, unassignedTextIds, tableSegments, currentIntersectionPoints, diagonalLines, cellBoundaries); } /// /// 교차점들로부터 대각선을 생성합니다. 특히 R2C2 topLeft에서 적절한 bottomRight를 찾아서 대각선을 그립니다. /// private List GenerateDiagonalLines(List intersectionPoints) { var diagonalLines = new List(); if (!intersectionPoints.Any()) { Debug.WriteLine("[DIAGONAL] 교차점이 없어 대각선을 생성할 수 없음"); return diagonalLines; } try { // 테이블 내부의 topLeft 후보 찾기 (R1C1과 박스 외곽 제외) var allTopLefts = intersectionPoints .Where(ip => IsValidTopLeft(ip.DirectionBits) && IsInsideTable(ip, intersectionPoints)) // 테이블 내부인지 확인 .OrderBy(ip => ip.Row) .ThenBy(ip => ip.Column) .ToList(); Debug.WriteLine($"[DIAGONAL] 테이블 내부 topLeft 후보 {allTopLefts.Count}개 발견"); foreach (var topLeft in allTopLefts) { Debug.WriteLine($"[DIAGONAL] topLeft R{topLeft.Row}C{topLeft.Column} 처리: ({topLeft.Position.X:F1}, {topLeft.Position.Y:F1}) DirectionBits={topLeft.DirectionBits}"); // 이 topLeft에 대한 첫 번째 bottomRight만 찾기 (row+1, col+1부터 시작) var bottomRight = FindFirstBottomRightForTopLeft(topLeft, intersectionPoints); if (bottomRight != null) { Debug.WriteLine($"[DIAGONAL] bottomRight 발견: R{bottomRight.Row}C{bottomRight.Column} ({bottomRight.Position.X:F1}, {bottomRight.Position.Y:F1})"); // 대각선 생성 var diagonal = new DiagonalLine { StartX = topLeft.Position.X, StartY = topLeft.Position.Y, EndX = bottomRight.Position.X, EndY = bottomRight.Position.Y, Color = "Green", Label = $"R{topLeft.Row}C{topLeft.Column}→R{bottomRight.Row}C{bottomRight.Column}" }; diagonalLines.Add(diagonal); Debug.WriteLine($"[DIAGONAL] 대각선 생성 완료: {diagonal.Label}"); } else { Debug.WriteLine($"[DIAGONAL] topLeft R{topLeft.Row}C{topLeft.Column}에 대한 bottomRight을 찾지 못함"); } } Debug.WriteLine($"[DIAGONAL] 총 {diagonalLines.Count}개 대각선 생성 완료"); } catch (System.Exception ex) { Debug.WriteLine($"[DIAGONAL] 대각선 생성 중 오류: {ex.Message}"); } return diagonalLines; } /// /// 대각선 정보로부터 셀의 4개 모서리 좌표를 계산합니다. /// private List ExtractCellBoundariesFromDiagonals(List diagonalLines) { var cellBoundaries = new List(); try { foreach (var diagonal in diagonalLines) { Debug.WriteLine($"[CELL_BOUNDARY] 대각선 처리: {diagonal.Label}"); // 대각선의 TopLeft, BottomRight 좌표 var topLeft = new Point3d(diagonal.StartX, diagonal.StartY, 0); var bottomRight = new Point3d(diagonal.EndX, diagonal.EndY, 0); // 나머지 두 모서리 계산 var topRight = new Point3d(bottomRight.X, topLeft.Y, 0); var bottomLeft = new Point3d(topLeft.X, bottomRight.Y, 0); var cellBoundary = new CellBoundary { TopLeft = topLeft, TopRight = topRight, BottomLeft = bottomLeft, BottomRight = bottomRight, Label = diagonal.Label, Width = Math.Abs(bottomRight.X - topLeft.X), Height = Math.Abs(topLeft.Y - bottomRight.Y) }; cellBoundaries.Add(cellBoundary); Debug.WriteLine($"[CELL_BOUNDARY] 셀 경계 생성: {diagonal.Label}"); Debug.WriteLine($" TopLeft: ({topLeft.X:F1}, {topLeft.Y:F1})"); Debug.WriteLine($" TopRight: ({topRight.X:F1}, {topRight.Y:F1})"); Debug.WriteLine($" BottomLeft: ({bottomLeft.X:F1}, {bottomLeft.Y:F1})"); Debug.WriteLine($" BottomRight: ({bottomRight.X:F1}, {bottomRight.Y:F1})"); Debug.WriteLine($" 크기: {cellBoundary.Width:F1} x {cellBoundary.Height:F1}"); } Debug.WriteLine($"[CELL_BOUNDARY] 총 {cellBoundaries.Count}개 셀 경계 생성 완료"); } catch (System.Exception ex) { Debug.WriteLine($"[CELL_BOUNDARY] 셀 경계 추출 중 오류: {ex.Message}"); } return cellBoundaries; } /// /// 셀 경계 내에 포함되는 모든 텍스트(DBText, MText)를 추출합니다. /// private void ExtractTextsFromCellBoundaries(Transaction tran, List cellBoundaries, List allTextIds) { try { Debug.WriteLine($"[CELL_TEXT] {cellBoundaries.Count}개 셀 경계에서 텍스트 추출 시작"); foreach (var cellBoundary in cellBoundaries) { var textsInCell = new List<(string text, double y)>(); foreach (var textId in allTextIds) { var textEntity = tran.GetObject(textId, OpenMode.ForRead); Point3d textPosition = Point3d.Origin; string textContent = ""; // DBText와 MText 처리 if (textEntity is DBText dbText) { textPosition = dbText.Position; textContent = dbText.TextString; Debug.WriteLine($"[CELL_TEXT] DBText 발견: '{textContent}' at ({textPosition.X:F1}, {textPosition.Y:F1})"); } else if (textEntity is MText mText) { textPosition = mText.Location; textContent = mText.Contents; Debug.WriteLine($"[CELL_TEXT] MText 발견: '{textContent}' at ({textPosition.X:F1}, {textPosition.Y:F1})"); } else { continue; // 다른 타입의 텍스트는 무시 } // 텍스트가 셀 경계 내에 있는지 확인 if (IsPointInCellBoundary(textPosition, cellBoundary)) { if (!string.IsNullOrWhiteSpace(textContent)) { textsInCell.Add((textContent.Trim(), textPosition.Y)); Debug.WriteLine($"[CELL_TEXT] ✅ {cellBoundary.Label}에 텍스트 추가: '{textContent.Trim()}' at Y={textPosition.Y:F1}"); } } } // Y값이 큰 것부터 정렬 (위에서 아래로)하여 콤마로 연결 var sortedTexts = textsInCell.OrderByDescending(t => t.y).Select(t => t.text); cellBoundary.CellText = string.Join(", ", sortedTexts); Debug.WriteLine($"[CELL_TEXT] {cellBoundary.Label} 최종 텍스트: '{cellBoundary.CellText}'"); } Debug.WriteLine($"[CELL_TEXT] 셀 경계 텍스트 추출 완료"); } catch (System.Exception ex) { Debug.WriteLine($"[CELL_TEXT] 셀 경계 텍스트 추출 중 오류: {ex.Message}"); } } /// /// 점이 셀 경계 내에 있는지 확인합니다. /// private bool IsPointInCellBoundary(Point3d point, CellBoundary cellBoundary) { // 단순한 사각형 범위 체크 double minX = Math.Min(cellBoundary.TopLeft.X, cellBoundary.BottomRight.X); double maxX = Math.Max(cellBoundary.TopLeft.X, cellBoundary.BottomRight.X); double minY = Math.Min(cellBoundary.TopLeft.Y, cellBoundary.BottomRight.Y); double maxY = Math.Max(cellBoundary.TopLeft.Y, cellBoundary.BottomRight.Y); bool isInside = point.X >= minX && point.X <= maxX && point.Y >= minY && point.Y <= maxY; if (isInside) { Debug.WriteLine($"[CELL_TEXT] 텍스트 위치 ({point.X:F1}, {point.Y:F1})가 {cellBoundary.Label} 범위 ({minX:F1}-{maxX:F1}, {minY:F1}-{maxY:F1}) 내에 있음"); } return isInside; } /// /// 셀 경계들을 분석하여 병합된 셀들을 찾고 같은 텍스트로 채웁니다. /// private void ProcessMergedCells(List cellBoundaries) { try { Debug.WriteLine($"[MERGE] {cellBoundaries.Count}개 셀 경계에서 병합 처리 시작"); // 라벨에서 Row/Column 정보 추출하여 그룹핑 var cellsByRowCol = new Dictionary<(int row, int col), CellBoundary>(); foreach (var cell in cellBoundaries) { var (row, col) = ParseRowColFromLabel(cell.Label); if (row > 0 && col > 0) { cellsByRowCol[(row, col)] = cell; } } Debug.WriteLine($"[MERGE] {cellsByRowCol.Count}개 셀의 Row/Col 정보 파싱 완료"); // 각 셀에 대해 병합된 영역 찾기 var processedCells = new HashSet<(int, int)>(); foreach (var kvp in cellsByRowCol) { var (row, col) = kvp.Key; var cell = kvp.Value; if (processedCells.Contains((row, col)) || string.IsNullOrEmpty(cell.CellText)) continue; // 이 셀에서 시작하는 병합 영역 찾기 var mergedCells = FindMergedRegion(cellsByRowCol, row, col, cell); if (mergedCells.Count > 1) { Debug.WriteLine($"[MERGE] R{row}C{col}에서 시작하는 {mergedCells.Count}개 병합 셀 발견"); // 모든 병합된 셀에 같은 텍스트 적용 foreach (var mergedCell in mergedCells) { mergedCell.CellText = cell.CellText; // 원본 셀의 텍스트를 모든 병합 셀에 복사 var (r, c) = ParseRowColFromLabel(mergedCell.Label); processedCells.Add((r, c)); Debug.WriteLine($"[MERGE] {mergedCell.Label}에 텍스트 복사: '{cell.CellText}'"); } } else { processedCells.Add((row, col)); } } Debug.WriteLine($"[MERGE] 병합 셀 처리 완료"); } catch (System.Exception ex) { Debug.WriteLine($"[MERGE] 병합 셀 처리 중 오류: {ex.Message}"); } } /// /// 라벨에서 Row, Column 번호를 파싱합니다. (예: "R2C3" → (2, 3)) /// private (int row, int col) ParseRowColFromLabel(string label) { try { if (string.IsNullOrEmpty(label)) return (0, 0); var parts = label.Replace("R", "").Split('C'); if (parts.Length == 2 && int.TryParse(parts[0], out int row) && int.TryParse(parts[1], out int col)) { return (row, col); } } catch (System.Exception ex) { Debug.WriteLine($"[MERGE] 라벨 파싱 오류: {label}, {ex.Message}"); } return (0, 0); } /// /// 주어진 셀에서 시작하는 병합된 영역을 찾습니다. /// private List FindMergedRegion(Dictionary<(int row, int col), CellBoundary> cellsByRowCol, int startRow, int startCol, CellBoundary originCell) { var mergedCells = new List { originCell }; try { // 수평 병합 확인 (오른쪽으로 확장) int maxCol = startCol; for (int col = startCol + 1; col <= startCol + 10; col++) // 최대 10개까지만 확인 { if (cellsByRowCol.TryGetValue((startRow, col), out var rightCell)) { if (IsSameCellContent(originCell, rightCell) || string.IsNullOrEmpty(rightCell.CellText)) { mergedCells.Add(rightCell); maxCol = col; } else { break; } } else { break; } } // 수직 병합 확인 (아래쪽으로 확장) int maxRow = startRow; for (int row = startRow + 1; row <= startRow + 10; row++) // 최대 10개까지만 확인 { bool canExtendRow = true; var rowCells = new List(); // 현재 행의 모든 열을 확인 for (int col = startCol; col <= maxCol; col++) { if (cellsByRowCol.TryGetValue((row, col), out var belowCell)) { if (IsSameCellContent(originCell, belowCell) || string.IsNullOrEmpty(belowCell.CellText)) { rowCells.Add(belowCell); } else { canExtendRow = false; break; } } else { canExtendRow = false; break; } } if (canExtendRow) { mergedCells.AddRange(rowCells); maxRow = row; } else { break; } } Debug.WriteLine($"[MERGE] R{startRow}C{startCol} 병합 영역: R{startRow}-R{maxRow}, C{startCol}-C{maxCol} ({mergedCells.Count}개 셀)"); } catch (System.Exception ex) { Debug.WriteLine($"[MERGE] 병합 영역 찾기 오류: {ex.Message}"); } return mergedCells; } /// /// 두 셀이 같은 내용을 가지는지 확인합니다. /// private bool IsSameCellContent(CellBoundary cell1, CellBoundary cell2) { return !string.IsNullOrEmpty(cell1.CellText) && !string.IsNullOrEmpty(cell2.CellText) && cell1.CellText.Trim() == cell2.CellText.Trim(); } /// /// CellBoundary의 텍스트 정보를 기존 테이블 데이터에 업데이트합니다. /// private object[,] UpdateTableDataWithCellBoundaries(object[,] originalTableData, List cellBoundaries) { try { if (originalTableData == null || cellBoundaries == null || cellBoundaries.Count == 0) return originalTableData; Debug.WriteLine($"[TABLE_UPDATE] 기존 테이블 ({originalTableData.GetLength(0)}x{originalTableData.GetLength(1)})을 {cellBoundaries.Count}개 셀 경계로 업데이트"); // CellBoundary에서 최대 Row/Column 찾기 int maxRow = 0, maxCol = 0; var cellBoundaryDict = new Dictionary<(int row, int col), string>(); foreach (var cell in cellBoundaries) { var (row, col) = ParseRowColFromLabel(cell.Label); if (row > 0 && col > 0) { maxRow = Math.Max(maxRow, row); maxCol = Math.Max(maxCol, col); // 텍스트가 있는 경우 딕셔너리에 저장 if (!string.IsNullOrEmpty(cell.CellText)) { cellBoundaryDict[(row, col)] = cell.CellText; } } } Debug.WriteLine($"[TABLE_UPDATE] CellBoundary 최대 크기: {maxRow}x{maxCol}, 텍스트 있는 셀: {cellBoundaryDict.Count}개"); // 새로운 테이블 크기 결정 (기존 테이블과 CellBoundary 중 큰 크기) int newRows = Math.Max(originalTableData.GetLength(0), maxRow); int newCols = Math.Max(originalTableData.GetLength(1), maxCol); var newTableData = new object[newRows, newCols]; // 기존 데이터 복사 for (int r = 0; r < originalTableData.GetLength(0); r++) { for (int c = 0; c < originalTableData.GetLength(1); c++) { newTableData[r, c] = originalTableData[r, c]; } } // CellBoundary 텍스트로 업데이트 (1-based → 0-based 변환) foreach (var kvp in cellBoundaryDict) { var (row, col) = kvp.Key; var text = kvp.Value; int arrayRow = row - 1; // 1-based → 0-based int arrayCol = col - 1; // 1-based → 0-based if (arrayRow >= 0 && arrayRow < newRows && arrayCol >= 0 && arrayCol < newCols) { // 기존 값과 새로운 값 비교 var existingValue = newTableData[arrayRow, arrayCol]?.ToString() ?? ""; if (string.IsNullOrEmpty(existingValue) || existingValue != text) { newTableData[arrayRow, arrayCol] = text; Debug.WriteLine($"[TABLE_UPDATE] [{arrayRow},{arrayCol}] 업데이트: '{existingValue}' → '{text}'"); } } } Debug.WriteLine($"[TABLE_UPDATE] 테이블 데이터 업데이트 완료: {newRows}x{newCols}"); return newTableData; } catch (System.Exception ex) { Debug.WriteLine($"[TABLE_UPDATE] 테이블 데이터 업데이트 중 오류: {ex.Message}"); return originalTableData; } } /// /// 교차점이 테이블 내부에 있는지 확인합니다. (R1C1과 박스 외곽 제외) /// private bool IsInsideTable(IntersectionPoint point, List allIntersections) { try { // R1C1은 제외 (박스 외곽) if (point.Row == 1 && point.Column == 1) { Debug.WriteLine($"[DIAGONAL] R{point.Row}C{point.Column} - R1C1 제외됨"); return false; } // 최소/최대 Row/Column 찾기 int minRow = allIntersections.Min(ip => ip.Row); int maxRow = allIntersections.Max(ip => ip.Row); int minCol = allIntersections.Min(ip => ip.Column); int maxCol = allIntersections.Max(ip => ip.Column); Debug.WriteLine($"[DIAGONAL] 테이블 범위: R{minRow}-R{maxRow}, C{minCol}-C{maxCol}"); // 박스 외곽 경계에 있는 교차점들 제외 bool isOnBoundary = (point.Row == minRow || point.Row == maxRow || point.Column == minCol || point.Column == maxCol); if (isOnBoundary) { Debug.WriteLine($"[DIAGONAL] R{point.Row}C{point.Column} - 박스 외곽 경계에 있어 제외됨"); return false; } Debug.WriteLine($"[DIAGONAL] R{point.Row}C{point.Column} - 테이블 내부로 판정"); return true; } catch (System.Exception ex) { Debug.WriteLine($"[DIAGONAL] IsInsideTable 오류: {ex.Message}"); return false; } } /// /// topLeft 교차점에 대한 첫 번째 bottomRight 교차점을 찾습니다. (row+1, col+1부터 점진적 탐색) /// private IntersectionPoint? FindFirstBottomRightForTopLeft(IntersectionPoint topLeft, List intersectionPoints) { try { // 교차점들을 Row/Column으로 딕셔너리 구성 var intersectionLookup = intersectionPoints .Where(i => i.Row > 0 && i.Column > 0) .GroupBy(i => i.Row) .ToDictionary(g => g.Key, g => g.ToDictionary(i => i.Column, i => i)); Debug.WriteLine($"[DIAGONAL] 교차점 룩업 구성: {intersectionLookup.Count}개 행"); // topLeft에서 시작해서 row+1, col+1부터 점진적으로 탐색 for (int targetRow = topLeft.Row + 1; targetRow <= topLeft.Row + 10; targetRow++) // 최대 10행까지만 탐색 { if (!intersectionLookup.ContainsKey(targetRow)) { Debug.WriteLine($"[DIAGONAL] 행 {targetRow}에 교차점이 없음"); continue; } var rowIntersections = intersectionLookup[targetRow]; Debug.WriteLine($"[DIAGONAL] 행 {targetRow}에 {rowIntersections.Count}개 교차점"); // col+1부터 차례대로 찾기 (가장 가까운 것부터) for (int targetColumn = topLeft.Column + 1; targetColumn <= topLeft.Column + 10; targetColumn++) // 최대 10열까지만 탐색 { if (!rowIntersections.ContainsKey(targetColumn)) { Debug.WriteLine($"[DIAGONAL] R{targetRow}C{targetColumn}에 교차점이 없음"); continue; } var candidate = rowIntersections[targetColumn]; Debug.WriteLine($"[DIAGONAL] 후보 R{targetRow}C{targetColumn} 검사: DirectionBits={candidate.DirectionBits}"); // bottomRight가 될 수 있는지 확인 (테이블 내부인지도 확인) if (IsValidBottomRight(candidate.DirectionBits) && IsInsideTable(candidate, intersectionPoints)) { Debug.WriteLine($"[DIAGONAL] 첫 번째 유효한 bottomRight 발견: R{targetRow}C{targetColumn}"); return candidate; } else if (candidate.DirectionBits == 13) // 수평 병합: col+1로 계속 검색 { Debug.WriteLine($"[DIAGONAL] R{targetRow}C{targetColumn}는 수평 병합(13), col+1로 계속 검색"); continue; // 같은 행에서 다음 열로 이동 } else if (candidate.DirectionBits == 11) // 수직 병합: row+1로 검색 { Debug.WriteLine($"[DIAGONAL] R{targetRow}C{targetColumn}는 수직 병합(11), row+1로 검색"); break; // 다음 행으로 이동 } else { Debug.WriteLine($"[DIAGONAL] R{targetRow}C{targetColumn}는 bottomRight로 부적절"); } } } Debug.WriteLine($"[DIAGONAL] topLeft R{topLeft.Row}C{topLeft.Column}에 대한 bottomRight을 찾지 못함"); return null; } catch (System.Exception ex) { Debug.WriteLine($"[DIAGONAL] FindFirstBottomRightForTopLeft 오류: {ex.Message}"); return null; } } /// /// topLeft 교차점에 대한 모든 가능한 bottomRight 교차점들을 찾습니다. /// private List FindAllBottomRightsForTopLeft(IntersectionPoint topLeft, List intersectionPoints) { var validBottomRights = new List(); try { // 교차점들을 Row/Column으로 딕셔너리 구성 var intersectionLookup = intersectionPoints .Where(i => i.Row > 0 && i.Column > 0) .GroupBy(i => i.Row) .ToDictionary(g => g.Key, g => g.ToDictionary(i => i.Column, i => i)); Debug.WriteLine($"[DIAGONAL] 교차점 룩업 구성: {intersectionLookup.Count}개 행"); // topLeft보다 오른쪽 아래에 있는 교차점들 중에서 찾기 int maxRow = intersectionLookup.Keys.Any() ? intersectionLookup.Keys.Max() : topLeft.Row; int maxCol = intersectionLookup.Values.SelectMany(row => row.Keys).Any() ? intersectionLookup.Values.SelectMany(row => row.Keys).Max() : topLeft.Column; Debug.WriteLine($"[DIAGONAL] 최대 행: {maxRow}, 최대 열: {maxCol}"); // topLeft보다 아래/오른쪽에 있는 모든 교차점들을 체크 for (int targetRow = topLeft.Row + 1; targetRow <= maxRow + 2; targetRow++) { if (!intersectionLookup.ContainsKey(targetRow)) continue; var rowIntersections = intersectionLookup[targetRow]; // topLeft와 같거나 오른쪽 컬럼들을 탐색 var availableColumns = rowIntersections.Keys .Where(col => col >= topLeft.Column) .OrderBy(col => col); foreach (int targetColumn in availableColumns) { var candidate = rowIntersections[targetColumn]; Debug.WriteLine($"[DIAGONAL] 후보 R{targetRow}C{targetColumn} 검사: DirectionBits={candidate.DirectionBits}"); // bottomRight가 될 수 있는지 확인 (테이블 내부인지도 확인) if (IsValidBottomRight(candidate.DirectionBits) && IsInsideTable(candidate, intersectionPoints)) { validBottomRights.Add(candidate); Debug.WriteLine($"[DIAGONAL] 유효한 bottomRight 추가: R{targetRow}C{targetColumn}"); } else { Debug.WriteLine($"[DIAGONAL] R{targetRow}C{targetColumn}는 bottomRight로 부적절 (DirectionBits 또는 테이블 외부)"); } } } Debug.WriteLine($"[DIAGONAL] topLeft R{topLeft.Row}C{topLeft.Column}에 대해 총 {validBottomRights.Count}개 bottomRight 발견"); return validBottomRights; } catch (System.Exception ex) { Debug.WriteLine($"[DIAGONAL] FindAllBottomRightsForTopLeft 오류: {ex.Message}"); return validBottomRights; } } /// /// topLeft 교차점에 대한 적절한 bottomRight 교차점을 찾습니다. (기존 메서드 - 호환성 유지) /// private IntersectionPoint? FindBottomRightForTopLeft(IntersectionPoint topLeft, List intersectionPoints) { try { // 교차점들을 Row/Column으로 딕셔너리 구성 var intersectionLookup = intersectionPoints .Where(i => i.Row > 0 && i.Column > 0) .GroupBy(i => i.Row) .ToDictionary(g => g.Key, g => g.ToDictionary(i => i.Column, i => i)); Debug.WriteLine($"[DIAGONAL] 교차점 룩업 구성: {intersectionLookup.Count}개 행"); // topLeft보다 오른쪽 아래에 있는 교차점들 중에서 찾기 int maxRow = intersectionLookup.Keys.Any() ? intersectionLookup.Keys.Max() : topLeft.Row; Debug.WriteLine($"[DIAGONAL] 최대 행 번호: {maxRow}"); // topLeft보다 아래 행들을 탐색 for (int targetRow = topLeft.Row + 1; targetRow <= maxRow + 2; targetRow++) { if (!intersectionLookup.ContainsKey(targetRow)) { Debug.WriteLine($"[DIAGONAL] 행 {targetRow}에 교차점이 없음"); continue; } var rowIntersections = intersectionLookup[targetRow]; Debug.WriteLine($"[DIAGONAL] 행 {targetRow}에 {rowIntersections.Count}개 교차점"); // topLeft와 같거나 오른쪽 컬럼들을 탐색 (컬럼 순서대로) var availableColumns = rowIntersections.Keys .Where(col => col >= topLeft.Column) .OrderBy(col => col); foreach (int targetColumn in availableColumns) { var candidate = rowIntersections[targetColumn]; Debug.WriteLine($"[DIAGONAL] 후보 R{targetRow}C{targetColumn} 검사: DirectionBits={candidate.DirectionBits}"); // bottomRight가 될 수 있는지 확인 if (IsValidBottomRight(candidate.DirectionBits)) { Debug.WriteLine($"[DIAGONAL] 유효한 bottomRight 발견: R{targetRow}C{targetColumn}"); return candidate; } else { Debug.WriteLine($"[DIAGONAL] R{targetRow}C{targetColumn}는 bottomRight로 부적절"); } } } Debug.WriteLine($"[DIAGONAL] topLeft R{topLeft.Row}C{topLeft.Column}에 대한 bottomRight을 찾지 못함"); return null; } catch (System.Exception ex) { Debug.WriteLine($"[DIAGONAL] FindBottomRightForTopLeft 오류: {ex.Message}"); return null; } } private (Dictionary> assignedTexts, List unassignedTextIds) AssignTextsToCells( Transaction tran, List cells, List textIds) { var assignedTexts = new Dictionary>(); var unassignedTextIds = new List(textIds); var assignedIds = new HashSet(); foreach (var cell in cells) { var textsInCell = new List<(string text, double y)>(); foreach (var textId in textIds) { if (assignedIds.Contains(textId)) continue; using (var dbText = tran.GetObject(textId, OpenMode.ForRead) as DBText) { if (dbText != null) { // Check if the text's alignment point is inside the cell if (IsPointInCell(dbText.Position, cell)) { textsInCell.Add((dbText.TextString, dbText.Position.Y)); assignedIds.Add(textId); } } } } // Y값이 큰 것부터 정렬 (위에서 아래로)하여 텍스트 설정 if (textsInCell.Any()) { var sortedTexts = textsInCell.OrderByDescending(t => t.y).Select(t => t.text); cell.CellText = string.Join("\n", sortedTexts); assignedTexts[cell] = textsInCell.Select(t => t.text).ToList(); } } unassignedTextIds.RemoveAll(id => assignedIds.Contains(id)); return (assignedTexts, unassignedTextIds); } private bool IsPointInCell(Point3d point, TableCell cell) { double tolerance = 1e-6; return point.X >= cell.MinPoint.X - tolerance && point.X <= cell.MaxPoint.X + tolerance && point.Y >= cell.MinPoint.Y - tolerance && point.Y <= cell.MaxPoint.Y + tolerance; } private object[,] CreateTableDataArray(List cells) { if (!cells.Any()) return new object[0, 0]; int rows = cells.Max(c => c.Row + c.RowSpan); int cols = cells.Max(c => c.Column + c.ColumnSpan); var tableData = new object[rows, cols]; foreach (var cell in cells) { if (cell.Row < rows && cell.Column < cols) { tableData[cell.Row, cell.Column] = cell.CellText; } } return tableData; } private void DetectAndMergeCells(List cells, List<(Point3d start, Point3d end, bool isHorizontal)> segments, double tolerance) { // This is a placeholder implementation. // The actual logic would be complex, involving checking for missing separators. // For now, we'll just log that it was called. Debug.WriteLine("[DEBUG] DetectAndMergeCells called, but not implemented."); } private bool IsPointOnSegment(Point3d point, Point3d start, Point3d end, double tolerance) { // Check if the point is within the bounding box of the segment bool inBounds = point.X >= Math.Min(start.X, end.X) - tolerance && point.X <= Math.Max(start.X, end.X) + tolerance && point.Y >= Math.Min(start.Y, end.Y) - tolerance && point.Y <= Math.Max(start.Y, end.Y) + tolerance; if (!inBounds) return false; // Check for collinearity double crossProduct = (point.Y - start.Y) * (end.X - start.X) - (point.X - start.X) * (end.Y - start.Y); return Math.Abs(crossProduct) < tolerance * tolerance; } } // DwgDataExtractor 클래스 끝 /// /// Note 추출 결과를 담는 클래스 /// public class NoteExtractionResult { public List NoteEntities { get; set; } = new List(); public List IntersectionPoints { get; set; } = new List(); public List<(Teigha.Geometry.Point3d topLeft, Teigha.Geometry.Point3d bottomRight, string label)> DiagonalLines { get; set; } = new List<(Teigha.Geometry.Point3d, Teigha.Geometry.Point3d, string)>(); public List TableSegments { get; set; } = new List(); public HashSet UsedTextIds { get; set; } = new HashSet(); // Note에서 사용된 텍스트 ID들 } /// /// /// 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...) public object[,] TableData { get; set; } = new object[0, 0]; // 테이블 데이터 (2D 배열) public List Cells { get; set; } = new List(); // 테이블 셀 정보 (병합용) public List TableSegments { get; set; } = new List(); // 테이블 세그먼트 정보 public List IntersectionPoints { get; set; } = new List(); // 교차점 정보 public List DiagonalLines { get; set; } = new List(); // 대각선 정보 public List CellBoundaries { get; set; } = new List(); // 정확한 셀 경계 정보 public string TableCsv { get { if (TableData == null || TableData.Length == 0) return ""; var csvLines = new List(); int rows = TableData.GetLength(0); int cols = TableData.GetLength(1); for (int r = 0; r < rows; r++) { var rowValues = new List(); for (int c = 0; c < cols; c++) { var cellValue = TableData[r, c]?.ToString() ?? ""; if (cellValue.Contains(",") || cellValue.Contains("\"") || cellValue.Contains("\n")) { cellValue = "\"" + cellValue.Replace("\"", "\"\"") + "\""; } rowValues.Add(cellValue); } csvLines.Add(string.Join(",", rowValues)); } return string.Join("\n", csvLines); } } } /// /// 테이블 셀 정보 클래스 /// public class TableCell { public Point3d MinPoint { get; set; } = new Point3d(); // 왼쪽 하단 public Point3d MaxPoint { get; set; } = new Point3d(); // 오른쪽 상단 public int Row { get; set; } = 0; // 행 번호 (0부터 시작) public int Column { get; set; } = 0; // 열 번호 (0부터 시작) public int RowSpan { get; set; } = 1; // 행 병합 수 public int ColumnSpan { get; set; } = 1; // 열 병합 수 public string CellText { get; set; } = ""; // 셀 내부의 텍스트 /// /// 셀의 중심 좌표를 반환합니다. /// public Point3d CenterPoint => new Point3d( (MinPoint.X + MaxPoint.X) / 2, (MinPoint.Y + MaxPoint.Y) / 2, 0 ); /// /// 셀의 너비를 반환합니다. /// public double Width => MaxPoint.X - MinPoint.X; /// /// 셀의 높이를 반환합니다. /// public double Height => MaxPoint.Y - MinPoint.Y; /// /// 점이 이 셀 내부에 있는지 확인합니다. /// public bool ContainsPoint(Point3d point, double tolerance = 0.01) { return point.X >= MinPoint.X - tolerance && point.X <= MaxPoint.X + tolerance && point.Y >= MinPoint.Y - tolerance && point.Y <= MaxPoint.Y + tolerance; } public override string ToString() { return $"Cell[{Row},{Column}] Size({RowSpan}x{ColumnSpan}) Area({MinPoint.X:F1},{MinPoint.Y:F1})-({MaxPoint.X:F1},{MaxPoint.Y:F1})"; } } /// /// 방향을 비트 플래그로 나타내는 상수 /// public static class DirectionFlags { public const int Right = 1; // 0001 public const int Up = 2; // 0010 public const int Left = 4; // 0100 public const int Down = 8; // 1000 // MERGED 셀 타입들 public const int HorizontalMerged = 16; // 0001 0000 - 세로선이 없음 (좌우 합병) public const int VerticalMerged = 32; // 0010 0000 - 가로선이 없음 (상하 합병) public const int CrossMerged = 48; // 0011 0000 - 모든 선이 없음 (완전 합병) } /// /// 교차점 정보를 담는 클래스 /// public class IntersectionPoint { public Point3d Position { get; set; } public int DirectionBits { get; set; } = 0; // 비트 플래그로 방향 저장 public int Row { get; set; } = -1; // 교차점의 행 번호 public int Column { get; set; } = -1; // 교차점의 열 번호 public bool HasRight => (DirectionBits & DirectionFlags.Right) != 0; public bool HasUp => (DirectionBits & DirectionFlags.Up) != 0; public bool HasLeft => (DirectionBits & DirectionFlags.Left) != 0; public bool HasDown => (DirectionBits & DirectionFlags.Down) != 0; // MERGED 셀 타입 확인 속성들 public bool IsHorizontalMerged => (DirectionBits & DirectionFlags.HorizontalMerged) != 0; public bool IsVerticalMerged => (DirectionBits & DirectionFlags.VerticalMerged) != 0; public bool IsCrossMerged => (DirectionBits & DirectionFlags.CrossMerged) != 0; public override string ToString() { var directions = new List(); if (HasRight) directions.Add("R"); if (HasUp) directions.Add("U"); if (HasLeft) directions.Add("L"); if (HasDown) directions.Add("D"); return $"Intersection[{DirectionBits}] at ({Position.X:F1},{Position.Y:F1}) [{string.Join(",", directions)}]"; } } /// /// 셀의 4개 모서리 좌표를 나타내는 클래스 /// public class CellBoundary { public Point3d TopLeft { get; set; } public Point3d TopRight { get; set; } public Point3d BottomLeft { get; set; } public Point3d BottomRight { get; set; } public string Label { get; set; } = ""; public double Width { get; set; } public double Height { get; set; } public string CellText { get; set; } = ""; // 셀 내 텍스트 내용 public override string ToString() { return $"CellBoundary[{Label}] ({Width:F1}x{Height:F1}) " + $"TL:({TopLeft.X:F1},{TopLeft.Y:F1}) BR:({BottomRight.X:F1},{BottomRight.Y:F1})"; } } }