feat(B05 평면 구조물 마커): 구조물 측점 마커 · 고름 정보 창 · 20 m 법선 · 끌기 · 방향키 1 m (399)

- 마커 = 종단 알약과 같은 목록(laneStructures) · 노선 위 알약 꼴(종류 색) · 스냅 · 간격과 무관
- 고름 = 알약 고름과 같은 길(onStructureSelect · 공용 고름) · 알약 벌룬 그대로 · 좌우 20 m 법선
- 끌기 · 방향키 = 노선 따라 정수 m 1 m 씩 · 시점 · 종점 밖 안 감 · 놓을 때 알약 끌기와 같은 길로 한 번
- 집기 차례 = 교각점 · 곡선 손잡이 다음 · 옛 마름모 지움
- 시험 test_399_plan_structure_marks · test_391 마름모 줄 고침

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte
This commit is contained in:
2026-10-17 21:43:51 +09:00
co-authored by Claude Opus 5.5
parent 614f12bb88
commit c3efa6dd7e
10 changed files with 938 additions and 56 deletions
@@ -0,0 +1,128 @@
"""399 B05 평면 구조물 측점 마커(요구 250-2) — 알약과 같은 목록 · 고름 = 공용 고름 · 정보 창(알약 벌룬) ·
20 m 법선 · 끌기 · 방향키 = 노선 따라 정수 m 1 m 씩 · 시점 · 종점 밖 안 감 · 놓을 때 한 번 옮김(알약과 같은 길).
TS 셈은 `helper_399_plan_structure_marks.cjs`(Node · 노선 셈 · 법선 · 걸음은 진짜).
./venv/Scripts/python.exe -m pytest resources/tester/test_399_plan_structure_marks.py -q
"""
import json
import subprocess
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
HELPER = Path(__file__).with_name("helper_399_plan_structure_marks.cjs")
def _read(*parts: str) -> str:
return (ROOT.joinpath(*parts)).read_text(encoding="utf-8")
@pytest.fixture(scope="module")
def got() -> dict:
run = subprocess.run( # noqa: S603 — 고정 실행 파일
["node", str(HELPER)],
capture_output=True,
text=True,
encoding="utf-8",
timeout=120,
)
assert run.returncode == 0, run.stderr
return json.loads(run.stdout)
def test_마커_목록은_알약_목록(got) -> None:
assert got["laneGotSource"] is True # 알약 레인과 같은 `laneStructures`
assert got["plain"]["marks"] == [
{"color": "#e11d48", "at": [80, 0]}, # 40 m × 2 px · 종류 색
{"color": "#2563eb", "at": [160.34, 0]},
]
assert got["plain"]["normals"] == 0 and got["balloonsAfterPlain"] == 0
lane = _read("B05_Profile", "B05_Profile_UI_Structures_Marks.ts")
assert "const structures = laneStructures(options.structures);" in lane
marks = _read("B05_Profile", "B05_Profile_UI_RouteEdit_StructureMarks.ts")
assert "laneStructures(source.marks())" in marks
bridge = _read("B05_Profile", "B05_Profile_UI_Page_Structures.ts")
assert "setStructures([...ownStructures, ...pipeMarks]);" in bridge
assert "marks: (): StructureInstance[] => [...ownStructures, ...pipeMarks]," in bridge
assert "planMarksChanged(deps.projectId);" in bridge
def test_고름_정보_창_법선(got) -> None:
pipe = got["pickedPipe"]
assert pipe["ringAt"] == [[160.34, 0]] # 관 고름(누가거리만)도 그 마커
assert pipe["normal"] == [[160.34, -40], [160.34, 40]] # 좌우 20 m
assert pipe["balloon"]["lines"] == ["이름 pipe pos 80.17", "옵션 배수관"] # 알약 벌룬 줄
assert pipe["balloon"]["color"] == "#2563eb"
assert got["pickedStructureBalloon"] == ["이름 box pos 40", "옵션 BOX"]
assert got["balloonsAfterRelease"] == 0
def test_공용_고름과_같은_길() -> None:
view = _read("A00_Common", "design", "design_view_profile.ts")
assert " onStructureSelect,\n onStructureMarkMove,\n" in view # 알약 콜백 = 같은 함수
assert "select: onStructureSelect," in view and "move: onStructureMarkMove," in view
render = _read("B05_Profile", "B05_Profile_UI_RouteEdit_Render.ts")
labels = render.index("drawContourLabels(context, scene, band, style);\n drawStructureMarks")
nodes = render.index("scene.planned.forEach((vertex, index) => {")
assert labels < nodes # 라벨 위 · 교각점 아래
def test_집기_차례는_교각점_손잡이_다음() -> None:
pointer = _read("B05_Profile", "B05_Profile_UI_RouteEdit_Pointer.ts")
node = pointer.index("if (hitNode >= 0) {")
handle = pointer.index("} else if (dragHandle) {")
mark = pointer.index("} else if (marks.press(px, py)) {")
tick = pointer.index('if (!pick || !("chainage" in pick)) return false;')
assert node < handle < mark < tick
def test_끌기_1m_정수_놓을_때_한_번(got) -> None:
assert got["pressMiss"] is False and got["pressA"] is True
assert got["selects"] == ["A"]
assert got["movesBeforeRelease"] == 0 # 끄는 동안은 미리보기만
assert got["dragMove"] == ["A", 43] # 3 m
assert "2+0 → 2+3(+3 m) 옮김" in got["dragDoneStatus"]
assert got["oldDecimalStay"] is True # 옛 소수 자리 눌렀다 떼기 = 안 옮김
assert got["oldDecimalMoved"] == ["pipe-80.17", 81]
def test_노선_밖_안_감(got) -> None:
assert got["pastEnd"] == ["A", 200]
assert got["beforeStart"] == ["A", 0]
assert got["logic"]["clamp"] == [0, 199] # 종점 199.6 → 199(정수 m 안쪽)
assert got["logic"]["step"] == [81, 0, 199]
def test_방향키_노선_따라_1m(got) -> None:
assert got["keyRight"] == ["A", 41]
assert got["keyLeft"] == ["A", 39]
assert got["keyUpPerpendicular"] == ["A", 41] # 직각 = → · ↑ 가 +
assert got["keyRightWestward"] == ["A", 39] # 노선이 서쪽으로 가면 → = 뒤로
assert got["keyWithNodePicked"] == 0 # 교각점 고름 = 교각점 방향키 몫
assert got["keyNoPick"] == 0
def test_고른_마커_찾기(got) -> None:
logic = got["logic"]
assert logic["round"] == [41, 42]
assert (logic["pickId"], logic["pickNear"], logic["pickFar"], logic["pickOther"]) == (
"A",
"A",
None,
None,
)
assert got["redrawnOnChange"] == 1
def test_편집기_줄_수_그대로() -> None:
edit = _read("B05_Profile", "B05_Profile_UI_RouteEdit.ts")
assert edit.count("\n") <= 695
for name in (
"B05_Profile_UI_RouteEdit_StructureMarks.ts",
"B05_Profile_UI_RouteEdit_Render.ts",
"B05_Profile_UI_RouteEdit_Pointer.ts",
):
assert _read("B05_Profile", name).count("\n") <= 700