- 나눈 조각으로 면 찾기(열린 영역 제외) · 영역 안 누름 = 고름 · 채움 강조 · 면적 · 둘레 - 선 고름 경사비율 · 닫힌 선 둘레 · 띠 고름 값 글 - 표 칸 누름 = 저장된 조각 그래프 고름 · 띠 강조 + 값 글 - 고른 칸 묶음 바깥 한 줄(겹선 없음) · 줄 물들임 없앰 · 합친 묶음 이름 칸 키 맞춤 - 259-2 · 259-3 이 같은 파일(상세 표 배선)이라 한 커밋 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3mdb91125mPoC6tpsQx4P
204 lines
8.0 KiB
Python
204 lines
8.0 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""PLAN 259 — B06 [상세] 영역 · 길이 · 표 강조.
|
|
|
|
못박는 것
|
|
- 259-1 공용 팬(`A00_Common/b_svg_zoom_pan.ts`) — 가운데 단추 팬 뒤 왼쪽 누름 click 이 안 삼켜짐 ·
|
|
거리이정표 꼴(빈 곳 왼쪽 끌기 팬)은 끌기 끝 click 만 막음.
|
|
- 259-1 암 경계선 — 설계선과 만나는 점에서 나뉨 · 고름 값 · 저장 꼴 · 서버 `pick_lines` 거울 ·
|
|
암 프리셋 아니면 다시 고름.
|
|
- 259-2 닫힌 영역 — 면 찾기(열린 영역 · 매달린 꼬리 제외) · 영역 고름 값(면적 · 둘레) · 붙은 영역 =
|
|
맞닿은 변 뺌 · 서버 거울 · 표 칸 → 저장된 조각 · 띠 짝 · 띠 값 글.
|
|
- 259-3 표 강조 — 고른 칸 묶음 바깥 한 줄(칸 사이 겹선 없음).
|
|
|
|
./venv/Scripts/python.exe -m pytest resources/tester/test_259_detail_region.py -q
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import subprocess
|
|
from math import hypot
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from B06_Section.B06_Section_Schema import CrossSectionPatch
|
|
from B07_DesignDetail.B07_DesignDetail_Engine_Cross_Table_Edit import cell_value, pick_lines
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
HERE = Path(__file__).resolve().parent
|
|
|
|
LINE = [(-6.0, 104.0), (-2.0, 98.0), (2.0, 98.0), (6.0, 104.0)]
|
|
DESIGN = {
|
|
"design_elevation_m": 98.0,
|
|
"geometry_preset": "rock",
|
|
"rock_boundary_offset_m": -1.0,
|
|
"road_edges": {
|
|
"left": {"offset_m": 2.0, "elevation_m": 98.0},
|
|
"right": {"offset_m": -2.0, "elevation_m": 98.0},
|
|
},
|
|
"design_line": [{"offset_m": x, "elevation_m": y} for x, y in LINE],
|
|
}
|
|
SAMPLES = [{"offset_m": x, "elevation_m": 100.0, "valid": True} for x in (-10, -5, 0, 5, 10)]
|
|
AXES = {
|
|
"widthPx": 600,
|
|
"heightPx": 300,
|
|
"pixelsPerMeter": 10,
|
|
"maxOffset": 14,
|
|
"displayMax": 110,
|
|
"elevationMid": 100,
|
|
"exaggeration": 2,
|
|
}
|
|
ROCK_X = 2 + 4 / 6 # 경사(4 : 6)가 암 경계선 99 m 와 만나는 offset
|
|
GROUND_X = 2 + 4 / 3 # 경사가 원지반 100 m 와 만나는 offset
|
|
|
|
pytestmark = pytest.mark.skipif(
|
|
not (ROOT / "config" / "node_modules").is_dir(), reason="프론트엔드 의존성 미설치"
|
|
)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def out() -> dict:
|
|
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
|
["node", str(HERE / "helper_259_detail_region.cjs")],
|
|
input=json.dumps({"design": DESIGN, "samples": SAMPLES, "axes": AXES}),
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
timeout=60,
|
|
cwd=os.fspath(ROOT),
|
|
)
|
|
assert proc.returncode == 0, proc.stderr
|
|
return json.loads(proc.stdout)
|
|
|
|
|
|
# ── 259-1 공용 팬 ────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_가운데_단추_팬_뒤_왼쪽_누름_통과(out):
|
|
assert out["panMiddle"] == {"firstClickStopped": False, "secondClickStopped": False}
|
|
|
|
|
|
def test_거리이정표_왼쪽_끌기_팬은_끌기_click_만_막음(out):
|
|
assert out["panLeft"] == {"afterDragStopped": True, "nextClickStopped": False}
|
|
|
|
|
|
# ── 259-1 암 경계선 나눔 · 고름 ───────────────────────────────────────────
|
|
|
|
|
|
def test_암_경계선이_설계선과_만나는_점에서_나뉨(out):
|
|
rock = [p for p in out["pieces"] if p["line"] == "rock"]
|
|
assert len(rock) == 3
|
|
ends = sorted(round(p["points"][0][0], 4) for p in rock)
|
|
assert ends == pytest.approx([-10.0, -ROCK_X, ROCK_X], abs=1e-3)
|
|
# 설계 경사도 암 경계선에서 잘림(경사 끝 · 암 교점 · 원지반 교점)
|
|
design_ends = {
|
|
round(q[0], 3)
|
|
for p in out["pieces"]
|
|
if p["line"] == "design"
|
|
for q in (p["points"][0], p["points"][-1])
|
|
}
|
|
assert round(ROCK_X, 3) in design_ends
|
|
|
|
|
|
def test_암_경계선_고름_값_저장_꼴(out):
|
|
length = 2 * ROCK_X
|
|
assert out["rockLength"] == pytest.approx(length, abs=1e-3)
|
|
assert out["rockLabel"] == (
|
|
f"암 경계선 길이 {length:.2f}m · 경사비율 0.0%\n수평 {length:.2f}m · 높이 0.00m"
|
|
)
|
|
source = out["rockSource"]
|
|
assert source["src"] == "length" and source["pieces"][0]["line"] == "rock"
|
|
patch = CrossSectionPatch(chainage_m=40.0, detail_table={"cells": {"fence": source}})
|
|
assert patch.model_dump()["detail_table"]["cells"]["fence"]["pieces"][0]["line"] == "rock"
|
|
|
|
|
|
def test_암_경계선_서버_거울(out):
|
|
number, _text, again = cell_value(out["rockSource"], DESIGN, SAMPLES)
|
|
assert not again and number == pytest.approx(out["rockLocal"], abs=1e-6)
|
|
assert pick_lines(DESIGN, SAMPLES)["rock"] == [[(x, 99.0) for x in (-10, -5, 0, 5, 10)]]
|
|
# 토사 프리셋이면 암 경계선 없음 → 다시 고름
|
|
soil = {**DESIGN, "geometry_preset": "soil"}
|
|
assert pick_lines(soil, SAMPLES)["rock"] == []
|
|
assert cell_value(out["rockSource"], soil, SAMPLES) == (None, None, True)
|
|
|
|
|
|
# ── 259-2 닫힌 영역 · 영역 고름 · 면적 ────────────────────────────────────
|
|
|
|
SLOPE_1M = hypot(4 / 6, 1.0) # 경사 조각 높이 1 m 길이
|
|
FACE_TOP = (2 * GROUND_X + 2 * ROCK_X) / 2 # 원지반 100 ~ 암 99 사이 사다리꼴
|
|
FACE_LOW = (2 * ROCK_X + 4) / 2 # 암 99 ~ 바닥 98 사이 사다리꼴
|
|
|
|
|
|
def test_면_찾기_열린_영역_제외(out):
|
|
faces = sorted(out["faces"], key=lambda f: -f["area"])
|
|
assert [f["area"] for f in faces] == pytest.approx([FACE_TOP, FACE_LOW], abs=1e-6)
|
|
assert faces[0]["lines"] == ["design", "design", "ground", "rock"]
|
|
assert faces[1]["lines"] == ["design", "design", "design", "rock"]
|
|
assert out["openFaces"] == 0 # 원지반만 = 열림
|
|
assert out["square"] == [{"area": pytest.approx(1.0), "edges": [0, 1, 2]}] # 꼬리 지움
|
|
|
|
|
|
def test_영역_고름_면적_둘레(out):
|
|
top = 2 * GROUND_X + 2 * ROCK_X + 2 * SLOPE_1M
|
|
low = 2 * ROCK_X + 4 + 2 * SLOPE_1M
|
|
assert sorted(out["faceLabels"]) == sorted(
|
|
[
|
|
f"영역 면적 {FACE_TOP:.2f}㎡\n둘레 {top:.2f}m",
|
|
f"영역 면적 {FACE_LOW:.2f}㎡\n둘레 {low:.2f}m",
|
|
]
|
|
)
|
|
|
|
|
|
def test_붙은_두_영역_맞닿은_변_뺌(out):
|
|
union = out["union"]
|
|
assert union["lines"] == ["design"] * 5 + ["ground"] # 암 경계선 빠짐
|
|
area = FACE_TOP + FACE_LOW
|
|
around = 2 * GROUND_X + 4 + 4 * SLOPE_1M
|
|
assert union["label"] == f"영역 2개 · 면적 {area:.2f}㎡\n둘레 {around:.2f}m"
|
|
assert (
|
|
union["selectLabel"]
|
|
== f"6개 · 길이 합 {around:.2f}m\n면적 {area:.2f}㎡ · 둘레 {around:.2f}m"
|
|
)
|
|
assert union["local"] == pytest.approx(area, abs=1e-3)
|
|
number, _text, again = cell_value(union["source"], DESIGN, SAMPLES)
|
|
assert not again and number == pytest.approx(union["local"], abs=1e-6)
|
|
|
|
|
|
# ── 259-2 표 칸 → 그래프 ─────────────────────────────────────────────────
|
|
|
|
|
|
def test_표_칸_저장된_조각을_그래프에서_고름(out):
|
|
assert out["same"] == {
|
|
"forward": True,
|
|
"reverse": True,
|
|
"otherLine": False,
|
|
"otherPiece": False,
|
|
}
|
|
pieces, band, text, none = out["cellGraph"]
|
|
assert pieces == {"pieces": out["rockSource"]["pieces"]}
|
|
assert band == {"band": "cut_rock"}
|
|
assert text is None and none is None
|
|
|
|
|
|
def test_띠_값_글(out):
|
|
assert out["bandLabel"] == ["깍기(토사) 면적 1.23㎡", None, None]
|
|
|
|
|
|
# ── 259-3 표 강조 바깥 한 줄 ──────────────────────────────────────────────
|
|
|
|
|
|
def test_고른_칸_묶음_바깥_한_줄(out):
|
|
parts = {"M" + p for p in out["outline"].split("M") if p}
|
|
assert parts == {
|
|
"M0 0H10",
|
|
"M0 0V10",
|
|
"M10 0V10",
|
|
"M0 20H10",
|
|
"M0 10V20",
|
|
"M10 10H20",
|
|
"M10 20H20",
|
|
"M20 10H30",
|
|
"M20 20H30",
|
|
"M30 10V20",
|
|
} # 칸 사이(10 · 20 세로) 겹선 없음
|