feat(설정 마우스): 화면 옮기기 버튼 고르기 — 왼 · 가운데 · 오른 · 지도 · CAD · 종단 · 그림 칸 모두 따름 (PLAN 588-4 · 요구 445 C3)
- ☰ [설정] 마우스 탭 = 휠 방향 + 옮기기 버튼 라디오 · 규칙 글 = 고른 버튼 끌기 옮김(가운데는 늘) - 끌면 옮김 · 덜 끌고 떼면 그 버튼 제 몫(고름 · 메뉴 · CAD Enter) · 오른쪽 끌기 뒤 메뉴 안 뜸 - 왼쪽 옮김은 페이지가 잡은 누름(B05 노드 · 손잡이 · 마커 · 재기 · D03 겹 · 그림 안 도형)에선 비킴 - 자리: B05 · B06 · D03 노선 지도 · CAD · 공용 SVG 확대 · B04 지도 · 3D(오른쪽만) · B05 배수 · 종단 가로 · B08 이정표 · M02 흐름도 · 노임 - 시험: CAD 오른쪽 끌기 옮김 · 옮기기 자리 배선 대조 · 낡은 헬퍼 둘 새 import 대응 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,7 @@
|
||||
* 그리는 일은 안 한다 — 넘겨받은 `layer` 에 `transform` 만 적는다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { wheelEventIn } from "./input_prefs";
|
||||
import { isPanButton, swallowNextMenu, wheelEventIn } from "./input_prefs";
|
||||
|
||||
/** 줌·팬 상태 — 다시 그릴 때 배율을 되살리는 데 쓴다(2026-08-22 사용자 ③). */
|
||||
export interface ZoomPanState {
|
||||
@@ -75,7 +75,7 @@ export interface SvgZoomPanOptions {
|
||||
/** 휠을 줌에 묶을지(기본 안 묶음). 묶으면 **커서 아래 지점**을 붙잡고 확대한다.
|
||||
* `"ctrl"` = Ctrl+휠만 줌 · 맨 휠은 페이지 스크롤에 둠. */
|
||||
wheelZoom?: boolean | "ctrl";
|
||||
/** 팬을 시작할 누름인지 — 안 주면 가운데 버튼만. 고르기가 없는 빈 곳 왼쪽 끌기를 더할 때 쓴다. */
|
||||
/** 팬을 시작할 누름인지 — 안 주면 ☰ [설정] 옮기기 버튼(가운데는 늘). 고르기가 없는 빈 곳 왼쪽 끌기를 더할 때 쓴다. */
|
||||
panStart?: (event: PointerEvent) => boolean;
|
||||
}
|
||||
|
||||
@@ -166,9 +166,14 @@ export function attachSvgZoomPan(options: SvgZoomPanOptions): ZoomPanHandle {
|
||||
svg.addEventListener("pointerdown", (event) => {
|
||||
// 누름마다 풂 — 가운데 단추 팬 뒤엔 click 이 안 와 `moved` 가 남아 왼쪽 누름을 삼켰음(259-1)
|
||||
moved = false;
|
||||
// 팬은 **가운데 버튼**만(기본). 좌클릭은 그 화면의 고르기 몫이다.
|
||||
if (!(options.panStart ? options.panStart(event) : event.button === 1)) return;
|
||||
event.preventDefault();
|
||||
// 팬 = ☰ [설정] 옮기기 버튼(가운데는 늘 · 588-4) · 왼쪽이면 안 도형이 잡은 누름은 비킴 · 끈 뒤 click 은 아래서 막음
|
||||
const claimed =
|
||||
event.button === 0 &&
|
||||
(event.defaultPrevented ||
|
||||
(event.target as Element | null)?.hasPointerCapture?.(event.pointerId));
|
||||
if (!(options.panStart ? options.panStart(event) : isPanButton(event.button) && !claimed))
|
||||
return;
|
||||
if (event.button !== 0) event.preventDefault(); // 왼쪽은 안 도형의 mousedown 을 살림
|
||||
panning = true;
|
||||
moved = false;
|
||||
lastX = event.clientX;
|
||||
@@ -191,6 +196,7 @@ export function attachSvgZoomPan(options: SvgZoomPanOptions): ZoomPanHandle {
|
||||
const endPan = (event: PointerEvent): void => {
|
||||
if (!panning) return;
|
||||
panning = false;
|
||||
if (moved && event.button === 2) swallowNextMenu(); // 오른쪽 끌어 옮김 뒤 메뉴 안 뜸
|
||||
svg.classList.remove("is-panning");
|
||||
try {
|
||||
svg.releasePointerCapture(event.pointerId);
|
||||
|
||||
@@ -86,9 +86,35 @@ export const panButton = (): PanButton => inputPrefs().panButton;
|
||||
export const isPanButton = (button: number): boolean =>
|
||||
button === 1 || button === PAN_BUTTONS.indexOf(panButton());
|
||||
|
||||
/** 왼쪽 아닌 옮기기 버튼(가운데 · 오른쪽을 고른 때) — 왼쪽이 고르기 · 돌림 몫인 화면용 */
|
||||
export const isAuxPanButton = (button: number): boolean => button !== 0 && isPanButton(button);
|
||||
|
||||
/** 단추 번호 → `buttons` 비트(왼 1 · 오른 2 · 가운데 4) — 끄는 중 놓침 알아채기 */
|
||||
export const buttonMask = (button: number): number => [1, 4, 2][button] ?? 0;
|
||||
|
||||
/** 이만큼(px) 끌어야 옮김 — 덜 움직이고 떼면 그 단추 제 몫(고름 · 메뉴 · Enter) */
|
||||
export const PAN_MOVE_PX = 4;
|
||||
|
||||
/** 오른쪽 끌기로 옮긴 뒤 놓을 때 뜨는 메뉴를 한 번 막음 */
|
||||
export function swallowNextMenu(): void {
|
||||
const stop = (event: Event): void => {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
};
|
||||
globalThis.addEventListener("contextmenu", stop, { capture: true, once: true });
|
||||
setTimeout(() => globalThis.removeEventListener("contextmenu", stop, true), 400);
|
||||
}
|
||||
|
||||
/** 오른쪽 누름으로 옮기기를 시작할 때 부름 — 끌고 놓으면 그 뒤 메뉴를 막음 */
|
||||
export function guardMenuAfterDrag(down: PointerEvent): void {
|
||||
if (down.button !== 2) return;
|
||||
const up = (event: PointerEvent): void => {
|
||||
const moved = Math.hypot(event.clientX - down.clientX, event.clientY - down.clientY);
|
||||
if (moved > PAN_MOVE_PX) swallowNextMenu();
|
||||
};
|
||||
globalThis.addEventListener("pointerup", up, { capture: true, once: true });
|
||||
}
|
||||
|
||||
/** 휠 한 번이 확대인가 — 설정 방향을 따름 */
|
||||
export const wheelIn = (deltaY: number): boolean =>
|
||||
inputPrefs().wheelZoom === "pull" ? deltaY > 0 : deltaY < 0;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as THREE from "three";
|
||||
import type { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
||||
import type { SurfaceBounds } from "./B04_PreProcess_Api_Fetch";
|
||||
import { wheelEventIn } from "../A00_Common/input_prefs";
|
||||
import { guardMenuAfterDrag, isAuxPanButton, wheelEventIn } from "../A00_Common/input_prefs";
|
||||
|
||||
export const SURFACE_CAMERA_FOV = 50;
|
||||
const LIGHT_VIEWER_BACKGROUND = 0xf5f7f9;
|
||||
@@ -224,7 +224,9 @@ export function bindCursorPivotControls(options: CursorPivotOptions): () => void
|
||||
// 가운데 버튼은 브라우저 기본 동작(페이지 자동 스크롤)이 팬과 겹치므로 막는다.
|
||||
if (event.button === 1) event.preventDefault();
|
||||
if (options.blocked?.() || !controls.enabled) return;
|
||||
if (event.button === 1 && panPointerId === null) {
|
||||
// 그랩 팬 = 가운데 · ☰ [설정] 옮기기 버튼이 오른쪽이면 오른쪽도(왼쪽은 늘 돌림 · 588-4)
|
||||
if (isAuxPanButton(event.button) && panPointerId === null) {
|
||||
guardMenuAfterDrag(event);
|
||||
// 그랩 팬 시작 — 커서 아래 지형점을 잡고, 시선 수직 평면 위에서 따라오게 한다.
|
||||
startGrabPan(event);
|
||||
panPointerId = event.pointerId;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createButton } from "@ui/ui_template_elements";
|
||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||
import { wheelZoomsIn } from "../A00_Common/touch_bridge";
|
||||
import { PAN_MOVE_PX, isPanButton, swallowNextMenu } from "../A00_Common/input_prefs";
|
||||
import { createProgressCircle } from "@ui/ui_template_progress";
|
||||
import { createToolbar } from "@ui/ui_template_toolbar";
|
||||
import {
|
||||
@@ -611,8 +612,8 @@ export function createSurfaceMapViewer(): SurfaceMapViewer {
|
||||
// 터치·펜은 팬도 겸하므로 여기서 반환하지 않고 기록만 남긴다 — 그래야 손가락으로도 마커를
|
||||
// 고를 수 있다(끌었으면 이동량 판정에서 걸러진다).
|
||||
if (event.button === 0) clickStart = { x: event.clientX, y: event.clientY };
|
||||
if (event.pointerType === "mouse" && event.button !== 1) {
|
||||
// 마우스 좌버튼은 선택 전용 — 팬을 시작하지 않는다.
|
||||
if (event.pointerType === "mouse" && !isPanButton(event.button)) {
|
||||
// 팬 = ☰ [설정] 옮기기 버튼(가운데는 늘 · 588-4) — 왼쪽이면 끌지 않고 뗀 것은 위 고르기 그대로.
|
||||
return;
|
||||
}
|
||||
dragStart = { x: event.clientX, y: event.clientY, offsetX, offsetY };
|
||||
@@ -651,7 +652,12 @@ export function createSurfaceMapViewer(): SurfaceMapViewer {
|
||||
offsetY = dragStart.offsetY + event.clientY - dragStart.y;
|
||||
scheduleDraw();
|
||||
});
|
||||
const stopDragging = (): void => {
|
||||
const stopDragging = (event?: PointerEvent): void => {
|
||||
// 오른쪽 끌어 옮김 뒤 메뉴 안 뜸(588-4)
|
||||
if (dragStart && event?.button === 2) {
|
||||
const moved = Math.hypot(event.clientX - dragStart.x, event.clientY - dragStart.y);
|
||||
if (moved > PAN_MOVE_PX) swallowNextMenu();
|
||||
}
|
||||
dragStart = null;
|
||||
viewport.style.removeProperty("cursor");
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { wheelZoomsIn } from "../A00_Common/touch_bridge";
|
||||
import { PAN_MOVE_PX, isPanButton, swallowNextMenu } from "../A00_Common/input_prefs";
|
||||
import {
|
||||
computeMaxScale,
|
||||
lonLatToScreen,
|
||||
@@ -116,7 +117,8 @@ export function bindDrainageInteractions(params: DrainageInteractParams): void {
|
||||
// 팬은 가운데(휠) 버튼 전용이다. 좌버튼은 유역선 핸들·배관 마커를 고르고 끄는 데만 쓴다
|
||||
// (좌버튼이 팬까지 겸하면 마커를 집으려다 지도가 딸려 움직인다 — 2026-08-01 사용자 지시).
|
||||
// 가운데 버튼이 없는 터치·펜은 그대로 끌어서 팬 할 수 있게 둔다.
|
||||
if (event.pointerType === "mouse" && event.button !== 1) return;
|
||||
// 팬 = ☰ [설정] 옮기기 버튼(가운데는 늘 · 588-4) — 왼쪽이면 끌지 않고 뗀 것은 유역 고르기 그대로.
|
||||
if (event.pointerType === "mouse" && !isPanButton(event.button)) return;
|
||||
const offset = params.getOffset();
|
||||
dragStart = { x: event.clientX, y: event.clientY, offsetX: offset.x, offsetY: offset.y };
|
||||
viewport.style.cursor = "grabbing";
|
||||
@@ -169,6 +171,10 @@ export function bindDrainageInteractions(params: DrainageInteractParams): void {
|
||||
basinClickStart = null;
|
||||
const dragged = pipeClickStart !== null;
|
||||
pipeClickStart = null;
|
||||
if (dragStart && event.button === 2) {
|
||||
const moved = Math.hypot(event.clientX - dragStart.x, event.clientY - dragStart.y);
|
||||
if (moved > PAN_MOVE_PX) swallowNextMenu(); // 오른쪽 끌어 옮김 뒤 메뉴 안 뜸(588-4)
|
||||
}
|
||||
if (!dragged && start && event.button === 0) {
|
||||
const rect = viewport.getBoundingClientRect();
|
||||
// 끌었으면 지도 조작이지 고르기가 아니다.
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { viewOf } from "../common_util/common_util_view_window";
|
||||
import { guardMenuAfterDrag, isPanButton } from "../A00_Common/input_prefs";
|
||||
|
||||
/** 프레임마다 남은 거리에서 좁히는 비율. 0.35면 60fps에서 약 0.13초에 도착한다 —
|
||||
* 눈으로는 이어져 보이면서, 멈춘 뒤 세로 눈금이 따라오는 시간도 예전만큼 짧다. */
|
||||
@@ -170,8 +171,10 @@ export function attachWheelHorizontalScroll(scroller: HTMLElement): void {
|
||||
}
|
||||
|
||||
scroller.addEventListener("pointerdown", (event) => {
|
||||
if (dragId !== null || (event.button !== 0 && event.button !== 1)) return;
|
||||
// 왼쪽 · 가운데 · ☰ [설정] 옮기기 버튼(오른쪽이면 끈 뒤 메뉴 안 뜸 · 588-4)
|
||||
if (dragId !== null || (event.button !== 0 && !isPanButton(event.button))) return;
|
||||
if ((event.target as HTMLElement | null)?.closest(DRAG_BLOCK_SELECTOR)) return;
|
||||
guardMenuAfterDrag(event);
|
||||
// 끌기가 시작되면 휠 관성 이동은 접는다 — 둘이 같은 scrollLeft를 다투면 튄다.
|
||||
stop();
|
||||
dragId = event.pointerId;
|
||||
|
||||
@@ -576,7 +576,6 @@ export async function mountRouteEditor(
|
||||
},
|
||||
});
|
||||
|
||||
// 확대·이동은 배수유역도와 같은 동작으로 — 휠은 당기면 확대, 팬은 가운데 버튼 전용.
|
||||
bindRouteEditNavigation({
|
||||
canvas,
|
||||
getView: () => view,
|
||||
@@ -587,6 +586,7 @@ export async function mountRouteEditor(
|
||||
// 돌린 지도에서는 손이 민 방향과 그림이 움직일 방향이 다르다 — 거꾸로 돌려 넘긴다.
|
||||
unrotateDelta: rotation.unrotateDelta,
|
||||
rotateWheel: rotation.wheel, // Ctrl+휠 = 5° 돌리기(390-3)
|
||||
claimed: pointer.claimed, // 왼쪽 옮기기 = 노드 · 손잡이 · 마커 · 재기 아닌 누름만(588-4)
|
||||
draw,
|
||||
});
|
||||
|
||||
|
||||
@@ -6,15 +6,22 @@
|
||||
* (2026-09-07 사용자 지시 ⑤). 한쪽만 고치면 두 지도가 서로 다르게 움직인다.
|
||||
* · 휠을 **당기면 확대**(`deltaY > 0` → 확대) · 계수 1.15 / 0.87
|
||||
* · 배율 상한은 「화면 폭 16m」로 계산(`computeMaxScale`), 하한 0.5
|
||||
* · **팬은 가운데(휠) 버튼 전용** — 왼쪽 버튼이 팬까지 겸하면 노드를 집으려다 지도가
|
||||
* 딸려 움직인다. 가운데 버튼이 없는 터치·펜은 그대로 끌어서 팬 한다.
|
||||
* · **팬 = ☰ [설정] 화면 옮기기 버튼**(기본 가운데 · 588-4) — 왼쪽을 고르면 페이지가 안 잡은 누름만
|
||||
* (노드 · 마커 · 그리기는 그대로) · 오른쪽은 끈 뒤 메뉴 안 뜸 · 덜 끌고 떼면 그 버튼 제 몫.
|
||||
* 가운데 버튼이 없는 터치·펜은 그대로 끌어서 팬 한다.
|
||||
*
|
||||
* ⚠ 커서 고정은 **화면 중심 기준**으로 셈한다 — `affineOf` 가 `centerX*(1−scale)` 을 품고
|
||||
* 있어 좌상단 기준으로 셈하면 확대할수록 커서 아래 지점이 밀린다(옛 모달의 버그).
|
||||
* ========================================================================== */
|
||||
|
||||
import { isPinchWheel, pickPx, wheelZoomsIn } from "../A00_Common/touch_bridge";
|
||||
import { isTrackpadPinch } from "../A00_Common/input_prefs";
|
||||
import {
|
||||
PAN_MOVE_PX,
|
||||
buttonMask,
|
||||
isPanButton,
|
||||
isTrackpadPinch,
|
||||
swallowNextMenu,
|
||||
} 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";
|
||||
@@ -36,8 +43,10 @@ export interface RouteEditNavigationParams {
|
||||
/** 돌린 지도 보정 — 화면에서 민 만큼(dx, dy)을 **그림 좌표의 만큼**으로 바꾼다.
|
||||
* 안 주면 안 돌린 것으로 본다(배수유역도는 회전이 없다). */
|
||||
unrotateDelta?: (dx: number, dy: number) => [number, number];
|
||||
/** 마우스 왼쪽 버튼으로도 팬 — 집을 노드가 없는 미리 보기 지도용. 편집 창은 가운데 버튼만. */
|
||||
/** 마우스 왼쪽 버튼으로도 팬 — 집을 노드가 없는 미리 보기 지도용. 편집 창은 설정 버튼만. */
|
||||
leftPan?: boolean;
|
||||
/** 왼쪽이 옮기기 버튼일 때 페이지가 그 누름을 잡았나(노드 · 마커) — 안 주면 포인터를 잡은 겹이 있으면 잡음 */
|
||||
claimed?: () => boolean;
|
||||
/** Ctrl+휠 = 돌리기(390-3) — 안 주면 Ctrl+휠도 확대(추천 지도). 손가락 · 트랙패드 벌림은 늘 확대. */
|
||||
rotateWheel?: (sign: 1 | -1) => void;
|
||||
draw: () => void;
|
||||
@@ -49,7 +58,14 @@ export function bindRouteEditNavigation(params: RouteEditNavigationParams): void
|
||||
const unrotateDelta = params.unrotateDelta ?? ((dx: number, dy: number) => [dx, dy]);
|
||||
/** 팬 중인 포인터 — 직전 자리만 쥔다(401). 누를 때 offset 을 쥐고 「처음 + 민 만큼」으로 덮으면
|
||||
* 끄는 중 휠 확대 · Ctrl+휠 회전이 바꾼 화면을 옛 값이 덮어 지도가 튄다 → 걸음마다 지금 화면에 더함. */
|
||||
let drag: { id: number; x: number; y: number; mask: number } | null = null;
|
||||
let drag: {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
mask: number;
|
||||
button: number;
|
||||
moved: number;
|
||||
} | null = null;
|
||||
|
||||
canvas.addEventListener(
|
||||
"wheel",
|
||||
@@ -87,20 +103,21 @@ export function bindRouteEditNavigation(params: RouteEditNavigationParams): void
|
||||
canvas.addEventListener("pointerdown", (event) => {
|
||||
// 가운데 버튼 기본 동작(페이지 자동 스크롤)이 팬과 겹치지 않게 막는다.
|
||||
if (event.button === 1) event.preventDefault();
|
||||
// 마우스는 가운데 버튼만 팬이다 — 왼쪽 버튼은 노드·손잡이 조작 몫.
|
||||
if (
|
||||
event.pointerType === "mouse" &&
|
||||
event.button !== 1 &&
|
||||
!(params.leftPan && event.button === 0)
|
||||
)
|
||||
return;
|
||||
// 마우스는 설정 버튼(가운데는 늘)만 팬 — 왼쪽이면 페이지(노드 · 손잡이 · 겹)가 안 잡은 누름만.
|
||||
const mouse = event.pointerType === "mouse";
|
||||
const left = event.button === 0 && !!params.leftPan;
|
||||
if (mouse && !left && !isPanButton(event.button)) return;
|
||||
const claimed = params.claimed ?? (() => canvas.hasPointerCapture(event.pointerId));
|
||||
if (mouse && event.button === 0 && !left && claimed()) return;
|
||||
if (drag) return; // 둘째 손가락은 팬을 넘겨받지 않음(벌림 확대 몫)
|
||||
// 쥔 단추(buttons 비트) — 가운데 4 · 왼쪽 1. 놓침(창 밖에서 놓고 들어옴)을 move 에서 알아챔.
|
||||
// 쥔 단추(buttons 비트). 놓침(창 밖에서 놓고 들어옴)을 move 에서 알아챔.
|
||||
drag = {
|
||||
id: event.pointerId,
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
mask: event.button === 1 ? 4 : 1,
|
||||
mask: mouse ? buttonMask(event.button) : 1,
|
||||
button: mouse ? event.button : -1,
|
||||
moved: 0,
|
||||
};
|
||||
canvas.style.cursor = "grabbing";
|
||||
canvas.setPointerCapture(event.pointerId);
|
||||
@@ -110,6 +127,7 @@ export function bindRouteEditNavigation(params: RouteEditNavigationParams): void
|
||||
if (!drag || event.pointerId !== drag.id) return;
|
||||
if (event.pointerType === "mouse" && !(event.buttons & drag.mask)) return stop();
|
||||
const [dx, dy] = unrotateDelta(event.clientX - drag.x, event.clientY - drag.y);
|
||||
drag.moved += Math.abs(event.clientX - drag.x) + Math.abs(event.clientY - drag.y);
|
||||
drag.x = event.clientX;
|
||||
drag.y = event.clientY;
|
||||
const view = getView();
|
||||
@@ -119,6 +137,7 @@ export function bindRouteEditNavigation(params: RouteEditNavigationParams): void
|
||||
|
||||
const stop = (event?: PointerEvent): void => {
|
||||
if (!drag || (event && event.pointerId !== drag.id)) return;
|
||||
if (drag.button === 2 && drag.moved > PAN_MOVE_PX) swallowNextMenu(); // 끈 뒤 메뉴 안 뜸
|
||||
drag = null;
|
||||
canvas.style.removeProperty("cursor");
|
||||
};
|
||||
|
||||
@@ -116,6 +116,8 @@ export interface RouteEditPointerParams {
|
||||
export interface RouteEditPointer {
|
||||
/** 노드나 손잡이를 끌고 있나 — 끄는 동안 기울기 라벨을 숨긴다. */
|
||||
dragging: () => boolean;
|
||||
/** 마지막 왼쪽 누름을 잡았나(재기 · 마커 · 노드 · 손잡이) — 왼쪽 옮기기(588-4)가 비킴 */
|
||||
claimed: () => boolean;
|
||||
nodeAt: (px: number, py: number) => number;
|
||||
segmentAt: (px: number, py: number) => number;
|
||||
}
|
||||
@@ -213,6 +215,7 @@ export function bindRouteEditPointer(params: RouteEditPointerParams): RouteEditP
|
||||
let dragOff = 0;
|
||||
/** 구조물 측점 마커(399) — 집기 차례 = 재기 다음 맨 앞(409) · 끌기 · 방향키 1 m. */
|
||||
const marks = bindStructureMarks(params, () => dragNode >= 0 || dragHandle !== null);
|
||||
let claimed = false;
|
||||
bindPlanStructureMenu(params, nodeAt); // 우클릭 = 종단 그래프와 같은 구조물 메뉴(513-1)
|
||||
/** 누름 · 맴돔 한 판정 — 노드 가운데 6 px = 노드 · 밖 = 마커 · 노드 · 손잡이 중 가까운 것(`pressWinner`). */
|
||||
const chooseAt = (px: number, py: number) => {
|
||||
@@ -234,6 +237,7 @@ export function bindRouteEditPointer(params: RouteEditPointerParams): RouteEditP
|
||||
|
||||
canvas.addEventListener("pointerdown", (event) => {
|
||||
if (event.button !== 0) return;
|
||||
claimed = true; // 왼쪽 옮기기(588-4)는 잡은 누름(재기 · 마커 · 노드 · 손잡이)에선 안 함
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const [px, py] = params.unrotate(event.clientX - rect.left, event.clientY - rect.top);
|
||||
if (event.shiftKey || params.measureMode()) {
|
||||
@@ -270,6 +274,7 @@ export function bindRouteEditPointer(params: RouteEditPointerParams): RouteEditP
|
||||
dragOff = params.offStation();
|
||||
dragHandle = choice.handle;
|
||||
dragMoved = false;
|
||||
claimed = dragNode >= 0 || dragHandle !== null;
|
||||
if (hitNode >= 0) {
|
||||
params.setPicked(hitNode); // 누른 자리를 고른다 — R 라벨이 그 곡선을 만진다.
|
||||
params.measure.clear(); // 잰 창과 곡선 패널은 같이 뜨지 않는다(㉔).
|
||||
@@ -504,5 +509,10 @@ export function bindRouteEditPointer(params: RouteEditPointerParams): RouteEditP
|
||||
canvas.addEventListener("pointercancel", endDrag);
|
||||
bindRouteEditKeys(params, () => dragNode >= 0 || dragHandle !== null); // 교각점 방향키(383-2)
|
||||
|
||||
return { dragging: () => dragNode >= 0 || dragHandle !== null, nodeAt, segmentAt };
|
||||
return {
|
||||
dragging: () => dragNode >= 0 || dragHandle !== null,
|
||||
claimed: () => claimed,
|
||||
nodeAt,
|
||||
segmentAt,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,7 +32,13 @@ import {
|
||||
import { Tool } from '../tools.ts';
|
||||
import { SelectState } from '../tools/select-tool.ts';
|
||||
import { ActorEvent, type MouseClickEvent } from '../tools/tool.types.ts';
|
||||
import { activeToolWantsFreeText, activeToolWantsIme, ImeSink, isImeKey, isPlainSpace } from './ime-sink.ts';
|
||||
import {
|
||||
activeToolWantsFreeText,
|
||||
activeToolWantsIme,
|
||||
ImeSink,
|
||||
isImeKey,
|
||||
isPlainSpace,
|
||||
} from './ime-sink.ts';
|
||||
import { drawInputField, dynamicInputLine } from './input-field-draw.ts';
|
||||
import { CAD_KEY_COMMANDS, cadKeyAction } from './input-shortcut.ts';
|
||||
import { wheelZoom } from './input-wheel.ts';
|
||||
@@ -40,6 +46,7 @@ import { isObjectSnap, noteShift, snapAt } from './ortho.ts';
|
||||
import { isWaitingForSecondSelectPoint, selectingActor } from './selecting.ts';
|
||||
import { openSnapMenu } from './snap-menu.ts';
|
||||
import { isMiddleDouble, zoomExtents } from './zoom-prompt.ts';
|
||||
import { isPanButton, PAN_MOVE_PX } from '../../../../A00_Common/input_prefs';
|
||||
import { attachTouch } from './input-touch.ts';
|
||||
import { NUMBER_REGEXP, type PointTextEvent } from './input-patterns.ts';
|
||||
import { commandCandidate, pushInput, recallInput } from './input-history.ts';
|
||||
@@ -82,6 +89,8 @@ export class InputController {
|
||||
private candidateIndex = 0;
|
||||
/** 왼쪽 버튼을 누른 화면 좌표 — 놓을 때 끌었는지 판단한다 */
|
||||
private leftPressLocation: Point | null = null;
|
||||
/** ☰ [설정] 옮기기 버튼이 왼쪽 · 오른쪽일 때 누른 자리(588-4) — 끌면 옮김 · 안 끌고 떼면 그 버튼 제 몫 */
|
||||
private panPress: { button: number; at: Point } | null = null;
|
||||
public readonly ime = new ImeSink((data) => {
|
||||
this.text += data;
|
||||
});
|
||||
@@ -168,6 +177,21 @@ export class InputController {
|
||||
}
|
||||
|
||||
public handleMouseUp(evt: MouseEvent) {
|
||||
const pan = this.panPress;
|
||||
if (pan && evt.button === pan.button) {
|
||||
this.panPress = null;
|
||||
setPanStartLocation(null);
|
||||
const at = this.getCanvasPoint(evt);
|
||||
if (Math.hypot(at.x - pan.at.x, at.y - pan.at.y) > PAN_MOVE_PX) {
|
||||
evt.preventDefault(); // 끌어 옮김 — Enter · 클릭 안 보냄
|
||||
return;
|
||||
}
|
||||
if (evt.button === MouseButton.Left) {
|
||||
this.sendMouseClick(evt);
|
||||
this.ime.sync(activeToolWantsIme());
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (evt.button === MouseButton.Right) {
|
||||
// 우클릭 = Enter(명령 중 확정 · 쉴 때 반복) · Shift+우클릭 = 점 물음에서 한 번 스냅 메뉴
|
||||
evt.preventDefault();
|
||||
@@ -319,6 +343,17 @@ export class InputController {
|
||||
setPanStartLocation(this.getCanvasPoint(evt));
|
||||
return;
|
||||
}
|
||||
if (
|
||||
isPanButton(evt.button) &&
|
||||
!isViewOnly() &&
|
||||
!(evt.target as HTMLElement | null)?.closest('.controls')
|
||||
) {
|
||||
// 왼쪽 · 오른쪽 옮기기 — 끌면 옮김 · 떼는 자리에서 클릭(왼쪽) · Enter(오른쪽)
|
||||
evt.preventDefault();
|
||||
this.panPress = { button: evt.button, at: this.getCanvasPoint(evt) };
|
||||
setPanStartLocation(this.panPress.at);
|
||||
return;
|
||||
}
|
||||
if (evt.button !== MouseButton.Left) return;
|
||||
if ((evt.target as HTMLElement | null)?.closest('.controls')) return;
|
||||
if (isViewOnly()) {
|
||||
|
||||
@@ -22,6 +22,7 @@ import { createButton, createSelectField, el, showToast } from "@ui/ui_template_
|
||||
import { createFloatPanel } from "@ui/ui_template_float_panel";
|
||||
import { createToolbar } from "@ui/ui_template_toolbar";
|
||||
import { attachSvgZoomPan, type ZoomPanState } from "../A00_Common/b_svg_zoom_pan";
|
||||
import { isAuxPanButton } from "../A00_Common/input_prefs";
|
||||
import {
|
||||
midKm,
|
||||
roadOf,
|
||||
@@ -476,7 +477,8 @@ export function createDiagram(
|
||||
onChange: (state) => (zoomState = state),
|
||||
wheelZoom: true,
|
||||
content: panContent(vw, vh), // 원배율에서도 팬(244-6 · 범위 없으면 1배는 안 움직임)
|
||||
panStart, // 가운데 버튼 · 빈 곳 왼쪽 끌기
|
||||
// 가운데 버튼 · 빈 곳 왼쪽 끌기 · ☰ [설정] 옮기기 버튼(588-4)
|
||||
panStart: (e) => panStart(e) || isAuxPanButton(e.button),
|
||||
});
|
||||
};
|
||||
refresh();
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { wheelZoomsIn } from "../../A00_Common/touch_bridge";
|
||||
import { guardMenuAfterDrag, isAuxPanButton } from "../../A00_Common/input_prefs";
|
||||
import { createButton, el, showConfirmDialog } from "@ui/ui_template_elements";
|
||||
import type { CfBox, CfKind, CfLogic, CfPill, CfPort } from "./M02_DF_Model";
|
||||
import {
|
||||
@@ -467,7 +468,8 @@ export function renderDiagram(
|
||||
(ev) => {
|
||||
// 손가락 빈 곳 끌기도 판 옮기기(기기대응 3-3)
|
||||
const touchBlank = ev.pointerType === "touch" && (ev.target === view || ev.target === world);
|
||||
if (ev.button === 1 || (ev.button === 0 && spaceDown) || touchBlank) {
|
||||
if (isAuxPanButton(ev.button) || (ev.button === 0 && spaceDown) || touchBlank) {
|
||||
guardMenuAfterDrag(ev);
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
dragPan(ev);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { wheelZoomsIn } from "../A00_Common/touch_bridge";
|
||||
import { guardMenuAfterDrag, isAuxPanButton } from "../A00_Common/input_prefs";
|
||||
import { createButton, el } from "@ui/ui_template_elements";
|
||||
import { formatMoney } from "../M01_MasterData/M01_MasterData_UI_Logic_Money";
|
||||
import "./M02_MasterTemplete_Labor_Diagram.css";
|
||||
@@ -282,10 +283,11 @@ function mountShell(host: HTMLElement): State {
|
||||
}),
|
||||
viewport,
|
||||
);
|
||||
// 판 옮기기 — 가운데 단추 · Space + 왼쪽 끌기 · 손가락 빈 곳 끌기(기기대응 3-3)
|
||||
// 판 옮기기 — 가운데 · ☰ [설정] 옮기기 버튼(588-4) · Space + 왼쪽 끌기 · 손가락 빈 곳 끌기(기기대응 3-3)
|
||||
viewport.addEventListener("pointerdown", (e) => {
|
||||
const touchBlank = e.pointerType === "touch" && (e.target === viewport || e.target === world);
|
||||
if (!(e.button === 1 || (e.button === 0 && st.space) || touchBlank)) return;
|
||||
if (!(isAuxPanButton(e.button) || (e.button === 0 && st.space) || touchBlank)) return;
|
||||
guardMenuAfterDrag(e);
|
||||
e.preventDefault();
|
||||
try {
|
||||
viewport.setPointerCapture(e.pointerId);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* 휠 = 기본 밂(앞으로 굴리면 확대 · 오토캐드 · 337) · 당김 저장 뒤 뒤집힘 · 핀치는 늘 벌리면 확대.
|
||||
* 별칭 = 명령 이름 > 내 별칭 > 기본 별칭 · 저장 즉시 따름.
|
||||
* 바꾼 키(588-3) = 새 키만 먹음 · 옛 키는 안 먹음 · 한글 자판도 자판 자리로.
|
||||
* 옮기기 버튼(588-4) = 오른쪽을 고르면 오른쪽 끌기로 화면 옮김 · 기본(가운데)이면 오른쪽 끌기는 안 옮김.
|
||||
* 실행: cd B07_DesignDetail/openwebcad && npx vitest run --dir ../../resources/tester/cad input-prefs-cad
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
@@ -12,7 +13,12 @@ import {
|
||||
matchCommandPrefixes,
|
||||
resolveCommandInput,
|
||||
} from "../../../B07_DesignDetail/openwebcad/src/commands/registry";
|
||||
import { getActiveToolActor, getSnapEnabled } from "../../../B07_DesignDetail/openwebcad/src/state";
|
||||
import {
|
||||
getActiveToolActor,
|
||||
getPanStartLocation,
|
||||
getScreenCanvasDrawController,
|
||||
getSnapEnabled,
|
||||
} from "../../../B07_DesignDetail/openwebcad/src/state";
|
||||
import { initApplication } from "../../../B07_DesignDetail/openwebcad/test/helpers/init-application";
|
||||
|
||||
const store = new Map<string, string>();
|
||||
@@ -128,3 +134,33 @@ describe("바꾼 키(588-3)", () => {
|
||||
expect(getSnapEnabled()).toBe(before);
|
||||
});
|
||||
});
|
||||
|
||||
describe("옮기기 버튼(588-4)", () => {
|
||||
const mouse = (button: number, x: number, y = 100) =>
|
||||
({
|
||||
button,
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
shiftKey: false,
|
||||
ctrlKey: false,
|
||||
target: null,
|
||||
preventDefault: vi.fn(),
|
||||
stopPropagation: vi.fn(),
|
||||
}) as unknown as MouseEvent;
|
||||
const rightDrag = (controller: ReturnType<typeof initApplication>) => {
|
||||
const before = getScreenCanvasDrawController().getScreenOffset().clone();
|
||||
controller.handleMouseDown(mouse(2, 100));
|
||||
controller.handleMouseMove(mouse(2, 160));
|
||||
controller.handleMouseUp(mouse(2, 160));
|
||||
const after = getScreenCanvasDrawController().getScreenOffset();
|
||||
return Math.hypot(after.x - before.x, after.y - before.y);
|
||||
};
|
||||
|
||||
it("기본(가운데) = 오른쪽 끌기 안 옮김 · 오른쪽 고름 = 옮김 · 놓으면 끝", () => {
|
||||
const controller = initApplication();
|
||||
expect(rightDrag(controller)).toBe(0);
|
||||
savePrefs({ panButton: "right" });
|
||||
expect(rightDrag(controller)).toBeGreaterThan(0);
|
||||
expect(getPanStartLocation()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -82,6 +82,7 @@ globalThis.ResizeObserver = class {
|
||||
const viewWindow = load(path.join(ROOT, "common_util", "common_util_view_window.ts"));
|
||||
const wheelMod = load(path.join(ROOT, "B05_Profile", "B05_Profile_UI_Profile_Wheel.ts"), {
|
||||
"../common_util/common_util_view_window": viewWindow,
|
||||
"../A00_Common/input_prefs": load(path.join(ROOT, "A00_Common", "input_prefs.ts")),
|
||||
});
|
||||
const viewport = load(path.join(ROOT, "A00_Common", "design", "design_profile_viewport.ts"), {
|
||||
"../../B05_Profile/B05_Profile_UI_Profile_Wheel": wheelMod,
|
||||
|
||||
@@ -36,6 +36,11 @@ global.Element = Element;
|
||||
|
||||
const stubs = {
|
||||
"../A00_Common/touch_bridge": { wheelZoomsIn: () => true },
|
||||
"../A00_Common/input_prefs": {
|
||||
isPanButton: (b) => b === 1,
|
||||
PAN_MOVE_PX: 4,
|
||||
swallowNextMenu() {},
|
||||
},
|
||||
"../B04_PreProcess/B04_PreProcess_UI_MapRender": {
|
||||
computeMaxScale: () => 10,
|
||||
lonLatToScreen: () => ({ x: 0, y: 0 }),
|
||||
|
||||
@@ -129,3 +129,24 @@ def test_바꿀_수_있는_줄은_배선됨(r):
|
||||
def test_손글_안내_지움_표는_등록부():
|
||||
assert "App_Settings_Keys_" not in _read("ui_template/ui_template_locale_common.ts")
|
||||
assert "App_Settings_Keys_" not in _read("A00_Common/app_settings.ts")
|
||||
|
||||
|
||||
# 588-4 — 화면 옮기기 자리는 모두 설정 버튼을 따름(가운데 고정 판정이 남지 않음)
|
||||
PAN_SITES = [
|
||||
"B05_Profile/B05_Profile_UI_RouteEdit_Input.ts", # B05 · B06 겹침 평면 · D03 지도
|
||||
"B07_DesignDetail/openwebcad/src/inputController/input-controller.ts",
|
||||
"A00_Common/b_svg_zoom_pan.ts",
|
||||
"B04_PreProcess/B04_PreProcess_UI_MapViewer.ts",
|
||||
"B04_PreProcess/B04_PreProcess_UI_Camera.ts",
|
||||
"B05_Profile/B05_Profile_UI_Drainage_Interact.ts",
|
||||
"B05_Profile/B05_Profile_UI_Profile_Wheel.ts",
|
||||
"B08_Quantity/B08_Quantity_UI_Milestone_Diagram.ts",
|
||||
"M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts",
|
||||
"M02_MasterTemplete/M02_MasterTemplete_Labor_Diagram.ts",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("rel", PAN_SITES)
|
||||
def test_옮기기_자리는_설정_버튼(rel):
|
||||
src = _read(rel)
|
||||
assert "isPanButton(" in src or "isAuxPanButton(" in src, rel
|
||||
|
||||
Reference in New Issue
Block a user