Files
Aislo/resources/tester/test_280_5_b07_drawing_scales.py
T

319 lines
14 KiB
Python

"""485-1 B07 도면 X · Y 축척(요구 280-5) — 프로젝트마다 `drawing_scales.json`.
값 없음 = 지금 결과 그대로 · 종단 X 바꿈 = 장 수 · 도각 축척 글 · 횡단 X ≠ Y 덧그림 자리 ·
토적 · 전개도 · 범위 밖 거절 · 바꾼 종류만 저장본 풂.
"""
import json
import shutil
import subprocess
from pathlib import Path
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from B07_DesignDetail import B07_DesignDetail_Engine_Cad_Development as dev
from B07_DesignDetail import B07_DesignDetail_Router_Support_Scale as scale_router
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 (
build_longitudinal_drawing,
longitudinal_chunks,
)
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_MassHaul import build_mass_haul_drawing
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Sheet import build_cross_sheet, plan_cross_sheets
from B07_DesignDetail.B07_DesignDetail_Engine_Template import (
DRAWING_SCALE_DEFAULTS,
drawing_scales_path,
entities_bbox,
read_drawing_scales,
use_project_templates,
)
from B07_DesignDetail.B07_DesignDetail_Router_Support_Io import _read_manifest, _write_manifest
from B07_DesignDetail.B07_DesignDetail_Router_Support_Scale import (
checked_scales,
saved_counts,
write_drawing_scales,
)
ROOT = Path(__file__).resolve().parents[2]
FIXTURE = Path(__file__).parent / "fixtures" / "b07_cross_overlay_sections.json"
NODE_MODULES = ROOT / "config" / "node_modules"
EXTENT_BUNDLE = ROOT / "config" / "b07_extent_node" / "B07_DesignDetail_Cross_Extent_Node.js"
SOURCE = {
"chainage_m": 0.0,
"samples": [
{"valid": True, "offset_m": float(x), "elevation_m": 100.0 + 0.2 * x}
for x in range(-10, 11)
],
}
DESIGN_LINE = [
{"offset_m": -4.0, "elevation_m": 99.0},
{"offset_m": 0.0, "elevation_m": 98.5},
{"offset_m": 4.0, "elevation_m": 99.5},
]
@pytest.fixture
def project(tmp_path: Path):
"""빈 프로젝트 폴더를 이 요청 프로젝트로 — 끝나면 시스템 양식으로(문맥 변수 새지 않게)."""
use_project_templates(tmp_path)
yield tmp_path
use_project_templates(None)
def _set(project: Path, scales: dict) -> None:
path = drawing_scales_path(project)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(scales), encoding="utf-8")
def _longitudinal(count: int = 66, interval: float = 20.0) -> dict:
xs = [index * interval for index in range(count)]
return {
"samples": [{"valid": True, "chainage_m": x, "elevation_m": 100.0 + 0.05 * x} for x in xs],
"stations": [{"chainage_m": x} for x in xs],
"design_profiles": [
{"samples": [{"chainage_m": x, "elevation_m": 100.5 + 0.05 * x} for x in xs]}
],
}
def _texts(drawing: dict) -> list[str]:
out: list[str] = []
def walk(entity: dict) -> None:
label = (entity.get("shapeData") or {}).get("label")
if isinstance(label, str):
out.append(label)
for child in entity.get("children") or []:
walk(child)
for entity in drawing["entities"]:
walk(entity)
return out
def _ground_xy(drawing: dict) -> list[tuple[float, float]]:
ground = next(e for e in drawing["entities"] if e["layerId"] == "b08-ground")
return [
(child["shapeData"]["startPoint"]["x"], child["shapeData"]["startPoint"]["y"])
for child in ground["children"]
]
def test_값_없음_기본_그대로(project: Path) -> None:
assert read_drawing_scales(project) == DRAWING_SCALE_DEFAULTS
assert read_drawing_scales(None) == DRAWING_SCALE_DEFAULTS
with_root = build_cross_drawing(SOURCE, "cross_00000m", DESIGN_LINE, None, None, "No.0")
longitudinal = _longitudinal()
chunks = longitudinal_chunks(longitudinal)
use_project_templates(None)
without = build_cross_drawing(SOURCE, "cross_00000m", DESIGN_LINE, None, None, "No.0")
assert with_root == without
assert longitudinal_chunks(longitudinal) == chunks
# 깨진 파일 · 어긋난 값 = 기본
use_project_templates(project)
drawing_scales_path(project).parent.mkdir(parents=True, exist_ok=True)
drawing_scales_path(project).write_text("{깨짐", encoding="utf-8")
assert read_drawing_scales(project) == DRAWING_SCALE_DEFAULTS
_set(project, {"cross": {"x": -5, "y": True}, "longitudinal": "x"})
assert read_drawing_scales(project) == DRAWING_SCALE_DEFAULTS
def test_종단_X_바꿈_장_수_그림_폭_도각_글(project: Path) -> None:
longitudinal = _longitudinal()
before = longitudinal_chunks(longitudinal)
first = build_longitudinal_drawing(longitudinal, before[0]["id"], before[0])
_set(project, {"longitudinal": {"x": 500, "y": 200}})
after = longitudinal_chunks(longitudinal)
# 1/500 = 1 m 당 2 mm — 한 장에 담는 측점이 줄어 장이 늘어남 · id 도 다시 매김
assert len(after) > len(before)
assert [c["id"] for c in after] == [f"longitudinal_{n}" for n in range(1, len(after) + 1)]
chunk = {**before[0], "id": "probe"}
wide = build_longitudinal_drawing(longitudinal, "probe", chunk)
def width(drawing: dict) -> float:
ground = [e for e in drawing["entities"] if e["layerId"] == "b08-ground"]
box = entities_bbox(ground)
return box[2] - box[0]
assert width(wide) == pytest.approx(2.0 * width(first))
assert any("500(H) · 200(V)" in text for text in _texts(wide))
assert any("1000(H) · 200(V)" in text for text in _texts(first))
def test_횡단_X_다른_Y_덧그림_자리_도각_글(project: Path) -> None:
base = build_cross_drawing(SOURCE, "cross_00000m", DESIGN_LINE, None, None, "No.0")
_set(project, {"cross": {"x": 100, "y": 50}})
tall = build_cross_drawing(SOURCE, "cross_00000m", DESIGN_LINE, None, None, "No.0")
placement = tall["cross_placements"][0]
assert (placement["mm_per_m_x"], placement["mm_per_m_y"]) == (10.0, 20.0)
assert placement["mm_per_m"] == 10.0 # 옛 칸 = 가로
assert base["cross_placements"][0]["mm_per_m_y"] == 10.0
(bx0, by0), (bx1, by1) = _ground_xy(base)[0], _ground_xy(base)[-1]
(tx0, ty0), (tx1, ty1) = _ground_xy(tall)[0], _ground_xy(tall)[-1]
assert tx1 - tx0 == pytest.approx(bx1 - bx0) # 가로 그대로
assert ty1 - ty0 == pytest.approx(2.0 * (by1 - by0)) # 세로 2 배
# 덧그림 식 = 서버가 그린 지반점과 같은 자리(TS `placeStructureEntities` 와 같은 식)
for sample in SOURCE["samples"][8:13]: # 그리는 창 안 측점(오프셋 -2 ~ 2 m)
x = sample["offset_m"] * placement["mm_per_m_x"] + placement["ox"]
y = (sample["elevation_m"] - placement["dy"]) * placement["mm_per_m_y"] + placement["oy"]
assert any((x, y) == pytest.approx(point) for point in _ground_xy(tall))
sheet = plan_cross_sheets([0.0])[0]
section = {"chainage": 0.0, "source": SOURCE, "design_line": DESIGN_LINE, "title": "No.0"}
texts = _texts(build_cross_sheet(sheet, [section]))
assert any("100(H) · 50(V)" in text for text in texts)
_set(project, {"cross": {"x": 200, "y": 200}})
texts = _texts(build_cross_sheet(sheet, [section]))
assert any(text.strip().endswith("200") and "(H)" not in text for text in texts)
def test_덧그림_TS_가_가로_세로_칸을_씀() -> None:
overlay = (ROOT / "B07_DesignDetail" / "B07_DesignDetail_Cross_Overlay.ts").read_text(
encoding="utf-8"
)
assert "placement.mm_per_m_x ?? placement.mm_per_m" in overlay
assert "placement.mm_per_m_y ?? placement.mm_per_m" in overlay
node = (ROOT / "B07_DesignDetail" / "B07_DesignDetail_Cross_Extent_Node.ts").read_text(
encoding="utf-8"
)
assert "input.mm_per_m_x ?? MM_PER_M" in node and "input.mm_per_m_y ?? MM_PER_M" in node
@pytest.mark.skipif(
shutil.which("node") is None or not EXTENT_BUNDLE.is_file(), reason="node · 번들 없음"
)
def test_덧그림_범위_Node_축척을_받음(tmp_path: Path) -> None:
detail = json.loads(FIXTURE.read_text(encoding="utf-8"))
def extents(mm_x: float, mm_y: float) -> dict:
source, target = tmp_path / "in.json", tmp_path / "out.json"
payload = {"detail": detail, "mm_per_m_x": mm_x, "mm_per_m_y": mm_y}
source.write_text(json.dumps(payload), encoding="utf-8")
subprocess.run(["node", str(EXTENT_BUNDLE), str(source), str(target)], check=True)
rows = json.loads(target.read_text(encoding="utf-8"))["extents"]
return {row["chainage_m"]: row for row in rows}
base, tall = extents(10.0, 10.0), extents(10.0, 20.0)
assert base and base.keys() == tall.keys()
for chainage, row in base.items():
# 가로 축척 같음 = 가로 범위(m) 같음 · 세로 축척만 바뀜
assert tall[chainage]["x_min"] == pytest.approx(row["x_min"])
assert tall[chainage]["x_max"] == pytest.approx(row["x_max"])
LONGITUDINAL = {"stations": [{"chainage_m": x} for x in (0.0, 20.0, 40.0)]}
MASS_HAUL = {
"points": [
{"chainage_m": 0.0, "cumulative_volume_m3": 0.0},
{"chainage_m": 20.0, "cumulative_volume_m3": 1000.0},
{"chainage_m": 40.0, "cumulative_volume_m3": 400.0},
]
}
def test_토적_가로_세로(project: Path) -> None:
auto = build_mass_haul_drawing(LONGITUDINAL, MASS_HAUL, "mass_haul")
assert "SCALE H=1:500 V=1:50,000" in _texts(auto) # 40 m = 자동 1/500
_set(project, {"mass_haul": {"x": 1000, "y": 100000}})
fixed = build_mass_haul_drawing(LONGITUDINAL, MASS_HAUL, "mass_haul")
assert "SCALE H=1:1,000 V=1:100,000" in _texts(fixed)
curve = next(e for e in fixed["entities"] if e["layerId"] == "b08-masshaul-curve")
end = curve["children"][0]["shapeData"]["endPoint"]
assert (end["x"], end["y"]) == pytest.approx((20.0, 10.0)) # 20 m · 1,000 ㎥ / 100 ㎥/mm
def test_전개도_가로_세로(project: Path) -> None:
rows = [dev._row("soil", "돌망태", i * 20.0, i * 20.0 + 6, 2.0, str(i)) for i in range(3)]
sheet = dev.development_sheets(rows)[0]
auto = build_development_drawing(sheet["id"], sheet, 20.0)
assert any(text.startswith("S=1:") for text in _texts(auto))
_set(project, {"development": {"x": 200, "y": 100}})
fixed = build_development_drawing(sheet["id"], sheet, 20.0)
assert "H=1:200 V=1:100" in _texts(fixed)
boxes = [e for e in fixed["entities"] if e["layerId"] == "b07-dev-box"]
box = entities_bbox(boxes[:1])
assert (box[2] - box[0], box[3] - box[1]) == pytest.approx((6 * 5.0, 2.0 * 10.0))
assert any("200(H) · 100(V)" in text for text in _texts(fixed))
def test_범위_검사() -> None:
assert checked_scales({"cross": {"x": 100, "y": None}}) == {"cross": {"x": 100, "y": None}}
for bad in (
{"plan": {"x": 500}},
{"standard": {"x": 50}},
{"cross": {"x": 5}},
{"cross": {"x": 2000}},
{"longitudinal": {"x": 500.5}},
{"longitudinal": {"x": True}},
{"longitudinal": {"x": "500"}},
{"cross": 100},
[],
):
with pytest.raises(ValueError):
checked_scales(bad)
def test_바꾼_종류만_저장본_풂(project: Path) -> None:
_write_manifest(
project,
{
"drawings": {
"longitudinal_1": {"kind": "longitudinal", "confirmed": True},
"longitudinal_2": {"kind": "longitudinal", "confirmed": True},
"cross_00020m": {"kind": "cross", "confirmed": True},
"mass_haul": {"kind": "mass_haul", "confirmed": True},
"cover": {"kind": "cover", "confirmed": True},
}
},
)
assert saved_counts(project) == {
"longitudinal": 2,
"cross": 1,
"mass_haul": 1,
"development": 0,
}
# 기본과 같은 값 = 바뀐 것 없음 = 안 풂
assert write_drawing_scales(project, project / "none.json", {"cross": {"x": 100}}) == ({}, [])
invalidated, stale = write_drawing_scales(
project, project / "none.json", {"longitudinal": {"x": 500, "y": None}}
)
assert (invalidated, stale) == ({"longitudinal": 2}, [])
drawings = _read_manifest(project)["drawings"]
assert not drawings["longitudinal_1"]["confirmed"]
assert drawings["cross_00020m"]["confirmed"] and drawings["cover"]["confirmed"]
assert read_drawing_scales(project)["longitudinal"] == {"x": 500, "y": 200}
# 보낸 종류만 바뀜 — 앞서 쓴 종단은 남음 · 횡단 = 측점 설계 잠정 몫
invalidated, stale = write_drawing_scales(
project, project / "none.json", {"cross": {"x": 100, "y": 50}}
)
assert (invalidated, stale) == ({"cross": 1}, [20])
assert read_drawing_scales(project)["longitudinal"]["x"] == 500
def test_GET_PUT_범위_밖_400(project: Path, monkeypatch: pytest.MonkeyPatch) -> None:
async def source(_project_id):
return 1, project, project / "none.json", False
monkeypatch.setattr(scale_router, "_confirmed_source", source)
app = FastAPI()
app.include_router(scale_router.router)
client = TestClient(app)
url = "/api/projects/9872e9a7-e483-427b-a6b2-72cdbc063bc3/drawing-scales"
got = client.get(url).json()
assert got["scales"] == DRAWING_SCALE_DEFAULTS
assert got["ranges"]["cross"]["x"] == [10, 1000]
assert got["saved"] == {"longitudinal": 0, "cross": 0, "mass_haul": 0, "development": 0}
bad = client.put(url, json={"scales": {"cross": {"x": 5}}})
assert bad.status_code == 400 and "1/10~1/1,000" in bad.json()["message"]
assert not drawing_scales_path(project).exists() # 거절 = 안 씀
put = client.put(url, json={"scales": {"longitudinal": {"x": 500, "y": 200}}})
assert put.status_code == 200
assert put.json()["scales"]["longitudinal"] == {"x": 500, "y": 200}
assert put.json()["invalidated"] == {}