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:
2026-10-29 19:08:40 +09:00
co-authored by Claude Opus 5.5
parent 2e4316567c
commit 883a098cd6
17 changed files with 216 additions and 34 deletions
@@ -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()) {