diff --git a/B07_DesignDetail/B07_DesignDetail_Engine_Cad_Long.py b/B07_DesignDetail/B07_DesignDetail_Engine_Cad_Long.py index 021e3a2b..7df1aa8c 100644 --- a/B07_DesignDetail/B07_DesignDetail_Engine_Cad_Long.py +++ b/B07_DesignDetail/B07_DesignDetail_Engine_Cad_Long.py @@ -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, diff --git a/B07_DesignDetail/B07_DesignDetail_Engine_Template.py b/B07_DesignDetail/B07_DesignDetail_Engine_Template.py index 65a08a25..c5472943 100644 --- a/B07_DesignDetail/B07_DesignDetail_Engine_Template.py +++ b/B07_DesignDetail/B07_DesignDetail_Engine_Template.py @@ -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 diff --git a/config/config_system.py b/config/config_system.py index b1622cc9..86aa1cfd 100644 --- a/config/config_system.py +++ b/config/config_system.py @@ -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")