- 새 src/commands/prompt.ts — PromptSpec · renderPrompt(키워드 물음은 「또는」 없음) · matchOption(initget 줄임) · 세션 기억 · OPT · SELECT_OBJECTS - 새 inputController/prompt-input.ts — routeTyped 사다리(점 · 옵션 묻는 중 명령 이름 거절 · 식 · 단위 거절 · 투명 ') · 알림 줄 · 덧물음 · cancelActiveCommand · undoCommand - 새 tools/factories/end-command.ts — final(END) 갈고리 → 같은 틱 선택 도구 · state.ts 갈고리 · LASTPOINT - sequence-tool v2 — ROUTE · id · prompt · when · next · options · keyword · loopFrom · escCommits · pickfirst · 확정 뒤 기본 끝 · Esc 늘 끝 - 흠 — N1 ②③ · N5 · N6 · N11 · N18(앞글자 실행 없앰) · H1(Ctrl · Alt 글자 안 쌓음 · Ctrl+C · X · Shift+C · Shift+V · 1) · 우클릭 = 쉴 때 반복 · SE 뺌 · ED 더함 - 시험 — cad-drive 도우미 · prompt-frame · prompt-contract · 옛 시험 손질(keyboard-chain · ime-sink · command-catalog · input-prefs-cad · eraser 녹화) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QfHKQs7J3hQELKMWQMUJEy
132 lines
4.6 KiB
TypeScript
132 lines
4.6 KiB
TypeScript
/**
|
||
* CAD 시험 공용 조작(337 설계 6-2) — 빈 도면 · 쉬는 상태(선택 도구)에서 키 · 마우스를 사람처럼 넣음.
|
||
* 시험 쪽 flatten-js 는 다른 사본(「Illegal Parameters」) — 점은 앱 코드로만 만들고 숫자로 판정.
|
||
* 시험 캔버스는 축척 1 · y 위 + · 세계 (x, y) = 화면 (x, 높이 − y).
|
||
*/
|
||
import { vi } from "vitest";
|
||
import type { InputController } from "../../../../B07_DesignDetail/openwebcad/src/inputController/input-controller";
|
||
import {
|
||
currentNotice,
|
||
currentPrompt,
|
||
} from "../../../../B07_DesignDetail/openwebcad/src/inputController/prompt-input";
|
||
import {
|
||
getActiveToolActor,
|
||
getEntities,
|
||
setSelectedEntityIds,
|
||
} from "../../../../B07_DesignDetail/openwebcad/src/state";
|
||
import { ActorEvent } from "../../../../B07_DesignDetail/openwebcad/src/tools/tool.types";
|
||
import { initApplication } from "../../../../B07_DesignDetail/openwebcad/test/helpers/init-application";
|
||
import { CANVAS_HEIGHT } from "../../../../B07_DesignDetail/openwebcad/test/helpers/tests.consts";
|
||
|
||
export type Mods = Partial<Pick<KeyboardEvent, "shiftKey" | "ctrlKey" | "altKey" | "metaKey">> & {
|
||
code?: string;
|
||
repeat?: boolean;
|
||
};
|
||
|
||
let controller: InputController;
|
||
|
||
/** 빈 도면 · 고른 것 없음 · 선택 도구(쉼)로 시작 — 앱 도우미가 켠 LINE 첫 물음에서 Esc */
|
||
export function initIdle(): InputController {
|
||
controller = initApplication();
|
||
getActiveToolActor()?.send({ type: ActorEvent.ESC });
|
||
setSelectedEntityIds([]);
|
||
return controller;
|
||
}
|
||
|
||
export const driver = (): InputController => controller;
|
||
|
||
/** 키 하나 — `key` 는 KeyboardEvent.key 꼴(「a」 · 「Enter」 · 「Escape」 · 「 」) */
|
||
export function key(k: string, mods: Mods = {}) {
|
||
const preventDefault = vi.fn();
|
||
controller.handleKeyStroke({
|
||
key: k,
|
||
code: mods.code ?? "",
|
||
keyCode: 0,
|
||
isComposing: false,
|
||
shiftKey: false,
|
||
ctrlKey: false,
|
||
altKey: false,
|
||
metaKey: false,
|
||
repeat: false,
|
||
target: null,
|
||
preventDefault,
|
||
stopPropagation: vi.fn(),
|
||
...mods,
|
||
} as unknown as KeyboardEvent);
|
||
return preventDefault;
|
||
}
|
||
|
||
/** 글자만 침(제출 안 함) */
|
||
export const typed = (text: string) => {
|
||
for (const ch of text) key(ch);
|
||
};
|
||
/** 글자 + Space(= Enter) */
|
||
export const type = (text: string) => {
|
||
typed(text);
|
||
key(" ");
|
||
};
|
||
/** 글자 + Enter */
|
||
export const typeEnter = (text: string) => {
|
||
typed(text);
|
||
key("Enter");
|
||
};
|
||
|
||
const mouse = (x: number, y: number, button: number, mods: Mods = {}) =>
|
||
({
|
||
button,
|
||
clientX: x,
|
||
clientY: CANVAS_HEIGHT - y,
|
||
target: null,
|
||
shiftKey: false,
|
||
ctrlKey: false,
|
||
preventDefault: () => {},
|
||
stopPropagation: () => {},
|
||
...mods,
|
||
}) as unknown as MouseEvent;
|
||
|
||
/** 세계 좌표 (x, y) 로 커서 이동 */
|
||
export const move = (x: number, y: number) =>
|
||
controller.handleMouseMove({ clientX: x, clientY: CANVAS_HEIGHT - y } as MouseEvent);
|
||
export const press = (x: number, y: number, mods: Mods = {}) => {
|
||
move(x, y);
|
||
controller.handleMouseDown(mouse(x, y, 0, mods));
|
||
};
|
||
export const release = (x: number, y: number, mods: Mods = {}) => {
|
||
move(x, y);
|
||
controller.handleMouseUp(mouse(x, y, 0, mods));
|
||
};
|
||
/** 왼쪽 누름 한 번(누름 + 뗌 같은 자리) */
|
||
export const click = (x: number, y: number, mods: Mods = {}) => {
|
||
press(x, y, mods);
|
||
release(x, y, mods);
|
||
};
|
||
/** a 에서 눌러 b 에서 뗌 */
|
||
export const drag = (a: [number, number], b: [number, number], mods: Mods = {}) => {
|
||
press(a[0], a[1], mods);
|
||
move(b[0], b[1]);
|
||
release(b[0], b[1], mods);
|
||
};
|
||
export const rightClick = (shift = false) =>
|
||
controller.handleMouseUp(mouse(0, 0, 2, { shiftKey: shift }));
|
||
/** 가운데 단추 두 번 */
|
||
export const middleDouble = (x: number, y: number) => {
|
||
for (let i = 0; i < 2; i++) {
|
||
controller.handleMouseDown(mouse(x, y, 1));
|
||
controller.handleMouseUp(mouse(x, y, 1));
|
||
}
|
||
};
|
||
|
||
/** 도면 객체(유형 이름 거름 — 「Line」 · 「Circle」 …) */
|
||
export const entities = (type?: string) =>
|
||
getEntities().filter((entity) => !type || entity.getType() === type);
|
||
/** 커서 곁 첫 줄(지금 물음) */
|
||
export const promptLine = () => currentPrompt().line;
|
||
/** 커서 곁 둘째 줄(거절 · 안내) */
|
||
export const noticeLine = () => currentNotice();
|
||
/** 맨 위 도구 이름(Tool) */
|
||
export const activeTool = () => getActiveToolActor()?.getSnapshot()?.context?.type;
|
||
export const toolState = () => getActiveToolActor()?.getSnapshot()?.value;
|
||
/** 친 글(커서 곁 입력 칸) */
|
||
export const inputText = () => (controller as unknown as { text: string }).text;
|
||
export const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
|