Files
Aislo/resources/tester/test_b07_long_sheet.py
T
eomsangdonandClaude Opus 5.5 1398625d2d 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>
2026-10-25 14:25:57 +09:00

252 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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 any(text.startswith("평면도 S=1/") for text in sheet["texts"])
assert sheet["bold"] # 이 장 구간 굵게
lows.append(min(x for x, _y in sheet["bold"]) - box[0])
# 자동 = 모든 장이 같은 축척 · 같은 자리 — 굵은 구간만 장마다 오른쪽으로 옮겨 감
assert lows == sorted(lows) and len(set(lows)) == len(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)