납품 도면 2장을 B07 도면 목록에 새로 붙였다. 계산은 하지 않는다 — 유토곡선은 B06 확정 시 저장한 산출물(longitudinal_sections.data.mass_haul)을, 유역도는 B04 세부유역 GeoJSON과 도엽 등고선·세류선을 읽어 좌표만 종이 mm로 옮긴다. - 도면 종류 확장: kind에 mass_haul·watershed 추가(Schema·Api_Fetch·openwebcad App.types), 12분류 라벨에 연결, 목록에 단장 도면 2건 상시 노출 - 엔진 신규: _Engine_Cad_MassHaul.py(축·곡선·평형선·띠 현·balloon·측점 테이블), _Engine_Cad_Basin.py(등고선·세류선 배경·노선·유역·구역별 정보표·방위표) - 척도 상수: 유토곡선 H 1/2,000 · 세로 1mm=50㎥, 유역도 1/6,000 (A1 고정) - 유토곡선 저장 payload에 띠·잔여의 기하 필드 추가 — 파이썬에 곡선 보간·토량 배분 로직을 복제하지 않기 위해 값을 낳는 쪽(TS 엔진)에서 함께 남긴다 - 유역도 배경은 여러 도엽을 합쳐 받은 뒤 도곽 크기로 절취(clip_line_to_box) - 배수규격은 소요 관경이 아니라 규격관(recommended_diameter_mm)으로 표기 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
178 lines
6.6 KiB
Python
178 lines
6.6 KiB
Python
"""B07 도각 템플릿 병합 — openwebcad JSON 템플릿을 도면 콘텐츠 둘레에 배치한다.
|
|
|
|
resources/template_2dDrawing/의 사전 변환 템플릿(A1 도각 등)을 로드해,
|
|
도면 콘텐츠 bbox에 맞춰 균등 스케일·이동시킨 뒤 잠금 프레임 레이어
|
|
(b08-frame) 엔티티로 병합한다. 콘텐츠 좌표(m)는 건드리지 않는다 —
|
|
템플릿 쪽을 확대해 콘텐츠를 감싼다.
|
|
|
|
A1 템플릿 기하(변환 시점 고정값): 전체 840x594, 하단 y17~47 표제란,
|
|
내부 작도 영역 (42, 47) ~ (812, 567).
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
from functools import lru_cache
|
|
from pathlib import Path
|
|
from typing import Any
|
|
from uuid import uuid5
|
|
|
|
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
|
|
_ENTITY_NS,
|
|
FRAME_LAYER_ID,
|
|
)
|
|
|
|
_TEMPLATE_DIR = Path(__file__).resolve().parent.parent / "resources" / "template_2dDrawing"
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
A1_TEMPLATE = "00_template_A1"
|
|
COMPASS_TEMPLATE = "00_template_compass" # 유역도 등 평면 도면의 방위표
|
|
# A1 내부 작도 영역(템플릿 좌표) — 콘텐츠가 이 영역 중앙에 오도록 배치한다.
|
|
_A1_INNER = (42.0, 47.0, 812.0, 567.0)
|
|
_CONTENT_MARGIN = 0.05 # 내부 작도 영역 대비 콘텐츠 여백 비율(각 방향 5%)
|
|
|
|
|
|
@lru_cache(maxsize=8)
|
|
def _load_template(name: str) -> dict[str, Any] | None:
|
|
path = _TEMPLATE_DIR / f"{name}.json"
|
|
if not path.exists():
|
|
return None
|
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
|
|
|
|
def entities_bbox(entities: list[dict[str, Any]]) -> tuple[float, float, float, float] | None:
|
|
"""엔티티 목록의 (min_x, min_y, max_x, max_y). 좌표가 없으면 None."""
|
|
xs: list[float] = []
|
|
ys: list[float] = []
|
|
|
|
def _collect(entity: dict[str, Any]) -> None:
|
|
shape = entity.get("shapeData") or {}
|
|
for key in ("startPoint", "endPoint", "basePoint", "point"):
|
|
p = shape.get(key)
|
|
if isinstance(p, dict):
|
|
xs.append(float(p["x"]))
|
|
ys.append(float(p["y"]))
|
|
center = shape.get("center")
|
|
if isinstance(center, dict):
|
|
r = float(shape.get("radius", 0.0))
|
|
xs.extend((float(center["x"]) - r, float(center["x"]) + r))
|
|
ys.extend((float(center["y"]) - r, float(center["y"]) + r))
|
|
for child in entity.get("children") or []:
|
|
_collect(child)
|
|
|
|
for entity in entities:
|
|
_collect(entity)
|
|
if not xs:
|
|
return None
|
|
return (min(xs), min(ys), max(xs), max(ys))
|
|
|
|
|
|
def _transform_entity(
|
|
entity: dict[str, Any], seed: str, scale: float, dx: float, dy: float
|
|
) -> dict[str, Any]:
|
|
"""템플릿 엔티티를 스케일+이동 복사한다. id는 도면별 결정적 재생성."""
|
|
out = dict(entity)
|
|
out["id"] = str(uuid5(_ENTITY_NS, seed))
|
|
out["layerId"] = FRAME_LAYER_ID
|
|
shape = entity.get("shapeData")
|
|
if isinstance(shape, dict):
|
|
new_shape = dict(shape)
|
|
for key in ("startPoint", "endPoint", "basePoint", "point", "center"):
|
|
p = shape.get(key)
|
|
if isinstance(p, dict):
|
|
new_shape[key] = {"x": p["x"] * scale + dx, "y": p["y"] * scale + dy}
|
|
if "radius" in shape:
|
|
new_shape["radius"] = shape["radius"] * scale
|
|
options = shape.get("options")
|
|
if isinstance(options, dict):
|
|
new_options = dict(options)
|
|
new_options["fontSize"] = options.get("fontSize", 4.0) * scale
|
|
new_shape["options"] = new_options
|
|
out["shapeData"] = new_shape
|
|
children = entity.get("children")
|
|
if isinstance(children, list):
|
|
out["children"] = [
|
|
_transform_entity(child, f"{seed}:{index}", scale, dx, dy)
|
|
for index, child in enumerate(children)
|
|
]
|
|
return out
|
|
|
|
|
|
def compass_entities(
|
|
drawing_id: str,
|
|
center: tuple[float, float],
|
|
size_mm: float,
|
|
template_name: str = COMPASS_TEMPLATE,
|
|
) -> list[dict[str, Any]]:
|
|
"""방위표 템플릿을 지정 위치에 지정 크기로 놓는다(잠금 프레임 레이어).
|
|
|
|
도각과 달리 콘텐츠를 감쌀 일이 없으므로 긴 변이 size_mm가 되도록 균등 축소한다.
|
|
"""
|
|
template = _load_template(template_name)
|
|
if not template:
|
|
return []
|
|
entities = template.get("entities", [])
|
|
bbox = entities_bbox(entities)
|
|
if not bbox:
|
|
return []
|
|
min_x, min_y, max_x, max_y = bbox
|
|
span = max(max_x - min_x, max_y - min_y, 1e-6)
|
|
scale = size_mm / span
|
|
dx = center[0] - (min_x + max_x) / 2.0 * scale
|
|
dy = center[1] - (min_y + max_y) / 2.0 * scale
|
|
return [
|
|
_transform_entity(entity, f"{drawing_id}:compass:{index}", scale, dx, dy)
|
|
for index, entity in enumerate(entities)
|
|
]
|
|
|
|
|
|
def usable_area() -> tuple[float, float]:
|
|
"""A1 내부 작도 영역에서 여백을 뺀 유효 크기(mm). 척도 고정 도면의 수용 한도."""
|
|
ix0, iy0, ix1, iy1 = _A1_INNER
|
|
return (
|
|
(ix1 - ix0) * (1.0 - 2.0 * _CONTENT_MARGIN),
|
|
(iy1 - iy0) * (1.0 - 2.0 * _CONTENT_MARGIN),
|
|
)
|
|
|
|
|
|
def frame_entities(
|
|
drawing_id: str,
|
|
content_bbox: tuple[float, float, float, float],
|
|
template_name: str = A1_TEMPLATE,
|
|
fit: bool = True,
|
|
) -> list[dict[str, Any]]:
|
|
"""콘텐츠 bbox를 감싸는 도각 프레임 엔티티 목록(잠금 레이어). 템플릿 없으면 빈 목록.
|
|
|
|
fit=False면 도각을 **실치수(1:1)** 로 두고 위치만 맞춘다 — 콘텐츠가 이미 종이
|
|
밀리미터로 그려진 척도 고정 도면(종단·횡단)용. fit=True는 척도가 없는 도면을
|
|
도각에 맞춰 늘리던 기존 동작이다.
|
|
"""
|
|
template = _load_template(template_name)
|
|
if not template:
|
|
return []
|
|
min_x, min_y, max_x, max_y = content_bbox
|
|
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()
|
|
scale = max(content_w / usable_w, content_h / usable_h) if fit else 1.0
|
|
if not fit and (content_w > usable_w or content_h > usable_h):
|
|
logger.warning(
|
|
"도면 콘텐츠가 A1 작도 영역을 넘습니다: %s (%.0fx%.0f mm > %.0fx%.0f mm)",
|
|
drawing_id,
|
|
content_w,
|
|
content_h,
|
|
usable_w,
|
|
usable_h,
|
|
)
|
|
|
|
# 콘텐츠 중심 = 내부 작도 영역 중심이 되도록 이동량 산출.
|
|
dx = (min_x + max_x) / 2.0 - (ix0 + ix1) / 2.0 * scale
|
|
dy = (min_y + max_y) / 2.0 - (iy0 + iy1) / 2.0 * scale
|
|
|
|
return [
|
|
_transform_entity(entity, f"{drawing_id}:frame:{index}", scale, dx, dy)
|
|
for index, entity in enumerate(template.get("entities", []))
|
|
]
|