319 lines
14 KiB
Python
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"] == {}
|