diff --git a/B07_DesignDetail/B07_DesignDetail_Engine_Cad.py b/B07_DesignDetail/B07_DesignDetail_Engine_Cad.py index f3c623d9c..2323abd8e 100644 --- a/B07_DesignDetail/B07_DesignDetail_Engine_Cad.py +++ b/B07_DesignDetail/B07_DesignDetail_Engine_Cad.py @@ -35,7 +35,7 @@ TABLE_LINE_COLOR = "#8ea0b5" TABLE_LABEL_COLOR = "#e8edf4" TABLE_VALUE_COLOR = "#ffe066" -# 종단도 분할 기준: 측점 30개 초과 시 30개 단위(경계 1측점 중복)로 나눈다. +# 종단도 분할 기준: 일반측점 30 간격(20 m 면 600 m) 단위(경계 1측점 중복) · 추가 · MC 측점은 안 셈. LONG_SPLIT_STATION_COUNT = 30 _ENTITY_NS = UUID("f15df4cc-fbb1-4bc9-b04c-63052fe43f96") diff --git a/B07_DesignDetail/B07_DesignDetail_Engine_Cad_Long.py b/B07_DesignDetail/B07_DesignDetail_Engine_Cad_Long.py index 7b5b33e76..85d246669 100644 --- a/B07_DesignDetail/B07_DesignDetail_Engine_Cad_Long.py +++ b/B07_DesignDetail/B07_DesignDetail_Engine_Cad_Long.py @@ -1,4 +1,4 @@ -"""B07 종단도 CAD 조립 — 30측점 N분할 + 그래프 축·격자 + 하단 측점 테이블. +"""B07 종단도 CAD 그림 — 그래프 축·격자 + 하단 측점 테이블(장 나눔 · 자리 · 도각 = `_LongSheet`). 납품 양식: - 그래프 영역: 좌측 Y축(표고 눈금·라벨), 기준선(X축, 최저 표고에서 5m 이상 @@ -17,11 +17,8 @@ from typing import Any from B07_DesignDetail.B07_DesignDetail_Engine_Cad import ( DESIGN_COLOR, DESIGN_LAYER_ID, - DRAWING_FORMAT, - FRAME_LAYER_ID, GROUND_COLOR, GROUND_LAYER_ID, - LONG_SPLIT_STATION_COUNT, LONG_TABLE_LAYER_ID, TABLE_LABEL_COLOR, TABLE_LINE_COLOR, @@ -31,7 +28,6 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad import ( _design_profile_points, _format, _interpolate, - _layer, _line_entity, _profile_alignment, _stations, @@ -42,18 +38,7 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad import ( polyline_entity, station_no_label, ) -from B07_DesignDetail.B07_DesignDetail_Engine_Cad_LongPlan import ( - LONG_PLAN_LAYER_ID, - long_plan_entities, - plan_box_overlaps, -) -from B07_DesignDetail.B07_DesignDetail_Engine_Template import ( - drawing_scale, - entities_bbox, - frame_entities, - scale_fields, - usable_area, -) +from B07_DesignDetail.B07_DesignDetail_Engine_Template import drawing_scale from common_util.common_util_route_profile import design_elevation_from_longitudinal @@ -88,55 +73,7 @@ _LONG_TABLE_ROWS: tuple[tuple[str, str, float], ...] = ( ) _FONT_SIZE = 2.2 _TICK_LEN = 0.8 # 측점 눈금 길이 - - -def _span(stations: list[dict[str, Any]], index: int, take: int) -> dict[str, float]: - return { - "start_m": float(stations[index]["chainage_m"]), - "end_m": float(stations[index + take - 1]["chainage_m"]), - } - - -def _fits_sheet(longitudinal: dict[str, Any], span: dict[str, float]) -> bool: - """이 구간을 고정 축척으로 그렸을 때 A1 작도 영역에 들고 평면 위치도 칸을 안 덮는가.""" - content = _long_content(longitudinal, "probe", span) - bbox = entities_bbox(content) - if bbox is None: - return True - usable_w, usable_h = usable_area() - fits = bbox[2] - bbox[0] <= usable_w + 1e-6 and bbox[3] - bbox[1] <= usable_h + 1e-6 - return fits and not plan_box_overlaps(longitudinal, content, bbox) - - -def longitudinal_chunks(longitudinal: dict[str, Any]) -> list[dict[str, Any]]: - """측점 30개 단위 분할 목록. 각 항목: {id, label, start_m, end_m}. - - 한 장이면 단일 도면(id="longitudinal", 기존 manifest 호환). 여러 장이면 - 경계 측점 1개를 중복시켜 도면 간 선이 이어지게 한다(납품 도면 관례). - 축척(H 1/1,000 · V 1/200)은 모든 장이 같다 — 작도 영역을 넘으면 측점을 줄여 - 장을 더 나눌 뿐 줄이지 않는다. - """ - stations = sorted(_stations(longitudinal), key=lambda s: float(s["chainage_m"])) - if not stations: - return [{"id": "longitudinal", "label": "종단도", "start_m": None, "end_m": None}] - spans: list[dict[str, float]] = [] - index = 0 - while True: - take = min(LONG_SPLIT_STATION_COUNT, len(stations) - index) - # ponytail: 한 측점씩 줄이며 다시 잰다 — 넘치는 장에서만 돎 - while take > 2 and not _fits_sheet(longitudinal, _span(stations, index, take)): - take -= 1 - spans.append(_span(stations, index, take)) - if index + take >= len(stations): - break - index += take - 1 # 경계 1측점 중복 - if len(spans) == 1: - return [{"id": "longitudinal", "label": "종단도", "number": 1, "total": 1, **spans[0]}] - return [ - {"id": f"longitudinal_{number}", "label": f"종단도({number})", "number": number, **span} - | {"total": len(spans)} - for number, span in enumerate(spans, start=1) - ] +_AXIS_GAP = 10.0 # Y축 · 측점표 머리 ↔ 첫 측점 여유(종이 mm · 525-1) def _long_table_values( @@ -474,7 +411,8 @@ def _long_table_entities( """종단 테이블: 가로 구분선 + 측점 눈금(세로선 없음) + 세로쓰기 값.""" 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 + header_x = chainages[0] - _AXIS_GAP + left = header_x - header_width right = chainages[-1] entities: list[dict[str, Any]] = [] @@ -507,7 +445,7 @@ def _long_table_entities( ) ) # 외곽·헤더 구분 세로선 (측점 세로선은 없음) - for seed, x in (("v-left", left), ("v-header", chainages[0]), ("v-right", right)): + for seed, x in (("v-left", left), ("v-header", header_x), ("v-right", right)): entities.append( _line_entity( f"{drawing_id}:ltgrid:{seed}", @@ -637,7 +575,9 @@ def _long_content( if stations: chainages = [float(s["chainage_m"]) * mm_h for s in stations] entities.extend( - _graph_grid_entities(drawing_id, chainages, chainages[0], chainages[-1], datum_m, top_m) + _graph_grid_entities( + drawing_id, chainages, chainages[0] - _AXIS_GAP, chainages[-1], datum_m, top_m + ) ) values = _long_table_values(stations, all_stations, ground_all, longitudinal, interval_m) @@ -657,41 +597,3 @@ def _long_content( ) return entities - - -def build_longitudinal_drawing( - longitudinal: dict[str, Any], drawing_id: str, chunk: dict[str, Any] -) -> dict[str, Any]: - """종단도 청크 하나를 지반선+계획선+그래프 축·격자+측점 테이블로 만든다.""" - entities = _long_content(longitudinal, drawing_id, chunk) - scale = drawing_scale("longitudinal") - # A1 도각: 콘텐츠가 이미 종이 mm라 도각도 실치수(1:1)로 두고 위치만 맞춘다. - # 그림 좌하단 = 작도영역 좌하단 — 장마다 같은 자리에서 시작한다. - bbox = entities_bbox(entities) - entities.extend(long_plan_entities(longitudinal, drawing_id, chunk, bbox)) # 521-2 - if bbox: - entities.extend( - frame_entities( - drawing_id, - bbox, - fit=False, - bottom_left=True, - fields={ - "도면명": "종단면도", - **scale_fields(("H", scale["x"]), ("V", scale["y"])), - }, - ) - ) - - return { - "format": DRAWING_FORMAT, - "entities": entities, - "layers": [ - _layer(GROUND_LAYER_ID, "원지반", locked=True), - _layer(DESIGN_LAYER_ID, "계획선"), - _layer(LONG_GRID_LAYER_ID, "그래프 격자", locked=True), - _layer(LONG_TABLE_LAYER_ID, "측점표"), - _layer(LONG_PLAN_LAYER_ID, "평면 위치도"), - _layer(FRAME_LAYER_ID, "도각", locked=True), - ], - } diff --git a/B07_DesignDetail/B07_DesignDetail_Engine_Cad_LongPlan.py b/B07_DesignDetail/B07_DesignDetail_Engine_Cad_LongPlan.py index d8ba01233..5b7b1886c 100644 --- a/B07_DesignDetail/B07_DesignDetail_Engine_Cad_LongPlan.py +++ b/B07_DesignDetail/B07_DesignDetail_Engine_Cad_LongPlan.py @@ -1,7 +1,8 @@ """B07 종단도 우측 상단 평면 위치도(521-2 · 요구 340-2) — 장마다 작도 영역 우상단 칸 하나. -칸 = 작도 영역 우상단 5 mm 안쪽 150×100 mm(작도 영역 좌하단 = 그림 좌하단 · `frame_entities` -bottom_left). 척도 = 종단 축척 `plan` 칸(None = 자동). 방위표만 도각 층. +칸 = 작도 영역 우상단 5 mm 안쪽 150×100 mm(작도 영역 좌하단 = 장 bbox 좌하단 = 그림 좌하단 +− 50 mm · `LongSheet._sheet_bbox` · `frame_entities` bottom_left). 척도 = 종단 축척 `plan` 칸 +(None = 자동). 방위표만 도각 층. - 자동: 전체 노선을 칸에 맞춤(분모 500 단위 올림) · 전체 노선 가는 선 · 이 장 구간 굵게. - 척도 정함: 이 장 중심 확대 · 이 장 굵게 · 밖 노선 흐린 파선 · 칸 밖은 잘림. 칸 내용 = 노선 · 이 장 시 · 종점 측점 글 · 방위표 · 「평면도 S=1/n」 · 「n/N 장」. @@ -43,7 +44,7 @@ _ROUTE_COLOR = "#7d8590" # 전체 · 밖 노선 — 흐리게 def plan_box(content_bbox: tuple[float, float, float, float]) -> tuple[float, float, float, float]: - """칸 (x0, y0, x1, y1) — 그림 좌표(작도 영역 좌하단 = 그림 좌하단).""" + """칸 (x0, y0, x1, y1) — 그림 좌표(작도 영역 좌하단 = 장 bbox 좌하단).""" usable_w, usable_h = usable_area() x1 = content_bbox[0] + usable_w - _INSET y1 = content_bbox[1] + usable_h - _INSET diff --git a/B07_DesignDetail/B07_DesignDetail_Engine_Cad_LongSheet.py b/B07_DesignDetail/B07_DesignDetail_Engine_Cad_LongSheet.py new file mode 100644 index 000000000..ffd22bb07 --- /dev/null +++ b/B07_DesignDetail/B07_DesignDetail_Engine_Cad_LongSheet.py @@ -0,0 +1,136 @@ +"""B07 종단도 장 — 일반측점 30 간격(600 m) 나눔 · 장 자리(그림 = 작도 영역 좌하단 + 50 mm) · 도각. + +추가 · MC 측점은 세지 않고 구간 안이면 함께 그린다(525-1 · 요구 343-3). 그림 본체는 +`Engine_Cad_Long._long_content`. +""" + +from bisect import bisect_right +from typing import Any + +from B07_DesignDetail.B07_DesignDetail_Engine_Cad import ( + DESIGN_LAYER_ID, + DRAWING_FORMAT, + FRAME_LAYER_ID, + GROUND_LAYER_ID, + LONG_SPLIT_STATION_COUNT, + LONG_TABLE_LAYER_ID, + _layer, + _stations, + infer_station_interval, +) +from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Long import LONG_GRID_LAYER_ID, _long_content +from B07_DesignDetail.B07_DesignDetail_Engine_Cad_LongPlan import ( + LONG_PLAN_LAYER_ID, + long_plan_entities, + plan_box_overlaps, +) +from B07_DesignDetail.B07_DesignDetail_Engine_Template import ( + drawing_scale, + entities_bbox, + frame_entities, + scale_fields, + usable_area, +) + +_SHEET_SHIFT = 50.0 # 그림 = 작도 영역 좌하단에서 오른쪽 · 위로(종이 mm · 525-1) + + +def _sheet_bbox( + bbox: tuple[float, float, float, float] | None, +) -> tuple[float, float, float, float] | None: + """그림 bbox 를 좌 · 하로 넓힌 장 기준 — 도각 · 평면 칸 · 넘침 판정이 이 좌하단에 맞춤.""" + if bbox is None: + return None + return (bbox[0] - _SHEET_SHIFT, bbox[1] - _SHEET_SHIFT, bbox[2], bbox[3]) + + +def _span(stations: list[dict[str, Any]], index: int, take: int) -> dict[str, float]: + return { + "start_m": float(stations[index]["chainage_m"]), + "end_m": float(stations[index + take - 1]["chainage_m"]), + } + + +def _fits_sheet(longitudinal: dict[str, Any], span: dict[str, float]) -> bool: + """이 구간을 고정 축척으로 그렸을 때 A1 작도 영역에 들고 평면 위치도 칸을 안 덮는가.""" + content = _long_content(longitudinal, "probe", span) + bbox = _sheet_bbox(entities_bbox(content)) + if bbox is None: + return True + usable_w, usable_h = usable_area() + fits = bbox[2] - bbox[0] <= usable_w + 1e-6 and bbox[3] - bbox[1] <= usable_h + 1e-6 + return fits and not plan_box_overlaps(longitudinal, content, bbox) + + +def longitudinal_chunks(longitudinal: dict[str, Any]) -> list[dict[str, Any]]: + """일반측점 30 간격(20 m 면 600 m) 단위 분할 목록. 각 항목: {id, label, start_m, end_m}. + + 추가 · MC 측점은 세지 않고 구간 안이면 함께 그린다(525-1 · 요구 343-3). + + 한 장이면 단일 도면(id="longitudinal", 기존 manifest 호환). 여러 장이면 + 경계 측점 1개를 중복시켜 도면 간 선이 이어지게 한다(납품 도면 관례). + 축척(H 1/1,000 · V 1/200)은 모든 장이 같다 — 작도 영역을 넘으면 측점을 줄여 + 장을 더 나눌 뿐 줄이지 않는다. + """ + stations = sorted(_stations(longitudinal), key=lambda s: float(s["chainage_m"])) + if not stations: + return [{"id": "longitudinal", "label": "종단도", "start_m": None, "end_m": None}] + chainages = [float(s["chainage_m"]) for s in stations] + length_m = LONG_SPLIT_STATION_COUNT * infer_station_interval(stations) + spans: list[dict[str, float]] = [] + index = 0 + while True: + take = max(2, bisect_right(chainages, chainages[index] + length_m + 1e-6) - index) + take = min(take, len(stations) - index) + # ponytail: 한 측점씩 줄이며 다시 잰다 — 넘치는 장에서만 돎 + while take > 2 and not _fits_sheet(longitudinal, _span(stations, index, take)): + take -= 1 + spans.append(_span(stations, index, take)) + if index + take >= len(stations): + break + index += take - 1 # 경계 1측점 중복 + if len(spans) == 1: + return [{"id": "longitudinal", "label": "종단도", "number": 1, "total": 1, **spans[0]}] + return [ + {"id": f"longitudinal_{number}", "label": f"종단도({number})", "number": number, **span} + | {"total": len(spans)} + for number, span in enumerate(spans, start=1) + ] + + +def build_longitudinal_drawing( + longitudinal: dict[str, Any], drawing_id: str, chunk: dict[str, Any] +) -> dict[str, Any]: + """종단도 청크 하나를 지반선+계획선+그래프 축·격자+측점 테이블로 만든다.""" + entities = _long_content(longitudinal, drawing_id, chunk) + scale = drawing_scale("longitudinal") + # A1 도각: 콘텐츠가 이미 종이 mm라 도각도 실치수(1:1)로 두고 위치만 맞춘다. + # 그림 좌하단 = 작도영역 좌하단 + (50, 50) mm — 장마다 같은 자리에서 시작한다. + bbox = _sheet_bbox(entities_bbox(entities)) + entities.extend(long_plan_entities(longitudinal, drawing_id, chunk, bbox)) # 521-2 + if bbox: + entities.extend( + frame_entities( + drawing_id, + bbox, + fit=False, + bottom_left=True, + fields={ + "도면명": "종단면도", + **scale_fields(("H", scale["x"]), ("V", scale["y"])), + }, + ) + ) + + return { + "format": DRAWING_FORMAT, + "entities": entities, + "layers": [ + _layer(GROUND_LAYER_ID, "원지반", locked=True), + _layer(DESIGN_LAYER_ID, "계획선"), + _layer(LONG_GRID_LAYER_ID, "그래프 격자", locked=True), + _layer(LONG_TABLE_LAYER_ID, "측점표"), + _layer(LONG_PLAN_LAYER_ID, "평면 위치도"), + _layer(FRAME_LAYER_ID, "도각", locked=True), + ], + } diff --git a/B07_DesignDetail/B07_DesignDetail_Router_Support.py b/B07_DesignDetail/B07_DesignDetail_Router_Support.py index 7231becdb..b77ce4c5c 100644 --- a/B07_DesignDetail/B07_DesignDetail_Router_Support.py +++ b/B07_DesignDetail/B07_DesignDetail_Router_Support.py @@ -40,7 +40,7 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Development import ( development_list, read_development, ) -from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Long import ( +from B07_DesignDetail.B07_DesignDetail_Engine_Cad_LongSheet import ( build_longitudinal_drawing, longitudinal_chunks, ) diff --git a/B07_DesignDetail/B07_DesignDetail_Router_Support_Io.py b/B07_DesignDetail/B07_DesignDetail_Router_Support_Io.py index e6e3b7494..836e33376 100644 --- a/B07_DesignDetail/B07_DesignDetail_Router_Support_Io.py +++ b/B07_DesignDetail/B07_DesignDetail_Router_Support_Io.py @@ -18,6 +18,8 @@ _STAGE_DIR = "B07_DesignDetail" _CROSS_ID = re.compile(r"^cross_(\d+)m$") _LONG_ID = re.compile(r"^longitudinal(?:_(\d+))?$") +# 종단 장 규칙 — 장 나눔 · 자리가 바뀌면 올림(옛 종단 확정본을 한 번 풂 · 521-2 칸 · 525-1 600 m) +LONG_SHEET_RULE = 2 # 노선 전장에 한 장씩만 나오는 도면 — 라우터가 원본 자료를 따로 실어 넘긴다. MASS_HAUL_ID = "mass_haul" COVER_ID = "cover" @@ -83,28 +85,16 @@ def _write_manifest(project_root: Path, manifest: dict[str, Any]) -> None: def release_old_long_sheets(project_root: Path) -> None: - """평면 위치도(521-2) 앞 확정 종단 저장본 — 종단만 모두 한 번 풂(칸 · 장 나눔이 바뀜). + """종단 장 규칙이 바뀌기 전 확정 저장본 — 종단만 모두 한 번 풂(칸 · 장 나눔 · 자리가 바뀜). - 풀린 장의 저장본은 `_write_manifest` 가 걷으니 다음부터는 새 저장본(칸 층 있음)만 남음. + 매니페스트 `long_sheet_rule` 이 지금 규칙과 다르면 풀고 적음 — 다음부터는 새 규칙 + 저장본만 남음. 풀린 장의 저장본은 `_write_manifest` 가 걷음. """ - # 엔진 → 양식 → 이 모듈 차례라 위에서 부르면 돌고 돎 - from B07_DesignDetail.B07_DesignDetail_Engine_Cad_LongPlan import LONG_PLAN_LAYER_ID - manifest = _read_manifest(project_root) - confirmed = [ - (drawing_id, entry) - for drawing_id, entry in manifest["drawings"].items() - if _LONG_ID.fullmatch(drawing_id) and entry.get("confirmed") - ] - if not confirmed: + if manifest.get("long_sheet_rule") == LONG_SHEET_RULE: return - # ponytail: 첫 장만 봄 — 옛 장은 한꺼번에 풀리므로 새 장과 섞이지 않음 - saved = _design_root(project_root) / "drawings" / f"{confirmed[0][0]}.json" - try: - if f'"{LONG_PLAN_LAYER_ID}"' in saved.read_text(encoding="utf-8"): - return - except OSError: - pass - for _drawing_id, entry in confirmed: - entry["confirmed"] = False + for drawing_id, entry in manifest["drawings"].items(): + if _LONG_ID.fullmatch(drawing_id): + entry["confirmed"] = False + manifest["long_sheet_rule"] = LONG_SHEET_RULE _write_manifest(project_root, manifest) diff --git a/resources/tester/test_280_5_b07_drawing_scales.py b/resources/tester/test_280_5_b07_drawing_scales.py index 33db4e526..558dc5d2e 100644 --- a/resources/tester/test_280_5_b07_drawing_scales.py +++ b/resources/tester/test_280_5_b07_drawing_scales.py @@ -19,7 +19,7 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad import build_cross_drawing from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Development_Draw import ( build_development_drawing, ) -from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Long import ( +from B07_DesignDetail.B07_DesignDetail_Engine_Cad_LongSheet import ( build_longitudinal_drawing, longitudinal_chunks, ) diff --git a/resources/tester/test_b07_long_sheet.py b/resources/tester/test_b07_long_sheet.py index 032141a8f..de7f936bf 100644 --- a/resources/tester/test_b07_long_sheet.py +++ b/resources/tester/test_b07_long_sheet.py @@ -1,29 +1,34 @@ -"""B07 종단도 — 장마다 그림 좌하단 = 작도영역 좌하단 · 고정 축척 · 넘치면 장을 더 나눔 (59-3). +"""B07 종단도 — 장마다 그림 좌하단 = 작도영역 좌하단 + 50 mm · 고정 축척 · 넘치면 장 더 나눔 (59-3). + +장 = 일반측점 30 간격(600 m) · 추가 · MC 측점은 안 셈 · Y축 ↔ 첫 측점 10 mm(525-1). 우측 상단 평면 위치도(521-2) — 칸 자리 · 그래프와 안 겹침 · 장마다 강조 구간 · 척도. """ import json import math +from uuid import uuid5 import pytest -from B07_DesignDetail.B07_DesignDetail_Engine_Cad import FRAME_LAYER_ID -from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Long import ( - build_longitudinal_drawing, - longitudinal_chunks, -) +from B07_DesignDetail.B07_DesignDetail_Engine_Cad import _ENTITY_NS, FRAME_LAYER_ID from B07_DesignDetail.B07_DesignDetail_Engine_Cad_LongPlan import ( LONG_PLAN_LAYER_ID, plan_box_overlaps, ) +from B07_DesignDetail.B07_DesignDetail_Engine_Cad_LongSheet import ( + build_longitudinal_drawing, + longitudinal_chunks, +) from B07_DesignDetail.B07_DesignDetail_Engine_Template import ( drawing_scales_path, entities_bbox, + frame_guide_area, usable_area, use_project_templates, ) from B07_DesignDetail.B07_DesignDetail_Router_Support_Io import ( + LONG_SHEET_RULE, _read_manifest, _write_manifest, release_old_long_sheets, @@ -121,13 +126,14 @@ def test_평면_칸_작도영역_우상단_안_겹침_장마다_강조() -> None usable_w, usable_h = usable_area() lows = [] for number, sheet in enumerate(sheets, start=1): - x0, y0 = sheet["bbox"][0], sheet["bbox"][1] + # 작도 영역 좌하단 = 그림 좌하단 − 50 mm(525-1) + x0, y0 = sheet["bbox"][0] - 50.0, sheet["bbox"][1] - 50.0 box = entities_bbox(sheet["plan"]) # 칸 = 작도 영역 우상단 5 mm 안쪽 150×100 mm assert box == pytest.approx( (x0 + usable_w - 155.0, y0 + usable_h - 105.0, x0 + usable_w - 5.0, y0 + usable_h - 5.0) ) - assert not plan_box_overlaps(longitudinal, sheet["graph"], sheet["bbox"]) + assert not plan_box_overlaps(longitudinal, sheet["graph"], (x0, y0, *sheet["bbox"][2:])) assert f"{number}/{len(sheets)} 장" in sheet["texts"] assert any(text.startswith("평면도 S=1/") for text in sheet["texts"]) assert sheet["bold"] # 이 장 구간 굵게 @@ -142,7 +148,8 @@ def test_칸을_덮으면_측점_줄여_장_더_나눔() -> None: with_box = _with_xy(_longitudinal(60, 25.0, 0.1)) assert longitudinal_chunks(with_box)[0]["end_m"] < plain[0]["end_m"] for sheet in _plan_sheets(with_box): - assert not plan_box_overlaps(with_box, sheet["graph"], sheet["bbox"]) + x0, y0, x1, y1 = sheet["bbox"] + assert not plan_box_overlaps(with_box, sheet["graph"], (x0 - 50.0, y0 - 50.0, x1, y1)) def test_좌표_없으면_칸_없음() -> None: @@ -168,32 +175,77 @@ def test_척도_정하면_이_장_확대_밖_흐린_파선(tmp_path) -> None: assert (min(xs) + max(xs)) / 2.0 == pytest.approx((box[0] + box[2]) / 2.0, abs=1.0) -def test_칸_앞_확정_종단만_한_번_풂(tmp_path) -> None: +def test_장_규칙_앞_확정_종단만_한_번_풂(tmp_path) -> None: drawings = tmp_path / "B07_DesignDetail" / "drawings" drawings.mkdir(parents=True) old = {"kind": "longitudinal", "confirmed": True, "file": "x"} _write_manifest( tmp_path, { + "long_sheet_rule": LONG_SHEET_RULE - 1, # 옛 규칙(521-2 칸까지) "drawings": { "longitudinal_1": dict(old), "longitudinal_2": dict(old), "cross_00020m": {"kind": "cross", "confirmed": True, "file": "x"}, - } + }, }, ) for name in ("longitudinal_1", "longitudinal_2", "cross_00020m"): (drawings / f"{name}.json").write_text('{"layers": []}', encoding="utf-8") release_old_long_sheets(tmp_path) - after = _read_manifest(tmp_path)["drawings"] + manifest = _read_manifest(tmp_path) + after = manifest["drawings"] assert not after["longitudinal_1"]["confirmed"] and not after["longitudinal_2"]["confirmed"] assert after["cross_00020m"]["confirmed"] assert not (drawings / "longitudinal_1.json").exists() # 옛 저장본 걷힘 - # 새 확정본(칸 층 있음)은 그대로 + assert manifest["long_sheet_rule"] == LONG_SHEET_RULE + # 새 규칙 뒤 확정본은 그대로 after["longitudinal_1"] = dict(old) - _write_manifest(tmp_path, {"drawings": after}) - (drawings / "longitudinal_1.json").write_text( - json.dumps({"layers": [{"id": LONG_PLAN_LAYER_ID}]}), encoding="utf-8" - ) + _write_manifest(tmp_path, manifest) release_old_long_sheets(tmp_path) assert _read_manifest(tmp_path)["drawings"]["longitudinal_1"]["confirmed"] + + +# ── 일반측점 30 간격 = 600 m · 축 여유 10 mm · 그림 자리 +50 mm(525-1 · 요구 343-3) ───────── +def _with_extras(count: int) -> dict: + """20 m 일반측점 + 추가(irregular) · MC(curve) 측점을 섞은 종단.""" + longitudinal = _longitudinal(count, 20.0, 0.0) + extras = [{"chainage_m": x + 7.0, "kind": "irregular"} for x in range(0, count * 20 - 20, 60)] + extras += [{"chainage_m": x + 13.0, "kind": "curve"} for x in range(0, count * 20 - 20, 100)] + longitudinal["stations"] = sorted( + longitudinal["stations"] + extras, key=lambda s: s["chainage_m"] + ) + return longitudinal + + +def test_추가_MC_측점은_안_세고_600m_로_나눔() -> None: + longitudinal = _with_extras(70) # 0 ~ 1,380 m + chunks = longitudinal_chunks(longitudinal) + assert [(c["start_m"], c["end_m"]) for c in chunks] == [ + (0.0, 600.0), + (600.0, 1200.0), + (1200.0, 1380.0), + ] + regular = [s["chainage_m"] for s in longitudinal["stations"] if "kind" not in s] + for chunk in chunks[:-1]: + inside = [x for x in regular if chunk["start_m"] < x <= chunk["end_m"]] + assert len(inside) == 30 # 경계 측점은 앞 장과 나눠 가짐 + + +def test_Y축_첫_측점_사이_10mm_그림_자리_50mm() -> None: + longitudinal = _longitudinal(40, 20.0, 0.05) + chunk = longitudinal_chunks(longitudinal)[0] + entities = build_longitudinal_drawing(longitudinal, "t", chunk)["entities"] + by_id = {e["id"]: e for e in entities} + + def start_x(seed: str) -> float: + return by_id[str(uuid5(_ENTITY_NS, seed))]["shapeData"]["startPoint"]["x"] + + assert start_x("t:lgv0") - start_x("t:lgy") == pytest.approx(10.0) + assert start_x("t:ltgrid:v-header") == pytest.approx(start_x("t:lgy")) + content = entities_bbox( + [e for e in entities if e["layerId"] not in (FRAME_LAYER_ID, LONG_PLAN_LAYER_ID)] + ) + area = frame_guide_area(entities) + assert content[0] - area[0] == pytest.approx(50.0) + assert content[1] - area[1] == pytest.approx(50.0)