feat(b07): 횡단도 표 위 CAD 칸 배치 · 없으면 옛 글 줄 (260-3)

- design.structure_note_cad 엔티티 = 범위 왼쪽 위를 표 왼쪽 · 윗변에 맞춤 · 표 층 · 배율 따름
- 표 · 테두리 · 시트 칸 높이 = 범위 높이 · 칸이 비면 아무것도 안 놓음 · 칸 없으면 옛 글 줄
- 시험 test_260_note_cad B07 몫

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TNMXVCZXMpHpVvqmFkeChL
This commit is contained in:
2026-10-11 14:10:20 +09:00
co-authored by Claude Opus 5.5
parent 29803ada06
commit a00bf2786a
4 changed files with 237 additions and 11 deletions
@@ -401,6 +401,96 @@ def _cross_note_entities(
]
# 표 위 CAD 칸(260-3) — B06 [상세] 빈 CAD(`design.structure_note_cad`) 엔티티를 표 왼쪽 위에 맞춤.
# 칸이 있으면(엔티티가 비었어도) 옛 글 대신 이것 · 없으면 옛 글 줄 그대로.
def cross_note_cad(design: dict[str, Any] | None) -> list[dict[str, Any]] | None:
"""설계의 표 위 CAD 엔티티 — 칸이 없으면 None(옛 글 줄로)."""
cad = design.get("structure_note_cad") if isinstance(design, dict) else None
if not isinstance(cad, dict):
return None
entities = cad.get("entities")
return [e for e in entities if isinstance(e, dict)] if isinstance(entities, list) else []
def _text_width(label: str, font: float) -> float:
"""글 폭 어림(mm) — 한글 · 한자 = 글자 크기 · 그 밖 = 0.6 배."""
return sum(font if ord(char) >= 0x2E80 else 0.6 * font for char in label)
def note_cad_bbox(entities: list[dict[str, Any]]) -> tuple[float, float, float, float] | None:
"""CAD 칸 범위 (min_x, min_y, max_x, max_y) — 글은 기준점이 세로 가운데(CAD `textBaseline`)라
글 높이 · 폭을 맞춤 쪽으로 더함 · 그 밖은 `entities_bbox`."""
from B07_DesignDetail.B07_DesignDetail_Engine_Template import entities_bbox
boxes = []
for entity in entities:
shape = entity.get("shapeData") or {}
base = shape.get("basePoint")
if entity.get("type") == "Text" and isinstance(base, dict):
options = shape.get("options") or {}
font = float(options.get("fontSize") or 0.0)
width = _text_width(str(shape.get("label") or ""), font)
x, y = float(base["x"]), float(base["y"])
left = {"center": x - width / 2.0, "right": x - width}.get(options.get("textAlign"), x)
boxes.append((left, y - font / 2.0, left + width, y + font / 2.0))
elif (box := entities_bbox([entity])) is not None:
boxes.append(box)
if not boxes:
return None
return (
min(b[0] for b in boxes),
min(b[1] for b in boxes),
max(b[2] for b in boxes),
max(b[3] for b in boxes),
)
def _on_layer(entity: dict[str, Any], layer_id: str) -> dict[str, Any]:
entity["layerId"] = layer_id
for child in entity.get("children") or []:
_on_layer(child, layer_id)
return entity
def cross_note_block_height(design: dict[str, Any] | None, scale: float = 1.0) -> float:
"""표 위 칸 높이(mm) — CAD 칸 = 범위 높이 × 배율 · 없으면 옛 글 줄 높이."""
cad = cross_note_cad(design)
if cad is None:
return cross_note_height(cross_note_lines(design), scale)
box = note_cad_bbox(cad)
return (box[3] - box[1]) * scale if box else 0.0
def cross_note_block_entities(
drawing_id: str,
design: dict[str, Any] | None,
top: float,
center_x: float = 0.0,
scale: float = 1.0,
) -> list[dict[str, Any]]:
"""표 위 칸 엔티티 — CAD 칸 = 범위 왼쪽 위를 표 왼쪽 끝 · `top` 에 맞춰 옮김(× 배율 · 표 층 ·
id `{도면}:notecad:{n}`) · 없으면 옛 글 줄(`_cross_note_entities`)."""
cad = cross_note_cad(design)
if cad is None:
return _cross_note_entities(drawing_id, cross_note_lines(design), top, center_x, scale)
box = note_cad_bbox(cad)
if box is None:
return []
from B07_DesignDetail.B07_DesignDetail_Engine_Template import _transform_entity
left = center_x - _CROSS_TABLE_WIDTH * scale / 2.0
dx, dy = left - box[0] * scale, top - box[3] * scale
return [
_on_layer(
_transform_entity(entity, f"{drawing_id}:notecad:{index}", scale, dx, dy),
CROSS_TABLE_LAYER_ID,
)
for index, entity in enumerate(cad)
]
def cross_table_width(scale: float = 1.0) -> float:
"""수량 산출표 가로 폭(mm)."""
return _CROSS_TABLE_WIDTH * scale