fix(B07): 도면 불러오기 — bulge 호 · OCS 거울 블록 · TEXT 정렬점 · 겹 블록 풂(84-2)

- LWPOLYLINE/POLYLINE 은 ezdxf path.make_path().flattening(0.2) 로 — bulge 호 마디 펴짐 · 압출 −Z 는 WCS 로
- TEXT 는 get_placement 정렬점 · OCS → WCS · 가로 정렬(왼 · 가운데 · 오른) · MTEXT 붙임점 가로 정렬
- INSERT 속 INSERT 까지 풂(16 겹 한도)
- 견본 DXF 시험(호 · bulge · 거울 블록 · 겹 블록 · TEXT 정렬) — 옛 코드에선 넷 실패

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UoSr2sqvYtBf1dtQdus88s
This commit is contained in:
2026-10-08 12:07:53 +09:00
co-authored by Claude Opus 5.5
parent e25b9ddf2f
commit 04dd8cd012
2 changed files with 206 additions and 15 deletions
@@ -29,6 +29,8 @@ from uuid import NAMESPACE_URL, uuid5
import ezdxf
from ezdxf import colors as ezdxf_colors
from ezdxf import path as ezdxf_path
from ezdxf.enums import TextEntityAlignment
from ezdxf.lldxf.encoding import decode_dxf_unicode, has_dxf_unicode
logger = logging.getLogger(__name__)
@@ -38,6 +40,8 @@ _DEFAULT_COLOR = "#f5f7fa"
# 곡선을 선분으로 풀 때 허용 오차(mm) — 도각 크기(840x594mm)에서 눈에 띄지 않는다.
_FLATTEN_MM = 0.2
_MAX_ENTITIES = 20000
# 블록 속 블록을 풀 깊이 — 자기를 품는 깨진 블록에서 끝없이 돌지 않게.
_MAX_BLOCK_DEPTH = 16
# 프로그램에 함께 담은 LibreDWG 자리 — `B07_DesignDetail/openwebcad/tools/libredwg/`.
@@ -117,16 +121,26 @@ def _plain(label: str) -> str:
return decode_dxf_unicode(label) if has_dxf_unicode(label) else label
def _text(index: int, entity: Any, label: str, insert: Any, height: float) -> dict[str, Any]:
rotation = float(getattr(entity.dxf, "rotation", 0.0) or 0.0)
def _text(
index: int,
entity: Any,
label: str,
insert: Any,
height: float,
direction: Any = None,
align: str = "left",
) -> dict[str, Any]:
if direction is None:
rotation = float(getattr(entity.dxf, "rotation", 0.0) or 0.0)
direction = (cos(radians(rotation)), sin(radians(rotation)))
return {
**_base(index, entity, "Text"),
"shapeData": {
"label": _plain(label),
"basePoint": _point(insert[0], insert[1]),
"options": {
"textDirection": _point(cos(radians(rotation)), sin(radians(rotation))),
"textAlign": "left",
"textDirection": _point(direction[0], direction[1]),
"textAlign": align,
"textColor": _color(entity),
"fontSize": float(height) or 3.0,
"fontFamily": "sans-serif",
@@ -143,16 +157,50 @@ def _flatten(entity: Any) -> list[Any] | None:
return None
def _polyline_points(entity: Any) -> tuple[list[Any], bool]:
"""폴리선 → WCS 점열. bulge(호 마디)는 펴고 OCS(압출 −Z 거울 블록)는 WCS 로.
`make_path` 가 닫힌 폴리선의 닫는 마디까지 넣으므로 닫힘 표시는 False.
면 · 메시 POLYLINE 처럼 경로로 못 바꾸면 꼭짓점을 그대로 잇는다.
"""
try:
return list(ezdxf_path.make_path(entity).flattening(_FLATTEN_MM)), False
except Exception:
if entity.dxftype() == "LWPOLYLINE":
return list(entity.get_points("xy")), bool(entity.closed)
return [vertex.dxf.location for vertex in entity.vertices], bool(entity.is_closed)
# DXF 가로 정렬 → CAD 글 정렬. 맞춤(ALIGNED · FIT)은 첫 점에서 왼쪽.
_TEXT_ALIGN = {
TextEntityAlignment.CENTER: "center",
TextEntityAlignment.MIDDLE: "center",
TextEntityAlignment.TOP_CENTER: "center",
TextEntityAlignment.MIDDLE_CENTER: "center",
TextEntityAlignment.BOTTOM_CENTER: "center",
TextEntityAlignment.RIGHT: "right",
TextEntityAlignment.TOP_RIGHT: "right",
TextEntityAlignment.MIDDLE_RIGHT: "right",
TextEntityAlignment.BOTTOM_RIGHT: "right",
}
def _text_placement(entity: Any) -> tuple[Any, Any, str]:
"""TEXT 의 (WCS 기준점, WCS 방향, 가로 정렬). 정렬점 · OCS(압출 −Z)를 따른다."""
align, first, _second = entity.get_placement()
ocs = entity.ocs()
rotation = radians(float(entity.dxf.rotation or 0.0))
direction = ocs.to_wcs((cos(rotation), sin(rotation), 0.0))
return ocs.to_wcs(first), direction, _TEXT_ALIGN.get(align, "left")
def _convert_entity(index: int, entity: Any) -> list[dict[str, Any]]:
kind = entity.dxftype()
if kind == "LINE":
return [_line(index, entity, entity.dxf.start, entity.dxf.end)]
if kind == "LWPOLYLINE":
shape = _polyline(index, entity, list(entity.get_points("xy")), bool(entity.closed))
return [shape] if shape else []
if kind == "POLYLINE":
points = [vertex.dxf.location for vertex in entity.vertices]
shape = _polyline(index, entity, points, bool(entity.is_closed))
if kind in ("LWPOLYLINE", "POLYLINE"):
points, closed = _polyline_points(entity)
shape = _polyline(index, entity, points, closed)
return [shape] if shape else []
if kind in ("CIRCLE", "ARC", "ELLIPSE", "SPLINE"):
points = _flatten(entity)
@@ -172,24 +220,32 @@ def _convert_entity(index: int, entity: Any) -> list[dict[str, Any]]:
label = str(entity.dxf.text or "").strip()
if not label:
return []
return [_text(index, entity, label, entity.dxf.insert, float(entity.dxf.height or 3.0))]
insert, direction, align = _text_placement(entity)
height = float(entity.dxf.height or 3.0)
return [_text(index, entity, label, insert, height, direction, align)]
if kind == "MTEXT":
label = str(entity.plain_text() or "").strip()
if not label:
return []
height = float(entity.dxf.char_height or 3.0)
return [_text(index, entity, label, entity.dxf.insert, height)]
# 붙임점 1~9 = 위 · 가운데 · 아래 줄의 왼 · 가운데 · 오른.
align = ("left", "center", "right")[(int(entity.dxf.attachment_point or 1) - 1) % 3]
return [_text(index, entity, label, entity.dxf.insert, height, align=align)]
return []
def _expand(entity: Any) -> list[Any]:
"""블록·치수처럼 속에 도형을 품은 것은 풀어서 낱개로 만든다. 못 풀면 버린다."""
def _expand(entity: Any, depth: int = 0) -> list[Any]:
"""블록·치수처럼 속에 도형을 품은 것은 풀어서 낱개로 — 블록 속 블록까지. 못 풀면 버린다."""
if entity.dxftype() in ("INSERT", "DIMENSION", "LEADER", "MULTILEADER"):
if depth >= _MAX_BLOCK_DEPTH:
logger.info("B07 도각 불러오기 — 블록이 %d 겹 넘게 겹쳐 건너뜀", _MAX_BLOCK_DEPTH)
return []
try:
return list(entity.virtual_entities())
inner = list(entity.virtual_entities())
except Exception:
logger.info("B07 도각 불러오기 — %s 는 풀지 못해 건너뜀", entity.dxftype())
return []
return [item for child in inner for item in _expand(child, depth + 1)]
return [entity]