Files
Aislo/resources/tester/test_261_3_detail_pick.py
T
eomsangdonandClaude Opus 5.5 2bf429ec98 feat(b06): 상세 누름 판정 거리로 — 보통 배율 얇은 띠 = 영역 · 선 가까이 = 선 (261-3 걸음 1)
누름 = 판정 선 굵기 대신 누른 자리 거리(`pickAt`) — 선 4 px 안 = 선 · 영역 안이면 맞은편 선까지 ⅓ 안이어야 선.
선 누름이 표 띠 누름보다 앞섬(stopImmediatePropagation) · 판정 선 굵기 10 → 8 px(걸침 표시만).
시험 test_261_3_detail_pick(견본 · 실측 4+15 그림) · 233-2 헬퍼 누름 꼴 맞춤.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3mdb91125mPoC6tpsQx4P
2026-10-11 15:37:19 +09:00

238 lines
9.3 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.
# -*- coding: utf-8 -*-
"""PLAN 261-3 — B06 [상세] 영역 고르기: 보통 배율 얇은 띠 누름 · 깎기 토사 / 암석 영역 나눔.
못박는 것
- 누름 판정 = 거리(`pickAt`) — 얇은 띠(6 px) 가운데 = 영역 · 선 1 px = 선 · 영역 밖 선 가까이 = 선 ·
넓은 영역은 선 4 px 안 = 선 · 확대 · 축소 배율에서도 같은 판정.
- 벽 · 집수정 둘레 = 구조물 선 — 설계선 끝 ~ 깎기 비탈 밑을 이어 깎기 암석(암선 ↔ 설계선) ·
깎기 토사(지반 ↔ 암선) 영역이 따로 닫힘 · 면적 = 사다리꼴 셈 · 서버 거울.
- 실측 그림(검증_랩탑메인 4+15 배수관 · 암 · 깎기 비탈) — 깎기 암석 영역이 생김 · 보통 배율 띠 누름 = 영역.
./venv/Scripts/python.exe -m pytest resources/tester/test_261_3_detail_pick.py -q
"""
import json
import os
import re
import subprocess
from pathlib import Path
import pytest
from B07_DesignDetail.B07_DesignDetail_Engine_Cross_Table_Edit import cell_value
ROOT = Path(__file__).resolve().parents[2]
HERE = Path(__file__).resolve().parent
PAD = {
k: float(v)
for k, v in re.findall(
r"(\w+): (\d+)",
re.search(
r"export const CROSS_PAD = \{([^}]*)\}",
(ROOT / "B06_Section" / "B06_Section_UI_Section_Common.ts").read_text(encoding="utf-8"),
).group(1),
)
}
pytestmark = pytest.mark.skipif(
not (ROOT / "config" / "node_modules").is_dir(), reason="프론트엔드 의존성 미설치"
)
# ── 견본: 암 지반(암선 = 지반 − 0.6 m · 보통 배율 6 px 띠) + 오른쪽 설계 비탈 + 왼쪽 집수정 · 깎기 비탈 ──
AXES = {
"pixelsPerMeter": 10,
"maxOffset": 14,
"displayMax": 106,
"elevationMid": 100,
"exaggeration": 1,
}
ROCK = -0.6
DESIGN = {
"design_elevation_m": 98.0,
"geometry_preset": "rock",
"rock_boundary_offset_m": ROCK,
"design_line": [{"offset_m": x, "elevation_m": y} for x, y in [(6, 104), (2, 98), (-2, 98)]],
}
SAMPLES = [{"offset_m": x, "elevation_m": 100.0, "valid": True} for x in (-10, -5, 0, 5, 10)]
def px(offset: float, elevation: float) -> list[float]:
return [
PAD["left"] + (AXES["maxOffset"] - offset) * AXES["pixelsPerMeter"],
PAD["top"] + (AXES["displayMax"] - elevation) * AXES["pixelsPerMeter"],
]
def el(tag: str, cls: str, pts, dl: bool = False) -> dict:
return {"tag": tag, "cls": cls, "dl": dl, "pts": [px(*p) for p in pts]}
GROUND = [(x, 100.0) for x in (-10, -5, 0, 5, 10)]
ELEMENTS = [
el("polyline", "b06-chart__cross-profile", GROUND),
el("polyline", "b06-chart__rock-boundary", [(x, y + ROCK) for x, y in GROUND]),
el("line", "b06-chart__design-cross", [(6, 104), (2, 98)], dl=True),
el("line", "b06-chart__design-cross", [(2, 98), (-2, 98)], dl=True),
el("polygon", "b06-chart__culvert-basin", [(-2, 98), (-2, 97), (-3, 97), (-3, 98)]),
el("polyline", "b06-chart__design-cross", [(-3, 98), (-4, 100)]), # 집수정 절토선(구조물)
]
RIGHT_ROCK = 2 + 1.4 * 4 / 6 # 설계 비탈(4 : 6) × 암선 99.4
RIGHT_GROUND = 2 + 2 * 4 / 6 # 설계 비탈 × 지반 100
LEFT_ROCK = -3 - 1.4 / 2 # 깎기 비탈(1 : 0.5) × 암선
LEFT_GROUND = -4.0
CUT_ROCK = ((RIGHT_ROCK - LEFT_ROCK) + 5) / 2 * 1.4 # 암선 ↔ 노면 · 집수정 윗변(-3 ~ 2)
CUT_SOIL = ((RIGHT_GROUND - LEFT_GROUND) + (RIGHT_ROCK - LEFT_ROCK)) / 2 * 0.6
# 누름(그림 px · 배율) — 지반 y 70 · 암선 y 76 · 노면 y 90
CLICKS = [
[198, 73, 1], # 띠 가운데 = 영역(토사)
[198, 71, 1], # 지반선 1 px = 선
[198, 75, 1], # 암선 1 px = 선
[198, 79, 1], # 넓은 암석 영역 · 암선 3 px = 선
[198, 81, 1], # 암선 5 px = 영역(암석)
[198, 67, 1], # 영역 밖 · 지반선 3 px = 선
[198, 60, 1], # 영역 밖 · 먼 곳 = 없음
[198, 73, 3.7], # 확대 — 띠 가운데 = 영역
[198, 73, 0.5], # 축소 — 띠 가운데 = 영역
]
def run(cases: list[dict]) -> dict:
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
["node", str(HERE / "helper_261_3_detail_pick.cjs")],
input=json.dumps({"cases": cases}),
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)
@pytest.fixture(scope="module")
def out() -> dict:
basinless = [e for e in ELEMENTS if "basin" not in e["cls"]]
return run(
[
{"name": "sample", "axes": AXES, "elements": ELEMENTS, "clicks": CLICKS},
{"name": "basinless", "axes": AXES, "elements": basinless},
]
)
def face_of(case: dict, lines: list[str]) -> tuple[int, dict]:
found = [(i, f) for i, f in enumerate(case["faces"]) if f["lines"] == lines]
assert len(found) == 1, [f["lines"] for f in case["faces"]]
return found[0]
def piece_line(case: dict, pick: dict) -> str:
return case["pieces"][pick["piece"]]["line"]
# ── 깎기 토사 · 암석 영역 따로 ─────────────────────────────────────────────
def test_깎기_암석_토사_영역_따로_면적(out):
case = out["sample"]
_, rock = face_of(case, ["design", "rock", "structure"])
_, soil = face_of(case, ["design", "ground", "rock", "structure"])
assert rock["area"] == pytest.approx(CUT_ROCK, abs=1e-6)
assert soil["area"] == pytest.approx(CUT_SOIL, abs=1e-6)
# 집수정 안 = 따로(넓이 1) · 셋뿐(열린 지반 · 암선 바깥 제외)
assert sorted(round(f["area"], 6) for f in case["faces"]) == sorted(
[round(CUT_ROCK, 6), round(CUT_SOIL, 6), 1.0]
)
def test_비탈_끝이_지반선에_닿기만_해도_나눔(out):
ground_ends = {
round(p["points"][k][0], 4)
for p in out["sample"]["pieces"]
if p["line"] == "ground"
for k in (0, -1)
}
assert {LEFT_GROUND, round(RIGHT_GROUND, 4)} <= ground_ends
def test_집수정_없으면_암석_영역_안_닫힘(out):
# 고침 전 꼴 — 설계선 끝 ~ 비탈 밑이 안 이어져 토사 띠만 남음
faces = out["basinless"]["faces"]
assert [f["lines"] for f in faces] == [["design", "ground", "rock", "structure"]]
assert faces[0]["area"] == pytest.approx(CUT_SOIL, abs=1e-6)
def test_암석_영역_서버_거울(out):
_, rock = face_of(out["sample"], ["design", "rock", "structure"])
number, _text, again = cell_value(rock["source"], DESIGN, SAMPLES)
assert not again and number == pytest.approx(CUT_ROCK, abs=1e-3)
# ── 누름 판정(거리) ──────────────────────────────────────────────────────
def test_얇은_띠_보통_배율_가운데_누름_영역(out):
case = out["sample"]
soil, _ = face_of(case, ["design", "ground", "rock", "structure"])
rock, _ = face_of(case, ["design", "rock", "structure"])
picks = case["clicks"]
assert picks[0] == {"face": soil}
assert picks[4] == {"face": rock}
assert picks[7] == {"face": soil} and picks[8] == {"face": soil}
def test_선_가까이_누름_선(out):
case = out["sample"]
picks = case["clicks"]
assert piece_line(case, picks[1]) == "ground"
assert piece_line(case, picks[2]) == "rock"
assert piece_line(case, picks[3]) == "rock"
assert piece_line(case, picks[5]) == "ground"
assert picks[6] is None
# ── 실측 그림(검증_랩탑메인 4+15) ─────────────────────────────────────────
@pytest.fixture(scope="module")
def real() -> dict:
elements = json.loads(
(HERE / "fixtures" / "b06_261_3_cross_415.json").read_text(encoding="utf-8")
)
ppm = 241.954642190761 / 20 # 눈금 20 m = 241.95 px · 과장 1
axes = {
"pixelsPerMeter": ppm,
"maxOffset": (708 - PAD["left"]) / ppm, # 0 m 눈금 x 708
"displayMax": 875 + (318.52362895766817 - PAD["top"]) / ppm, # 875 m 눈금 y 318.52
"elevationMid": 870,
"exaggeration": 1,
}
case = {"name": "real", "axes": axes, "elements": elements}
first = run([case])["real"]
band = [(i, f) for i, f in enumerate(first["faces"]) if "ground" in f["lines"]]
soil, face = max(band, key=lambda item: item[1]["area"]) # 깎기 토사 띠
# 띠의 지반 조각(가장 긴 것) 가운데 꼭짓점 — 그 밑 3 px(띠 가운데) · 1 px(지반선)
ground = max(
(i for i in face["edges"] if first["pieces"][i]["line"] == "ground"),
key=lambda i: len(first["pieces"][i]["points"]),
)
path = first["px"]["paths"][ground]
x, y = path[len(path) // 2]
case["clicks"] = [[x, y + 3, 1], [x, y + 1, 1]]
return {**run([case])["real"], "soil": soil, "ground": ground}
def test_실측_깎기_암석_영역_생김(real):
rock = [f for f in real["faces"] if f["lines"] == ["design", "rock", "structure"]]
assert rock and max(f["area"] for f in rock) == pytest.approx(18.24, abs=0.05)
soil = real["faces"][real["soil"]]
assert soil["lines"] == ["design", "ground", "rock", "structure"]
assert soil["area"] == pytest.approx(6.00, abs=0.05)
def test_실측_보통_배율_띠_누름(real):
assert real["clicks"][0] == {"face": real["soil"]}
assert real["clicks"][1] == {"piece": real["ground"]}