feat(B05 평면 도구줄): 구분선 · 스냅 간격 고르기 · Ctrl+휠 5° 회전 · [기울기] 기본 꺼짐 (390-1 · 2 · 3)
- [측점 스냅] · 간격 | 분석 지형 구분선(분석 지형 숨으면 같이 숨김) · [기울기] 기본 끔 - 스냅 간격 0.1 · 1 · 10 m · 측점 No.(기본) — 프로젝트별 화면 기억 · 스냅 끔이면 흐림 · MC 를 간격 배수에 · 허용 오차 간격 따라(0.1 m = 0.005 m) - Ctrl+휠 한 칸 5° 회전(단추 15° 그대로 · 손가락 · 트랙패드 벌림 = 확대 · 추천 지도 안 돎) · 도움말 한 줄 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
This commit is contained in:
@@ -56,7 +56,7 @@ function columns(planned, lock = []) {
|
||||
};
|
||||
}
|
||||
|
||||
function measure(planned) {
|
||||
function measure(planned, interval = INTERVAL) {
|
||||
const built = curve.buildEditedPolyline(
|
||||
planned,
|
||||
planned.map(() => true),
|
||||
@@ -74,9 +74,9 @@ function measure(planned) {
|
||||
return {
|
||||
curves: built.curves.length,
|
||||
mcs,
|
||||
residues: mcs.map((mc) => Math.abs(mc - Math.round(mc / INTERVAL) * INTERVAL)),
|
||||
residues: mcs.map((mc) => Math.abs(mc - Math.round(mc / interval) * interval)),
|
||||
innerStations: total.filter((s) => s > 0 && s < length).length,
|
||||
off: fit.offStationCount(built.vertices, built.curves, INTERVAL),
|
||||
off: fit.offStationCount(built.vertices, built.curves, interval),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -96,6 +96,15 @@ console.log(
|
||||
fromLater: { ...measure(fromLater), moved: moved(ROUTE, fromLater) },
|
||||
everyLocked: { ...measure(everyLocked), moved: moved(ROUTE, everyLocked) },
|
||||
again: moved(all, fit.fitCurveStations(columns(all), LIMITS, 1)),
|
||||
// 390-2 스냅 간격 — 0.1 · 1 · 10 m 배수 · 측점 밖 개수도 같은 간격
|
||||
steps: Object.fromEntries(
|
||||
[0.1, 1, 10].map((step) => {
|
||||
const fitted = fit.fitCurveStations(columns(ROUTE), { ...LIMITS, intervalM: step }, 1);
|
||||
const offBefore = measure(ROUTE, step).off;
|
||||
return [step, { offBefore, ...measure(fitted, step), moved: moved(ROUTE, fitted) }];
|
||||
}),
|
||||
),
|
||||
tol: [0.1, 1, 10, 20].map((step) => fit.mcTolM(step)),
|
||||
noInterval: moved(ROUTE, fit.fitCurveStations(columns(ROUTE), { ...LIMITS, intervalM: 0 }, 1)),
|
||||
start: {
|
||||
moved4: fit.fitStart(
|
||||
|
||||
@@ -165,8 +165,51 @@ const afterInsert = JSON.parse(JSON.stringify(columns));
|
||||
edits.spliceNode(columns, 1);
|
||||
const afterRemove = JSON.parse(JSON.stringify(columns));
|
||||
|
||||
// ⑤ Ctrl+휠 = 5° 돌리기(390-3) · 손가락 · 트랙패드 벌림 · 맨 휠 = 확대 · rotateWheel 없으면(추천 지도) 확대
|
||||
const input = loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_UI_RouteEdit_Input.ts"));
|
||||
const rotateMod = loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_UI_RouteEdit_Rotate.ts"));
|
||||
function wheelCase(withRotate, event) {
|
||||
const handlers = {};
|
||||
const canvas = {
|
||||
addEventListener: (name, fn) => (handlers[name] = fn),
|
||||
getBoundingClientRect: () => ({ left: 0, top: 0, width: 400, height: 300 }),
|
||||
};
|
||||
let wheelView = {
|
||||
width: 400,
|
||||
height: 300,
|
||||
scale: 1,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
mapRect: { width: 400 },
|
||||
};
|
||||
let changes = 0;
|
||||
const rotation = rotateMod.createMapRotation({
|
||||
overlay: { querySelector: () => null },
|
||||
size: () => wheelView,
|
||||
onChange: () => (changes += 1),
|
||||
});
|
||||
input.bindRouteEditNavigation({
|
||||
canvas,
|
||||
getView: () => wheelView,
|
||||
setView: (next) => (wheelView = next),
|
||||
getMeta: () => null,
|
||||
rotateWheel: withRotate ? rotation.wheel : undefined,
|
||||
draw: () => {},
|
||||
});
|
||||
handlers.wheel({ clientX: 200, clientY: 150, preventDefault: () => {}, ...event });
|
||||
return { deg: (rotation.radians() * 180) / Math.PI, scale: wheelView.scale, changes };
|
||||
}
|
||||
const wheel = {
|
||||
ctrlDown: wheelCase(true, { ctrlKey: true, deltaY: 100 }),
|
||||
ctrlUp: wheelCase(true, { ctrlKey: true, deltaY: -100 }),
|
||||
trackpad: wheelCase(true, { ctrlKey: true, deltaY: -4 }),
|
||||
plain: wheelCase(true, { ctrlKey: false, deltaY: -100 }),
|
||||
recommend: wheelCase(false, { ctrlKey: true, deltaY: 100 }),
|
||||
};
|
||||
|
||||
process.stdout.write(
|
||||
JSON.stringify({
|
||||
wheel,
|
||||
saved,
|
||||
again,
|
||||
readBack,
|
||||
|
||||
@@ -65,6 +65,21 @@ def test_맞춘_선은_다시_맞춰도_그대로_간격_0_은_안_맞춤(got) -
|
||||
assert max(got["again"]) == 0 and max(got["noInterval"]) == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("step", ["0.1", "1", "10"])
|
||||
def test_스냅_간격_배수에_맞춤(got, step) -> None:
|
||||
"""390-2 — MC 를 고른 간격 배수에 · 측점 밖 개수도 같은 간격 · 밀림은 간격 절반 안."""
|
||||
size = float(step)
|
||||
case = got["steps"][step]
|
||||
assert case["offBefore"] == 5 and case["off"] == 0 and case["curves"] == 5
|
||||
assert max(case["residues"]) <= min(TOL, size / 20)
|
||||
assert max(case["moved"]) <= size / 2 + 1e-6
|
||||
assert case["moved"][0] == 0 and case["moved"][-1] == 0
|
||||
|
||||
|
||||
def test_허용_오차는_간격_따라(got) -> None:
|
||||
assert got["tol"] == pytest.approx([0.005, 0.05, 0.05, 0.05])
|
||||
|
||||
|
||||
def test_시작_자리는_고친_꺾임점_앞(got) -> None:
|
||||
start = got["start"]
|
||||
assert start["moved4"] == 3 and start["radius2"] == 1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""381-2 B05 평면 [측점] [기울기] 켬끔(요구 241-11).
|
||||
|
||||
- 단추 = 절/성토선 왼쪽 · 켬끔 모듈(기본 켬 · 페이지 동안)
|
||||
- 단추 = 절/성토선 왼쪽 · 켬끔 모듈(기본 [측점] 켬 · [기울기] 끔(390-1 요구 242-17) · 페이지 동안)
|
||||
- [측점] 끔 = 눈금 · 번호 안 그림 · 눈금 집기 안 함(라벨 0개 = 전부 집힘 함정) · 시점 · 종점 이름표 남김
|
||||
- [기울기] 끔 = 기울기 라벨(높이 묻기) 안 부름
|
||||
"""
|
||||
@@ -14,9 +14,9 @@ def _read(name: str) -> str:
|
||||
return (B05 / name).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_켬끔_모듈_기본_켬():
|
||||
def test_켬끔_모듈_기본_측점_켬_기울기_끔():
|
||||
marks = _read("B05_Profile_UI_RouteEdit_Marks.ts")
|
||||
assert "{ stations: true, slopes: true }" in marks
|
||||
assert "{ stations: true, slopes: false }" in marks
|
||||
assert "scheduleRedraw();" in marks
|
||||
assert "export function scheduleRedraw" in _read("B05_Profile_UI_RouteEdit_Footprint.ts")
|
||||
|
||||
|
||||
@@ -208,7 +208,7 @@ def test_노선_추천_다듬기_23_3():
|
||||
return open(os.path.join(page, name), encoding="utf-8").read()
|
||||
|
||||
chrome = read("B05_Profile_UI_RouteEdit_Chrome.ts")
|
||||
# 366-2 — [?] | ↶ ↷ | 3D | ⟲ ⟳ · 1m 등고선 · 절/성토선 · 거리 재기 | 측점 스냅 · 분석 지형(맨 오른쪽)
|
||||
# 366-2 — [?] | ↶ ↷ | 3D | ⟲ ⟳ · 1m 등고선 · 절/성토선 · 거리 재기 | 측점 스냅 · 간격 | 분석 지형(맨 오른쪽)
|
||||
bar = chrome[
|
||||
chrome.index("b05-routeedit__tools") : chrome.index('<div class="b05-routeedit__hint">')
|
||||
]
|
||||
@@ -229,14 +229,20 @@ def test_노선_추천_다듬기_23_3():
|
||||
'data-act="measure"',
|
||||
"|",
|
||||
'data-act="station-snap"',
|
||||
'class="b05-routeedit__terrain"',
|
||||
'data-act="snap-step"', # 390-2 스냅 간격 = [측점 스냅] 오른쪽
|
||||
"|", # 390-1 [측점 스냅] | 분석 지형
|
||||
'class="b05-routeedit__terrain" hidden',
|
||||
]
|
||||
at = 0
|
||||
for mark in order:
|
||||
key = "b05-route__view-separator" if mark == "|" else mark
|
||||
at = bar.index(key, at) + 1
|
||||
assert bar.count("b05-route__view-separator") == 4
|
||||
assert chrome.count("b05-route__view-separator") == 4
|
||||
assert bar.count("b05-route__view-separator") == 5
|
||||
assert chrome.count("b05-route__view-separator") == 5
|
||||
# 분석 지형이 숨으면 앞 구분선도 숨김 · 스냅 끔이면 간격 고르기 흐림
|
||||
style = read("B05_Profile_UI_Style_RouteEdit.css")
|
||||
assert ".b05-route__view-separator:has(+ .b05-routeedit__terrain[hidden])" in style
|
||||
assert '[data-act="station-snap"][aria-pressed="false"] + .b05-routeedit__snap-step' in style
|
||||
assert "b05-routeedit__spin" not in chrome # 회전 = 도구줄(추천 모달 CSS 는 남음)
|
||||
shared = read("B05_Profile_UI_RouteEdit_Recommend_Map.ts")
|
||||
for name in ("bindRouteEditNavigation", "createMapRotation", "fitCanvas", "metricToScreen"):
|
||||
|
||||
@@ -84,3 +84,13 @@ def test_숨김_크기_0_뒤_다시_보이면_그대로(results):
|
||||
assert back["cx"] == pytest.approx(kept["cx"], abs=1e-6)
|
||||
assert back["cy"] == pytest.approx(kept["cy"], abs=1e-6)
|
||||
assert back["pxPerMeter"] == pytest.approx(kept["pxPerMeter"], rel=1e-9)
|
||||
|
||||
|
||||
def test_Ctrl_휠은_5도_돌리기_벌림과_맨_휠은_확대(results):
|
||||
"""390-3 — 마우스 Ctrl+휠 한 칸 = 5° · 트랙패드 벌림 · 맨 휠 = 확대 · 추천 지도(rotateWheel 없음) = 확대."""
|
||||
w = results["wheel"]
|
||||
assert w["ctrlDown"] == {"deg": pytest.approx(5), "scale": 1, "changes": 1}
|
||||
assert w["ctrlUp"]["deg"] == pytest.approx(-5) and w["ctrlUp"]["scale"] == 1
|
||||
for case in ("trackpad", "plain", "recommend"):
|
||||
assert w[case]["deg"] == 0 and w[case]["changes"] == 0, case
|
||||
assert w[case]["scale"] != 1, case
|
||||
|
||||
Reference in New Issue
Block a user