feat(M02): 도면 양식 작도 영역 칸 drawing_area — 박힌 _A1_INNER 대신 양식에서 읽음 (PLAN 10-3)

- 00_template_A1.json 에 drawing_area [42, 47, 812, 567]
- Engine_Template drawing_area(이름) · 칸이 없거나 어긋나면 A1 기본값 · usable_area · usable_bbox · frame_entities 가 씀
- 도각 저장은 고치기 전 양식의 작도 영역을 이어 받음
- 시험 test_m02_drawing_area.py

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PxvYb5ufV1kWdBvZbpDfu6
This commit is contained in:
2026-09-25 09:36:07 +09:00
co-authored by Claude Opus 5.5
parent bc73b28823
commit df0b7d0aa2
3 changed files with 100 additions and 8 deletions
@@ -6,7 +6,8 @@ resources/master_template/drawing/의 사전 변환 템플릿(A1 도각 등)을
템플릿 쪽을 확대해 콘텐츠를 감싼다.
A1 템플릿 기하(변환 시점 고정값): 전체 840x594, 하단 y17~47 표제란,
내부 작도 영역 (42, 47) ~ (812, 567).
내부 작도 영역 (42, 47) ~ (812, 567). 작도 영역은 양식 칸 `drawing_area` 가 정본이고
칸이 없는 양식(옛 회사 도각)은 이 값으로 떨어진다.
"""
import binascii
@@ -37,7 +38,7 @@ _SVG_NS_UUID = uuid5(NAMESPACE_URL, "aislo/b07/signature-vector")
A1_TEMPLATE = "00_template_A1"
COMPASS_TEMPLATE = "00_template_compass" # 유역도 등 평면 도면의 방위표
# A1 내부 작도 영역(템플릿 좌표) — 콘텐츠가 이 영역 중앙에 오도록 배치한다.
# A1 내부 작도 영역(템플릿 좌표) — 양식에 `drawing_area` 칸이 없을 때만 쓴다.
_A1_INNER = (42.0, 47.0, 812.0, 567.0)
# 내부 작도 영역 대비 콘텐츠 여백 비율(각 방향). 5%는 도각 안쪽에 70mm 가까이를
# 비워 횡단 장이 한 장 더 늘었다 — 2%로 줄여 작도 영역을 쓴다(2026-08-30 사용자).
@@ -102,6 +103,23 @@ def _load_template(name: str) -> dict[str, Any] | None:
return _read_template(str(path), path.stat().st_mtime_ns)
def drawing_area(name: str = A1_TEMPLATE) -> tuple[float, float, float, float]:
"""양식의 내부 작도 영역 (x0, y0, x1, y1) — 콘텐츠가 이 영역 중앙에 놓인다.
양식 칸 `drawing_area` 를 읽는다. 없거나 어긋나면 A1 기본값(`_A1_INNER`).
"""
area = (_load_template(name) or {}).get("drawing_area")
if (
isinstance(area, list)
and len(area) == 4
and all(isinstance(value, (int, float)) for value in area)
and area[0] < area[2]
and area[1] < area[3]
):
return (float(area[0]), float(area[1]), float(area[2]), float(area[3]))
return _A1_INNER
def template_entities(name: str = A1_TEMPLATE) -> list[dict[str, Any]]:
"""도각 원본 엔티티(실치수 1:1). 편집 화면이 그대로 싣고, 저장도 이 좌표계로 받는다."""
template = _load_template(name)
@@ -173,9 +191,17 @@ def save_company_template(
validate_template_entities(entities)
path = company_template_path(company_dir, name)
path.parent.mkdir(parents=True, exist_ok=True)
# 작도 영역은 CAD 가 모르는 칸이라 편집본에 없다 — 고치기 전 양식의 값을 이어 받는다.
previous = path if path.is_file() else _TEMPLATE_DIR / f"{name}.json"
area = (
json.loads(previous.read_text(encoding="utf-8")).get("drawing_area")
if previous.is_file()
else None
)
document = {
"format": DRAWING_FORMAT,
"source": "B07 도각 편집 화면",
**({"drawing_area": area} if area else {}),
# 도각은 도면마다 잠금 층 하나로 붙으므로 편집 중 새로 만든 층은 도각으로 모은다.
"entities": [{**entity, "layerId": FRAME_LAYER_ID} for entity in entities],
"layers": [{"id": FRAME_LAYER_ID, "name": "도각", "isVisible": True, "isLocked": False}],
@@ -293,15 +319,15 @@ def usable_bbox() -> tuple[float, float, float, float]:
여백을 뺀 한도(`usable_area()`)보다 커서 "작도 영역을 넘습니다" 경고가 뜬다 —
내용이 없는데 넘칠 리 없다. 중심이 같으므로 도각 배치(이동량 0)는 그대로다.
"""
ix0, iy0, ix1, iy1 = _A1_INNER
ix0, iy0, ix1, iy1 = drawing_area()
width, height = usable_area()
cx, cy = (ix0 + ix1) / 2.0, (iy0 + iy1) / 2.0
return (cx - width / 2.0, cy - height / 2.0, cx + width / 2.0, cy + height / 2.0)
def usable_area() -> tuple[float, float]:
"""A1 내부 작도 영역에서 여백을 뺀 유효 크기(mm). 척도 고정 도면의 수용 한도."""
ix0, iy0, ix1, iy1 = _A1_INNER
def usable_area(name: str = A1_TEMPLATE) -> tuple[float, float]:
"""양식 작도 영역에서 여백을 뺀 유효 크기(mm). 척도 고정 도면의 수용 한도."""
ix0, iy0, ix1, iy1 = drawing_area(name)
return (
(ix1 - ix0) * (1.0 - 2.0 * _CONTENT_MARGIN),
(iy1 - iy0) * (1.0 - 2.0 * _CONTENT_MARGIN),
@@ -444,8 +470,8 @@ def frame_entities(
content_w = max(max_x - min_x, 1e-6)
content_h = max(max_y - min_y, 1e-6)
ix0, iy0, ix1, iy1 = _A1_INNER
usable_w, usable_h = usable_area()
ix0, iy0, ix1, iy1 = drawing_area(template_name)
usable_w, usable_h = usable_area(template_name)
scale = max(content_w / usable_w, content_h / usable_h) if fit else 1.0
# 한도와 **같은** 크기는 넘친 것이 아니다 — 부동소수 오차만큼의 여유를 둔다
# (수용 한도를 그대로 넘기는 빈 도면이 마지막 자리 오차로 경고를 냈다).