3053 lines
142 KiB
C#
3053 lines
142 KiB
C#
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})";
|
||
}
|
||
}
|
||
} |