feat(B07): 종단면도를 작성 척도(1/1,000 x 1/200)로 고정
도면에 척도 개념이 없어 도각을 콘텐츠 크기에 맞춰 늘렸다 줄였다. 사용자가 정한 작성 척도로 그림 크기가 결정되도록 좌표를 종이 밀리미터로 옮긴다. - config_system에 DRAWING_SCALE_* 상수 추가 (종단 1/1,000 x 1/200, 횡단 1/100, A1) - 종단 좌표를 종이 mm로: 거리 x1.0, 표고는 기준선 기준 x5.0 (세로 5배 과장) - 기준선(datum)을 종이 y=0으로 정규화, Y축 눈금 라벨은 실제 표고 유지 - frame_entities(fit=False) 추가 — 도각을 실치수 1:1로 두고 위치만 맞춘다. 콘텐츠가 작도 영역(693x468mm)을 넘으면 경고 로그 - 실측: 도각 840x594mm(A1 실치수), 콘텐츠 370x213mm (노선 350m = 350mm) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -46,6 +46,11 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Template import (
|
||||
frame_entities,
|
||||
)
|
||||
from common_util.common_util_route_profile import design_elevation_from_longitudinal
|
||||
from config.config_system import DRAWING_SCALE_LONG_H, DRAWING_SCALE_LONG_V
|
||||
|
||||
# 도면 좌표 = 종이 mm. 실거리 1 m가 종이에서 차지하는 mm (1/1,000 -> 1.0, 1/200 -> 5.0).
|
||||
MM_H = 1000.0 / DRAWING_SCALE_LONG_H
|
||||
MM_V = 1000.0 / DRAWING_SCALE_LONG_V
|
||||
|
||||
# 종단 전용 레이어: 그래프 축·격자(잠금 — 참조용, 편집 제외).
|
||||
LONG_GRID_LAYER_ID = "b08-long-grid"
|
||||
@@ -187,10 +192,17 @@ def _graph_grid_entities(
|
||||
chainages: list[float],
|
||||
x0: float,
|
||||
x1: float,
|
||||
datum_y: float,
|
||||
top_y: float,
|
||||
datum_m: float,
|
||||
top_m: float,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""그래프 영역: 기준선(X축)·Y축(표고 눈금/라벨)·측점 회색 세로선."""
|
||||
"""그래프 영역: 기준선(X축)·Y축(표고 눈금/라벨)·측점 회색 세로선.
|
||||
|
||||
x는 종이 mm, datum_m·top_m은 실제 표고(m)로 받는다 — 눈금 라벨이 표고라서
|
||||
m으로 돌고 좌표만 MM_V를 곱한다.
|
||||
"""
|
||||
# 기준선을 종이 y=0으로 두고 표고 차이만 MM_V로 올린다.
|
||||
datum_y = 0.0
|
||||
top_y = (top_m - datum_m) * MM_V
|
||||
entities: list[dict[str, Any]] = [
|
||||
# 기준선(X축)
|
||||
_line_entity(
|
||||
@@ -202,9 +214,10 @@ def _graph_grid_entities(
|
||||
),
|
||||
]
|
||||
# Y축 표고 눈금·라벨 (5m 간격, 가로쓰기)
|
||||
level = datum_y
|
||||
level_m = datum_m
|
||||
tick_index = 0
|
||||
while level <= top_y + 1e-6:
|
||||
while level_m <= top_m + 1e-6:
|
||||
level = (level_m - datum_m) * MM_V
|
||||
entities.append(
|
||||
_line_entity(
|
||||
f"{drawing_id}:lgyt{tick_index}",
|
||||
@@ -217,7 +230,7 @@ def _graph_grid_entities(
|
||||
entities.append(
|
||||
_text_entity(
|
||||
f"{drawing_id}:lgyl{tick_index}",
|
||||
_format(level),
|
||||
_format(level_m),
|
||||
x0 - 1.6,
|
||||
level,
|
||||
LONG_GRID_LAYER_ID,
|
||||
@@ -226,7 +239,7 @@ def _graph_grid_entities(
|
||||
align="right",
|
||||
)
|
||||
)
|
||||
level += 5.0
|
||||
level_m += 5.0
|
||||
tick_index += 1
|
||||
# 측점별 회색 세로선 (기준선 → 그래프 상단)
|
||||
for index, x in enumerate(chainages):
|
||||
@@ -265,8 +278,8 @@ def _curve_row_entities(
|
||||
evc = curve.get("evc_m")
|
||||
if not isinstance(bvc, (int, float)) or not isinstance(evc, (int, float)):
|
||||
continue
|
||||
start = max(float(bvc), x0)
|
||||
end = min(float(evc), x1)
|
||||
start = max(float(bvc) * MM_H, x0)
|
||||
end = min(float(evc) * MM_H, x1)
|
||||
if end <= start:
|
||||
continue
|
||||
seed = f"{drawing_id}:lcurve:{index}"
|
||||
@@ -387,8 +400,8 @@ def _grade_row_entities(
|
||||
grade = segment.get("grade_percent")
|
||||
if not isinstance(from_m, (int, float)) or not isinstance(to_m, (int, float)):
|
||||
continue
|
||||
start = max(float(from_m), x0) + radius
|
||||
end = min(float(to_m), x1) - radius
|
||||
start = max(float(from_m) * MM_H, x0) + radius
|
||||
end = min(float(to_m) * MM_H, x1) - radius
|
||||
if end <= start:
|
||||
continue
|
||||
seed = f"{drawing_id}:lgrade:{index}"
|
||||
@@ -419,14 +432,16 @@ def _grade_row_entities(
|
||||
|
||||
# 구배 변화점: 원(정원) 또는 노선 시·종점 반원 + 내부 세로쓰기 계획고
|
||||
for index, (chainage, is_route_end) in enumerate(
|
||||
_grade_break_points(segments, x0, x1, route_start, route_end)
|
||||
_grade_break_points(segments, x0 / MM_H, x1 / MM_H, route_start, route_end)
|
||||
):
|
||||
# chainage는 월드 거리 — 계획고 조회에 그대로 쓰고, 좌표에는 MM_H를 곱한다.
|
||||
x_paper = chainage * MM_H
|
||||
seed = f"{drawing_id}:lgradebp:{index}"
|
||||
if is_route_end:
|
||||
entities.append(
|
||||
_arc_entity(
|
||||
seed,
|
||||
(chainage, y_mid),
|
||||
(x_paper, y_mid),
|
||||
radius,
|
||||
0.0,
|
||||
math.pi,
|
||||
@@ -437,7 +452,7 @@ def _grade_row_entities(
|
||||
else:
|
||||
entities.append(
|
||||
_circle_entity(
|
||||
seed, (chainage, y_mid), radius, LONG_TABLE_LAYER_ID, TABLE_LINE_COLOR
|
||||
seed, (x_paper, y_mid), radius, LONG_TABLE_LAYER_ID, TABLE_LINE_COLOR
|
||||
)
|
||||
)
|
||||
elevation = design_elevation_from_longitudinal(longitudinal, chainage)
|
||||
@@ -446,7 +461,7 @@ def _grade_row_entities(
|
||||
_text_entity(
|
||||
f"{seed}:txt",
|
||||
_format(elevation),
|
||||
chainage,
|
||||
x_paper,
|
||||
y_mid,
|
||||
LONG_TABLE_LAYER_ID,
|
||||
_FONT_SIZE * 0.8,
|
||||
@@ -468,8 +483,8 @@ def _long_table_entities(
|
||||
route_end: float,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""종단 테이블: 가로 구분선 + 측점 눈금(세로선 없음) + 세로쓰기 값."""
|
||||
chainages = [float(s["chainage_m"]) for s in stations]
|
||||
header_width = max(interval_m, 12.0)
|
||||
chainages = [float(s["chainage_m"]) * MM_H for s in stations]
|
||||
header_width = max(interval_m * MM_H, 12.0)
|
||||
left = chainages[0] - header_width
|
||||
right = chainages[-1]
|
||||
entities: list[dict[str, Any]] = []
|
||||
@@ -606,29 +621,38 @@ def build_longitudinal_drawing(
|
||||
)
|
||||
interval_m = infer_station_interval(all_stations)
|
||||
|
||||
# 좌표는 종이 mm — 거리에 MM_H, 표고는 기준선(datum) 기준으로 MM_V를 곱한다.
|
||||
elevations = [y for _x, y in [*ground_points, *design_points]]
|
||||
min_e = min(elevations) if elevations else 0.0
|
||||
max_e = max(elevations) if elevations else 10.0
|
||||
# 기준선(datum): 최저 표고에서 5m 이상 여유를 두고 5m 단위로 내림 → 종이 y=0.
|
||||
datum_m = math.floor((min_e - 5.0) / 5.0) * 5.0
|
||||
top_m = max_e + 3.0
|
||||
|
||||
def paper(point: tuple[float, float]) -> tuple[float, float]:
|
||||
return (point[0] * MM_H, (point[1] - datum_m) * MM_V)
|
||||
|
||||
entities: list[dict[str, Any]] = []
|
||||
ground = polyline_entity(drawing_id, ground_points, GROUND_LAYER_ID, GROUND_COLOR)
|
||||
ground = polyline_entity(
|
||||
drawing_id, [paper(p) for p in ground_points], GROUND_LAYER_ID, GROUND_COLOR
|
||||
)
|
||||
if ground:
|
||||
entities.append(ground)
|
||||
design = polyline_entity(drawing_id, design_points, DESIGN_LAYER_ID, DESIGN_COLOR)
|
||||
design = polyline_entity(
|
||||
drawing_id, [paper(p) for p in design_points], DESIGN_LAYER_ID, DESIGN_COLOR
|
||||
)
|
||||
if design:
|
||||
entities.append(design)
|
||||
|
||||
if stations:
|
||||
chainages = [float(s["chainage_m"]) for s in stations]
|
||||
elevations = [y for _x, y in [*ground_points, *design_points]]
|
||||
min_e = min(elevations) if elevations else 0.0
|
||||
max_e = max(elevations) if elevations else 10.0
|
||||
# 기준선(datum): 최저 표고에서 5m 이상 여유를 두고 5m 단위로 내림.
|
||||
datum_y = math.floor((min_e - 5.0) / 5.0) * 5.0
|
||||
top_y = max_e + 3.0
|
||||
chainages = [float(s["chainage_m"]) * MM_H for s in stations]
|
||||
entities.extend(
|
||||
_graph_grid_entities(drawing_id, chainages, chainages[0], chainages[-1], datum_y, top_y)
|
||||
_graph_grid_entities(drawing_id, chainages, chainages[0], chainages[-1], datum_m, top_m)
|
||||
)
|
||||
|
||||
values = _long_table_values(stations, all_stations, ground_all, longitudinal, interval_m)
|
||||
all_chainages = sorted(float(s["chainage_m"]) for s in all_stations)
|
||||
table_top = datum_y - 3.0 # 그래프-테이블 영역 분리 간격
|
||||
table_top = -3.0 # 그래프-테이블 영역 분리 간격(mm, 기준선 y=0 아래)
|
||||
entities.extend(
|
||||
_long_table_entities(
|
||||
drawing_id,
|
||||
@@ -642,10 +666,10 @@ def build_longitudinal_drawing(
|
||||
)
|
||||
)
|
||||
|
||||
# A1 도각 프레임: 콘텐츠 bbox를 감싸도록 배치 (잠금 레이어, 좌표는 콘텐츠 불변).
|
||||
# A1 도각: 콘텐츠가 이미 종이 mm라 도각도 실치수(1:1)로 두고 위치만 맞춘다.
|
||||
bbox = entities_bbox(entities)
|
||||
if bbox:
|
||||
entities.extend(frame_entities(drawing_id, bbox))
|
||||
entities.extend(frame_entities(drawing_id, bbox, fit=False))
|
||||
|
||||
return {
|
||||
"format": DRAWING_FORMAT,
|
||||
|
||||
@@ -10,6 +10,7 @@ A1 템플릿 기하(변환 시점 고정값): 전체 840x594, 하단 y17~47 표
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
@@ -22,6 +23,8 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
|
||||
|
||||
_TEMPLATE_DIR = Path(__file__).resolve().parent.parent / "resources" / "template_2dDrawing"
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
A1_TEMPLATE = "00_template_A1"
|
||||
# A1 내부 작도 영역(템플릿 좌표) — 콘텐츠가 이 영역 중앙에 오도록 배치한다.
|
||||
_A1_INNER = (42.0, 47.0, 812.0, 567.0)
|
||||
@@ -94,12 +97,27 @@ def _transform_entity(
|
||||
return out
|
||||
|
||||
|
||||
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를 감싸는 도각 프레임 엔티티 목록(잠금 레이어). 템플릿 없으면 빈 목록."""
|
||||
"""콘텐츠 bbox를 감싸는 도각 프레임 엔티티 목록(잠금 레이어). 템플릿 없으면 빈 목록.
|
||||
|
||||
fit=False면 도각을 **실치수(1:1)** 로 두고 위치만 맞춘다 — 콘텐츠가 이미 종이
|
||||
밀리미터로 그려진 척도 고정 도면(종단·횡단)용. fit=True는 척도가 없는 도면을
|
||||
도각에 맞춰 늘리던 기존 동작이다.
|
||||
"""
|
||||
template = _load_template(template_name)
|
||||
if not template:
|
||||
return []
|
||||
@@ -108,9 +126,17 @@ def frame_entities(
|
||||
content_h = max(max_y - min_y, 1e-6)
|
||||
|
||||
ix0, iy0, ix1, iy1 = _A1_INNER
|
||||
usable_w = (ix1 - ix0) * (1.0 - 2.0 * _CONTENT_MARGIN)
|
||||
usable_h = (iy1 - iy0) * (1.0 - 2.0 * _CONTENT_MARGIN)
|
||||
scale = max(content_w / usable_w, content_h / usable_h)
|
||||
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
|
||||
|
||||
@@ -745,6 +745,17 @@ VWORLD_LOGIN_PW = os.getenv("VWORLD_LOGIN_PW", "")
|
||||
# 선택: 수동 발급 세션 쿠키 (예: "JSESSIONID=..; SSCSID=.."). 비어 있으면 자동 로그인 사용.
|
||||
VWORLD_SESSION_COOKIE = os.getenv("VWORLD_SESSION_COOKIE", "")
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# B07 도면 작성 척도 (2026-08-29 사용자 확정)
|
||||
# 종단면도: 가로 1/1,000 · 세로 1/200 (세로 5배 과장)
|
||||
# 횡단면도: 가로·세로 1/100
|
||||
# 도면 좌표는 종이 밀리미터다. 실거리 1 m가 종이에서 몇 mm인지 = 1000 / 축척분모.
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
DRAWING_SCALE_LONG_H = 1000 # 종단면도 가로 축척 분모
|
||||
DRAWING_SCALE_LONG_V = 200 # 종단면도 세로 축척 분모
|
||||
DRAWING_SCALE_CROSS = 100 # 횡단면도 가로·세로 축척 분모
|
||||
DRAWING_SHEET = "A1" # 용지 규격 (840x594 mm 도각 템플릿)
|
||||
|
||||
# 시스템 리소스 로그 (루트 log 폴더에 단일 파일, 1개월 보관)
|
||||
LOG_BASE_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "log")
|
||||
RESOURCE_LOG_PATH = os.path.join(LOG_BASE_DIR, "system_resources.log")
|
||||
|
||||
Reference in New Issue
Block a user