252 lines
11 KiB
Python
252 lines
11 KiB
Python
"""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 _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,
|
||
)
|
||
|
||
|
||
def _longitudinal(count: int, interval: float, slope: float) -> dict:
|
||
xs = [index * interval for index in range(count)]
|
||
return {
|
||
"samples": [{"valid": True, "chainage_m": x, "elevation_m": 100.0 + slope * x} for x in xs],
|
||
"stations": [{"chainage_m": x} for x in xs],
|
||
"design_profiles": [
|
||
{"samples": [{"chainage_m": x, "elevation_m": 100.5 + slope * x} for x in xs]}
|
||
],
|
||
}
|
||
|
||
|
||
def _sheets(longitudinal: dict) -> list[tuple[tuple, tuple]]:
|
||
result = []
|
||
for chunk in longitudinal_chunks(longitudinal):
|
||
entities = build_longitudinal_drawing(longitudinal, chunk["id"], chunk)["entities"]
|
||
content = entities_bbox([e for e in entities if e.get("layerId") != FRAME_LAYER_ID])
|
||
frame = entities_bbox([e for e in entities if e.get("layerId") == FRAME_LAYER_ID])
|
||
result.append((content, frame))
|
||
return result
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
("count", "interval", "slope"),
|
||
[(66, 20.0, 0.05), (40, 40.0, 0.0), (40, 20.0, 0.25)],
|
||
)
|
||
def test_every_sheet_fits_and_starts_at_same_corner(count, interval, slope) -> None:
|
||
longitudinal = _longitudinal(count, interval, slope)
|
||
sheets = _sheets(longitudinal)
|
||
usable_w, usable_h = usable_area()
|
||
corners = set()
|
||
for content, frame in sheets:
|
||
assert content[2] - content[0] <= usable_w + 1e-6
|
||
assert content[3] - content[1] <= usable_h + 1e-6
|
||
corners.add((round(content[0] - frame[0], 3), round(content[1] - frame[1], 3)))
|
||
assert len(corners) == 1
|
||
chunks = longitudinal_chunks(longitudinal)
|
||
# 이어지는 장은 경계 측점 하나를 나눠 갖고 끝까지 덮는다.
|
||
assert chunks[0]["start_m"] == 0.0
|
||
assert chunks[-1]["end_m"] == (count - 1) * interval
|
||
for before, after in zip(chunks, chunks[1:]):
|
||
assert before["end_m"] == after["start_m"]
|
||
|
||
|
||
def test_wide_interval_splits_more() -> None:
|
||
# 40 m 간격 30측점 = 1,160 mm > 작도영역 폭 → 30개보다 적게 담는다
|
||
chunks = longitudinal_chunks(_longitudinal(40, 40.0, 0.0))
|
||
assert len(chunks) > 2
|
||
|
||
|
||
# ── 우측 상단 평면 위치도(521-2 · 요구 340-2) ─────────────────────────────────
|
||
def _with_xy(longitudinal: dict) -> dict:
|
||
for station in longitudinal["stations"]:
|
||
chainage = station["chainage_m"]
|
||
station["center_x"] = 200000.0 + chainage
|
||
station["center_y"] = 500000.0 + 60.0 * math.sin(chainage / 150.0)
|
||
return longitudinal
|
||
|
||
|
||
def _plan_sheets(longitudinal: dict) -> list[dict]:
|
||
"""장마다 그래프 bbox · 칸 엔티티 · 굵은(이 장) 선 꼭짓점 · 글."""
|
||
sheets = []
|
||
for chunk in longitudinal_chunks(longitudinal):
|
||
entities = build_longitudinal_drawing(longitudinal, chunk["id"], chunk)["entities"]
|
||
graph = [e for e in entities if e["layerId"] not in (FRAME_LAYER_ID, LONG_PLAN_LAYER_ID)]
|
||
plan = [e for e in entities if e["layerId"] == LONG_PLAN_LAYER_ID]
|
||
bold = [
|
||
(child["shapeData"][key]["x"], child["shapeData"][key]["y"])
|
||
for entity in plan
|
||
for child in entity.get("children") or []
|
||
if child["lineWidth"] == 3
|
||
for key in ("startPoint", "endPoint")
|
||
]
|
||
texts = [e["shapeData"]["label"] for e in plan if e["type"] == "Text"]
|
||
sheets.append(
|
||
{
|
||
"graph": graph,
|
||
"bbox": entities_bbox(graph),
|
||
"plan": plan,
|
||
"bold": bold,
|
||
"texts": texts,
|
||
}
|
||
)
|
||
return sheets
|
||
|
||
|
||
def test_평면_칸_작도영역_우상단_안_겹침_장마다_강조() -> None:
|
||
longitudinal = _with_xy(_longitudinal(66, 20.0, 0.05))
|
||
sheets = _plan_sheets(longitudinal)
|
||
usable_w, usable_h = usable_area()
|
||
lows = []
|
||
for number, sheet in enumerate(sheets, start=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"], (x0, y0, *sheet["bbox"][2:]))
|
||
assert f"{number}/{len(sheets)} 장" in sheet["texts"]
|
||
assert "평면도 S=1/5,000" in sheet["texts"] # 기본 지정값(자동 없음 · 523-1)
|
||
assert sheet["bold"] # 이 장 구간 굵게
|
||
lows.append(min(x for x, _y in sheet["bold"]) - box[0])
|
||
# 지정 축척 = 장마다 이 장 구간을 칸 가운데로 — 굵은 구간 왼끝이 칸 안
|
||
assert all(0.0 <= low <= 150.0 for low in lows)
|
||
|
||
|
||
def test_칸을_덮으면_측점_줄여_장_더_나눔() -> None:
|
||
# 25 m 간격 30 측점 = 725 mm(폭 안) · 오르막 그래프가 우상단 칸을 덮음
|
||
plain = longitudinal_chunks(_longitudinal(60, 25.0, 0.1))
|
||
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):
|
||
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:
|
||
assert all(not sheet["plan"] for sheet in _plan_sheets(_longitudinal(40, 20.0, 0.0)))
|
||
|
||
|
||
def test_척도_정하면_이_장_확대_밖_흐린_파선(tmp_path) -> None:
|
||
longitudinal = _with_xy(_longitudinal(66, 20.0, 0.05))
|
||
use_project_templates(tmp_path)
|
||
try:
|
||
path = drawing_scales_path(tmp_path)
|
||
path.parent.mkdir(parents=True, exist_ok=True)
|
||
path.write_text(json.dumps({"longitudinal": {"plan": 3000}}), encoding="utf-8")
|
||
sheets = _plan_sheets(longitudinal)
|
||
finally:
|
||
use_project_templates(None)
|
||
for sheet in sheets:
|
||
assert "평면도 S=1/3,000" in sheet["texts"]
|
||
assert any(entity.get("lineDash") for entity in sheet["plan"]) # 밖 노선 파선
|
||
box = entities_bbox(sheet["plan"])
|
||
xs = [x for x, _y in sheet["bold"]]
|
||
# 이 장 중심 = 칸 노선 자리 가운데
|
||
assert (min(xs) + max(xs)) / 2.0 == pytest.approx((box[0] + box[2]) / 2.0, abs=1.0)
|
||
|
||
|
||
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)
|
||
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, 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)
|