Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QfHKQs7J3hQELKMWQMUJEy
720 lines
23 KiB
TypeScript
720 lines
23 KiB
TypeScript
/**
|
|
* 337 가-1 · 가-2 옵션 틀 — 물음 꼴 · 옵션 맞추기 · 친 글 사다리 · 거절 · Esc · 빈 Enter · 우클릭 · 명령 끝 · 엔진 v2 · 기억 · 덧물음 · Ctrl.
|
|
* 설계 `docs/raw/조사/2026-10-15_337_CAD오토캐드조작_설계.md` 2 장 · 6-3 가 줄.
|
|
* 실행: cd B07_DesignDetail/openwebcad && npx vitest run --dir ../../resources/tester/cad prompt-frame
|
|
*/
|
|
import { readFileSync } from "node:fs";
|
|
import { 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 {
|
|
OPT,
|
|
type OptionSpec,
|
|
type PromptSpec,
|
|
SELECT_OBJECTS,
|
|
matchOption,
|
|
previousSelection,
|
|
recall,
|
|
remember,
|
|
renderPrompt,
|
|
} from "../../../B07_DesignDetail/openwebcad/src/commands/prompt";
|
|
import { TOOL_STATE_MACHINES } from "../../../B07_DesignDetail/openwebcad/src/commands/registry";
|
|
import {
|
|
getLastCommandId,
|
|
runCommandInput,
|
|
} from "../../../B07_DesignDetail/openwebcad/src/commands/run-command";
|
|
import {
|
|
pushSubPrompt,
|
|
routeTyped,
|
|
} from "../../../B07_DesignDetail/openwebcad/src/inputController/prompt-input";
|
|
import {
|
|
getActiveToolActor,
|
|
getSelectedEntityIds,
|
|
setActiveToolActor,
|
|
setSelectedEntityIds,
|
|
} from "../../../B07_DesignDetail/openwebcad/src/state";
|
|
import { commandCandidate } from "../../../B07_DesignDetail/openwebcad/src/inputController/input-history";
|
|
import { getSnapOverride } from "../../../B07_DesignDetail/openwebcad/src/inputController/input-snap-override";
|
|
import { getScreenCanvasDrawController } from "../../../B07_DesignDetail/openwebcad/src/state";
|
|
import { Tool } from "../../../B07_DesignDetail/openwebcad/src/tools";
|
|
import { createSequenceTool } from "../../../B07_DesignDetail/openwebcad/src/tools/factories/sequence-tool";
|
|
import {
|
|
activeTool,
|
|
click,
|
|
driver,
|
|
entities,
|
|
initIdle,
|
|
inputText,
|
|
key,
|
|
noticeLine,
|
|
promptLine,
|
|
rightClick,
|
|
toolState,
|
|
type,
|
|
typeEnter,
|
|
typed,
|
|
} from "./helpers/cad-drive";
|
|
|
|
type Xy = { x: number; y: number };
|
|
const opt = (key: string, kw: string, label: string): OptionSpec => ({ key, kw, en: key, label });
|
|
const start = (machine: unknown) => setActiveToolActor(new Actor(machine as never), false);
|
|
const enter = () => key("Enter");
|
|
const lineIds = () => entities("Line").map((entity) => entity.id);
|
|
|
|
beforeEach(() => {
|
|
initIdle();
|
|
});
|
|
|
|
describe("옵션 맞추기(2-4)", () => {
|
|
const prompt: PromptSpec = {
|
|
text: "다음 점 지정",
|
|
options: [
|
|
opt("CENTER", "C", "중심"),
|
|
opt("CLOSE", "CL", "닫기"),
|
|
{ key: "LTYPE", kw: "LT", en: "LTYPE", label: "선종류" },
|
|
OPT.UNDO,
|
|
OPT.ALL,
|
|
],
|
|
absent: [{ key: "ELEVATION", kw: "E", en: "ELEVATION", label: "고도" }],
|
|
};
|
|
const keyOf = (text: string) => {
|
|
const hit = matchOption(text, prompt);
|
|
return hit && "option" in hit
|
|
? hit.option.key
|
|
: hit && "absent" in hit
|
|
? `없음:${hit.absent.key}`
|
|
: null;
|
|
};
|
|
|
|
it("대소문자 · 앞 `_` · 빈칸 · kw · en · 사이 길이", () => {
|
|
expect(keyOf("c")).toBe("CENTER");
|
|
expect(keyOf(" _center ")).toBe("CENTER");
|
|
expect(keyOf("CE")).toBe("CENTER");
|
|
expect(keyOf("cl")).toBe("CLOSE"); // kw 딱 맞음 우선
|
|
expect(keyOf("clo")).toBe("CLOSE");
|
|
expect(keyOf("u")).toBe("UNDO");
|
|
expect(keyOf("und")).toBe("UNDO");
|
|
expect(keyOf("lty")).toBe("LTYPE");
|
|
});
|
|
|
|
it("kw 보다 짧으면 안 맞음 · 숨김도 맞음 · 없는 옵션 안내 · 엉뚱한 글", () => {
|
|
expect(keyOf("l")).toBeNull(); // LT 의 L 은 모자람(initget)
|
|
expect(keyOf("all")).toBe("ALL");
|
|
expect(keyOf("e")).toBe("없음:ELEVATION");
|
|
expect(keyOf("zz")).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("물음 그림(2-3)", () => {
|
|
it("옵션 · 기본 · 숨김", () => {
|
|
expect(renderPrompt({ text: "다음 점 지정", options: [OPT.CLOSE, OPT.UNDO] })).toBe(
|
|
"다음 점 지정 또는 [닫기(C)/명령 취소(U)]:",
|
|
);
|
|
expect(
|
|
renderPrompt({
|
|
text: "기준점 지정",
|
|
options: [OPT.DISPLACEMENT],
|
|
default: { kind: "option", key: "DISPLACEMENT" },
|
|
}),
|
|
).toBe("기준점 지정 또는 [변위(D)] <변위>:");
|
|
expect(renderPrompt(SELECT_OBJECTS)).toBe("객체 선택:");
|
|
expect(
|
|
renderPrompt({
|
|
text: "두 번째 점 지정",
|
|
default: { kind: "enter", label: "첫 번째 점을 변위로 사용" },
|
|
}),
|
|
).toBe("두 번째 점 지정 <첫 번째 점을 변위로 사용>:");
|
|
});
|
|
|
|
it("값 기본 — 소수 넷 · 정수 · 각 · 좌표 · 기억이 처음 값을 이김 · null 은 안 그림", () => {
|
|
const length: PromptSpec = {
|
|
text: "직사각형의 길이 지정",
|
|
default: { kind: "value", remember: "T.RECT", initial: 10 },
|
|
};
|
|
expect(renderPrompt(length)).toBe("직사각형의 길이 지정 <10.0000>:");
|
|
remember("T.RECT", 12.5);
|
|
expect(renderPrompt(length)).toBe("직사각형의 길이 지정 <12.5000>:");
|
|
expect(recall("T.RECT")).toBe(12.5);
|
|
expect(
|
|
renderPrompt({
|
|
text: "변의 수 입력",
|
|
default: { kind: "value", remember: "T.INT", initial: 4, format: "int" },
|
|
}),
|
|
).toBe("변의 수 입력 <4>:");
|
|
expect(
|
|
renderPrompt({
|
|
text: "각 지정",
|
|
default: { kind: "value", remember: "T.ANG", initial: 22.5, format: "ang" },
|
|
}),
|
|
).toBe("각 지정 <22.5>:");
|
|
expect(
|
|
renderPrompt({
|
|
text: "변위 지정",
|
|
default: { kind: "value", remember: "T.XY", initial: { x: 0, y: 0 } },
|
|
}),
|
|
).toBe("변위 지정 <0.0000, 0.0000>:");
|
|
expect(
|
|
renderPrompt({
|
|
text: "반지름 지정",
|
|
default: { kind: "value", remember: "T.NULL", initial: null },
|
|
}),
|
|
).toBe("반지름 지정:");
|
|
});
|
|
});
|
|
|
|
/** 빈 Enter 넷 갈래 · 숫자 검사 · 키워드 — 변의 수 → 중심 → 내접/외접 → 점 되풀이 */
|
|
const commits: unknown[][] = [];
|
|
const polygonLike = createSequenceTool({
|
|
tool: Tool.POLYGON,
|
|
repeatLastStep: true,
|
|
steps: [
|
|
{
|
|
kind: "number",
|
|
id: "sides",
|
|
prompt: {
|
|
text: "변의 수 입력",
|
|
default: { kind: "value", remember: "T.SIDES", initial: 4, format: "int" },
|
|
number: { int: true, min: 3, max: 1024 },
|
|
},
|
|
},
|
|
{
|
|
kind: "point",
|
|
id: "center",
|
|
prompt: {
|
|
text: "중심 지정",
|
|
default: { kind: "value", remember: "T.CENTER", initial: { x: 5, y: 6 } },
|
|
},
|
|
},
|
|
{
|
|
kind: "keyword",
|
|
id: "ic",
|
|
prompt: {
|
|
text: "옵션 입력",
|
|
options: [opt("INSCRIBED", "I", "원에 내접"), opt("CIRCUMSCRIBED", "C", "원에 외접")],
|
|
default: { kind: "option", key: "INSCRIBED", label: "I" },
|
|
},
|
|
},
|
|
{
|
|
kind: "point",
|
|
id: "pts",
|
|
prompt: { text: "다음 점 지정", default: { kind: "enter", label: "끝" } },
|
|
},
|
|
],
|
|
commit: (input) => {
|
|
const center = input.get<Xy>("center");
|
|
commits.push([input.get("sides"), center.x, center.y, input.get("ic"), input.points().length]);
|
|
},
|
|
});
|
|
|
|
describe("빈 Enter 넷 갈래 · 거절 · 기억 · 끝(2-5 · 2-6 · 2-8 · 2-9)", () => {
|
|
beforeEach(() => {
|
|
commits.length = 0;
|
|
});
|
|
|
|
it("값(숫자) · 값(좌표) · 옵션 · enter → 확정 → 같은 틱에 쉬는 상태", () => {
|
|
start(polygonLike);
|
|
expect(promptLine()).toBe("변의 수 입력 <4>:");
|
|
enter(); // 4
|
|
expect(promptLine()).toBe("중심 지정 <5.0000, 6.0000>:");
|
|
enter(); // (5, 6)
|
|
expect(promptLine()).toBe("옵션 입력 [원에 내접(I)/원에 외접(C)] <I>:");
|
|
enter(); // INSCRIBED
|
|
type("1,1");
|
|
expect(promptLine()).toBe("다음 점 지정 <끝>:");
|
|
enter(); // 되풀이 점 = 확정
|
|
expect(commits).toEqual([[4, 5, 6, "INSCRIBED", 2]]);
|
|
expect(activeTool()).toBe(Tool.SELECT); // await 없이
|
|
expect(toolState()).toBe("WAITING_FOR_FIRST_SELECT_POINT");
|
|
});
|
|
|
|
it("숫자 물음 — 식 · 단위 · 정수 · 범위 · 명령 이름 거절 · 받은 값은 다음 <기본>", () => {
|
|
start(polygonLike);
|
|
const rejects: Array<[string, string]> = [
|
|
["3.2*1.5", "숫자 값을 요구함."],
|
|
["2m", "숫자 값을 요구함."],
|
|
["l", "숫자 값을 요구함."],
|
|
["2.5", "정수 값을 요구함."],
|
|
["2", "3에서 1024 사이의 정수를 요구함."],
|
|
];
|
|
for (const [text, message] of rejects) {
|
|
type(text);
|
|
expect(noticeLine()).toBe(message);
|
|
expect(toolState()).toBe("STEP_0");
|
|
expect(inputText()).toBe("");
|
|
}
|
|
type("6");
|
|
type("0,0");
|
|
type("c"); // 소문자 키워드
|
|
type("1,0");
|
|
enter();
|
|
expect(commits).toEqual([[6, 0, 0, "CIRCUMSCRIBED", 2]]);
|
|
start(polygonLike);
|
|
expect(promptLine()).toBe("변의 수 입력 <6>:"); // 세션 기억
|
|
enter();
|
|
expect(promptLine()).toBe("중심 지정 <0.0000, 0.0000>:"); // 누른 점도 기억
|
|
});
|
|
|
|
it("점 물음 — 명령 이름 · 엉뚱한 글 거절(도구 그대로) · 키워드 물음 거절 글", () => {
|
|
start(polygonLike);
|
|
enter();
|
|
type("circle");
|
|
expect(noticeLine()).toBe("점 또는 옵션 키워드를 요구함.");
|
|
expect(activeTool()).toBe(Tool.POLYGON);
|
|
type("0,0");
|
|
type("x");
|
|
expect(noticeLine()).toBe("옵션 키워드가 잘못됨.");
|
|
expect(toolState()).toBe("STEP_2");
|
|
});
|
|
|
|
it("Esc 한 번 = 명령 끝(확정 없음) · 기록 줄", () => {
|
|
start(polygonLike);
|
|
enter();
|
|
type("0,0");
|
|
key("Escape");
|
|
expect(activeTool()).toBe(Tool.SELECT);
|
|
expect(commits).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("엔진 v2 — loopFrom · when · mode · next · escCommits · undo · close · pickfirst(2-13)", () => {
|
|
it("loopFrom = 앞 값 둠 · mode · when · next · 되풀이 첫 물음 Enter = 끝", () => {
|
|
const seen: unknown[] = [];
|
|
const tool = createSequenceTool({
|
|
tool: Tool.DIST,
|
|
loopFrom: "at",
|
|
steps: [
|
|
{
|
|
kind: "number",
|
|
id: "d",
|
|
prompt: { text: "거리 지정", default: { kind: "value", remember: "T.D", initial: 1 } },
|
|
},
|
|
{
|
|
kind: "point",
|
|
id: "at",
|
|
prompt: { text: "점 지정", options: [opt("MODE", "M", "모드")] },
|
|
options: { MODE: { mode: "m", next: "extra" } },
|
|
},
|
|
{
|
|
kind: "number",
|
|
id: "extra",
|
|
when: (input) => input.mode === "m" && !input.has("extra"),
|
|
next: "at",
|
|
prompt: { text: "덧 값" },
|
|
},
|
|
],
|
|
commit: (input) =>
|
|
seen.push([
|
|
input.get("d"),
|
|
input.get<Xy>("at").x,
|
|
input.has("extra") ? input.get("extra") : null,
|
|
input.mode,
|
|
]),
|
|
});
|
|
start(tool);
|
|
enter(); // d = 1
|
|
type("1,1");
|
|
expect(seen).toEqual([[1, 1, null, null]]);
|
|
expect(promptLine()).toBe("점 지정 또는 [모드(M)]:"); // 거리는 둠 · 점부터 다시
|
|
type("m");
|
|
expect(promptLine()).toBe("덧 값:");
|
|
type("7");
|
|
type("2,2");
|
|
expect(seen[1]).toEqual([1, 2, 7, "m"]);
|
|
enter(); // 한 번 돈 뒤 첫 되풀이 물음 Enter = 끝
|
|
expect(activeTool()).toBe(Tool.SELECT);
|
|
});
|
|
|
|
const polys: Array<{ points: number; closed: boolean }> = [];
|
|
const plineLike = createSequenceTool({
|
|
tool: Tool.DIST,
|
|
repeatLastStep: true,
|
|
escCommits: true,
|
|
steps: [
|
|
{ kind: "point", prompt: { text: "시작점 지정" } },
|
|
{
|
|
kind: "point",
|
|
prompt: (input) => ({
|
|
text: "다음 점 지정",
|
|
options: input.points().length >= 3 ? [OPT.CLOSE, OPT.UNDO] : [OPT.UNDO],
|
|
}),
|
|
options: { CLOSE: "close", UNDO: "undo" },
|
|
},
|
|
],
|
|
commit: (input) => polys.push({ points: input.points().length, closed: input.closed }),
|
|
});
|
|
|
|
it("undo · close(셋째 점부터) · escCommits · 점 하나 Esc = 버림", () => {
|
|
polys.length = 0;
|
|
start(plineLike);
|
|
type("0,0");
|
|
type("10,0");
|
|
type("c"); // 아직 닫기 없음
|
|
expect(noticeLine()).toBe("점 또는 옵션 키워드를 요구함.");
|
|
type("10,10");
|
|
expect(promptLine()).toBe("다음 점 지정 또는 [닫기(C)/명령 취소(U)]:");
|
|
type("u");
|
|
type("20,0");
|
|
key("Escape"); // 모은 값으로 확정 뒤 끝
|
|
expect(polys).toEqual([{ points: 3, closed: false }]);
|
|
expect(activeTool()).toBe(Tool.SELECT);
|
|
|
|
start(plineLike);
|
|
type("0,0");
|
|
type("10,0");
|
|
type("10,10");
|
|
type("c");
|
|
expect(polys[1]).toEqual({ points: 3, closed: true });
|
|
|
|
start(plineLike);
|
|
type("0,0");
|
|
key("Escape");
|
|
expect(polys).toHaveLength(2);
|
|
});
|
|
|
|
it("고르기 첫 단계 — 고른 것 없이 Enter = 끝 · 고른 채 시작 = 바로 다음 물음(PICKFIRST) · 이전(P) 기억", () => {
|
|
const picked: number[] = [];
|
|
const tool = createSequenceTool({
|
|
tool: Tool.DIST,
|
|
steps: [{ kind: "selection" }, { kind: "point", prompt: { text: "기준점 지정" } }],
|
|
commit: (input) => picked.push(input.entities(0).length),
|
|
});
|
|
start(tool);
|
|
expect(promptLine()).toBe("객체 선택:");
|
|
enter();
|
|
expect(activeTool()).toBe(Tool.SELECT);
|
|
expect(picked).toEqual([]);
|
|
|
|
type("l");
|
|
type("0,0");
|
|
type("10,0");
|
|
key("Escape");
|
|
const ids = lineIds();
|
|
setSelectedEntityIds(ids);
|
|
start(tool);
|
|
expect(promptLine()).toBe("기준점 지정:");
|
|
expect(previousSelection()).toEqual(ids);
|
|
type("5,5");
|
|
expect(picked).toEqual([1]);
|
|
});
|
|
|
|
it("고르기 명령 Esc = 끝 + 고름 풂", () => {
|
|
const tool = createSequenceTool({
|
|
tool: Tool.DIST,
|
|
steps: [{ kind: "selection" }, { kind: "point", prompt: { text: "기준점 지정" } }],
|
|
commit: () => {},
|
|
});
|
|
type("l");
|
|
type("0,0");
|
|
type("10,0");
|
|
key("Escape");
|
|
setSelectedEntityIds(lineIds());
|
|
start(tool);
|
|
key("Escape");
|
|
expect(activeTool()).toBe(Tool.SELECT);
|
|
expect(getSelectedEntityIds()).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("쉼 · 명령 이름 · 별칭(2-5 ⑨ · 2-17 · N18)", () => {
|
|
it("앱 시작 = 쉼(선택 도구) — main.tsx 가 켜는 도구 · 「l ⏎」 = 첫 번째 점 지정", () => {
|
|
const main = readFileSync(
|
|
new URL("../../../B07_DesignDetail/openwebcad/src/main.tsx", import.meta.url),
|
|
"utf8",
|
|
);
|
|
const started = [...main.matchAll(/TOOL_STATE_MACHINES\[Tool\.(\w+)\]/g)].map((m) => m[1]);
|
|
expect(started).toEqual(["SELECT"]);
|
|
start(TOOL_STATE_MACHINES[Tool.SELECT]);
|
|
expect(activeTool()).toBe(Tool.SELECT);
|
|
const idle = promptLine();
|
|
expect(idle).not.toBe("첫 번째 점 지정:");
|
|
typeEnter("l");
|
|
expect(activeTool()).toBe(Tool.LINE);
|
|
expect(promptLine()).toBe("첫 번째 점 지정:");
|
|
key("Escape");
|
|
expect(promptLine()).toBe(idle);
|
|
});
|
|
|
|
it("딱 맞음만 — R · LE · CIR · SE = 알 수 없는 명령 · l = LINE · ed = TEXTEDIT", () => {
|
|
for (const text of ["r", "le", "cir", "se"]) {
|
|
type(text);
|
|
expect(noticeLine()).toBe(`알 수 없는 명령 「${text.toUpperCase()}」.`);
|
|
expect(activeTool()).toBe(Tool.SELECT);
|
|
}
|
|
type("ed");
|
|
expect(activeTool()).toBe(Tool.TEXTEDIT);
|
|
key("Escape");
|
|
type("l");
|
|
expect(activeTool()).toBe(Tool.LINE);
|
|
});
|
|
|
|
it("빈 Enter · Space · 우클릭(쉴 때) = 앞 명령 반복", () => {
|
|
type("l");
|
|
key("Escape");
|
|
expect(activeTool()).toBe(Tool.SELECT);
|
|
rightClick();
|
|
expect(activeTool()).toBe(Tool.LINE);
|
|
key("Escape");
|
|
key(" ");
|
|
expect(activeTool()).toBe(Tool.LINE);
|
|
});
|
|
|
|
it("Esc 한 번 = 선 끝(그린 토막 남음) · 쉴 때 Esc = 고름 풂", () => {
|
|
type("l");
|
|
type("0,0");
|
|
type("10,0");
|
|
typed("12");
|
|
key("Escape");
|
|
expect(activeTool()).toBe(Tool.SELECT);
|
|
expect(inputText()).toBe("");
|
|
expect(entities("Line")).toHaveLength(1);
|
|
setSelectedEntityIds(lineIds());
|
|
key("Escape");
|
|
expect(getSelectedEntityIds()).toEqual([]);
|
|
});
|
|
|
|
it("명령행 소문자 옵션(N6) — 선 c = 닫기", () => {
|
|
type("l");
|
|
type("0,0");
|
|
type("10,0");
|
|
type("10,10");
|
|
driver().submitText("c");
|
|
expect(entities("Line")).toHaveLength(3);
|
|
});
|
|
});
|
|
|
|
describe("명령 바꿈 · 되돌림 · 덧물음 · 투명(2-8 · 2-11 · N1 · N11)", () => {
|
|
const drawSelected = () => {
|
|
type("l");
|
|
type("0,0");
|
|
type("20,0");
|
|
key("Escape");
|
|
setSelectedEntityIds(lineIds());
|
|
};
|
|
|
|
it("N1 — 이동 기준점 뒤 딴 명령 = 끊고(원본 되살림) 켬 · 선 1 개", () => {
|
|
drawSelected();
|
|
type("move");
|
|
type("0,0");
|
|
runCommandInput("CIRCLE");
|
|
expect(activeTool()).toBe(Tool.CIRCLE);
|
|
expect(entities("Line")).toHaveLength(1);
|
|
});
|
|
|
|
it("N1 — 이동 도중 Ctrl+Z = 끊고 뒤 되돌림 · 같은 id 두 벌 없음", () => {
|
|
drawSelected();
|
|
type("move");
|
|
type("0,0");
|
|
key("z", { ctrlKey: true, code: "KeyZ" });
|
|
const ids = entities().map((entity) => entity.id);
|
|
expect(new Set(ids).size).toBe(ids.length);
|
|
expect(activeTool()).toBe(Tool.SELECT);
|
|
});
|
|
|
|
it("쉴 때 U = UNDO · 명령 중 U 는 옵션일 때만", () => {
|
|
type("l");
|
|
type("0,0");
|
|
type("10,0");
|
|
key("Escape");
|
|
expect(entities("Line")).toHaveLength(1);
|
|
type("u");
|
|
expect(entities("Line")).toHaveLength(0);
|
|
start(polygonLike);
|
|
type("u");
|
|
expect(noticeLine()).toBe("숫자 값을 요구함.");
|
|
});
|
|
|
|
it("덧물음 — 친 점 · 누른 점이 덧물음 먼저 · {point} = 원래 물음에", () => {
|
|
const seen: Xy[] = [];
|
|
type("l");
|
|
pushSubPrompt({
|
|
prompt: { text: "덧 점 지정" },
|
|
kind: "point",
|
|
onPoint: (point) => {
|
|
seen.push({ x: point.x, y: point.y });
|
|
return seen.length === 1 ? "stay" : { point };
|
|
},
|
|
});
|
|
expect(promptLine()).toBe("덧 점 지정:");
|
|
type("3,4");
|
|
expect(promptLine()).toBe("덧 점 지정:"); // stay
|
|
click(30, 40);
|
|
expect(seen).toEqual([
|
|
{ x: 3, y: 4 },
|
|
{ x: 30, y: 40 },
|
|
]);
|
|
expect(toolState()).toBe("WAITING_FOR_END_POINT"); // (30, 40) 이 선 첫 점으로
|
|
type("@10,0");
|
|
const [line] = entities("Line") as unknown as Array<{ segment: { start: Xy; end: Xy } }>;
|
|
expect(line.segment.start.x).toBeCloseTo(30, 3);
|
|
expect(line.segment.end.x).toBeCloseTo(40, 3);
|
|
});
|
|
|
|
it("투명 `'` — 도구형은 거절 · 즉시형은 도구 둔 채", () => {
|
|
type("l");
|
|
type("'line");
|
|
expect(noticeLine()).toBe("「LINE」 명령은 투명하게 쓸 수 없음.");
|
|
expect(activeTool()).toBe(Tool.LINE);
|
|
routeTyped("'zoomin");
|
|
routeTyped("'zoomout");
|
|
expect(getLastCommandId()).toBe("ZOOMOUT");
|
|
expect(activeTool()).toBe(Tool.LINE);
|
|
});
|
|
});
|
|
|
|
describe("Ctrl · Alt 글자(H1)", () => {
|
|
it("Ctrl+C = COPYCLIP · Ctrl+글자 · Alt+글자는 쌓지 않음", () => {
|
|
key("c", { ctrlKey: true, code: "KeyC" });
|
|
expect(getLastCommandId()).toBe("COPYCLIP");
|
|
expect(activeTool()).toBe(Tool.SELECT); // 원(C) 아님
|
|
key("ㅌ", { ctrlKey: true, code: "KeyX" });
|
|
expect(getLastCommandId()).toBe("CUTCLIP");
|
|
key("l", { altKey: true });
|
|
key("q", { ctrlKey: true, code: "KeyQ" });
|
|
expect(inputText()).toBe("");
|
|
typeEnter("l");
|
|
expect(activeTool()).toBe(Tool.LINE);
|
|
});
|
|
});
|
|
|
|
// ---- 가-2 — ↑↓ · Tab · FROM · M2P · @ · # · 스냅 이름 · H11 ----
|
|
|
|
const segs = () =>
|
|
(entities("Line") as unknown as Array<{ segment: { start: Xy; end: Xy } }>).map(
|
|
(line) => line.segment,
|
|
);
|
|
const expectSeg = (seg: { start: Xy; end: Xy }, x1: number, y1: number, x2: number, y2: number) => {
|
|
expect(
|
|
[seg.start.x, seg.start.y, seg.end.x, seg.end.y].map((v) => Math.round(v * 1000) / 1000),
|
|
).toEqual([x1, y1, x2, y2]);
|
|
};
|
|
|
|
describe("가-2 ↑↓ 기록 · 최근 값 · 후보(2-12 · H11)", () => {
|
|
it("쉴 때 ↑↓ = 친 명령만(알 수 없는 글 · 알림 줄 안 섞임) · 끝을 지나면 빈 글", () => {
|
|
type("l");
|
|
key("Escape");
|
|
type("zz"); // 알 수 없는 명령 — 기록 안 됨
|
|
type("circle");
|
|
key("Escape");
|
|
key("ArrowUp");
|
|
expect(inputText()).toBe("CIRCLE");
|
|
key("ArrowUp");
|
|
expect(inputText()).toBe("L");
|
|
key("ArrowDown");
|
|
expect(inputText()).toBe("CIRCLE");
|
|
key("ArrowDown");
|
|
expect(inputText()).toBe("");
|
|
});
|
|
|
|
it("명령 중 ↑ = 그 물음 최근 값", () => {
|
|
start(polygonLike);
|
|
type("7");
|
|
key("Escape");
|
|
start(polygonLike);
|
|
key("ArrowUp");
|
|
expect(inputText()).toBe("7");
|
|
key("Escape");
|
|
});
|
|
|
|
it("Tab · ↑↓ = 후보 돌림(처음 친 글 기준) · Shift+↑ = 화면 옮김", () => {
|
|
typed("ci");
|
|
key("Tab");
|
|
expect(inputText()).toBe(commandCandidate("CI", 0));
|
|
key("Tab");
|
|
expect(inputText()).toBe(commandCandidate("CI", 1));
|
|
key("ArrowUp");
|
|
expect(inputText()).toBe(commandCandidate("CI", 0));
|
|
key("Escape");
|
|
const before = getScreenCanvasDrawController().getScreenOffset().y;
|
|
key("ArrowUp", { shiftKey: true });
|
|
expect(getScreenCanvasDrawController().getScreenOffset().y).not.toBe(before);
|
|
key("ArrowDown", { shiftKey: true });
|
|
});
|
|
});
|
|
|
|
describe("가-2 점 물음 보조(1-4)", () => {
|
|
it("FROM — 기준점 · <간격띄우기> @dx,dy → 그 점이 원래 물음에", () => {
|
|
type("l");
|
|
type("from");
|
|
expect(promptLine()).toBe("기준점:");
|
|
type("10,10");
|
|
expect(promptLine()).toBe("<간격띄우기>:");
|
|
type("@5,0");
|
|
type("@10,0");
|
|
expectSeg(segs()[0], 15, 10, 25, 10);
|
|
});
|
|
|
|
it("M2P — 두 점의 가운데", () => {
|
|
type("l");
|
|
type("m2p");
|
|
expect(promptLine()).toBe("중간점의 첫 번째 점:");
|
|
type("0,0");
|
|
expect(promptLine()).toBe("중간점의 두 번째 점:");
|
|
type("10,0");
|
|
type("@0,5");
|
|
expectSeg(segs()[0], 5, 0, 5, 5);
|
|
});
|
|
|
|
it("@ = 마지막 점 · 기준점 없는 첫 점 @dx,dy = 마지막 점 기준 · # = 절대", () => {
|
|
type("l");
|
|
type("0,0");
|
|
type("10,0");
|
|
key("Escape");
|
|
type("l");
|
|
type("@");
|
|
type("@0,10");
|
|
expectSeg(segs()[1], 10, 0, 10, 10);
|
|
key("Escape");
|
|
type("l");
|
|
type("@1,1"); // 마지막 점 (10, 10) 기준
|
|
type("#0,0");
|
|
expectSeg(segs()[2], 11, 11, 0, 0);
|
|
});
|
|
|
|
it("스냅 이름 — 세 글자~영문 전체 · QUA · NOD · 없는 스냅 안내 · 두 글자는 아님", () => {
|
|
type("l");
|
|
for (const [text, snap] of [
|
|
["endp", "END"],
|
|
["endpoint", "END"],
|
|
["qua", "QUA"],
|
|
["node", "NOD"],
|
|
["_non", "NON"],
|
|
]) {
|
|
type(text);
|
|
expect(getSnapOverride()).toBe(snap);
|
|
}
|
|
type("tangent");
|
|
expect(noticeLine()).toBe("「TAN」 스냅은 이 CAD 에 없음.");
|
|
type("ext");
|
|
expect(noticeLine()).toBe("「EXT」 스냅은 이 CAD 에 없음.");
|
|
expect(activeTool()).toBe(Tool.LINE); // EXT · INS 가 명령으로 안 돎(N14)
|
|
key("Escape");
|
|
expect(getSnapOverride()).toBeNull();
|
|
});
|
|
|
|
it("옵션 영문 전체 = 옵션 · 줄임 CEN = 스냅", () => {
|
|
const seen: string[] = [];
|
|
const tool = createSequenceTool({
|
|
tool: Tool.DIST,
|
|
steps: [
|
|
{
|
|
kind: "point",
|
|
prompt: { text: "호의 두 번째 점 지정", options: [opt("CENTER", "C", "중심")] },
|
|
options: { CENTER: { run: () => seen.push("CENTER") } },
|
|
},
|
|
],
|
|
commit: () => {},
|
|
});
|
|
start(tool);
|
|
type("cen");
|
|
expect(getSnapOverride()).toBe("CEN");
|
|
expect(seen).toEqual([]);
|
|
type("center");
|
|
expect(seen).toEqual(["CENTER"]);
|
|
key("Escape");
|
|
});
|
|
});
|