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:
@@ -75,6 +75,8 @@ export const STATE_REGISTRY = {
|
||||
"routeedit-locks": { bucket: "pref", scope: "project" },
|
||||
/** B05 계획노선 편집 창 — 분석 지형(las · sheet). 확정 지표면은 안 바꿈 */
|
||||
"routeedit-terrain": { bucket: "pref", scope: "project" },
|
||||
/** B05 계획노선 편집 창 — 측점 스냅 간격(0.1 · 1 · 10 · station) */
|
||||
"routeedit-snap-step": { bucket: "pref", scope: "project" },
|
||||
"masshaul-open": {
|
||||
bucket: "pref",
|
||||
scope: "global",
|
||||
|
||||
@@ -91,6 +91,7 @@ import {
|
||||
import type { DesignPageHost } from "../A00_Common/design/design_page";
|
||||
import * as draftLink from "./B05_Profile_UI_RouteEdit_Draft";
|
||||
import { bindRouteShift } from "./B05_Profile_UI_RouteEdit_Shift";
|
||||
import { bindStationSnap, paintStationSnap } from "./B05_Profile_UI_RouteEdit_SnapStep";
|
||||
import * as fit from "./logic/B05_Profile_RouteEdit_StationFit";
|
||||
import "./B05_Profile_UI_Style_RouteEdit.css";
|
||||
|
||||
@@ -173,11 +174,7 @@ export async function mountRouteEditor(
|
||||
curveSpans(line, curveInfo),
|
||||
);
|
||||
};
|
||||
const setStationSnap = (on: boolean): void => {
|
||||
stationSnap = on;
|
||||
stationSnapButton.classList.toggle("is-active", on);
|
||||
stationSnapButton.setAttribute("aria-pressed", String(on));
|
||||
};
|
||||
const setStationSnap = (on: boolean) => paintStationSnap(stationSnapButton, (stationSnap = on));
|
||||
/** 되돌리기 사진첩 — 노선을 읽은 뒤에 선다(그전에는 되돌릴 것이 없다). */
|
||||
let history: RouteEditHistory | null = null;
|
||||
let meta: VWorldMeta | null = null;
|
||||
@@ -264,8 +261,8 @@ export async function mountRouteEditor(
|
||||
const slope = createSlopeLabels({ projectId, isClosed: () => closed, onChange: () => draw() });
|
||||
/** 1m 간격 등고선 켬끔 — 기본 꺼짐(계획서 18 ②). */
|
||||
const showMinorContours = bindMinorContourToggle(minorContourButton, () => draw());
|
||||
stationSnapButton.addEventListener("click", () => {
|
||||
setStationSnap(!stationSnap);
|
||||
const snapStepM = bindStationSnap(chrome, projectId, stationIntervalM, (toggle) => {
|
||||
if (toggle) setStationSnap(!stationSnap);
|
||||
if (stationSnap && history) applyEdit("곡선 가운데(MC)를 일반측점에 맞췄습니다."); // 전체 · 한 걸음
|
||||
draw();
|
||||
});
|
||||
@@ -416,7 +413,7 @@ export async function mountRouteEditor(
|
||||
violationCount: nodeInfo.filter((node) => node.violations.length).length,
|
||||
minRadiusM,
|
||||
fresh: nodeInfo.length === 0,
|
||||
offStation: stationSnap ? fit.offStationCount(plannedLine, curveInfo, stationIntervalM) : 0,
|
||||
offStation: stationSnap ? fit.offStationCount(plannedLine, curveInfo, snapStepM()) : 0,
|
||||
});
|
||||
|
||||
// ── 곡선 라벨 — 고른 꺾임점 옆에 뜨는 조작 패널. 배선은 `_CurveBar` 몫 ──
|
||||
@@ -450,7 +447,7 @@ export async function mountRouteEditor(
|
||||
/** 한 번의 편집을 마무리한다 — 다시 그리고, 라벨·상태줄을 맞추고, 되돌리기에 쌓는다.
|
||||
* 측점 스냅 켬이면 고친 자리 앞 곡선부터 MC 를 일반측점에(366-3 · 되살림 `fitting=false` 은 안 맞춤). */
|
||||
function applyEdit(message: string, record = true, fitting = true): void {
|
||||
const limits = { minRadiusM, limitRadiusM, limitArcM, intervalM: stationIntervalM };
|
||||
const limits = { minRadiusM, limitRadiusM, limitArcM, intervalM: snapStepM() };
|
||||
const from = fit.fitStart(history?.current?.(), snapshotNow());
|
||||
if (stationSnap && fitting) planned = fit.fitCurveStations(snapshotNow(), limits, from);
|
||||
markEdited();
|
||||
@@ -574,6 +571,7 @@ export async function mountRouteEditor(
|
||||
getMeta: () => meta,
|
||||
// 돌린 지도에서는 손이 민 방향과 그림이 움직일 방향이 다르다 — 거꾸로 돌려 넘긴다.
|
||||
unrotateDelta: rotation.unrotateDelta,
|
||||
rotateWheel: rotation.wheel, // Ctrl+휠 = 5° 돌리기(390-3)
|
||||
draw,
|
||||
});
|
||||
|
||||
|
||||
@@ -110,6 +110,16 @@ export function createRouteEditChrome(
|
||||
<button type="button" class="ui-btn ui-btn--glass" data-act="station-snap"
|
||||
aria-pressed="false"
|
||||
title="켜면 곡선 가운데(MC)를 가까운 일반측점에 맞춥니다 — 놓을 때 고친 자리부터 교각점을 간격 절반 안에서 밂 · 고정 교각점은 그대로 · 못 맞춘 곡선은 상태줄 「측점 밖 곡선」 · [저장] 때 저장">측점 스냅</button>
|
||||
<label class="b05-routeedit__terrain b05-routeedit__snap-step"
|
||||
title="측점 스냅 간격 — 곡선 가운데(MC)를 이 간격 배수에 맞춥니다(측점 No. = 측점 간격)">간격
|
||||
<select class="ui-select ui-select--sm" data-act="snap-step">
|
||||
<option value="0.1">0.1 m</option>
|
||||
<option value="1">1 m</option>
|
||||
<option value="10">10 m</option>
|
||||
<option value="station">측점 No.</option>
|
||||
</select>
|
||||
</label>
|
||||
<i class="b05-route__view-separator" aria-hidden="true"></i>
|
||||
<label class="b05-routeedit__terrain" hidden
|
||||
title="노선 편집 분석(등고선 · 지반고 · 기울기 · 횡단 미리보기 · 추천)에 쓸 지형 — 확정 지표면 · 종횡단은 안 바뀝니다">분석 지형
|
||||
<select class="ui-select ui-select--sm" data-act="terrain">
|
||||
@@ -124,6 +134,7 @@ export function createRouteEditChrome(
|
||||
<span>선 두 번 클릭 = 노드 추가</span><span>노드 오른쪽 클릭 = 삭제</span>
|
||||
<span>측점 눈금 클릭 = 횡단 미리보기</span><span>Shift+클릭 = 거리·기울기</span>
|
||||
<span>가운데(휠) 버튼 끌기 = 지도 이동</span><span>휠 = 확대</span>
|
||||
<span>Ctrl+휠 = 돌리기(한 칸 5°)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="b05-routeedit__measure" hidden>
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
* 있어 좌상단 기준으로 셈하면 확대할수록 커서 아래 지점이 밀린다(옛 모달의 버그).
|
||||
* ========================================================================== */
|
||||
|
||||
import { pickPx, wheelZoomsIn } from "../A00_Common/touch_bridge";
|
||||
import { isPinchWheel, pickPx, wheelZoomsIn } from "../A00_Common/touch_bridge";
|
||||
import { isTrackpadPinch } from "../A00_Common/input_prefs";
|
||||
import { computeMaxScale, type ViewState } from "../B04_PreProcess/B04_PreProcess_UI_MapRender";
|
||||
import type { VWorldMeta } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||
import type { StationLabelBox } from "../B04_PreProcess/B04_PreProcess_UI_MapOverlays";
|
||||
@@ -37,6 +38,8 @@ export interface RouteEditNavigationParams {
|
||||
unrotateDelta?: (dx: number, dy: number) => [number, number];
|
||||
/** 마우스 왼쪽 버튼으로도 팬 — 집을 노드가 없는 미리 보기 지도용. 편집 창은 가운데 버튼만. */
|
||||
leftPan?: boolean;
|
||||
/** Ctrl+휠 = 돌리기(390-3) — 안 주면 Ctrl+휠도 확대(추천 지도). 손가락 · 트랙패드 벌림은 늘 확대. */
|
||||
rotateWheel?: (sign: 1 | -1) => void;
|
||||
draw: () => void;
|
||||
}
|
||||
|
||||
@@ -50,6 +53,8 @@ export function bindRouteEditNavigation(params: RouteEditNavigationParams): void
|
||||
"wheel",
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
if (params.rotateWheel && event.ctrlKey && !isPinchWheel(event) && !isTrackpadPinch(event))
|
||||
return params.rotateWheel(Math.sign(event.deltaY) as 1 | -1); // 벌림 아님 = |deltaY| ≥ 50
|
||||
const view = getView();
|
||||
const maxScale = computeMaxScale(
|
||||
getMeta(),
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
*
|
||||
* [측점] 끔 = 측점 눈금 · 번호 안 그림 · 눈금 집기 안 함(시점 · 종점 이름표는 남음).
|
||||
* [기울기] 끔 = 측점 사이 기울기 라벨 안 그림 → 높이도 안 물음.
|
||||
* 켬끔은 이 페이지 동안만(기본 켬 · 계정 설정 아님) — `_Footprint` 와 같은 꼴.
|
||||
* 켬끔은 이 페이지 동안만(기본 [측점] 켬 · [기울기] 끔(요구 242-17) · 계정 설정 아님) — `_Footprint` 와 같은 꼴.
|
||||
* ========================================================================== */
|
||||
|
||||
import { scheduleRedraw } from "./B05_Profile_UI_RouteEdit_Footprint";
|
||||
|
||||
export type RouteEditMark = "stations" | "slopes";
|
||||
|
||||
const shown: Record<RouteEditMark, boolean> = { stations: true, slopes: true };
|
||||
const shown: Record<RouteEditMark, boolean> = { stations: true, slopes: false };
|
||||
|
||||
export const markShown = (mark: RouteEditMark): boolean => shown[mark];
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
/** 단추 한 번에 도는 각(도) — 15°씩 스물넷이면 한 바퀴. 90°씩이면 맞출 자리가 넷뿐이고,
|
||||
* 5°씩이면 한 바퀴에 일흔두 번이라 성가시다. */
|
||||
const ROTATE_STEP_DEG = 15;
|
||||
/** Ctrl+휠 한 칸에 도는 각(도 · 390-3 요구 242-16) — 단추보다 잘게. */
|
||||
const WHEEL_STEP_DEG = 5;
|
||||
|
||||
/** **글자를 눈높이로 세울지**(계획서 0-9 ㉚, 2026-09-12 사용자 지시).
|
||||
*
|
||||
@@ -43,6 +45,8 @@ export interface MapRotation {
|
||||
uprightRad: () => number;
|
||||
/** 기억해 둔 각으로 되돌린다(계획서 18 ③). 다시 그리기는 부르는 쪽 몫. */
|
||||
set: (radians: number) => void;
|
||||
/** Ctrl+휠 한 칸 — `sign` 1 = 시계 · -1 = 반시계로 `WHEEL_STEP_DEG` 돌고 알림. */
|
||||
wheel: (sign: 1 | -1) => void;
|
||||
}
|
||||
|
||||
export function createMapRotation(params: MapRotationParams): MapRotation {
|
||||
@@ -60,14 +64,18 @@ export function createMapRotation(params: MapRotationParams): MapRotation {
|
||||
return [cx + dx * cos - dy * sin, cy + dx * sin + dy * cos];
|
||||
};
|
||||
|
||||
const turn = (deg: number): void => {
|
||||
radians += (deg * Math.PI) / 180;
|
||||
params.onChange();
|
||||
};
|
||||
|
||||
for (const [act, sign] of [
|
||||
["rotate-ccw", -1],
|
||||
["rotate-cw", 1],
|
||||
] as const) {
|
||||
params.overlay.querySelector(`[data-act="${act}"]`)?.addEventListener("click", () => {
|
||||
radians += (sign * ROTATE_STEP_DEG * Math.PI) / 180;
|
||||
params.onChange();
|
||||
});
|
||||
params.overlay
|
||||
.querySelector(`[data-act="${act}"]`)
|
||||
?.addEventListener("click", () => turn(sign * ROTATE_STEP_DEG));
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -75,6 +83,7 @@ export function createMapRotation(params: MapRotationParams): MapRotation {
|
||||
unrotate: (px, py) => spin(px, py, -radians),
|
||||
rerotate: (px, py) => spin(px, py, radians),
|
||||
uprightRad: () => (UPRIGHT_LABELS ? -radians : 0),
|
||||
wheel: (sign) => turn(sign * WHEEL_STEP_DEG),
|
||||
set: (next) => {
|
||||
radians = Number.isFinite(next) ? next : 0;
|
||||
},
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/* =============================================================================
|
||||
* B05_Profile_UI_RouteEdit_SnapStep.ts
|
||||
* 노선 편집 [측점 스냅] 단추 · 스냅 간격 고르기(390-2 · 요구 242-11).
|
||||
*
|
||||
* 간격 = 0.1 m · 1 m · 10 m · 측점 No.(편집 창 측점 간격 · 기본) — 곡선 MC 를 그 간격 배수에 맞춤.
|
||||
* 고른 값은 프로젝트별 화면 기억(pref `routeedit-snap-step`) · [저장] 대상 아님(서버 · 추천은 측점 No. 그대로).
|
||||
* 스냅 끔이면 고르기는 흐림(CSS) · 고를 수는 있음.
|
||||
* ========================================================================== */
|
||||
|
||||
import { readState, writeState } from "../A00_Common/b_page_state";
|
||||
import type { RouteEditChrome } from "./B05_Profile_UI_RouteEdit_Chrome";
|
||||
|
||||
const STEPS = ["0.1", "1", "10", "station"];
|
||||
|
||||
/** [측점 스냅] 단추 켬끔 모양(`is-active` · `aria-pressed`). */
|
||||
export function paintStationSnap(button: HTMLButtonElement, on: boolean): void {
|
||||
button.classList.toggle("is-active", on);
|
||||
button.setAttribute("aria-pressed", String(on));
|
||||
}
|
||||
|
||||
/**
|
||||
* 단추 · 고르기 배선 — `onChange(true)` = 단추 누름(켬끔 바꿈) · `onChange(false)` = 간격 바꿈.
|
||||
* 돌려주는 함수 = 지금 스냅 간격(m).
|
||||
*/
|
||||
export function bindStationSnap(
|
||||
chrome: Pick<RouteEditChrome, "overlay" | "stationSnapButton">,
|
||||
projectId: string,
|
||||
stationIntervalM: number,
|
||||
onChange: (toggle: boolean) => void,
|
||||
): () => number {
|
||||
const select = chrome.overlay.querySelector<HTMLSelectElement>('[data-act="snap-step"]')!;
|
||||
const saved = readState<string>("routeedit-snap-step", projectId);
|
||||
select.value = saved && STEPS.includes(saved) ? saved : "station";
|
||||
select.addEventListener("change", () => {
|
||||
writeState("routeedit-snap-step", select.value, projectId);
|
||||
onChange(false);
|
||||
});
|
||||
chrome.stationSnapButton.addEventListener("click", () => onChange(true));
|
||||
return () => (select.value === "station" ? stationIntervalM : Number(select.value));
|
||||
}
|
||||
@@ -321,6 +321,15 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 분석 지형이 숨으면 앞 구분선도(390-1) · 스냅 끔이면 간격 고르기 흐림(390-2). */
|
||||
.b05-route__view-separator:has(+ .b05-routeedit__terrain[hidden]) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-act="station-snap"][aria-pressed="false"] + .b05-routeedit__snap-step {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* ㉔ 잰 값 — 지도 오른쪽 아래 작은 창(뜬 종단 판이 덮으면 가려진 채 · 349-0). 닫으면 잰 것이 지워진다. */
|
||||
.b05-routeedit__measure {
|
||||
position: absolute;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* 밀어 MC(BC · EC 누가거리 가운데)를 가까운 일반측점(앞 MC 측점 + 간격 이상 · No.1 이상)에 둠.
|
||||
* · 곡선 하한을 새로 깨거나(`shortfallCrossed`) 앞 곡선 MC 를 흔들면 버림 → 그 곡선은 그대로.
|
||||
* · 반지름 · 켬끔은 안 바꿈 — IP 자리만. 합치기 · 곡선 끄기 · 되돌아가기 없음.
|
||||
* · 간격 = 스냅 간격(390-2 · 0.1 · 1 · 10 m · 측점 No.) · 허용 오차도 간격 따라(`mcTolM`).
|
||||
* 파이썬 짝 아님 — [저장] 때 서버가 저장된 IP 로 선을 다시 짬(`buildEditedPolyline` 짝)이라 MC 그대로.
|
||||
* 순수 계산 — 화면 · fetch · sessionStorage 없음. 부르는 쪽은 `B05_Profile_UI_RouteEdit`.
|
||||
* ========================================================================== */
|
||||
@@ -29,6 +30,9 @@ import { curveSpans } from "./B05_Profile_UI_RouteEdit_Frame";
|
||||
/** MC 가 측점에서 이만큼 안이면 「위」(m) — 파이썬 `MC_TOL_M` 과 같은 값. */
|
||||
export const MC_TOL_M = 0.05;
|
||||
|
||||
/** 간격별 허용 오차 — 0.1 m 간격도 실제로 맞게 간격의 1/20 이하(390-2 · 1 m 이상은 `MC_TOL_M`). */
|
||||
export const mcTolM = (intervalM: number): number => Math.min(MC_TOL_M, intervalM / 20);
|
||||
|
||||
/** 선을 짜는 값 — 편집 창이 쥔 하한 · 간격. */
|
||||
export interface FitLimits {
|
||||
minRadiusM: number;
|
||||
@@ -66,7 +70,7 @@ export function offStationCount(
|
||||
for (const [start, end] of spans) {
|
||||
const mc = (start + end) / 2;
|
||||
const n = Math.round(mc / intervalM);
|
||||
if (n >= 1 && Math.abs(mc - n * intervalM) <= MC_TOL_M) hits.add(n);
|
||||
if (n >= 1 && Math.abs(mc - n * intervalM) <= mcTolM(intervalM)) hits.add(n);
|
||||
}
|
||||
return spans.length - hits.size;
|
||||
}
|
||||
@@ -122,7 +126,7 @@ function solve(
|
||||
const tried = at(d1);
|
||||
if (!tried || d1 === d0) return null;
|
||||
const f1 = tried.mc - target;
|
||||
if (Math.abs(f1) <= MC_TOL_M / 5) return tried;
|
||||
if (Math.abs(f1) <= mcTolM(limits.intervalM) / 5) return tried;
|
||||
const slope = (f1 - f0) / (d1 - d0);
|
||||
if (Math.abs(slope) < 0.2) return null;
|
||||
d0 = d1;
|
||||
@@ -149,7 +153,7 @@ export function fitCurveStations(
|
||||
const mc = mcOf(built, k);
|
||||
if (mc === null) continue;
|
||||
const near = Math.max(lastStation + 1, Math.round(mc / interval));
|
||||
const onStation = Math.abs(mc - near * interval) <= MC_TOL_M;
|
||||
const onStation = Math.abs(mc - near * interval) <= mcTolM(interval);
|
||||
const fitted =
|
||||
onStation || k < from || cols.apexLock[k] || Math.abs(near * interval - mc) > interval / 2
|
||||
? null
|
||||
@@ -162,7 +166,7 @@ export function fitCurveStations(
|
||||
) &&
|
||||
(previous < 0 ||
|
||||
Math.abs((mcOf(fitted.built, previous) ?? Infinity) - (mcOf(built, previous) ?? 0)) <=
|
||||
MC_TOL_M / 5);
|
||||
mcTolM(interval) / 5);
|
||||
if (fitted && keep) ({ planned, built } = fitted);
|
||||
if (onStation || keep) lastStation = near;
|
||||
previous = k;
|
||||
|
||||
@@ -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