feat(381-2 B05 평면): [측점] [기울기] 켬끔 단추(절/성토선 왼쪽 · 기본 켬 · 페이지 동안)

- 새 _Marks 켬끔 모듈 · 다시 그림은 _Footprint scheduleRedraw 공유
- [측점] 끔 = 눈금 · 번호 · 눈금 집기 없음 · 시점 · 종점 이름표 남김
- [기울기] 끔 = 기울기 라벨 · 높이 요청 없음
- 시험 test_381_2_routeedit_marks · 도구줄 차례 시험에 두 단추

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013QyNXY2TUKXCFMwAca2APG
This commit is contained in:
2026-10-17 14:44:40 +09:00
co-authored by Claude Opus 5.5
parent e9d30cdeac
commit 29bf8c998c
7 changed files with 121 additions and 15 deletions
@@ -1,5 +1,6 @@
import { createButton } from "@ui/ui_template_elements";
import { bindFootprintToggle } from "./B05_Profile_UI_RouteEdit_Footprint";
import { bindMarkToggle } from "./B05_Profile_UI_RouteEdit_Marks";
/* =============================================================================
* B05_Profile_UI_RouteEdit_Chrome.ts
@@ -11,7 +12,8 @@ import { bindFootprintToggle } from "./B05_Profile_UI_RouteEdit_Footprint";
* **배치**(352-1) — 지도가 칸 전체 · 머리 줄 · 테두리 없음. 도구는 3D 와 같은 떠 있는 유리 단추
* (지도 왼쪽 위 · ☰ 비킴), [?] 조작 설명은 도구줄 아래 2열, 상태·범례는 왼쪽 아래, 잰 값은 오른쪽
* 아래. 횡단 두 판은 지도 오른쪽을 덮는 판(아래 종단 판이 그 위).
* 도구줄 차례(366-2) — [?] | ↶ ↷ | 3D | ⟲ ⟳ · 1m 등고선 · 절/성토선 · 거리 재기 | 측점 스냅 · 분석 지형.
* 도구줄 차례(366-2 · 381-2) — [?] | ↶ ↷ | 3D | ⟲ ⟳ · 1m 등고선 · 측점 · 기울기 · 절/성토선 · 거리 재기 |
* 측점 스냅 · 분석 지형.
*
* 자리(345-3) — 모달이 아니라 종단 보기 가운데 위 본문(옛 3D 자리)에 붙는다. 덮개 · 전체 화면 ·
* 닫기 단추는 걷었다 · [취소] · [확인] · [노선 반영] · [저장 전으로]는 없앰(346-3 · 352-2 — 노선은 페이지
@@ -89,6 +91,10 @@ export function createRouteEditChrome(
<button type="button" class="ui-btn ui-btn--glass" data-act="contour-minor"
aria-pressed="false" disabled
title="1m 간격 등고선을 얇고 옅게 겹칩니다 (확대하면 보임)">1m 등고선</button>
<button type="button" class="ui-btn ui-btn--glass" data-act="stations"
title="측점 눈금 · 번호를 평면에 보이기 · 숨기기 (시점 · 종점 이름표는 늘 보임)">측점</button>
<button type="button" class="ui-btn ui-btn--glass" data-act="slopes"
title="측점 사이 기울기(%)를 평면에 보이기 · 숨기기 (확대해야 보임)">기울기</button>
<button type="button" class="ui-btn ui-btn--glass" data-act="footprint"
title="도로 끝 · 비탈 끝(깎기 붉음 · 쌓기 청록)을 평면에 겹칩니다">절/성토선</button>
<button type="button" class="ui-btn ui-btn--glass" data-act="measure"
@@ -199,6 +205,8 @@ export function createRouteEditChrome(
const terrainSelect = overlay.querySelector<HTMLSelectElement>('[data-act="terrain"]')!;
const stationSnapButton = overlay.querySelector<HTMLButtonElement>('[data-act="station-snap"]')!;
bindFootprintToggle(overlay.querySelector<HTMLButtonElement>('[data-act="footprint"]')!);
bindMarkToggle(overlay.querySelector<HTMLButtonElement>('[data-act="stations"]')!, "stations");
bindMarkToggle(overlay.querySelector<HTMLButtonElement>('[data-act="slopes"]')!, "slopes");
return {
overlay,
@@ -30,7 +30,8 @@ let redraw: (() => void) | null = null;
let frame = 0;
let listening: { projectId: string; stop: () => void } | null = null;
function scheduleRedraw(): void {
/** 마지막 장면을 다음 프레임에 한 번 다시 그림 — [측점] · [기울기] 켬끔(`_Marks`)도 씀. */
export function scheduleRedraw(): void {
if (frame) return;
frame = requestAnimationFrame(() => {
frame = 0;
@@ -0,0 +1,30 @@
/* =============================================================================
* B05_Profile_UI_RouteEdit_Marks.ts
* 노선 편집 평면 [측점] [기울기] 켬끔(요구 241-11).
*
* [측점] 끔 = 측점 눈금 · 번호 안 그림 · 눈금 집기 안 함(시점 · 종점 이름표는 남음).
* [기울기] 끔 = 측점 사이 기울기 라벨 안 그림 → 높이도 안 물음.
* 켬끔은 이 페이지 동안만(기본 켬 · 계정 설정 아님) — `_Footprint` 와 같은 꼴.
* ========================================================================== */
import { scheduleRedraw } from "./B05_Profile_UI_RouteEdit_Footprint";
export type RouteEditMark = "stations" | "slopes";
const shown: Record<RouteEditMark, boolean> = { stations: true, slopes: true };
export const markShown = (mark: RouteEditMark): boolean => shown[mark];
/** [측점] · [기울기] 단추 — 누르면 켬끔 · 다시 그림. */
export function bindMarkToggle(button: HTMLButtonElement, mark: RouteEditMark): void {
const paint = (): void => {
button.classList.toggle("is-active", shown[mark]);
button.setAttribute("aria-pressed", String(shown[mark]));
};
paint();
button.addEventListener("click", () => {
shown[mark] = !shown[mark];
paint();
scheduleRedraw();
});
}
@@ -25,6 +25,7 @@ import {
stationAtScreen,
stationLabelBoxes,
} from "./B05_Profile_UI_RouteEdit_Input";
import { markShown } from "./B05_Profile_UI_RouteEdit_Marks";
import {
curveShortfalls,
radiusFloorM,
@@ -188,6 +189,8 @@ export function bindRouteEditPointer(params: RouteEditPointerParams): RouteEditP
} else if (
// 측점 눈금을 누르면 그 측점 횡단을 따로 띄운다(계획서 0-9 ⑧). 노드·손잡이 다음이다.
(() => {
// [측점] 끔(381-2) — 라벨 0개면 눈금이 전부 집히므로 아예 안 봄.
if (!markShown("stations")) return false;
const chainage = stationAtScreen(
state.plannedLine.length ? state.plannedLine : state.planned,
toScreen,
+17 -13
View File
@@ -32,6 +32,7 @@ import type { MeasureMark } from "./B05_Profile_UI_RouteEdit_Measure";
import { stationLabel as formatStation } from "../common_util/common_util_station";
import { arcLocked, straightLocked } from "./B05_Profile_UI_RouteEdit_Lock";
import { drawFootprint } from "./B05_Profile_UI_RouteEdit_Footprint";
import { markShown } from "./B05_Profile_UI_RouteEdit_Marks";
/** 노드 반지름(px). */
const NODE_R = 4;
@@ -307,7 +308,7 @@ export function drawRouteEditScene(context: CanvasRenderingContext2D, scene: Rou
drawCrossWidth(context, scene, line); // 측점 라벨이 위에 오게 먼저 그린다.
drawStationMarks(context, scene, line);
scene.overStations?.(context);
if (markShown("slopes")) scene.overStations?.(context); // 끄면 높이도 안 물음(381-2)
drawMeasureMarks(context, scene, line);
// 등고 높이값은 **맨 나중에** 얹는다(2026-09-12 사용자 지시 ⑦) — 노선·측점 눈금보다 먼저
// 그리면 숫자가 그 아래 깔려 안 읽힌다. 띠(300m)는 다시 씌워 그리는 범위는 그대로 둔다.
@@ -494,18 +495,21 @@ function drawStationMarks(
setStationLabelBoxes(context.canvas, labels);
if (line.length < 2) return;
// 눈금은 B04 지도·배수유역도와 같은 한 곳이 그린다 — 표기가 화면마다 갈리지 않게.
drawStationTicks(
context,
line.map(([x, y]) => ({ x, y })),
{
intervalM: scene.stationIntervalM,
pxPerMeter: scene.pxPerMeter,
toScreen: (x, y) => scene.toScreen([x, y]),
uprightRad: scene.uprightRad,
onLabel: (box) => labels.push(box),
chainages: scene.stations ?? undefined,
},
);
// [측점] 끔(381-2)이면 눈금 · 번호만 빼고 시점 · 종점 이름표는 남김.
if (markShown("stations")) {
drawStationTicks(
context,
line.map(([x, y]) => ({ x, y })),
{
intervalM: scene.stationIntervalM,
pxPerMeter: scene.pxPerMeter,
toScreen: (x, y) => scene.toScreen([x, y]),
uprightRad: scene.uprightRad,
onLabel: (box) => labels.push(box),
chainages: scene.stations ?? undefined,
},
);
}
const total = polylineLengthM(line);
const last = line.length - 1;
endLabel(context, scene, line[0], line[1], `시점 ${formatStation(0, scene.stationIntervalM)}`);
@@ -0,0 +1,58 @@
"""381-2 B05 평면 [측점] [기울기] 켬끔(요구 241-11).
- 단추 = 절/성토선 왼쪽 · 켬끔 모듈(기본 켬 · 페이지 동안)
- [측점] 끔 = 눈금 · 번호 안 그림 · 눈금 집기 안 함(라벨 0개 = 전부 집힘 함정) · 시점 · 종점 이름표 남김
- [기울기] 끔 = 기울기 라벨(높이 묻기) 안 부름
"""
from pathlib import Path
B05 = Path(__file__).resolve().parents[2] / "B05_Profile"
def _read(name: str) -> str:
return (B05 / name).read_text(encoding="utf-8")
def test_켬끔_모듈_기본_켬():
marks = _read("B05_Profile_UI_RouteEdit_Marks.ts")
assert "{ stations: true, slopes: true }" in marks
assert "scheduleRedraw();" in marks
assert "export function scheduleRedraw" in _read("B05_Profile_UI_RouteEdit_Footprint.ts")
def test_단추는_절성토선_왼쪽_배선():
chrome = _read("B05_Profile_UI_RouteEdit_Chrome.ts")
stations = chrome.index('data-act="stations"')
slopes = chrome.index('data-act="slopes"')
assert stations < slopes < chrome.index('data-act="footprint"')
assert ">측점</button>" in chrome and ">기울기</button>" in chrome
assert '[data-act="stations"]\')!, "stations")' in chrome
assert '[data-act="slopes"]\')!, "slopes")' in chrome
def test_측점_끄면_눈금만_빠지고_이름표_남음():
render = _read("B05_Profile_UI_RouteEdit_Render.ts")
body = render[render.index("function drawStationMarks(") : render.index("function endLabel(")]
# 라벨 상자를 먼저 비우고(누름 판정) · 눈금은 켬일 때만 · 이름표는 밖
assert body.index("setStationLabelBoxes(") < body.index('if (markShown("stations"))')
guard = body.index('if (markShown("stations"))')
assert guard < body.index("drawStationTicks(") < body.index("`시점 ")
assert body.count("endLabel(") == 2
def test_기울기_끄면_라벨도_높이_요청도_없음():
render = _read("B05_Profile_UI_RouteEdit_Render.ts")
assert 'if (markShown("slopes")) scene.overStations?.(context);' in render
assert render.count("scene.overStations?.(context)") == 1
def test_측점_끄면_눈금_집기_없음():
pointer = _read("B05_Profile_UI_RouteEdit_Pointer.ts")
# 집기에 쓰는 눈금 찾기는 켬일 때만 — 라벨 0개면 전부 집히는 함정
at = 0
calls = 0
while (at := pointer.find("stationAtScreen(", at + 1)) >= 0:
calls += 1
assert 'markShown("stations")' in pointer[at - 300 : at], at
assert calls >= 1
@@ -223,6 +223,8 @@ def test_노선_추천_다듬기_23_3():
'data-act="rotate-ccw"',
'data-act="rotate-cw"',
'data-act="contour-minor"',
'data-act="stations"', # 381-2 [측점] [기울기] = 절/성토선 왼쪽
'data-act="slopes"',
'data-act="footprint"',
'data-act="measure"',
"|",