diff --git a/A00_Common/app_settings.ts b/A00_Common/app_settings.ts index 3991bdcaf..f0942acda 100644 --- a/A00_Common/app_settings.ts +++ b/A00_Common/app_settings.ts @@ -56,6 +56,8 @@ const KEY_ROWS: [LocaleKey, LocaleKey][] = [ /** 비교 · 저장 꼴 — 별칭 명령 차례를 맞춤 */ const normalize = (p: InputPrefs): InputPrefs => ({ wheelZoom: p.wheelZoom, + panButton: p.panButton, + keys: Object.fromEntries(Object.entries(p.keys).sort(([a], [b]) => (a < b ? -1 : 1))), cadAliases: Object.fromEntries(Object.entries(p.cadAliases).sort(([a], [b]) => (a < b ? -1 : 1))), }); const snapshot = (p: InputPrefs): string => JSON.stringify(normalize(p)); diff --git a/A00_Common/input_prefs.ts b/A00_Common/input_prefs.ts index a13c87f7c..fee8b3e38 100644 --- a/A00_Common/input_prefs.ts +++ b/A00_Common/input_prefs.ts @@ -6,13 +6,20 @@ * - 없음 · 깨짐 · Node(저장소 없음) = 기본값(= 지금 동작) · 의존 없음 · 불러올 때 window 안 만짐(CAD 도 import) * - 휠 확대 방향 하나로 모든 화면 일체 — 기본 밂 = 휠을 앞으로(deltaY < 0) 굴리면 확대(오토캐드) · 당김 = 몸 쪽으로 * - 핀치(트랙패드 · 손가락)는 설정과 상관없이 늘 벌리면 확대(deltaY < 0) + * - 화면 옮기기 단추 하나(PLAN 588-4) — 고른 단추로 끌면 옮김 · 안 끌고 떼면 그 단추 제 몫 · 가운데는 늘 옮김 + * - 바꾼 키(`keys`)는 기본과 다른 것만 — 판정 · 등록부는 `key_registry.ts` * ========================================================================== */ export type WheelZoom = "pull" | "push"; +export type PanButton = "left" | "middle" | "right"; export interface InputPrefs { /** 휠 확대 방향 — pull = 당기면 확대 · push = 밀면 확대 */ wheelZoom: WheelZoom; + /** 화면 옮기기 단추 */ + panButton: PanButton; + /** 바꾼 단축키 `{동작 id: [키]}` — 기본과 다른 것만(`key_registry.ts`) */ + keys: Record; /** CAD 내 별칭 `{명령: [별칭]}`(대문자 · acad.pgp 식 — 명령 이름 > 내 별칭 > 기본 별칭) */ cadAliases: Record; } @@ -20,7 +27,25 @@ export interface InputPrefs { /** `stateKey("input-prefs")` 와 같은 키(시험이 맞춰 봄) — 등록표를 불러오면 window 가 딸려 와서 글자로 둠 */ export const INPUT_PREFS_KEY = "aislo:pref:input-prefs"; -export const DEFAULT_INPUT_PREFS: InputPrefs = { wheelZoom: "push", cadAliases: {} }; +export const DEFAULT_INPUT_PREFS: InputPrefs = { + wheelZoom: "push", + panButton: "middle", + keys: {}, + cadAliases: {}, +}; + +const PAN_BUTTONS: PanButton[] = ["left", "middle", "right"]; + +const defaults = (): InputPrefs => ({ ...DEFAULT_INPUT_PREFS, keys: {}, cadAliases: {} }); + +const cleanKeys = (raw: unknown): Record => { + const out: Record = {}; + if (!raw || typeof raw !== "object") return out; + for (const [id, list] of Object.entries(raw as Record)) { + if (Array.isArray(list)) out[id] = list.filter((k): k is string => typeof k === "string"); + } + return out; +}; const cleanAliases = (raw: unknown): Record => { const out: Record = {}; @@ -39,17 +64,31 @@ const cleanAliases = (raw: unknown): Record => { export function inputPrefs(): InputPrefs { try { const raw = globalThis.localStorage?.getItem(INPUT_PREFS_KEY); - if (!raw) return { ...DEFAULT_INPUT_PREFS, cadAliases: {} }; + if (!raw) return defaults(); const value = JSON.parse(raw) as Partial | null; return { wheelZoom: value?.wheelZoom === "pull" ? "pull" : "push", + panButton: PAN_BUTTONS.includes(value?.panButton as PanButton) + ? (value!.panButton as PanButton) + : "middle", + keys: cleanKeys(value?.keys), cadAliases: cleanAliases(value?.cadAliases), }; } catch { - return { ...DEFAULT_INPUT_PREFS, cadAliases: {} }; + return defaults(); } } +/** 고른 화면 옮기기 단추 */ +export const panButton = (): PanButton => inputPrefs().panButton; + +/** 이 단추(`MouseEvent.button` 0 왼 · 1 가운데 · 2 오른)로 끌면 화면 옮김 — 고른 단추 · 가운데는 늘 */ +export const isPanButton = (button: number): boolean => + button === 1 || button === PAN_BUTTONS.indexOf(panButton()); + +/** 단추 번호 → `buttons` 비트(왼 1 · 오른 2 · 가운데 4) — 끄는 중 놓침 알아채기 */ +export const buttonMask = (button: number): number => [1, 4, 2][button] ?? 0; + /** 휠 한 번이 확대인가 — 설정 방향을 따름 */ export const wheelIn = (deltaY: number): boolean => inputPrefs().wheelZoom === "pull" ? deltaY > 0 : deltaY < 0; diff --git a/resources/tester/helper_input_prefs.cjs b/resources/tester/helper_input_prefs.cjs index 6e4e0aa90..281794eb0 100644 --- a/resources/tester/helper_input_prefs.cjs +++ b/resources/tester/helper_input_prefs.cjs @@ -31,6 +31,7 @@ const sample = () => ({ padPinch: prefs.wheelEventIn({ ctrlKey: true, deltaY: 3.5 }), fingerSpread: prefs.wheelEventIn({ ctrlKey: true, deltaY: -9.5 }, true), bridgePull: bridge.wheelZoomsIn({ ctrlKey: false, deltaY: 100 }), + pan: [0, 1, 2].map((button) => prefs.isPanButton(button)), }); out.node = { key: prefs.INPUT_PREFS_KEY, default: prefs.DEFAULT_INPUT_PREFS, ...sample() }; out.registryKey = page.stateKey("input-prefs"); @@ -46,6 +47,10 @@ globalThis.localStorage = { const set = (v) => store.set(prefs.INPUT_PREFS_KEY, v); set(JSON.stringify({ wheelZoom: "pull", cadAliases: { line: ["ll", 3], ARC: [] } })); out.saved = sample(); // 기본 밂(337) 의 반대를 저장 +for (const button of ["left", "right", "sideways"]) { + set(JSON.stringify({ panButton: button, keys: { "plan.delete": ["X", 3], bad: "Y" } })); + out[`pan_${button}`] = sample(); +} set("{깨짐"); out.broken = sample(); set(JSON.stringify({ wheelZoom: "sideways" })); diff --git a/resources/tester/test_input_prefs.py b/resources/tester/test_input_prefs.py index 48be36476..e0244bda4 100644 --- a/resources/tester/test_input_prefs.py +++ b/resources/tester/test_input_prefs.py @@ -3,6 +3,7 @@ · 기본 = 밂(휠을 앞으로 = 확대 · 오토캐드 · 337) · 당김 저장 뒤 뒤집힘 · 깨짐 · 모르는 값 · 막힌 저장소 · Node = 기본 · 핀치(트랙패드 · 손가락)는 설정과 상관없이 늘 벌리면 확대 · 키 = 등록표 `input-prefs`(pref · global) 와 같은 글자 +· 화면 옮기기 단추(588-4) = 기본 가운데 · 고른 단추 + 가운데 · 모르는 값 = 가운데 · 바꾼 키 = 글자 목록만 짝: `helper_input_prefs.cjs`(실제 TS 를 Node 로 돌림). """ @@ -45,6 +46,7 @@ def r() -> dict: PULL = {"pull": True, "push": False, "mouseCtrl": True, "bridgePull": True} PUSH = {"pull": False, "push": True, "mouseCtrl": False, "bridgePull": False} PINCH = {"padSpread": True, "padPinch": False, "fingerSpread": True} +DEFAULT = {"wheelZoom": "push", "panButton": "middle", "keys": {}, "cadAliases": {}} def _pick(case: dict, keys: dict) -> dict: @@ -53,7 +55,7 @@ def _pick(case: dict, keys: dict) -> dict: def test_Node_기본_밂(r): n = r["node"] - assert n["default"] == {"wheelZoom": "push", "cadAliases": {}} + assert n["default"] == DEFAULT assert n["prefs"] == n["default"] assert _pick(n, PULL) == PUSH and _pick(n, PINCH) == PINCH @@ -70,7 +72,7 @@ def test_당김_저장_뒤_뒤집힘_핀치는_그대로(r): @pytest.mark.parametrize("case", ["broken", "unknown", "empty", "blocked"]) def test_깨짐_모르는_값_없음_막힘_기본(r, case): c = r[case] - assert c["prefs"] == {"wheelZoom": "push", "cadAliases": {}} + assert c["prefs"] == DEFAULT assert _pick(c, PULL) == PUSH and _pick(c, PINCH) == PINCH @@ -112,3 +114,20 @@ def test_휠_자리_공용_판정(rel): def test_700줄_안(): for rel in WHEEL_SITES: assert _read(rel).count("\n") <= 700, rel + + +@pytest.mark.parametrize( + ("case", "button", "pan"), + [ + ("node", "middle", [False, True, False]), + ("pan_left", "left", [True, True, False]), + ("pan_right", "right", [False, True, True]), + ("pan_sideways", "middle", [False, True, False]), + ], +) +def test_화면_옮기기_단추_고른_것_더하기_가운데(r, case, button, pan): + c = r[case] + assert c["prefs"]["panButton"] == button + assert c["pan"] == pan + if case != "node": + assert c["prefs"]["keys"] == {"plan.delete": ["X"]} # 글자 아닌 것 · 목록 아닌 것 버림