Files
manual_wpf/Models/DwgDataExtractor.cs

1059 lines
46 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using Teigha.DatabaseServices;
using Teigha.Geometry;
using Teigha.Runtime;
namespace DwgExtractorManual.Models
{
/// <summary>
/// DWG <20><><EFBFBD>Ͽ<EFBFBD><CFBF><EFBFBD> <20>ؽ<EFBFBD>Ʈ <20><>ƼƼ<C6BC><C6BC> <20><><EFBFBD><EFBFBD><EFBFBD>ϴ<EFBFBD> Ŭ<><C5AC><EFBFBD><EFBFBD>
/// </summary>
internal class DwgDataExtractor
{
private readonly FieldMapper fieldMapper;
public DwgDataExtractor(FieldMapper fieldMapper)
{
this.fieldMapper = fieldMapper ?? throw new ArgumentNullException(nameof(fieldMapper));
}
/// <summary>
/// DWG <20><><EFBFBD>Ͽ<EFBFBD><CFBF><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>͸<EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>Ͽ<EFBFBD> ExcelRowData <20><><EFBFBD><EFBFBD>Ʈ<EFBFBD><C6AE> <20><>ȯ
/// </summary>
public DwgExtractionResult ExtractFromDwgFile(string filePath, IProgress<double>? progress = null, CancellationToken cancellationToken = default)
{
var result = new DwgExtractionResult();
if (!File.Exists(filePath))
{
Debug.WriteLine($"? <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20>ʽ<EFBFBD><CABD>ϴ<EFBFBD>: {filePath}");
return result;
}
try
{
progress?.Report(0);
cancellationToken.ThrowIfCancellationRequested();
using (var database = new Database(false, true))
{
database.ReadDwgFile(filePath, FileOpenMode.OpenForReadAndWriteNoShare, false, null);
progress?.Report(10);
using (var tran = database.TransactionManager.StartTransaction())
{
var bt = tran.GetObject(database.BlockTableId, OpenMode.ForRead) as BlockTable;
using (var btr = tran.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForRead) as BlockTableRecord)
{
int totalEntities = btr.Cast<ObjectId>().Count();
int processedCount = 0;
var fileName = Path.GetFileNameWithoutExtension(database.Filename);
if (string.IsNullOrEmpty(fileName))
{
fileName = "Unknown_File";
}
foreach (ObjectId entId in btr)
{
cancellationToken.ThrowIfCancellationRequested();
using (var ent = tran.GetObject(entId, OpenMode.ForRead) as Entity)
{
string layerName = GetLayerName(ent.LayerId, tran, database);
ProcessEntity(ent, tran, database, layerName, fileName, result);
}
processedCount++;
double currentProgress = 10.0 + (double)processedCount / totalEntities * 80.0;
progress?.Report(Math.Min(currentProgress, 90.0));
}
}
tran.Commit();
}
}
progress?.Report(100);
return result;
}
catch (OperationCanceledException)
{
Debug.WriteLine("? <20>۾<EFBFBD><DBBE><EFBFBD> <20><>ҵǾ<D2B5><C7BE><EFBFBD><EFBFBD>ϴ<EFBFBD>.");
progress?.Report(0);
return result;
}
catch (Teigha.Runtime.Exception ex)
{
progress?.Report(0);
Debug.WriteLine($"? DWG <20><><EFBFBD><EFBFBD> ó<><C3B3> <20><> Teigha <20><><EFBFBD><EFBFBD> <20>߻<EFBFBD>: {ex.Message}");
return result;
}
catch (System.Exception ex)
{
progress?.Report(0);
Debug.WriteLine($"? <20>Ϲ<EFBFBD> <20><><EFBFBD><EFBFBD> <20>߻<EFBFBD>: {ex.Message}");
return result;
}
}
/// <summary>
/// DWG <20><><EFBFBD>Ͽ<EFBFBD><CFBF><EFBFBD> <20>ؽ<EFBFBD>Ʈ <20><>ƼƼ<C6BC><C6BC><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>Ͽ<EFBFBD> Height <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20>Բ<EFBFBD> <20><>ȯ<EFBFBD>մϴ<D5B4>.
/// </summary>
public List<TextEntityInfo> ExtractTextEntitiesWithHeight(string filePath)
{
var attRefEntities = new List<TextEntityInfo>();
var otherTextEntities = new List<TextEntityInfo>();
try
{
using (var database = new Database(false, true))
{
database.ReadDwgFile(filePath, FileOpenMode.OpenForReadAndWriteNoShare, false, null);
using (var tran = database.TransactionManager.StartTransaction())
{
var bt = tran.GetObject(database.BlockTableId, OpenMode.ForRead) as BlockTable;
using (var btr = tran.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForRead) as BlockTableRecord)
{
foreach (ObjectId entId in btr)
{
using (var ent = tran.GetObject(entId, OpenMode.ForRead) as Entity)
{
string layerName = GetLayerName(ent.LayerId, tran, database);
// AttributeReference ó<><C3B3>
if (ent is BlockReference blr)
{
foreach (ObjectId attId in blr.AttributeCollection)
{
using (var attRef = tran.GetObject(attId, OpenMode.ForRead) as AttributeReference)
{
if (attRef != null)
{
var textString = attRef.TextString == null ? "" : attRef.TextString;
attRefEntities.Add(new TextEntityInfo
{
Height = attRef.Height,
Type = "AttRef",
Layer = layerName,
Tag = attRef.Tag,
Text = textString,
});
}
}
}
}
// DBText ó<><C3B3>
else if (ent is DBText dbText)
{
otherTextEntities.Add(new TextEntityInfo
{
Height = dbText.Height,
Type = "DBText",
Layer = layerName,
Tag = "",
Text = dbText.TextString
});
}
// MText ó<><C3B3>
else if (ent is MText mText)
{
otherTextEntities.Add(new TextEntityInfo
{
Height = mText.Height,
Type = "MText",
Layer = layerName,
Tag = "",
Text = mText.Contents
});
}
}
}
}
tran.Commit();
}
}
}
catch (System.Exception ex)
{
Debug.WriteLine($"? <20>ؽ<EFBFBD>Ʈ <20><>ƼƼ <20><><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD> ({Path.GetFileName(filePath)}): {ex.Message}");
}
var sortedAttRefEntities = attRefEntities.OrderByDescending(e => e.Height).ToList();
var sortedOtherTextEntities = otherTextEntities.OrderByDescending(e => e.Height).ToList();
sortedAttRefEntities.AddRange(sortedOtherTextEntities);
return sortedAttRefEntities;
}
private void ProcessEntity(Entity ent, Transaction tran, Database database, string layerName, string fileName, DwgExtractionResult result)
{
// AttributeDefinition <20><><EFBFBD><EFBFBD>
if (ent is AttributeDefinition attDef)
{
var titleBlockRow = new TitleBlockRowData
{
Type = attDef.GetType().Name,
Name = attDef.BlockName,
Tag = attDef.Tag,
Prompt = attDef.Prompt,
Value = attDef.TextString,
Path = database.Filename,
FileName = Path.GetFileName(database.Filename)
};
result.TitleBlockRows.Add(titleBlockRow);
// <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>
AddMappingData(fileName, attDef.Tag, attDef.TextString, result);
}
// BlockReference <20><> <20><> <20><><EFBFBD><EFBFBD> AttributeReference <20><><EFBFBD><EFBFBD>
else if (ent is BlockReference blr)
{
foreach (ObjectId attId in blr.AttributeCollection)
{
using (var attRef = tran.GetObject(attId, OpenMode.ForRead) as AttributeReference)
{
if (attRef != null && attRef.TextString.Trim() != "")
{
var titleBlockRow = new TitleBlockRowData
{
Type = attRef.GetType().Name,
Name = blr.Name,
Tag = attRef.Tag,
Prompt = GetPromptFromAttributeReference(tran, blr, attRef.Tag),
Value = attRef.TextString,
Path = database.Filename,
FileName = Path.GetFileName(database.Filename)
};
result.TitleBlockRows.Add(titleBlockRow);
// <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>
var aiLabel = fieldMapper.ExpresswayToAilabel(attRef.Tag);
if (aiLabel != null)
{
AddMappingData(fileName, attRef.Tag, attRef.TextString, result);
}
}
}
}
}
// DBText <20><>ƼƼ <20><><EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD> <20><>Ʈ)
else if (ent is DBText dbText)
{
var textEntityRow = new TextEntityRowData
{
Type = "DBText",
Layer = layerName,
Text = dbText.TextString,
Path = database.Filename,
FileName = Path.GetFileName(database.Filename)
};
result.TextEntityRows.Add(textEntityRow);
}
// MText <20><>ƼƼ <20><><EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD> <20><>Ʈ)
else if (ent is MText mText)
{
var textEntityRow = new TextEntityRowData
{
Type = "MText",
Layer = layerName,
Text = mText.Contents,
Path = database.Filename,
FileName = Path.GetFileName(database.Filename)
};
result.TextEntityRows.Add(textEntityRow);
}
}
private void AddMappingData(string fileName, string tag, string attValue, DwgExtractionResult result)
{
var aiLabel = fieldMapper.ExpresswayToAilabel(tag);
var mapKey = fieldMapper.AilabelToDocAiKey(aiLabel);
if (!string.IsNullOrEmpty(aiLabel))
{
var finalMapKey = mapKey ?? aiLabel;
result.AddMappingData(fileName, finalMapKey, aiLabel, tag, attValue, "");
}
else
{
var finalMapKey = mapKey ?? tag;
if (!string.IsNullOrEmpty(finalMapKey))
{
result.AddMappingData(fileName, finalMapKey, tag, tag, attValue, "");
}
}
}
private string? GetPromptFromAttributeReference(Transaction tr, BlockReference blockref, string tag)
{
string? prompt = null;
BlockTableRecord? blockDef = tr.GetObject(blockref.BlockTableRecord, OpenMode.ForRead) as BlockTableRecord;
if (blockDef == null) return null;
foreach (ObjectId objId in blockDef)
{
AttributeDefinition? attDef = tr.GetObject(objId, OpenMode.ForRead) as AttributeDefinition;
if (attDef != null)
{
if (attDef.Tag.Equals(tag, System.StringComparison.OrdinalIgnoreCase))
{
prompt = attDef.Prompt;
break;
}
}
}
return prompt;
}
private string GetLayerName(ObjectId layerId, Transaction transaction, Database database)
{
try
{
using (var layerTableRecord = transaction.GetObject(layerId, OpenMode.ForRead) as LayerTableRecord)
{
return layerTableRecord?.Name ?? "";
}
}
catch (System.Exception ex)
{
Debug.WriteLine($"Layer <20≯<EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>: {ex.Message}");
return "";
}
}
/// <summary>
/// 도면에서 Note와 관련된 텍스트들을 추출합니다.
/// </summary>
public List<NoteEntityInfo> ExtractNotesFromDrawing(string filePath)
{
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)
{
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 boxTextIds = FindTextsInNoteBox(tran, noteText, noteBox.Value, dbTextIds);
Debug.WriteLine($"[DEBUG] 박스 내 텍스트: {boxTextIds.Count}개");
// Note 자체를 그룹의 첫 번째로 추가
currentNoteGroup.Add(new NoteEntityInfo
{
Type = "Note",
Layer = GetLayerName(noteText.LayerId, tran, database),
Text = noteText.TextString,
Path = database.Filename,
FileName = Path.GetFileName(database.Filename),
X = noteText.Position.X,
Y = noteText.Position.Y,
SortOrder = 0 // Note는 항상 먼저
});
// 박스 내 텍스트들을 좌표별로 정렬하여 그룹에 추가
var sortedBoxTexts = GetSortedNoteContents(tran, boxTextIds, database);
currentNoteGroup.AddRange(sortedBoxTexts);
}
else
{
Debug.WriteLine($"[DEBUG] Note '{noteText.TextString}'에 대한 박스를 찾을 수 없음");
// 박스가 없어도 Note 헤더는 추가
currentNoteGroup.Add(new NoteEntityInfo
{
Type = "Note",
Layer = GetLayerName(noteText.LayerId, tran, database),
Text = noteText.TextString,
Path = database.Filename,
FileName = Path.GetFileName(database.Filename),
X = noteText.Position.X,
Y = noteText.Position.Y,
SortOrder = 0
});
}
noteGroups.Add(currentNoteGroup);
}
}
// Note 그룹들을 Y 좌표별로 정렬 (위에서 아래로)
var sortedNoteGroups = noteGroups
.OrderByDescending(group => group[0].Y) // 각 그룹의 첫 번째 항목(NOTE 헤더)의 Y 좌표로 정렬
.ToList();
// 정렬된 그룹들을 하나의 리스트로 합치기
foreach (var group in sortedNoteGroups)
{
noteEntities.AddRange(group);
}
}
tran.Commit();
}
}
}
catch (System.Exception ex)
{
Debug.WriteLine($"❌ Note 추출 중 오류: {ex.Message}");
}
Debug.WriteLine($"[DEBUG] 최종 Note 엔티티 정렬 완료: {noteEntities.Count}개");
return noteEntities;
}
/// <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 박스 내의 텍스트들을 좌표에 따라 정렬합니다 (위에서 아래로, 왼쪽에서 오른쪽으로).
/// </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>
/// 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...)
}
}