diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Label.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Label.ts index de2c38274..f292f93cd 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Label.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Label.ts @@ -54,6 +54,16 @@ export function centerDirectionOf( return length <= 1e-9 ? null : [sx / length, sy / length]; } +/** 패널 하단 글 — **내각만** 남긴다(2026-09-12 사용자 지시 ㉗) · 고정 여부는 단추 색, 하한은 + * 칸이 막음. 155° 이상은 L 하한을 안 거는 까닭이 보이게. 곡선 없는 교각점(삼각)도 내각(403-1). */ +export function curveInfoText(curveOn: boolean, inner: number | null, optional: boolean): string { + const angle = inner ? `내각 ${Math.round(inner)}°` : ""; + if (!curveOn) return [angle, "곡선 없음 — 직선이 그대로 꺾입니다"].filter(Boolean).join(" · "); + return angle && optional + ? `${angle} · ${CURVE_OPTIONAL_INNER_ANGLE_DEG}° 이상 — 곡선 생략 가능` + : angle; +} + /** 꺾임점과 패널 사이 여백(px) — 손잡이(접선점 네모)를 가리지 않을 만큼 띄운다. */ const GAP_PX = 40; /** 자동 자리를 고를 방위 수 — 16방위(22.5°마다). */ @@ -342,19 +352,7 @@ export function createCurveLabel( state.radiusShown === null ? "" : String(Math.round(state.radiusShown * 10) / 10); arc.value = state.arcLengthShown === null ? "" : String(Math.round(state.arcLengthShown * 10) / 10); - const inner = state.innerAngleDeg; - // 하단에는 **내각만** 남긴다(2026-09-12 사용자 지시 ㉗) — 고정 여부는 단추 색으로, - // 하한은 칸이 이미 막으므로 글로 또 적을 까닭이 없다. - // 내각 155° 이상은 법이 곡선을 안 둬도 된다고 한 자리다 — L 하한을 안 거는 까닭이 - // 여기서 보여야 한다(2026-09-12 사용자 확정). - info.textContent = state.curveOn - ? inner - ? `내각 ${Math.round(inner)}°` + - (state.curveOptional - ? ` · ${CURVE_OPTIONAL_INNER_ANGLE_DEG}° 이상 — 곡선 생략 가능` - : "") - : "" - : "곡선 없음 — 직선이 그대로 꺾입니다"; + info.textContent = curveInfoText(state.curveOn, state.innerAngleDeg, state.curveOptional); place(state); // 글자가 바뀌면 상자 높이가 한 박자 늦게 자란다 — 다음 그림 직전에 한 번 더 맞춘다. requestAnimationFrame(() => { diff --git a/resources/tester/helper_403_triangle_angle.cjs b/resources/tester/helper_403_triangle_angle.cjs new file mode 100644 index 000000000..72e54682e --- /dev/null +++ b/resources/tester/helper_403_triangle_angle.cjs @@ -0,0 +1,34 @@ +/* 403 곡선 창 하단 글 헬퍼 — `_Label.curveInfoText` 를 트랜스파일해 경우별 글을 JSON 으로 낸다. */ +const fs = require("fs"); +const path = require("path"); +const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); +const B05 = path.join(__dirname, "..", "..", "B05_Profile"); + +const STUBS = { + "@ui/ui_template_elements": { createButton: () => null }, + "./B05_Profile_UI_Panel_Fields": { numberField: () => null, toggleButton: () => null }, +}; +function loadTs(full) { + const js = ts.transpileModule(fs.readFileSync(full, "utf8"), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, + }).outputText; + const box = { exports: {} }; + const localRequire = (request) => { + if (STUBS[request]) return STUBS[request]; + if (request.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`)); + return require(request); + }; + new Function("exports", "module", "require", js)(box.exports, box, localRequire); + return box.exports; +} + +const { curveInfoText } = loadTs(path.join(B05, "B05_Profile_UI_RouteEdit_Label.ts")); +process.stdout.write( + JSON.stringify({ + curve: curveInfoText(true, 132.4, false), + curveOptional: curveInfoText(true, 160.2, true), + triangle: curveInfoText(false, 132.6, false), + triangleOptional: curveInfoText(false, 168.4, true), + triangleUnknown: curveInfoText(false, null, false), + }), +); diff --git a/resources/tester/test_403_triangle_angle.py b/resources/tester/test_403_triangle_angle.py new file mode 100644 index 000000000..21bcd1003 --- /dev/null +++ b/resources/tester/test_403_triangle_angle.py @@ -0,0 +1,47 @@ +"""403 곡선 없는 교각점(삼각) 각도 글(요구 250-5) — 곡선 창 하단에 곡선 있는 자리와 같은 「내각 N°」. + +Node 헬퍼(`helper_403_triangle_angle.cjs`)가 `_Label.curveInfoText` 를 트랜스파일해 돌림. +./venv/Scripts/python.exe -m pytest resources/tester/test_403_triangle_angle.py -q +""" + +import json +import subprocess +from pathlib import Path + +import pytest + +HERE = Path(__file__).resolve().parent + + +@pytest.fixture(scope="module") +def texts(): + proc = subprocess.run( + ["node", str(HERE / "helper_403_triangle_angle.cjs")], + capture_output=True, + text=True, + encoding="utf-8", + timeout=60, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +def test_곡선_있는_자리_그대로(texts): + assert texts["curve"] == "내각 132°" + assert texts["curveOptional"] == "내각 160° · 155° 이상 — 곡선 생략 가능" + + +def test_삼각_자리도_내각(texts): + assert texts["triangle"] == "내각 133° · 곡선 없음 — 직선이 그대로 꺾입니다" + assert texts["triangleOptional"].startswith("내각 168° · ") + + +def test_내각_모르면_곡선_없음만(texts): + assert texts["triangleUnknown"] == "곡선 없음 — 직선이 그대로 꺾입니다" + + +def test_화면이_같은_함수를_씀(): + label = (HERE.parents[1] / "B05_Profile" / "B05_Profile_UI_RouteEdit_Label.ts").read_text( + encoding="utf-8" + ) + assert "info.textContent = curveInfoText(state.curveOn, state.innerAngleDeg" in label