diff --git a/B07_DesignDetail/B07_DesignDetail_Engine_Template.py b/B07_DesignDetail/B07_DesignDetail_Engine_Template.py index 7e6c8c36..68199346 100644 --- a/B07_DesignDetail/B07_DesignDetail_Engine_Template.py +++ b/B07_DesignDetail/B07_DesignDetail_Engine_Template.py @@ -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 # 한도와 **같은** 크기는 넘친 것이 아니다 — 부동소수 오차만큼의 여유를 둔다 # (수용 한도를 그대로 넘기는 빈 도면이 마지막 자리 오차로 경고를 냈다). diff --git a/resources/master_template/drawing/00_template_A1.json b/resources/master_template/drawing/00_template_A1.json index d0b83d57..63d94bf1 100644 --- a/resources/master_template/drawing/00_template_A1.json +++ b/resources/master_template/drawing/00_template_A1.json @@ -1,6 +1,7 @@ { "format": 6, "source": "00_templete_A1.dxf (남의 프로젝트 자료 제거 · 플레이스홀더화)", + "drawing_area": [42, 47, 812, 567], "entities": [ { "id": "4afa84ae-9c15-50ec-8a76-db87d04d6311", diff --git a/resources/tester/test_m02_drawing_area.py b/resources/tester/test_m02_drawing_area.py new file mode 100644 index 00000000..77b16a31 --- /dev/null +++ b/resources/tester/test_m02_drawing_area.py @@ -0,0 +1,65 @@ +"""M02 도면 양식 작도 영역 칸(`drawing_area`) — 양식이 정본 · 없으면 A1 기본값 (PLAN 10-3).""" + +import json + +from B07_DesignDetail import B07_DesignDetail_Engine_Template as engine + + +def _company_template(tmp_path, document): + path = engine.company_template_path(tmp_path) + path.parent.mkdir(parents=True) + path.write_text(json.dumps(document), encoding="utf-8") + return path + + +def test_system_a1_reads_drawing_area(): + engine.use_company_templates(None) + assert engine.drawing_area() == (42.0, 47.0, 812.0, 567.0) + # 도각 배치는 옮기기 전 박힌 값과 같다 — 콘텐츠 중심 = 작도 영역 중심. + frame = engine.frame_entities("t", (0.0, 0.0, 100.0, 100.0), fit=False) + assert frame + + +def test_missing_area_falls_back_to_a1(tmp_path): + _company_template(tmp_path, {"format": 6, "entities": [], "layers": []}) + engine.use_company_templates(tmp_path) + try: + assert engine.drawing_area() == engine._A1_INNER + finally: + engine.use_company_templates(None) + + +def test_template_area_moves_content(tmp_path): + line = { + "id": "a", + "type": "Line", + "shapeData": {"startPoint": {"x": 0, "y": 0}, "endPoint": {"x": 10, "y": 0}}, + } + _company_template( + tmp_path, + {"format": 6, "drawing_area": [0, 0, 400, 200], "entities": [line], "layers": []}, + ) + engine.use_company_templates(tmp_path) + try: + assert engine.drawing_area() == (0.0, 0.0, 400.0, 200.0) + assert engine.usable_area() == (400 * 0.96, 200 * 0.96) + # 콘텐츠 중심(500, 500) 이 작도 영역 중심(200, 100) 에 온다 — 이동량 (300, 400). + placed = engine.frame_entities("t", (490.0, 490.0, 510.0, 510.0), fit=False) + assert placed[0]["shapeData"]["startPoint"] == {"x": 300.0, "y": 400.0} + finally: + engine.use_company_templates(None) + + +def test_bad_area_falls_back(tmp_path): + _company_template(tmp_path, {"drawing_area": [10, 10, 5, 5], "entities": []}) + engine.use_company_templates(tmp_path) + try: + assert engine.drawing_area() == engine._A1_INNER + finally: + engine.use_company_templates(None) + + +def test_save_keeps_area(tmp_path): + # 편집본(CAD)에는 작도 영역 칸이 없다 — 저장이 시스템 양식 값을 이어 받는다. + path = engine.save_company_template(tmp_path, []) + assert json.loads(path.read_text(encoding="utf-8"))["drawing_area"] == [42, 47, 812, 567]