Files
Aislo/resources/tester/cad/mouse-acad.test.ts
T
eomsangdonandClaude Opus 5.5 db8462c6d4 feat(B07 CAD): 337-2 마-1 오토캐드 마우스 — 누름 더함 · Shift 뺌 · 상자 더함/뺌 · 명령 중 끌기 상자 · 스침 · 그립 누르고-옮기고-누르기 · 그립 숫자 · 보기 전용 문 · 직교 강제 · Shift 임시 직교 · 휠 기본 밂 · 휠 두 번 범위
- 고르기: 누름 = 더함 · Shift = 뺌 · Ctrl = 넣고 빼기 · 상자 = 앞 고름 ∪ 상자 · Shift 상자 = 뺌(H4 · H5) · 명령 안 고르기 단계도 누를 때 첫 점 · 스침 강조(ic/selecting.ts)
- 그립: 쉴 때만 집음(N7) · 누르고 떼면 대기 → 다음 누름에 놓음 · 끌어 놓기(H3) · 거리 숫자(N8) · 확정 · 읽기 전용 도면은 그립 · Delete 막음(H2)
- 직교: F8 = 기준점에서 가로 · 세로 강제 · Shift 누른 동안 뒤집음 · 객체 스냅 점 · 좌표 · 각도 잠금은 안 덮음 · 클릭 사건 snapped 채움(ic/ortho.ts · H8) · 가이드는 F3 와 뗌 · 가이드 잡는 거리 = 화면 픽셀(H9)
- 휠: 기본 밂(앞으로 굴리면 확대 · 오토캐드 · 모든 화면 · ☰ 설정 뒤집기 그대로) · 가운데 단추 두 번 = 범위 줌 · ZOOM · ⛶ 앞 화면 기억(ic/zoom-prompt.ts)
- 시험: mouse-acad 새로 · 휠 방향 시험 손질(input-wheel · input-prefs-cad · input_prefs · app_settings · 164 · 220_1 · 221_1 · 244_8 기대값 뒤집음 · 266 pull 박음)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Se7ytqbohmBQnfYpwYFnYf
2026-10-15 23:41:46 +09:00

309 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* CAD 마우스 · 고르기 · 그립 · 직교 · 휠 — 오토캐드 조작(PLAN 337-2 마 · 요구 229-3 · 설계 4 장 · 6-3 마).
* 누름 더함 · Shift 뺌 · 상자 더함/뺌 · 명령 중 끌기 상자 · 스침 · N7 · 그립 누름-뗌 대기 · 끌어 놓음 · 그립 숫자 ·
* H2 문 · 직교 강제 · Shift 임시 직교 · 스냅 점 안 덮음 · F3 꺼도 직교 · H9 · 휠 두 번 범위 · 휠 기본 밂.
* 시험 쪽 flatten-js 는 다른 사본 — 점은 앱 코드(LINE 도구)로만 만들고 숫자로 판정.
* 실행: cd B07_DesignDetail/openwebcad && npx vitest run --dir ../../resources/tester/cad mouse-acad
*/
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("react-toastify", () => ({
toast: Object.assign(vi.fn(), { error: vi.fn(), info: vi.fn(), warn: vi.fn(), success: vi.fn() }),
}));
import { Actor } from "xstate";
import { InputController } from "../../../B07_DesignDetail/openwebcad/src/inputController/input-controller";
import { zoomPrevious } from "../../../B07_DesignDetail/openwebcad/src/inputController/zoom-prompt";
import { wheelZoom } from "../../../B07_DesignDetail/openwebcad/src/inputController/input-wheel";
import {
getActiveToolActor,
getAngleGuideEntities,
getEntities,
getHighlightedEntityIds,
getScreenCanvasDrawController,
getSelectedEntityIds,
setActiveToolActor,
setAngleStep,
setEntities,
setHostReadOnly,
setSelectedEntityIds,
setSnapEnabled,
} from "../../../B07_DesignDetail/openwebcad/src/state";
import { Tool } from "../../../B07_DesignDetail/openwebcad/src/tools";
import { selectToolStateMachine } from "../../../B07_DesignDetail/openwebcad/src/tools/select-tool";
import { TOOL_STATE_MACHINES } from "../../../B07_DesignDetail/openwebcad/src/commands/registry";
import { runCommandInput } from "../../../B07_DesignDetail/openwebcad/src/commands/run-command";
import { initApplication } from "../../../B07_DesignDetail/openwebcad/test/helpers/init-application";
import { CANVAS_HEIGHT } from "../../../B07_DesignDetail/openwebcad/test/helpers/tests.consts";
type Xy = { x: number; y: number };
type Seg = { start: Xy; end: Xy };
type Mods = { shiftKey?: boolean; ctrlKey?: boolean; button?: number };
let controller: InputController;
/** 세계 (x, y) → 마우스 사건 — 시험 캔버스 화면 원점 0 · y 위 + · 배율 따름 */
const ev = (x: number, y: number, mods: Mods = {}) => {
const scale = getScreenCanvasDrawController().getScreenScale();
return {
button: 0,
shiftKey: false,
ctrlKey: false,
clientX: x * scale,
clientY: CANVAS_HEIGHT - y * scale,
target: null,
preventDefault: () => {},
stopPropagation: () => {},
...mods,
} as unknown as MouseEvent;
};
const move = (x: number, y: number, mods?: Mods) => controller.handleMouseMove(ev(x, y, mods));
const press = (x: number, y: number, mods?: Mods) => {
move(x, y, mods);
controller.handleMouseDown(ev(x, y, mods));
};
const release = (x: number, y: number, mods?: Mods) => {
move(x, y, mods);
controller.handleMouseUp(ev(x, y, mods));
};
const click = (x: number, y: number, mods?: Mods) => {
press(x, y, mods);
release(x, y, mods);
};
const key = (k: string) =>
controller.handleKeyStroke({
key: k,
code: "",
isComposing: false,
shiftKey: false,
ctrlKey: false,
altKey: false,
metaKey: false,
repeat: false,
target: null,
preventDefault: vi.fn(),
stopPropagation: vi.fn(),
} as unknown as KeyboardEvent);
const typeEnter = (text: string) => {
for (const ch of text) key(ch);
key("Enter");
};
const toolType = () => getActiveToolActor()?.getSnapshot()?.context?.type;
const idle = () => setActiveToolActor(new Actor(selectToolStateMachine));
const lineTool = () => setActiveToolActor(new Actor(TOOL_STATE_MACHINES[Tool.LINE]));
const segOf = (id: string) =>
(getEntities().find((e) => e.id === id) as unknown as { segment: Seg }).segment;
const lastSeg = () => {
const lines = getEntities().filter((e) => e.getType() === "Line");
return (lines[lines.length - 1] as unknown as { segment: Seg }).segment;
};
const expectXy = (point: Xy, x: number, y: number) => {
expect(point.x).toBeCloseTo(x, 3);
expect(point.y).toBeCloseTo(y, 3);
};
/** LINE 도구로 선 하나(앱 쪽 점) — 그 id */
const drawLine = (x1: number, y1: number, x2: number, y2: number): string => {
lineTool();
click(x1, y1);
click(x2, y2);
idle();
const lines = getEntities();
return lines[lines.length - 1].id;
};
let a = "";
let b = "";
beforeEach(() => {
controller = initApplication();
setEntities([], true);
setSelectedEntityIds([]);
setAngleStep(0, false); // 극 · 직교 끔 — 가이드 스냅이 판정을 흔들지 않게
a = drawLine(0, 0, 100, 0);
b = drawLine(0, 200, 100, 200);
setAngleStep(45, false);
setSelectedEntityIds([]);
idle();
});
afterEach(() => {
setAngleStep(45, false);
setSnapEnabled(true);
setHostReadOnly(false);
vi.restoreAllMocks();
});
describe("고르기 — 누름 더함 · Shift 뺌 · 상자(H4 · H5)", () => {
it("쉴 때 누름 = 더함 · Shift+누름 = 뺌", () => {
click(50, 0);
expect(getSelectedEntityIds()).toEqual([a]);
click(50, 200);
expect(getSelectedEntityIds()).toEqual([a, b]);
click(25, 0, { shiftKey: true }); // 가운데 (50, 0) 은 그립
expect(getSelectedEntityIds()).toEqual([b]);
});
it("상자 = 앞 고름 ∪ 상자 · Shift+상자 = 앞 고름 − 상자(끌기 · 클릭-클릭 둘 다)", () => {
click(50, 0);
// 창(왼→오) 끌기로 B
press(-20, 180);
release(120, 220);
expect(getSelectedEntityIds().sort()).toEqual([a, b].sort());
// Shift + 클릭-클릭 상자로 A 뺌
click(-20, -20, { shiftKey: true });
click(120, 20, { shiftKey: true });
expect(getSelectedEntityIds()).toEqual([b]);
});
it("명령 중 — 누름 = 더함 · 끌기 상자 · 스침 강조", () => {
runCommandInput("MOVE");
expect(toolType()).toBe(Tool.MOVE);
move(50, 1);
expect(getHighlightedEntityIds()).toEqual([a]);
move(50, 100);
expect(getHighlightedEntityIds()).toEqual([]);
click(50, 0);
press(-20, 180); // 빈 곳 누름 → 끌어 뗌 = 상자
release(120, 220);
expect(getSelectedEntityIds().sort()).toEqual([a, b].sort());
expect(toolType()).toBe(Tool.MOVE);
});
});
describe("그립 — 누르고-옮기고-누르기 · 끌기 · 숫자 · N7 · H2", () => {
it("그립 누르고 떼면 대기(안 밀림 · H3) → 다음 누름에 놓음", () => {
click(50, 0);
click(100, 0); // 끝 그립
expect(toolType()).toBe(Tool.GRIP_EDIT);
expectXy(segOf(a).end, 100, 0);
click(130, 60);
expectXy(segOf(a).end, 130, 60);
expect(toolType()).toBe(Tool.SELECT);
});
it("그립 끌어 놓음 = 놓은 자리", () => {
click(50, 0);
press(100, 0);
release(140, 70);
expectXy(segOf(a).end, 140, 70);
expect(toolType()).toBe(Tool.SELECT);
});
it("그립 숫자 = 그립 점에서 커서 쪽 거리(N8)", () => {
click(50, 0);
click(100, 0);
move(100, 80);
typeEnter("30");
expectXy(segOf(a).end, 100, 30);
});
it("명령 안 고르기 중엔 그립을 안 집음(N7)", () => {
runCommandInput("MOVE");
click(50, 0);
click(100, 0);
expect(toolType()).toBe(Tool.MOVE);
expectXy(segOf(a).end, 100, 0);
});
it("확정 · 읽기 전용 도면 — 그립 · Delete 막음(H2)", () => {
click(50, 0);
setHostReadOnly(true);
click(100, 0);
expect(toolType()).toBe(Tool.SELECT);
key("Delete");
expect(getEntities().map((e) => e.id)).toContain(a);
setHostReadOnly(false);
key("Delete");
expect(getEntities().map((e) => e.id)).not.toContain(a);
});
});
describe("직교(H8) · 가이드 거리(H9)", () => {
const startLine = () => {
lineTool();
click(300, 300);
};
it("F8 = 직교 강제 — 누름 · 거리 숫자 · 꺼짐 비교", () => {
setAngleStep(90, false);
startLine();
click(400, 340); // 가로가 더 멂 → (400, 300)
expectXy(lastSeg().end, 400, 300);
move(420, 500); // 세로 쪽 → 거리 50 = 위로
typeEnter("50");
expectXy(lastSeg().end, 400, 350);
setAngleStep(0, false);
click(470, 380);
expectXy(lastSeg().end, 470, 380);
});
it("Shift 누른 동안 임시 직교 · 직교 켠 채 Shift = 풂", () => {
setAngleStep(0, false);
startLine();
click(400, 340, { shiftKey: true });
expectXy(lastSeg().end, 400, 300);
setAngleStep(90, false);
click(450, 370, { shiftKey: true });
expectXy(lastSeg().end, 450, 370);
});
it("객체 스냅 점은 직교가 안 덮음 · F3 꺼도 직교", () => {
setAngleStep(90, false);
lineTool();
click(10, 100);
click(98, 197); // B 시작점 근처 (100, 200) — 끝점 스냅
expectXy(lastSeg().end, 100, 200);
setSnapEnabled(false);
lineTool();
click(10, 100);
click(98, 197);
expectXy(lastSeg().end, 10, 197);
});
it("가이드 잡는 거리 = 화면 픽셀(배율 2 → 도면 7.5 · 0.5 → 30)", () => {
setAngleStep(90, false);
const view = getScreenCanvasDrawController();
view.setScreenScale(2);
startLine();
move(380, 310); // 가로 가이드에서 10
expect(getAngleGuideEntities()).toHaveLength(0);
view.setScreenScale(0.5);
move(380, 320); // 20
expect(getAngleGuideEntities()).toHaveLength(1);
view.setScreenScale(1);
});
});
describe("휠", () => {
it("기본 = 밂(앞으로 굴리면 확대 · 오토캐드) — 저장 없음", () => {
expect(wheelZoom({ deltaY: -100, deltaMode: 0, ctrlKey: false })?.delta).toBeLessThan(0);
expect(wheelZoom({ deltaY: 100, deltaMode: 0, ctrlKey: false })?.delta).toBeGreaterThan(0);
});
it("휠 두 번 = 범위 줌 · 이전 화면 되돌림 · 느리면 아님", () => {
const view = getScreenCanvasDrawController() as unknown as {
zoomToFitScreen: () => void;
setScreenScale: (s: number) => void;
getScreenScale: () => number;
};
view.zoomToFitScreen = vi.fn(() => view.setScreenScale(3));
const now = vi.spyOn(Date, "now");
now.mockReturnValue(1000);
press(10, 10, { button: 1 });
now.mockReturnValue(1300);
press(11, 11, { button: 1 });
expect(view.zoomToFitScreen).toHaveBeenCalledTimes(1);
expect(view.getScreenScale()).toBe(3);
expect(zoomPrevious()).toBe(true);
expect(view.getScreenScale()).toBe(1);
now.mockReturnValue(5000);
press(10, 10, { button: 1 });
now.mockReturnValue(5600);
press(10, 10, { button: 1 });
expect(view.zoomToFitScreen).toHaveBeenCalledTimes(1);
});
});