Merge remote-tracking branches 'origin/sub_laptop_1' and 'origin/sub_laptop_3' into main_laptop_1
This commit is contained in:
@@ -534,6 +534,7 @@ export async function mountRouteEditor(
|
||||
const base = stationSnap ? history?.current?.()?.planned : undefined;
|
||||
if (base) planned = fit.fitDrag(snapshotNow(), base, index, fitLimits());
|
||||
if (base) markEdited();
|
||||
return base !== undefined;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -29,8 +29,8 @@ interface Hold {
|
||||
index: number;
|
||||
before: Vertex[];
|
||||
start: Vertex;
|
||||
/** 키로 옮긴 자리(맞추기 전) — 다음 걸음은 여기서(맞춤이 쌓이지 않음). */
|
||||
at: Vertex;
|
||||
/** 스냅 미리보기 전 노선(맞추기 전) — 다음 걸음 · 하한 검사는 여기서(맞춤이 쌓이지 않음). */
|
||||
raw: Vertex[] | null;
|
||||
moved: boolean;
|
||||
}
|
||||
|
||||
@@ -41,15 +41,25 @@ export function bindRouteEditKeys(params: RouteEditPointerParams, dragging: () =
|
||||
|
||||
const metricAt = (x: number, y: number): Vertex => params.toMetric(...params.unrotate(x, y));
|
||||
|
||||
/** 스냅 미리보기 — 맞추기 전 노선을 쥔 뒤 맞춤(끔이면 쥐지 않음). */
|
||||
function fitPreview(now: Hold, planned: Vertex[]): void {
|
||||
const raw = planned.map((vertex): Vertex => [vertex[0], vertex[1]]);
|
||||
now.raw = params.fitDrag(now.index) ? raw : null;
|
||||
}
|
||||
|
||||
/** 한 걸음 — 하한을 새로 깨면 그 걸음만 되돌림(끌기와 같은 검사). */
|
||||
function step(now: Hold): void {
|
||||
const state = params.state();
|
||||
if (now.raw) {
|
||||
now.raw.forEach((vertex, index) => (state.planned[index] = vertex));
|
||||
params.markEdited();
|
||||
}
|
||||
const node = state.planned[now.index];
|
||||
if (!node) return;
|
||||
const snapStepM = params.stations() !== null ? params.snapStepM() : undefined;
|
||||
const [dx, dy] = arrowStep(now.key, now.shift, metricAt, snapStepM);
|
||||
const before = curveShortfalls(state.nodeInfo, state.limitRadiusM, state.limitArcM);
|
||||
state.planned[now.index] = [now.at[0] + dx, now.at[1] + dy];
|
||||
state.planned[now.index] = [node[0] + dx, node[1] + dy];
|
||||
params.markEdited();
|
||||
const after = params.state();
|
||||
if (
|
||||
@@ -57,6 +67,7 @@ export function bindRouteEditKeys(params: RouteEditPointerParams, dragging: () =
|
||||
) {
|
||||
state.planned[now.index] = node;
|
||||
params.markEdited();
|
||||
fitPreview(now, state.planned);
|
||||
status.textContent =
|
||||
`${params.routeHead()} — 하한에 걸려 더 못 옮깁니다` +
|
||||
`(곡선반지름 ${state.limitRadiusM}m${state.limitArcM > 0 ? ` · 곡선 길이 ${state.limitArcM}m` : ""}).`;
|
||||
@@ -64,8 +75,7 @@ export function bindRouteEditKeys(params: RouteEditPointerParams, dragging: () =
|
||||
return;
|
||||
}
|
||||
now.moved = true;
|
||||
now.at = state.planned[now.index];
|
||||
params.fitDrag(now.index);
|
||||
fitPreview(now, state.planned);
|
||||
status.textContent = `${params.routeHead()} — 방향키로 옮기는 중(뗄 때 한 걸음). ${params.curveHint()}`;
|
||||
params.draw();
|
||||
}
|
||||
@@ -112,7 +122,7 @@ export function bindRouteEditKeys(params: RouteEditPointerParams, dragging: () =
|
||||
index,
|
||||
before: state.planned.map(([x, y]): Vertex => [x, y]),
|
||||
start: [node[0], node[1]],
|
||||
at: [node[0], node[1]],
|
||||
raw: null,
|
||||
moved: false,
|
||||
};
|
||||
hold = now;
|
||||
|
||||
@@ -101,8 +101,9 @@ export interface RouteEditPointerParams {
|
||||
commit: () => void;
|
||||
/** 스냅 간격(m · 390-5) — 스냅 켬이면 방향키 한 걸음. */
|
||||
snapStepM: () => number;
|
||||
/** 끄는 중 · 방향키 걸음 미리보기(390-5) — 스냅 켬이면 IP `index` 자리 그대로 MC 를 간격 배수에. */
|
||||
fitDrag: (index: number) => void;
|
||||
/** 끄는 중 · 방향키 걸음 미리보기(390-5) — 스냅 켬이면 IP `index` 자리 그대로 MC 를 간격 배수에 ·
|
||||
* 맞췄으면 true(끔이면 false · 아무 일 없음). */
|
||||
fitDrag: (index: number) => boolean;
|
||||
}
|
||||
|
||||
export interface RouteEditPointer {
|
||||
@@ -188,6 +189,8 @@ export function bindRouteEditPointer(params: RouteEditPointerParams): RouteEditP
|
||||
/** 이번 끌기에서 **실제로 움직였나** — 그냥 눌러 고르기만 한 것은 되돌릴 걸음이 아니다
|
||||
* (2026-09-07 실화면: 노드를 클릭만 해도 [되돌리기]가 켜졌다). */
|
||||
let dragMoved = false;
|
||||
/** 스냅 미리보기 전 자리(390-5) — 하한 검사는 맞춘 선이 아닌 이것으로(맞춤이 IP 를 ±간격/2 밂). */
|
||||
let dragRaw: Vertex[] | null = null;
|
||||
|
||||
canvas.addEventListener("pointerdown", (event) => {
|
||||
if (event.button !== 0) return;
|
||||
@@ -208,6 +211,7 @@ export function bindRouteEditPointer(params: RouteEditPointerParams): RouteEditP
|
||||
const hitNode = pick && "node" in pick ? pick.node : -1;
|
||||
// 교각점을 고정한 자리는 **고르기만** 되고 안 끌린다(2026-09-12 사용자 지시 ①).
|
||||
dragNode = hitNode >= 0 && state.apexLock[hitNode] !== true ? hitNode : -1;
|
||||
dragRaw = null;
|
||||
dragHandle = hitNode >= 0 ? null : handleAt(px, py);
|
||||
dragMoved = false;
|
||||
if (hitNode >= 0) {
|
||||
@@ -274,6 +278,12 @@ export function bindRouteEditPointer(params: RouteEditPointerParams): RouteEditP
|
||||
canvas.setPointerCapture(event.pointerId);
|
||||
});
|
||||
|
||||
/** 스냅 미리보기 — 맞추기 전 자리를 쥔 뒤 맞춤(끔이면 쥐지 않음). */
|
||||
const fitPreview = (planned: Vertex[]): void => {
|
||||
const raw = planned.map((vertex): Vertex => [vertex[0], vertex[1]]);
|
||||
dragRaw = params.fitDrag(dragNode) ? raw : null;
|
||||
};
|
||||
|
||||
canvas.addEventListener("pointermove", (event) => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const [px, py] = params.unrotate(event.clientX - rect.left, event.clientY - rect.top);
|
||||
@@ -303,6 +313,10 @@ export function bindRouteEditPointer(params: RouteEditPointerParams): RouteEditP
|
||||
return;
|
||||
}
|
||||
if (dragNode >= 0) {
|
||||
if (dragRaw) {
|
||||
dragRaw.forEach((vertex, index) => (state.planned[index] = vertex));
|
||||
params.markEdited();
|
||||
}
|
||||
// 옮기기 **전**에 하한을 지키던 자리 — 이미 밑돌던 자리는 그대로 고칠 수 있어야 하므로
|
||||
// **지키던 자리가 넘어가는 것만** 막는다(계획서 0-9 ④).
|
||||
const before = curveShortfalls(state.nodeInfo, state.limitRadiusM, state.limitArcM);
|
||||
@@ -320,13 +334,14 @@ export function bindRouteEditPointer(params: RouteEditPointerParams): RouteEditP
|
||||
// 접선 자리가 모자라 R 이 하한 아래로 눌리는 자리다 — 그 걸음만 되돌린다.
|
||||
state.planned[dragNode] = previous;
|
||||
params.markEdited();
|
||||
fitPreview(state.planned);
|
||||
status.textContent =
|
||||
`${params.routeHead()} — 하한에 걸려 더 못 옮깁니다` +
|
||||
`(곡선반지름 ${state.limitRadiusM}m${state.limitArcM > 0 ? ` · 곡선 길이 ${state.limitArcM}m` : ""}).`;
|
||||
params.draw();
|
||||
return;
|
||||
}
|
||||
params.fitDrag(dragNode); // 스냅 켬 = 끄는 동안 MC 를 간격 배수에(390-5 · 놓을 때 정본)
|
||||
fitPreview(state.planned); // 스냅 켬 = 끄는 동안 MC 를 간격 배수에(390-5 · 놓을 때 정본)
|
||||
// 끄는 동안에도 상태줄이 살아 있어야 한다 — 예전에는 여기서 아무 말이 없어
|
||||
// 「곡선이 사라졌다」는 인상만 남았다(2026-09-07 사용자 지적 ②).
|
||||
status.textContent = `${params.routeHead()} — 옮기는 중. ${params.curveHint()}`;
|
||||
@@ -370,6 +385,7 @@ export function bindRouteEditPointer(params: RouteEditPointerParams): RouteEditP
|
||||
dragNode = -1;
|
||||
dragHandle = null;
|
||||
dragMoved = false;
|
||||
dragRaw = null;
|
||||
// 끄는 동안 숨겨 둔 기울기 라벨 — 놓은 노선 높이를 한 번 묻게 다시 그린다(계획서 18 ①).
|
||||
if (moved) {
|
||||
params.slopeRefresh();
|
||||
|
||||
@@ -200,6 +200,11 @@
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* 접힌 손잡이 = 지도 오른쪽 끝에 딱 붙임(393-5 · 빈 틈 0) */
|
||||
.b05-routeedit__side-wrap:has(.ui-side-panels.is-collapsed) {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.b05-routeedit__side-wrap .ui-side-panels__panel {
|
||||
box-shadow: 0 12px 40px rgb(0 0 0 / 45%);
|
||||
}
|
||||
@@ -236,11 +241,11 @@
|
||||
background: var(--color-surface-raised);
|
||||
}
|
||||
|
||||
/* 측점 = 그래프 왼쪽 위 작은 글(393-4 · 제목 행 없음) */
|
||||
/* 측점 = 그래프 왼쪽 위 작은 글(393-4 · 제목 행 없음) — 그림 칸 안(높이 축 글 · B06 CROSS_PAD 58 · 10 비킴) */
|
||||
.b05-routeedit__cross-station {
|
||||
position: absolute;
|
||||
top: calc(var(--spacing-12) + 4px);
|
||||
left: calc(var(--spacing-12) + 6px);
|
||||
top: calc(var(--spacing-12) + 14px);
|
||||
left: calc(var(--spacing-12) + 64px);
|
||||
z-index: 1;
|
||||
padding: 0 4px;
|
||||
border-radius: var(--radius-4, 4px);
|
||||
|
||||
@@ -98,6 +98,9 @@ def test_끌기_방향키_편집기_이음() -> None:
|
||||
assert "params.fitDrag(dragNode)" in pointer
|
||||
assert "params.fitDrag(now.index)" in keys and "params.snapStepM()" in keys
|
||||
assert "fit.fitDrag(snapshotNow(), base, index, fitLimits())" in edit
|
||||
# 하한 검사는 맞추기 전 노선으로(맞춤이 IP 를 ±간격/2 밀어 10 m · 측점 No. 에서 헛걸림)
|
||||
assert "dragRaw.forEach((vertex, index) => (state.planned[index] = vertex))" in pointer
|
||||
assert "now.raw.forEach((vertex, index) => (state.planned[index] = vertex))" in keys
|
||||
assert edit.count("\n") <= 695
|
||||
|
||||
|
||||
|
||||
@@ -129,3 +129,13 @@ def test_페이지_수명_닫기_없음():
|
||||
assert "container: overlay," in main
|
||||
apply = _read("B05_Profile_UI_RouteEdit_Apply.ts")
|
||||
assert "close" not in apply.split("export function bindRouteApply", 1)[1]
|
||||
|
||||
|
||||
def test_393_5_접힌_손잡이는_지도_오른쪽_끝에_붙음():
|
||||
css = _read("B05_Profile_UI_Style_RouteEdit.css")
|
||||
flush = _block(css, ".b05-routeedit__side-wrap:has(.ui-side-panels.is-collapsed)")
|
||||
assert "right: 0;" in flush
|
||||
# 펼친 판은 그대로 띄움
|
||||
assert "right: var(--spacing-12);" in _block(css, ".b05-routeedit__side-wrap")
|
||||
side = (ROOT / "ui_template" / "ui_template_side_panels.ts").read_text(encoding="utf-8")
|
||||
assert 'root.classList.toggle("is-collapsed", now === null);' in side
|
||||
|
||||
Reference in New Issue
Block a user