diff --git a/B07_DesignDetail/openwebcad/src/commands/registry.ts b/B07_DesignDetail/openwebcad/src/commands/registry.ts index f6974dcb2..581c2716b 100644 --- a/B07_DesignDetail/openwebcad/src/commands/registry.ts +++ b/B07_DesignDetail/openwebcad/src/commands/registry.ts @@ -23,6 +23,7 @@ import { } from './commands.utility'; import { ALIGN_COMMANDS, MODIFY_COMMANDS } from './commands.modify'; import { VIEW_COMMANDS } from './commands.view'; +import { inputPrefs } from '../../../../A00_Common/input_prefs'; const ALL_COMMANDS: CadCommand[] = [ ...DRAW_COMMANDS, @@ -112,11 +113,24 @@ export const TOOL_STATE_MACHINES = Object.fromEntries( ]) ) as Record; -/** 명령행 입력 해석 — 정확한 이름·별칭이 먼저, 없으면 접두사 후보의 첫 항목 */ +/** 내 별칭(☰ [설정] · `cadAliases`) 이 가리키는 명령들 — 부를 때마다 읽어 저장 즉시 따름 */ +function userAliasCommands(match: (alias: string) => boolean): CadCommand[] { + return Object.entries(inputPrefs().cadAliases).flatMap(([id, aliases]) => { + const command = COMMANDS_BY_ID.get(id); + return command && aliases.some(match) ? [command] : []; + }); +} + +/** 명령행 입력 해석 — 이름 > 내 별칭 > 기본 별칭(acad.pgp 식), 없으면 접두사 후보의 첫 항목 */ export function resolveCommandInput(input: string): CadCommand | undefined { const text = input.trim().toUpperCase(); if (!text) return undefined; - return COMMANDS_BY_ID.get(text) ?? COMMANDS_BY_ALIAS.get(text) ?? matchCommandPrefixes(text)[0]; + return ( + COMMANDS_BY_ID.get(text) ?? + userAliasCommands((alias) => alias === text)[0] ?? + COMMANDS_BY_ALIAS.get(text) ?? + matchCommandPrefixes(text)[0] + ); } /** 접두사가 같은 명령 후보 (커서 옆 자동완성 목록에 쓴다) */ @@ -127,7 +141,8 @@ export function matchCommandPrefixes(input: string): CadCommand[] { (command.aliases ?? []).some((alias) => alias.startsWith(text)) ); const byId = ALL_COMMANDS.filter((command) => command.id.startsWith(text)); - return [...new Set([...byAlias, ...byId])]; + const byUser = userAliasCommands((alias) => alias.startsWith(text)); + return [...new Set([...byUser, ...byAlias, ...byId])]; } /** 명령행 자동완성에 보여줄 문자열 (예: "LINE (L) 선") */ diff --git a/B07_DesignDetail/openwebcad/src/inputController/input-wheel.test.ts b/B07_DesignDetail/openwebcad/src/inputController/input-wheel.test.ts index 3bc6389ce..299400f90 100644 --- a/B07_DesignDetail/openwebcad/src/inputController/input-wheel.test.ts +++ b/B07_DesignDetail/openwebcad/src/inputController/input-wheel.test.ts @@ -22,8 +22,8 @@ describe('CAD 휠 = 늘 줌(153-3)', () => { expect(wheel(-1, 2)).toEqual({ delta: WHEEL_PAGE_PX, exponent: WHEEL_ZOOM_EXPONENT }); }); - it('ctrl+휠(핀치)은 핀치 지수', () => { - expect(wheel(-7, 0, true)).toEqual({ delta: 7, exponent: PINCH_ZOOM_EXPONENT }); + it('ctrl+휠(핀치)은 핀치 지수 · 벌리면(deltaY < 0) 확대(332)', () => { + expect(wheel(-7, 0, true)).toEqual({ delta: -7, exponent: PINCH_ZOOM_EXPONENT }); }); it('델타 0 은 안 움직임', () => { diff --git a/B07_DesignDetail/openwebcad/src/inputController/input-wheel.ts b/B07_DesignDetail/openwebcad/src/inputController/input-wheel.ts index f6ea49e8b..db4242522 100644 --- a/B07_DesignDetail/openwebcad/src/inputController/input-wheel.ts +++ b/B07_DesignDetail/openwebcad/src/inputController/input-wheel.ts @@ -4,6 +4,7 @@ import { WHEEL_PAGE_PX, WHEEL_ZOOM_EXPONENT, } from '../App.consts.ts'; +import { wheelEventIn } from '../../../../A00_Common/input_prefs'; /** * 휠 한 번 → 화면 확대(`zoomScreen` 인자). 휠은 늘 줌 · 커서 자리 고정(153-3) — 마우스 · 트랙패드를 @@ -17,8 +18,12 @@ export function wheelZoom( const unitPx = evt.deltaMode === 1 ? WHEEL_LINE_PX : evt.deltaMode === 2 ? WHEEL_PAGE_PX : 1; const deltaY = evt.deltaY * unitPx; if (deltaY === 0) return null; - // Aislo: wheel direction is inverted on purpose - pulling the wheel zooms in, pushing - // zooms out, matching the B04/B05 maps and the B06 cross sections (2026-08-02). + // 방향 = ☰ [설정] 휠 확대 방향(기본 당김 = 몸 쪽으로 굴리면 확대 · 모든 화면 일체) · + // 트랙패드 핀치는 늘 벌리면 확대. zoomScreen 은 음수 = 확대. // 핀치(ctrl+휠)는 델타가 잘아 지수만 크게. - return { delta: -deltaY, exponent: evt.ctrlKey ? PINCH_ZOOM_EXPONENT : WHEEL_ZOOM_EXPONENT }; + const size = Math.abs(deltaY); + return { + delta: wheelEventIn({ ctrlKey: evt.ctrlKey, deltaY }) ? -size : size, + exponent: evt.ctrlKey ? PINCH_ZOOM_EXPONENT : WHEEL_ZOOM_EXPONENT, + }; } diff --git a/B07_DesignDetail/openwebcad/tsconfig.app.json b/B07_DesignDetail/openwebcad/tsconfig.app.json index 5ce3e3e8e..87ee7f72c 100644 --- a/B07_DesignDetail/openwebcad/tsconfig.app.json +++ b/B07_DesignDetail/openwebcad/tsconfig.app.json @@ -25,6 +25,6 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, - "include": ["src", "../../A00_Common/key_shortcut.ts"], + "include": ["src", "../../A00_Common/key_shortcut.ts", "../../A00_Common/input_prefs.ts"], "exclude": ["node_modules"] } diff --git a/resources/tester/cad/input-prefs-cad.test.ts b/resources/tester/cad/input-prefs-cad.test.ts new file mode 100644 index 000000000..18731237f --- /dev/null +++ b/resources/tester/cad/input-prefs-cad.test.ts @@ -0,0 +1,97 @@ +/** + * CAD 설정 배선(PLAN 332-3) — ☰ [설정] 휠 확대 방향 · 내 별칭(`A00_Common/input_prefs.ts`). + * 휠 = 기본 당김(지금과 같음) · 밀기 저장 뒤 뒤집힘 · 핀치는 늘 벌리면 확대. + * 별칭 = 명령 이름 > 내 별칭 > 기본 별칭 · 저장 즉시 따름. + * 실행: cd B07_DesignDetail/openwebcad && npx vitest run --dir ../../resources/tester/cad input-prefs-cad + */ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { INPUT_PREFS_KEY } from "../../../A00_Common/input_prefs"; +import { wheelZoom } from "../../../B07_DesignDetail/openwebcad/src/inputController/input-wheel"; +import { + matchCommandPrefixes, + resolveCommandInput, +} from "../../../B07_DesignDetail/openwebcad/src/commands/registry"; +import { getActiveToolActor } from "../../../B07_DesignDetail/openwebcad/src/state"; +import { initApplication } from "../../../B07_DesignDetail/openwebcad/test/helpers/init-application"; + +const store = new Map(); + +const savePrefs = (value: unknown) => store.set(INPUT_PREFS_KEY, JSON.stringify(value)); + +beforeEach(() => { + store.clear(); + vi.stubGlobal("localStorage", { + getItem: (key: string) => store.get(key) ?? null, + setItem: (key: string, value: string) => store.set(key, value), + removeItem: (key: string) => store.delete(key), + }); +}); + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +const wheel = (deltaY: number, ctrlKey = false) => + wheelZoom({ deltaY, deltaMode: 0, ctrlKey })?.delta; + +describe("휠 확대 방향", () => { + it("저장 없음 = 당김(몸 쪽 deltaY > 0 → 확대 = 음수 delta) — 지금과 같음", () => { + expect(wheel(100)).toBe(-100); + expect(wheel(-100)).toBe(100); + }); + + it("밀기 저장 → 뒤집힘 · 깨진 값 = 당김", () => { + savePrefs({ wheelZoom: "push" }); + expect(wheel(100)).toBe(100); + expect(wheel(-100)).toBe(-100); + store.set(INPUT_PREFS_KEY, "{깨짐"); + expect(wheel(100)).toBe(-100); + }); + + it("트랙패드 핀치(ctrl · 잔 델타)는 설정과 상관없이 벌리면(deltaY < 0) 확대", () => { + expect(wheel(-7, true)).toBe(-7); + expect(wheel(7, true)).toBe(7); + savePrefs({ wheelZoom: "push" }); + expect(wheel(-7, true)).toBe(-7); + // 마우스 Ctrl+휠(큰 델타)은 설정 방향 + expect(wheel(100, true)).toBe(100); + }); +}); + +describe("내 별칭", () => { + it("기본 별칭 그대로 · 내 별칭이 기본 별칭을 이김 · 명령 이름이 내 별칭을 이김", () => { + expect(resolveCommandInput("L")?.id).toBe("LINE"); + savePrefs({ cadAliases: { CIRCLE: ["L"], ARC: ["LINE"] } }); + expect(resolveCommandInput("l")?.id).toBe("CIRCLE"); + expect(resolveCommandInput("LINE")?.id).toBe("LINE"); + }); + + it("새 별칭 · 자동완성 후보 맨 앞 · 없는 명령 이름은 무시", () => { + savePrefs({ cadAliases: { RECTANG: ["QQ"], NOPE: ["QZ"] } }); + expect(resolveCommandInput("QQ")?.id).toBe("RECTANG"); + expect(matchCommandPrefixes("Q")[0]?.id).toBe("RECTANG"); + expect(resolveCommandInput("QZ")?.id).not.toBe("NOPE"); + }); + + it("CAD 키 입력 — 「qq␣」 = 사각형 명령", () => { + savePrefs({ cadAliases: { RECTANG: ["QQ"] } }); + const controller = initApplication(); + for (const k of ["q", "q", " "]) { + controller.handleKeyStroke({ + key: k, + code: "", + keyCode: 0, + isComposing: false, + shiftKey: false, + ctrlKey: false, + altKey: false, + metaKey: false, + repeat: false, + target: null, + preventDefault: vi.fn(), + stopPropagation: vi.fn(), + } as unknown as KeyboardEvent); + } + expect(getActiveToolActor()?.getSnapshot()?.context.type).toBe("RECTANGLE"); + }); +});