feat(B07): 종단도 장 = 일반측점 30 간격 600 m · Y축 여유 10 mm · 그림 자리 +50 mm (525-1 · 요구 343-3)

- 장 나눔 = 추가 · MC 측점 안 세고 일반측점 30 간격(20 m 면 600 m) · 경계 측점 나눠 가짐 · 넘치면 줄이는 규칙 그대로
- Y축 · 측점표 머리 = 첫 측점 왼쪽 10 mm
- 그림 = 작도 영역 좌하단에서 오른쪽 50 · 위 50 mm(도각 · 평면 칸 · 넘침 판정이 같은 장 bbox)
- 장 나눔 · 도각을 Engine_Cad_LongSheet.py 로 뗌(Engine_Cad_Long.py 700줄 한도)
- 옛 종단 확정본 풂 = 칸 층 찾기 → 매니페스트 long_sheet_rule(2) 비교 — 521-2 뒤 확정본도 자리가 바뀌어 한 번 더 풂 · 낡은 시험(칸 층 기준) 고침

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 14:25:57 +09:00
co-authored by Claude Opus 5.5
parent 7e2bc3a8e7
commit 1398625d2d
8 changed files with 231 additions and 150 deletions
@@ -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")
@@ -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),
],
}
@@ -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
@@ -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),
],
}
@@ -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,
)
@@ -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)
@@ -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,
)
+69 -17
View File
@@ -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)