Files
manual_wpf/Models/DwgDataExtractor.cs
2025-08-12 14:33:18 +09:00

3053 lines
142 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
{
/// <summary>
/// DWG <20><><EFBFBD>Ͽ<EFBFBD><CFBF><EFBFBD> <20>ؽ<EFBFBD>Ʈ <20><>ƼƼ<C6BC><C6BC> <20><><EFBFBD><EFBFBD><EFBFBD>ϴ<EFBFBD> Ŭ<><C5AC><EFBFBD><EFBFBD>
/// </summary>
internal class DwgDataExtractor
{
private readonly FieldMapper fieldMapper;
public DwgDataExtractor(FieldMapper fieldMapper)
{
this.fieldMapper = fieldMapper ?? throw new ArgumentNullException(nameof(fieldMapper));
}
/// <summary>
/// DWG <20><><EFBFBD>Ͽ<EFBFBD><CFBF><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>͸<EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>Ͽ<EFBFBD> ExcelRowData <20><><EFBFBD><EFBFBD>Ʈ<EFBFBD><C6AE> <20><>ȯ
/// </summary>
public DwgExtractionResult ExtractFromDwgFile(string filePath, IProgress<double>? progress = null, CancellationToken cancellationToken = default)
{
var result = new DwgExtractionResult();
if (!File.Exists(filePath))
{
Debug.WriteLine($"? <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20>ʽ<EFBFBD><CABD>ϴ<EFBFBD>: {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<ObjectId>().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("? <20>۾<EFBFBD><DBBE><EFBFBD> <20><>ҵǾ<D2B5><C7BE><EFBFBD><EFBFBD>ϴ<EFBFBD>.");
progress?.Report(0);
return result;
}
catch (Teigha.Runtime.Exception ex)
{
progress?.Report(0);
Debug.WriteLine($"? DWG <20><><EFBFBD><EFBFBD> ó<><C3B3> <20><> Teigha <20><><EFBFBD><EFBFBD> <20>߻<EFBFBD>: {ex.Message}");
return result;
}
catch (System.Exception ex)
{
progress?.Report(0);
Debug.WriteLine($"? <20>Ϲ<EFBFBD> <20><><EFBFBD><EFBFBD> <20>߻<EFBFBD>: {ex.Message}");
return result;
}
}
/// <summary>
/// DWG <20><><EFBFBD>Ͽ<EFBFBD><CFBF><EFBFBD> <20>ؽ<EFBFBD>Ʈ <20><>ƼƼ<C6BC><C6BC><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>Ͽ<EFBFBD> Height <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20>Բ<EFBFBD> <20><>ȯ<EFBFBD>մϴ<D5B4>.
/// </summary>
public List<TextEntityInfo> ExtractTextEntitiesWithHeight(string filePath)
{
var attRefEntities = new List<TextEntityInfo>();
var otherTextEntities = new List<TextEntityInfo>();
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 ó<><C3B3>
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 ó<><C3B3>
else if (ent is DBText dbText)
{
otherTextEntities.Add(new TextEntityInfo
{
Height = dbText.Height,
Type = "DBText",
Layer = layerName,
Tag = "",
Text = dbText.TextString
});
}
// MText ó<><C3B3>
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($"? <20>ؽ<EFBFBD>Ʈ <20><>ƼƼ <20><><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD> ({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 <20><><EFBFBD><EFBFBD>
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);
// <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>
AddMappingData(fileName, attDef.Tag, attDef.TextString, result);
}
// BlockReference <20><> <20><> <20><><EFBFBD><EFBFBD> AttributeReference <20><><EFBFBD><EFBFBD>
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);
// <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>
var aiLabel = fieldMapper.ExpresswayToAilabel(attRef.Tag);
if (aiLabel != null)
{
AddMappingData(fileName, attRef.Tag, attRef.TextString, result);
}
}
}
}
}
// DBText <20><>ƼƼ <20><><EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD> <20><>Ʈ)
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 <20><>ƼƼ <20><><EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD> <20><>Ʈ)
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 <20≯<EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>: {ex.Message}");
return "";
}
}
/// <summary>
/// 도면에서 Note와 관련된 텍스트들을 추출합니다.
/// </summary>
public NoteExtractionResult ExtractNotesFromDrawing(string filePath)
{
var result = new NoteExtractionResult();
var noteEntities = new List<NoteEntityInfo>();
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<ObjectId>().ToList();
var dbTextIds = new List<ObjectId>();
var polylineIds = new List<ObjectId>();
var lineIds = new List<ObjectId>();
// 먼저 모든 관련 엔터티들의 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<List<NoteEntityInfo>>();
// 이미 사용된 박스들을 추적 (중복 할당 방지)
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;
}
// 특정 노트만 테스트하기 위한 필터 (디버깅용)
// if (noteText == null || !noteText.TextString.Contains("도로용지경계 기준 노트"))
// {
// continue;
// }
Debug.WriteLine($"[DEBUG] Note 처리 중: '{noteText.TextString}' at {noteText.Position}");
// 이 Note에 대한 그룹 생성
var currentNoteGroup = new List<NoteEntityInfo>();
// 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);
}
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}개");
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;
}
/// <summary>
/// DBText 중에서 "Note"가 포함된 텍스트들을 찾습니다.
/// </summary>
private List<ObjectId> FindNoteTexts(Transaction tran, List<ObjectId> dbTextIds)
{
var noteTextIds = new List<ObjectId>();
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;
}
/// <summary>
/// 모든 Line과 Polyline 엔터티를 통합된 Line 세그먼트 리스트로 변환합니다.
/// Polyline은 구성 Line 세그먼트로 분해됩니다.
/// </summary>
private List<Line> GetAllLineSegments(Transaction tran, List<ObjectId> polylineIds, List<ObjectId> lineIds)
{
var allLineSegments = new List<Line>();
// 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;
}
/// <summary>
/// Note 텍스트 아래에 있는 콘텐츠 박스를 찾습니다.
/// Note의 정렬점을 기준으로 안정적인 검색 라인을 사용합니다.
/// </summary>
private (Point3d minPoint, Point3d maxPoint)? FindNoteBox(
Transaction tran, DBText noteText, List<ObjectId> polylineIds, List<ObjectId> 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();
}
}
}
/// <summary>
/// 식별된 상단선으로부터 지능적으로 박스의 나머지 세 변을 추적합니다.
/// 작은 간격에 대해 관용적입니다.
/// </summary>
private (Point3d minPoint, Point3d maxPoint)? TraceBoxFromTopLine(
List<Line> 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;
}
}
/// <summary>
/// 선분이 수직선인지 확인합니다.
/// </summary>
private bool IsVerticalLine(Line line, double noteHeight)
{
return Math.Abs(line.StartPoint.X - line.EndPoint.X) < noteHeight * 0.1;
}
/// <summary>
/// 선분이 수평선인지 확인합니다.
/// </summary>
private bool IsHorizontalLine(Line line, double noteHeight)
{
return Math.Abs(line.StartPoint.Y - line.EndPoint.Y) < noteHeight * 0.1;
}
/// <summary>
/// 선분이 지정된 점에 연결되어 있는지 확인합니다.
/// </summary>
private bool IsLineConnectedToPoint(Line line, Point3d point, double tolerance)
{
return point.DistanceTo(line.StartPoint) < tolerance || point.DistanceTo(line.EndPoint) < tolerance;
}
/// <summary>
/// 선분에서 지정된 점으로부터 가장 먼 끝점을 반환합니다.
/// </summary>
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;
}
/// <summary>
/// 두 선분이 교차하는지 확인합니다.
/// </summary>
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;
}
}
/// <summary>
/// 박스가 유효한 Note 박스인지 확인합니다.
/// </summary>
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;
}
}
/// <summary>
/// 점들의 경계 상자를 계산합니다.
/// </summary>
private (Point3d minPoint, Point3d maxPoint)? CalculateBounds(List<Point3d> 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;
}
}
/// <summary>
/// Note 박스 내부의 텍스트들을 찾습니다.
/// </summary>
private List<ObjectId> FindTextsInNoteBox(
Transaction tran, DBText noteText, (Point3d minPoint, Point3d maxPoint) noteBox, List<ObjectId> allTextIds)
{
var boxTextIds = new List<ObjectId>();
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;
}
/// <summary>
/// Note 박스 내부의 Line과 Polyline들을 찾습니다.
/// </summary>
private List<ObjectId> FindLinesInNoteBox(
Transaction tran, (Point3d minPoint, Point3d maxPoint) noteBox, List<ObjectId> polylineIds, List<ObjectId> lineIds)
{
var boxLineIds = new List<ObjectId>();
// 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;
}
/// <summary>
/// 박스 내부의 Line/Polyline에서 테이블을 구성하는 수평·수직 세그먼트들을 찾습니다.
/// </summary>
private List<(Point3d start, Point3d end, bool isHorizontal)> FindTableSegmentsInBox(
Transaction tran, List<ObjectId> 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;
}
/// <summary>
/// 새로운 교차점 기반 알고리즘으로 셀들을 추출합니다.
/// </summary>
private List<TableCell> 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<IntersectionPoint> LastIntersectionPoints { get; private set; } = new List<IntersectionPoint>();
// 시각화를 위한 대각선 정보 저장
public List<(Point3d topLeft, Point3d bottomRight, string label)> LastDiagonalLines { get; private set; } = new List<(Point3d, Point3d, string)>();
/// <summary>
/// 교차점들을 찾고 타입을 분류합니다.
/// </summary>
public List<IntersectionPoint> CalculateIntersectionPointsFromSegments(List<(Point3d start, Point3d end, bool isHorizontal)> tableSegments, double tolerance)
{
return FindAndClassifyIntersections(tableSegments, tolerance);
}
private List<IntersectionPoint> FindAndClassifyIntersections(List<(Point3d start, Point3d end, bool isHorizontal)> tableSegments, double tolerance)
{
var intersectionPoints = new List<IntersectionPoint>();
// 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;
}
/// <summary>
/// 교차점에서 연결된 선분들의 방향을 비트 플래그로 분석합니다.
/// </summary>
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;
}
/// <summary>
/// 실제 교차점들로부터 완전한 격자망을 생성합니다.
/// 빈 위치에도 가상 교차점을 생성하여 균일한 RXCX 구조를 만듭니다.
/// </summary>
private List<IntersectionPoint> CreateCompleteGridFromIntersections(List<IntersectionPoint> actualIntersections, List<(Point3d start, Point3d end, bool isHorizontal)> tableSegments, double tolerance)
{
if (actualIntersections.Count == 0) return new List<IntersectionPoint>();
// 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<IntersectionPoint>();
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;
}
/// <summary>
/// 가상 교차점의 DirectionBits를 선분 정보를 바탕으로 추론합니다.
/// </summary>
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;
}
/// <summary>
/// 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, ... 계속 진행
/// </summary>
private IntersectionPoint FindBottomRightByRowColumn(IntersectionPoint topLeft, List<IntersectionPoint> 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;
}
/// <summary>
/// 교차점들로부터 셀들을 추출합니다.
/// </summary>
private List<TableCell> ExtractCellsFromIntersections(List<IntersectionPoint> intersections, List<(Point3d start, Point3d end, bool isHorizontal)> tableSegments, double tolerance)
{
var cells = new List<TableCell>();
// 시각화를 위한 대각선 정보 초기화
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;
}
/// <summary>
/// NOTE 아래 박스 내의 모든 라인들의 bounding box를 기준으로 테이블 경계 내부의 교차점들만 필터링합니다.
/// </summary>
private List<IntersectionPoint> FilterIntersectionsWithinTableBounds(List<IntersectionPoint> 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;
}
/// <summary>
/// 셀들에 Row/Column 번호를 할당합니다.
/// </summary>
private void AssignRowColumnNumbers(List<TableCell> 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})");
}
}
/// <summary>
/// 대각선 라벨을 Row/Column 번호로 업데이트합니다.
/// </summary>
private void UpdateDiagonalLabels(List<TableCell> 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}");
}
}
/// <summary>
/// 교차점이 셀의 topLeft가 될 수 있는지 확인합니다.
/// 9번, 11번, 13번, 15번이 topLeft 후보
/// </summary>
public bool IsValidTopLeft(int directionBits)
{
return directionBits == 9 || // Right+Down (ㄱ형)
directionBits == 11 || // Right+Up+Down (ㅏ형)
directionBits == 13 || // Right+Left+Down (ㅜ형)
directionBits == 15; // 모든 방향 (+형)
}
/// <summary>
/// topLeft에 대응하는 bottomRight 후보를 찾습니다.
/// X축으로 가장 가까운 유효한 bottomRight를 반환 (horizontal merge 자동 처리)
/// </summary>
private IntersectionPoint FindBottomRightCandidate(IntersectionPoint topLeft, List<IntersectionPoint> 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;
}
/// <summary>
/// 교차점이 셀의 bottomRight가 될 수 있는지 확인합니다.
/// 15번, 14번, 6번, 7번이 bottomRight 후보
/// </summary>
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;
}
/// <summary>
/// 두 모서리 점으로부터 Row/Column 번호가 설정된 셀을 생성합니다.
/// </summary>
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 = ""
};
}
/// <summary>
/// 두 모서리 점으로부터 셀을 생성합니다.
/// </summary>
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 = ""
};
}
/// <summary>
/// 기존 격자 기반 셀 추출 (백업용)
/// </summary>
private List<TableCell> ExtractTableCellsLegacy(List<(Point3d start, Point3d end, bool isHorizontal)> tableSegments, double tolerance)
{
var cells = new List<TableCell>();
// 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;
}
/// <summary>
/// 선분들의 실제 교차점만 찾습니다 (끝점은 제외).
/// </summary>
private List<Point3d> FindRealIntersections(List<(Point3d start, Point3d end, bool isHorizontal)> segments, double tolerance)
{
var intersections = new HashSet<Point3d>();
// 수평선과 수직선의 실제 교차점만 찾기 (끝점에서의 만남은 제외)
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();
}
/// <summary>
/// 모든 선분들의 교차점을 찾습니다 (기존 함수 유지).
/// </summary>
private List<Point3d> FindAllIntersections(List<(Point3d start, Point3d end, bool isHorizontal)> segments, double tolerance)
{
var intersections = new HashSet<Point3d>();
// 모든 세그먼트의 끝점들을 교차점으로 추가
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();
}
/// <summary>
/// 두 선분의 교차점을 구합니다.
/// </summary>
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;
}
/// <summary>
/// 두 점 사이에 선분이 존재하는지 확인합니다.
/// </summary>
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;
}
/// <summary>
/// 점이 선분 위에 있는지 확인합니다.
/// </summary>
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;
}
/// <summary>
/// Note 박스 내의 텍스트들을 좌표에 따라 정렬합니다 (위에서 아래로, 왼쪽에서 오른쪽으로).
/// </summary>
private List<NoteEntityInfo> GetSortedNoteContents(Transaction tran, List<ObjectId> boxTextIds, Database database)
{
var noteContents = new List<NoteEntityInfo>();
// 먼저 모든 텍스트와 좌표 정보를 수집
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;
}
/// <summary>
/// 점이 선분의 끝점인지 확인합니다.
/// </summary>
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);
}
/// <summary>
/// 새 셀이 기존 셀들과 중첩되는지 확인합니다.
/// </summary>
private bool IsOverlappingWithExistingCells(TableCell newCell, List<TableCell> 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; // 중첩되지 않음
}
/// <summary>
/// 테이블의 모든 교차점에서 DirectionBits를 계산합니다.
/// </summary>
private List<IntersectionPoint> CalculateIntersectionDirections(List<Point3d> intersections, List<(Point3d start, Point3d end, bool isHorizontal)> segments, double tolerance)
{
var intersectionPoints = new List<IntersectionPoint>();
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<TableCell> cells, List<ObjectId> nonTableTextIds, List<(Point3d start, Point3d end, bool isHorizontal)> tableSegments, List<IntersectionPoint> intersectionPoints, List<DiagonalLine> diagonalLines, List<CellBoundary> cellBoundaries) ExtractTableAndTextsFromNoteBox(
Transaction tran,
DBText noteText,
(Point3d minPoint, Point3d maxPoint) noteBox,
List<ObjectId> polylineIds,
List<ObjectId> lineIds,
List<ObjectId> 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<IntersectionPoint>(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);
}
/// <summary>
/// 교차점들로부터 대각선을 생성합니다. 특히 R2C2 topLeft에서 적절한 bottomRight를 찾아서 대각선을 그립니다.
/// </summary>
private List<DiagonalLine> GenerateDiagonalLines(List<IntersectionPoint> intersectionPoints)
{
var diagonalLines = new List<DiagonalLine>();
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;
}
/// <summary>
/// 대각선 정보로부터 셀의 4개 모서리 좌표를 계산합니다.
/// </summary>
private List<CellBoundary> ExtractCellBoundariesFromDiagonals(List<DiagonalLine> diagonalLines)
{
var cellBoundaries = new List<CellBoundary>();
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;
}
/// <summary>
/// 셀 경계 내에 포함되는 모든 텍스트(DBText, MText)를 추출합니다.
/// </summary>
private void ExtractTextsFromCellBoundaries(Transaction tran, List<CellBoundary> cellBoundaries, List<ObjectId> allTextIds)
{
try
{
Debug.WriteLine($"[CELL_TEXT] {cellBoundaries.Count}개 셀 경계에서 텍스트 추출 시작");
foreach (var cellBoundary in cellBoundaries)
{
var textsInCell = new List<string>();
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());
Debug.WriteLine($"[CELL_TEXT] ✅ {cellBoundary.Label}에 텍스트 추가: '{textContent.Trim()}'");
}
}
}
// 셀 경계에 찾은 텍스트들을 콤마로 연결하여 저장
cellBoundary.CellText = string.Join(", ", textsInCell);
Debug.WriteLine($"[CELL_TEXT] {cellBoundary.Label} 최종 텍스트: '{cellBoundary.CellText}'");
}
Debug.WriteLine($"[CELL_TEXT] 셀 경계 텍스트 추출 완료");
}
catch (System.Exception ex)
{
Debug.WriteLine($"[CELL_TEXT] 셀 경계 텍스트 추출 중 오류: {ex.Message}");
}
}
/// <summary>
/// 점이 셀 경계 내에 있는지 확인합니다.
/// </summary>
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;
}
/// <summary>
/// 셀 경계들을 분석하여 병합된 셀들을 찾고 같은 텍스트로 채웁니다.
/// </summary>
private void ProcessMergedCells(List<CellBoundary> 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}");
}
}
/// <summary>
/// 라벨에서 Row, Column 번호를 파싱합니다. (예: "R2C3" → (2, 3))
/// </summary>
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);
}
/// <summary>
/// 주어진 셀에서 시작하는 병합된 영역을 찾습니다.
/// </summary>
private List<CellBoundary> FindMergedRegion(Dictionary<(int row, int col), CellBoundary> cellsByRowCol, int startRow, int startCol, CellBoundary originCell)
{
var mergedCells = new List<CellBoundary> { 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<CellBoundary>();
// 현재 행의 모든 열을 확인
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;
}
/// <summary>
/// 두 셀이 같은 내용을 가지는지 확인합니다.
/// </summary>
private bool IsSameCellContent(CellBoundary cell1, CellBoundary cell2)
{
return !string.IsNullOrEmpty(cell1.CellText) &&
!string.IsNullOrEmpty(cell2.CellText) &&
cell1.CellText.Trim() == cell2.CellText.Trim();
}
/// <summary>
/// CellBoundary의 텍스트 정보를 기존 테이블 데이터에 업데이트합니다.
/// </summary>
private object[,] UpdateTableDataWithCellBoundaries(object[,] originalTableData, List<CellBoundary> 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;
}
}
/// <summary>
/// 교차점이 테이블 내부에 있는지 확인합니다. (R1C1과 박스 외곽 제외)
/// </summary>
private bool IsInsideTable(IntersectionPoint point, List<IntersectionPoint> 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;
}
}
/// <summary>
/// topLeft 교차점에 대한 첫 번째 bottomRight 교차점을 찾습니다. (row+1, col+1부터 점진적 탐색)
/// </summary>
private IntersectionPoint? FindFirstBottomRightForTopLeft(IntersectionPoint topLeft, List<IntersectionPoint> 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;
}
}
/// <summary>
/// topLeft 교차점에 대한 모든 가능한 bottomRight 교차점들을 찾습니다.
/// </summary>
private List<IntersectionPoint> FindAllBottomRightsForTopLeft(IntersectionPoint topLeft, List<IntersectionPoint> intersectionPoints)
{
var validBottomRights = new List<IntersectionPoint>();
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;
}
}
/// <summary>
/// topLeft 교차점에 대한 적절한 bottomRight 교차점을 찾습니다. (기존 메서드 - 호환성 유지)
/// </summary>
private IntersectionPoint? FindBottomRightForTopLeft(IntersectionPoint topLeft, List<IntersectionPoint> 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<TableCell, List<string>> assignedTexts, List<ObjectId> unassignedTextIds) AssignTextsToCells(
Transaction tran,
List<TableCell> cells,
List<ObjectId> textIds)
{
var assignedTexts = new Dictionary<TableCell, List<string>>();
var unassignedTextIds = new List<ObjectId>(textIds);
var assignedIds = new HashSet<ObjectId>();
foreach (var cell in cells)
{
var textsInCell = new List<string>();
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);
cell.CellText = string.Join("\n", textsInCell);
assignedIds.Add(textId);
}
}
}
}
if(textsInCell.Any())
{
assignedTexts[cell] = textsInCell;
}
}
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<TableCell> 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<TableCell> 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 클래스 끝
/// <summary>
/// Note 추출 결과를 담는 클래스
/// </summary>
public class NoteExtractionResult
{
public List<NoteEntityInfo> NoteEntities { get; set; } = new List<NoteEntityInfo>();
public List<IntersectionPoint> IntersectionPoints { get; set; } = new List<IntersectionPoint>();
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<SegmentInfo> TableSegments { get; set; } = new List<SegmentInfo>();
}
/// <summary>
/// <summary>
/// DWG <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> Ŭ<><C5AC><EFBFBD><EFBFBD>
/// </summary>
public class DwgExtractionResult
{
public List<TitleBlockRowData> TitleBlockRows { get; set; } = new List<TitleBlockRowData>();
public List<TextEntityRowData> TextEntityRows { get; set; } = new List<TextEntityRowData>();
public Dictionary<string, Dictionary<string, (string, string, string, string)>> FileToMapkeyToLabelTagValuePdf { get; set; }
= new Dictionary<string, Dictionary<string, (string, string, string, string)>>();
public void AddMappingData(string fileName, string mapKey, string aiLabel, string dwgTag, string dwgValue, string pdfValue)
{
if (!FileToMapkeyToLabelTagValuePdf.ContainsKey(fileName))
{
FileToMapkeyToLabelTagValuePdf[fileName] = new Dictionary<string, (string, string, string, string)>();
}
FileToMapkeyToLabelTagValuePdf[fileName][mapKey] = (aiLabel, dwgTag, dwgValue, pdfValue);
}
}
/// <summary>
/// Title Block <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
/// </summary>
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; } = "";
}
/// <summary>
/// Text Entity <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
/// </summary>
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; } = "";
}
/// <summary>
/// <20>ؽ<EFBFBD>Ʈ <20><>ƼƼ <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> Ŭ<><C5AC><EFBFBD><EFBFBD>
/// </summary>
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; } = "";
}
/// <summary>
/// Note 엔티티 정보 클래스
/// </summary>
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<TableCell> Cells { get; set; } = new List<TableCell>(); // 테이블 셀 정보 (병합용)
public List<SegmentInfo> TableSegments { get; set; } = new List<SegmentInfo>(); // 테이블 세그먼트 정보
public List<IntersectionInfo> IntersectionPoints { get; set; } = new List<IntersectionInfo>(); // 교차점 정보
public List<DiagonalLine> DiagonalLines { get; set; } = new List<DiagonalLine>(); // 대각선 정보
public List<CellBoundary> CellBoundaries { get; set; } = new List<CellBoundary>(); // 정확한 셀 경계 정보
public string TableCsv
{
get
{
if (TableData == null || TableData.Length == 0) return "";
var csvLines = new List<string>();
int rows = TableData.GetLength(0);
int cols = TableData.GetLength(1);
for (int r = 0; r < rows; r++)
{
var rowValues = new List<string>();
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);
}
}
}
/// <summary>
/// 테이블 셀 정보 클래스
/// </summary>
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; } = ""; // 셀 내부의 텍스트
/// <summary>
/// 셀의 중심 좌표를 반환합니다.
/// </summary>
public Point3d CenterPoint => new Point3d(
(MinPoint.X + MaxPoint.X) / 2,
(MinPoint.Y + MaxPoint.Y) / 2,
0
);
/// <summary>
/// 셀의 너비를 반환합니다.
/// </summary>
public double Width => MaxPoint.X - MinPoint.X;
/// <summary>
/// 셀의 높이를 반환합니다.
/// </summary>
public double Height => MaxPoint.Y - MinPoint.Y;
/// <summary>
/// 점이 이 셀 내부에 있는지 확인합니다.
/// </summary>
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})";
}
}
/// <summary>
/// 방향을 비트 플래그로 나타내는 상수
/// </summary>
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 - 모든 선이 없음 (완전 합병)
}
/// <summary>
/// 교차점 정보를 담는 클래스
/// </summary>
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<string>();
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)}]";
}
}
/// <summary>
/// 셀의 4개 모서리 좌표를 나타내는 클래스
/// </summary>
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})";
}
}
}