feat(B05 평면): 366-1 등고선 자물쇠 고른 때만 — 지나기만 하면 안 뜸 · 고른 가닥을 누른 자리 옆 · 잠김이면 잠근 높이 아무 가닥 누를 때 풀기 단추 · 다음 클릭 · 휠 · 떠남 0.7 s 에 숨김
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
This commit is contained in:
@@ -194,7 +194,7 @@ export async function mountRouteEditor(
|
||||
/** 등고선 자물쇠(353-2) — 잠긴 동안 빈 곳 · 다른 가닥을 눌러도 고른 것이 안 바뀜. */
|
||||
const contourLock = createContourLock(
|
||||
canvas,
|
||||
() => contours && { layer: contours.layer, view, stepM: contourStepM(), rotation },
|
||||
() => contours && { ...contours, view, rotation, stepM: contourStepM(), picked: pickedContour },
|
||||
(index) => ((pickedContour = index), draw()),
|
||||
);
|
||||
/** 측점 횡단 미리보기 창 — 측점 눈금을 누르면 뜬다(계획서 0-9 ⑧). */
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
/* =============================================================================
|
||||
* B05_Profile_UI_RouteEdit_ContourLock.ts
|
||||
* 평면 등고선 자물쇠(353-2) — 등고선 위를 지나거나 고른 가닥 위에 뜨는 작은 단추.
|
||||
* 평면 등고선 자물쇠(353-2) — 가닥을 고른 때만 누른 자리 옆에 뜨는 작은 단추(366-1 · 지나기만 = 안 뜸).
|
||||
*
|
||||
* 누르면 그 높이를 고정(빈 곳 · 다른 가닥을 눌러도 고른 것이 안 바뀜) · 다시 누르면 풂.
|
||||
* 누르면 그 높이를 고정(빈 곳 · 다른 가닥을 눌러도 고른 것이 안 바뀜) · 잠근 높이 가닥을 다시
|
||||
* 누르면 풀기 단추 · 다음 클릭 · 휠 · 떠남(0.7 s)에 숨김.
|
||||
* 높이값으로 쥐어 지형을 바꿔도 같은 높이를 다시 짚음 · 페이지 동안만(저장 안 함).
|
||||
* 규칙은 `logic/B05_Profile_ContourLock` · 호버 판정은 집기와 같은 `hitPreparedLayer`.
|
||||
* 규칙은 `logic/B05_Profile_ContourLock` · 누름 판정은 집기와 같은 `hitPreparedLayer`.
|
||||
* ========================================================================== */
|
||||
|
||||
import { pickPx } from "../A00_Common/touch_bridge";
|
||||
@@ -22,8 +23,10 @@ import {
|
||||
import "./B05_Profile_UI_Style_RouteEdit_ContourLock.css";
|
||||
|
||||
/** 집기(`_Pointer` CONTOUR_HIT_PX)와 같은 거리(px). */
|
||||
const HOVER_HIT_PX = 6;
|
||||
/** 커서가 선을 떠난 뒤 단추가 남는 시간(ms) — 단추로 옮겨 갈 틈. */
|
||||
const HIT_PX = 6;
|
||||
/** 누름 ~ 뗌 사이 이만큼(px) 넘게 움직이면 끌기 — 고르기 아님. */
|
||||
const MOVE_PX = 4;
|
||||
/** 커서가 지도를 떠난 뒤 단추가 남는 시간(ms) — 단추로 옮겨 갈 틈. */
|
||||
const HIDE_DELAY_MS = 700;
|
||||
/** 단추는 커서 오른쪽 위로 이만큼(px) — 선과 커서를 안 가림. */
|
||||
const OFFSET_PX = 12;
|
||||
@@ -33,12 +36,14 @@ const LOCK_SVG = `<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="t
|
||||
<path class="b05-contourlock__shackle" d="M5 7V5a3 3 0 0 1 6 0v2" fill="none"
|
||||
stroke="currentColor" stroke-width="1.6"/></svg>`;
|
||||
|
||||
/** 호버 판정에 쓰는 지금 화면 — 등고선이 없으면 null. */
|
||||
/** 누름 판정에 쓰는 지금 화면 — 등고선이 없으면 null. */
|
||||
export interface ContourLockScene {
|
||||
layer: PreparedLayer;
|
||||
view: ViewState;
|
||||
/** 지금 화면 등고선 간격 — 그리지 않은 줄은 호버도 안 됨. */
|
||||
/** 지금 화면 등고선 간격 — 그리지 않은 줄은 안 잡힘. */
|
||||
stepM: number;
|
||||
/** 고른 가닥 번호(없으면 -1) — 안 잠김이면 이 가닥을 누를 때만 뜸. */
|
||||
picked: number;
|
||||
rotation: { unrotate: (px: number, py: number) => [number, number] };
|
||||
}
|
||||
|
||||
@@ -68,8 +73,8 @@ export function createContourLock(
|
||||
/** 단추가 가리키는 가닥 — 누르면 이 가닥을 잠금. */
|
||||
let target = -1;
|
||||
let hideTimer = 0;
|
||||
let frame = 0;
|
||||
let last: PointerEvent | null = null;
|
||||
let downX = 0;
|
||||
let downY = 0;
|
||||
|
||||
const levelOf = (index: number): number | null =>
|
||||
index >= 0 ? (scene()?.layer.features[index]?.labelValue ?? null) : null;
|
||||
@@ -96,17 +101,24 @@ export function createContourLock(
|
||||
hideTimer = 0;
|
||||
};
|
||||
|
||||
const hover = (event: PointerEvent): void => {
|
||||
const hide = (): void => {
|
||||
keep();
|
||||
button.hidden = true;
|
||||
};
|
||||
|
||||
/** 누름을 뗀 자리 — 고른 가닥(잠김이면 잠근 높이 가닥)일 때만 그 옆에 띄움. */
|
||||
const showAt = (event: PointerEvent): void => {
|
||||
const now = scene();
|
||||
// 단추를 누른 채(노드 · 손잡이 끌기 · 화면 이동) 지나가면 안 봄
|
||||
if (!now || event.buttons !== 0) return hideLater();
|
||||
// 끌기 · 화면 이동 · 재기(Shift)는 고르기가 아님
|
||||
const moved = Math.hypot(event.clientX - downX, event.clientY - downY) > MOVE_PX;
|
||||
if (!now || event.button !== 0 || event.shiftKey || moved) return;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const [px, py] = now.rotation.unrotate(event.clientX - rect.left, event.clientY - rect.top);
|
||||
const hitPx = pickPx(HOVER_HIT_PX);
|
||||
const hitPx = pickPx(HIT_PX);
|
||||
const index = hitPreparedLayer(now.layer, now.view, px, py, hitPx, now.stepM);
|
||||
const state = lockButtonState(levelOf(index), lockedLevel);
|
||||
if (!state.visible) return hideLater();
|
||||
keep();
|
||||
if (!lockButtonState(levelOf(index), lockedLevel, index >= 0 && index === now.picked).visible) {
|
||||
return;
|
||||
}
|
||||
target = index;
|
||||
const origin = wrap.getBoundingClientRect();
|
||||
button.style.left = `${Math.round(event.clientX - origin.left + OFFSET_PX)}px`;
|
||||
@@ -115,17 +127,14 @@ export function createContourLock(
|
||||
paint();
|
||||
};
|
||||
|
||||
// 한 프레임에 한 번만 잼 — 도엽 등고선은 수천 가닥.
|
||||
canvas.addEventListener("pointermove", (event) => {
|
||||
last = event;
|
||||
if (frame) return;
|
||||
frame = requestAnimationFrame(() => {
|
||||
frame = 0;
|
||||
if (last) hover(last);
|
||||
});
|
||||
// 다음 클릭 · 휠에 숨김 — 집기(`_Pointer`)보다 먼저 달려 있어 고른 뒤 pointerup 이 다시 띄움.
|
||||
canvas.addEventListener("pointerdown", (event) => {
|
||||
downX = event.clientX;
|
||||
downY = event.clientY;
|
||||
hide();
|
||||
});
|
||||
// 고른 직후(누름을 뗀 자리)에도 띄움 — 터치는 호버가 없음.
|
||||
canvas.addEventListener("pointerup", hover);
|
||||
canvas.addEventListener("pointerup", showAt);
|
||||
canvas.addEventListener("wheel", hide, { passive: true });
|
||||
canvas.addEventListener("pointerleave", hideLater);
|
||||
button.addEventListener("pointerenter", keep);
|
||||
button.addEventListener("pointerleave", hideLater);
|
||||
|
||||
@@ -21,13 +21,15 @@ export function featureOfLevel(levels: ContourLevels, level: number | null): num
|
||||
return levels.findIndex((value) => value === level);
|
||||
}
|
||||
|
||||
/** 커서 아래 가닥 높이 · 잠근 높이 → 단추 — 안 잠김 = 높이 있는 가닥 위에서 · 잠김 = 그 높이 위에서만. */
|
||||
/** 누른 가닥 높이 · 잠근 높이 · 고른 가닥인지 → 단추(366-1) — 안 잠김 = 높이 있는 고른 가닥을
|
||||
* 누를 때만 · 잠김 = 잠근 높이의 아무 가닥을 누를 때(풀기). 지나가기만 하면 안 뜸. */
|
||||
export function lockButtonState(
|
||||
hoverLevel: number | null,
|
||||
hitLevel: number | null,
|
||||
lockedLevel: number | null,
|
||||
selected: boolean,
|
||||
): LockButtonState {
|
||||
if (lockedLevel !== null) return { visible: hoverLevel === lockedLevel, pressed: true };
|
||||
return { visible: hoverLevel !== null, pressed: false };
|
||||
if (lockedLevel !== null) return { visible: hitLevel === lockedLevel, pressed: true };
|
||||
return { visible: hitLevel !== null && selected, pressed: false };
|
||||
}
|
||||
|
||||
/** 단추 누름 — 잠김이면 풂(null) · 아니면 그 가닥 높이로 잠금. */
|
||||
|
||||
@@ -26,10 +26,11 @@ new Function("exports", "module", js)(m.exports, m);
|
||||
const L = m.exports;
|
||||
const levels = [101, 102, 105, 102, null];
|
||||
console.log(JSON.stringify({
|
||||
hoverFree: L.lockButtonState(102, null),
|
||||
hoverNone: L.lockButtonState(null, null),
|
||||
hoverLocked: L.lockButtonState(102, 102),
|
||||
hoverOther: L.lockButtonState(105, 102),
|
||||
hitPicked: L.lockButtonState(102, null, true),
|
||||
hitOnly: L.lockButtonState(102, null, false),
|
||||
hitNone: L.lockButtonState(null, null, true),
|
||||
hitLocked: L.lockButtonState(102, 102, false),
|
||||
hitOther: L.lockButtonState(105, 102, true),
|
||||
lock: L.toggleLock(null, 102),
|
||||
unlock: L.toggleLock(102, 105),
|
||||
pickFree: L.canPickContour(null),
|
||||
@@ -50,14 +51,16 @@ def got() -> dict:
|
||||
return json.loads(run.stdout)
|
||||
|
||||
|
||||
def test_안_잠김_단추는_높이_있는_가닥_위에서(got) -> None:
|
||||
assert got["hoverFree"] == {"visible": True, "pressed": False}
|
||||
assert got["hoverNone"]["visible"] is False
|
||||
def test_안_잠김_단추는_고른_가닥을_누를_때만(got) -> None:
|
||||
"""366-1 — 지나기만 · 고르지 않은 가닥 = 안 뜸."""
|
||||
assert got["hitPicked"] == {"visible": True, "pressed": False}
|
||||
assert got["hitOnly"]["visible"] is False
|
||||
assert got["hitNone"]["visible"] is False
|
||||
|
||||
|
||||
def test_잠김_단추는_그_높이_위에서만_눌린_모양(got) -> None:
|
||||
assert got["hoverLocked"] == {"visible": True, "pressed": True}
|
||||
assert got["hoverOther"]["visible"] is False
|
||||
def test_잠김_단추는_잠근_높이_아무_가닥을_누를_때_풀기(got) -> None:
|
||||
assert got["hitLocked"] == {"visible": True, "pressed": True}
|
||||
assert got["hitOther"]["visible"] is False
|
||||
|
||||
|
||||
def test_누르면_잠금_다시_누르면_풂(got) -> None:
|
||||
@@ -79,3 +82,9 @@ def test_편집기_이음() -> None:
|
||||
lock = (B05 / "B05_Profile_UI_RouteEdit_ContourLock.ts").read_text("utf-8")
|
||||
assert "hitPreparedLayer(" in lock and 'setAttribute("aria-pressed"' in lock
|
||||
assert "sessionStorage" not in lock and "writeState" not in lock # 페이지 동안만
|
||||
# 366-1 — 지나기(pointermove)로 안 띄움 · 뗀 자리 · 다음 클릭 · 휠 · 떠남에 숨김
|
||||
assert '"pointermove"' not in lock
|
||||
assert 'canvas.addEventListener("pointerup", showAt);' in lock
|
||||
assert 'canvas.addEventListener("wheel", hide' in lock
|
||||
assert 'canvas.addEventListener("pointerleave", hideLater);' in lock
|
||||
assert "picked: pickedContour" in edit
|
||||
|
||||
Reference in New Issue
Block a user