Merge remote-tracking branch 'origin/sub_laptop_3' into main_laptop_1

This commit is contained in:
2026-10-16 00:11:27 +09:00
4 changed files with 684 additions and 59 deletions
+319
View File
@@ -0,0 +1,319 @@
/**
* 337 나-2 — 구성선 · 글자 · 해치 넷(XLINE · TEXT · MTEXT · HATCH)을 오토캐드 물음 차례대로.
* 설계 `docs/raw/조사/2026-10-15_337_CAD오토캐드조작_설계.md` 1-1 · 6-3 나 줄(XLINE 뒤 넷).
* 실행: cd B07_DesignDetail/openwebcad && npx vitest run --dir ../../resources/tester/cad text-acad
*/
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 { remember } from "../../../B07_DesignDetail/openwebcad/src/commands/prompt";
import { cancelActiveCommand } from "../../../B07_DesignDetail/openwebcad/src/inputController/prompt-input";
import { getSelectedEntityIds } from "../../../B07_DesignDetail/openwebcad/src/state";
import { Tool } from "../../../B07_DesignDetail/openwebcad/src/tools";
import {
activeTool,
click,
entities,
initIdle,
key,
noticeLine,
promptLine,
type,
typeEnter,
typed,
} from "./helpers/cad-drive";
type Xy = { x: number; y: number };
// biome-ignore lint/suspicious/noExplicitAny: 앱 객체 속을 숫자로만 읽음
type Any = any;
const enter = () => key("Enter");
const close = (a: number, b: number) => expect(a).toBeCloseTo(b, 6);
const ends = (line: Any): [Xy, Xy] => [line.getStartPoint(), line.getEndPoint()];
const texts = () =>
entities("Text").map((text: Any) => ({
label: text.getLabel() as string,
at: text.basePoint as Xy,
size: text.getTextOptions().fontSize as number,
dir: text.getTextOptions().textDirection as Xy,
}));
const idle = () => expect(activeTool()).toBe(Tool.SELECT);
beforeEach(() => {
initIdle();
remember("XLINE.ANGLE", 0);
remember("TEXTSIZE", 2.5);
remember("TEXT.ANGLE", 0);
});
describe("XLINE(XL)", () => {
it("첫 물음 · H 되풀이 · Enter 끝", () => {
type("xl");
expect(promptLine()).toBe("점 지정 또는 [수평(H)/수직(V)/각도(A)]:");
type("h");
expect(promptLine()).toBe("통과점을 지정:");
type("0,10");
type("0,20");
expect(promptLine()).toBe("통과점을 지정:");
enter();
idle();
const lines = entities("Line").map(ends);
expect(lines).toHaveLength(2);
lines.forEach(([a, b], index) => {
close(a.y, 10 * (index + 1));
close(b.y, 10 * (index + 1));
expect(Math.abs(b.x - a.x)).toBeGreaterThan(1000);
});
});
it("V · Esc = 끝(그은 선 남음)", () => {
type("xl");
type("v");
type("5,0");
key("Escape");
idle();
const [[a, b]] = entities("Line").map(ends);
close(a.x, 5);
close(b.x, 5);
});
it("A — 각도 물음 · 기억 · 통과점", () => {
type("xl");
type("a");
expect(promptLine()).toBe("X선의 각도 입력 <0>:");
type("45");
expect(promptLine()).toBe("통과점을 지정:");
type("0,0");
enter();
type("xl");
type("a");
expect(promptLine()).toBe("X선의 각도 입력 <45>:");
enter(); // 45
type("10,0");
enter();
idle();
const lines = entities("Line").map(ends);
expect(lines).toHaveLength(2);
for (const [a, b] of lines) close((b.y - a.y) / (b.x - a.x), 1);
const [, [a2]] = lines;
close(a2.y - 0, a2.x - 10); // (10, 0) 을 지남
});
it("두 점 방향 되풀이 · 없는 옵션 안내 · 첫 통과점 Enter = 그냥 끝", () => {
type("xl");
type("b");
expect(noticeLine()).toBe("「이등분(B)」 옵션은 이 CAD 에 없음.");
type("0,0");
type("10,10");
type("10,-10");
enter();
idle();
const slopes = entities("Line")
.map(ends)
.map(([a, b]) => (b.y - a.y) / (b.x - a.x));
expect(slopes).toHaveLength(2);
close(slopes[0], 1);
close(slopes[1], -1);
type("xl");
type("h");
enter();
idle();
expect(entities("Line")).toHaveLength(2);
});
});
describe("TEXT(DT)", () => {
it("시작점 → 높이 → 각 → 줄 이어 치기 → 빈 줄 끝", () => {
type("dt");
expect(promptLine()).toBe("문자의 시작점 지정:");
type("s");
expect(noticeLine()).toBe("「스타일(S)」 옵션은 이 CAD 에 없음.");
type("0,0");
expect(promptLine()).toBe("높이 지정 <2.5000>:");
type("-2");
expect(noticeLine()).toBe("값은 양수이고 0이 아니어야 함.");
type("5");
expect(promptLine()).toBe("문자의 회전 각도 지정 <0>:");
enter();
expect(promptLine()).toBe("문자 입력:");
typeEnter("ab c"); // 글 물음의 Space = 빈칸
expect(promptLine()).toBe("문자 입력:");
typeEnter("de");
enter(); // 빈 줄 = 끝
idle();
const [first, second] = texts();
expect([first.label, second.label]).toEqual(["ab c", "de"]);
expect([first.size, second.size]).toEqual([5, 5]);
close(first.at.x, 0);
close(first.at.y, 0);
close(second.at.x, 0);
close(second.at.y, -5 * 1.35);
});
it("높이 기억 · 시작점 Enter = 앞 글 아래(높이 · 각 안 물음) · 처음 줄 빈 Enter = 아무것도 없이 끝", () => {
type("dt");
type("0,0");
type("4");
enter();
typeEnter("one");
enter();
type("dt");
enter(); // 시작점 Enter
expect(promptLine()).toBe("문자 입력:");
typeEnter("two");
enter();
const [one, two] = texts();
expect(two.size).toBe(4);
close(two.at.x, one.at.x);
close(two.at.y, one.at.y - 4 * 1.35);
type("dt");
type("50,50");
expect(promptLine()).toBe("높이 지정 <4.0000>:");
enter();
enter();
enter(); // 첫 줄에서 빈 Enter
idle();
expect(texts()).toHaveLength(2);
});
it("회전 각 — 글 방향 · 다음 줄은 돌린 아래 · Esc = 치던 줄 버림", () => {
type("dt");
type("10,0");
type("2");
type("90");
typeEnter("up");
typeEnter("next");
typed("zz");
key("Escape");
idle();
const [up, next] = texts();
expect(texts()).toHaveLength(2);
close(up.dir.x, 0);
close(up.dir.y, 1);
close(next.at.x, 10 + 2 * 1.35);
close(next.at.y, 0);
type("dt");
expect(promptLine()).toBe("문자의 시작점 지정:");
type("0,0");
expect(promptLine()).toBe("높이 지정 <2.0000>:");
enter();
expect(promptLine()).toBe("문자의 회전 각도 지정 <90>:");
key("Escape");
});
});
describe("MTEXT(T · MT)", () => {
it("첫 구석 → H → 반대 구석 → 글(\\P 줄바꿈) · 상자 왼쪽 위에서", () => {
type("t");
expect(promptLine()).toBe("첫 번째 구석 지정:");
type("0,100");
expect(promptLine()).toBe("반대 구석 지정 또는 [높이(H)/폭(W)]:");
type("c");
expect(noticeLine()).toBe("「열(C)」 옵션은 이 CAD 에 없음.");
type("h");
expect(promptLine()).toBe("높이 지정 <2.5000>:");
type("4");
expect(promptLine()).toBe("반대 구석 지정 또는 [높이(H)/폭(W)]:");
type("50,60");
expect(promptLine()).toBe("문자 입력 (줄바꿈 \\P):");
typeEnter("a\\Pb");
idle();
const [a, b] = texts();
expect([a.label, b.label]).toEqual(["a", "b"]);
expect(a.size).toBe(4);
close(a.at.x, 0);
close(a.at.y, 96);
close(b.at.y, 96 - 4 * 1.35);
});
it("W — 폭 숫자 → 글 · 높이 = 기억(TEXTSIZE) · 빈 글 = 안 만들고 끝", () => {
type("mt");
type("10,10");
type("w");
expect(promptLine()).toBe("폭 지정:");
type("30");
expect(promptLine()).toBe("문자 입력 (줄바꿈 \\P):");
typeEnter("x");
idle();
const [x] = texts();
close(x.at.x, 10);
close(x.at.y, 10 - 2.5);
type("t");
type("0,0");
type("5,5");
enter();
idle();
expect(texts()).toHaveLength(1);
});
});
describe("HATCH(H · BH)", () => {
const boxes = () => {
for (const [a, b] of [
["0,0", "100,100"],
["40,40", "60,60"],
]) {
type("rec");
type(a);
type(b);
cancelActiveCommand();
}
expect(entities()).toHaveLength(2);
};
const spans = () =>
entities("Hatch").map((hatch: Any) => {
const xs = (hatch.getPoints() as Xy[]).map((point) => point.x);
return [Math.min(...xs), Math.max(...xs)];
});
beforeEach(boxes);
it("내부 점 = 품은 것 중 가장 작은 경계 · 되풀이 · 같은 경계 한 번 · Enter = 만듦", () => {
type("h");
expect(promptLine()).toBe("내부 점 선택 또는 [객체 선택(S)/명령 취소(U)]:");
click(50, 50);
click(10, 10);
click(15, 15);
expect(promptLine()).toBe("내부 점 선택 또는 [객체 선택(S)/명령 취소(U)]:");
enter();
idle();
expect(spans()).toEqual([
[40, 60],
[0, 100],
]);
});
it("U = 마지막 경계 빼기 · 경계 없는 점은 무시 · Esc = 안 만듦", () => {
type("h");
click(50, 50);
click(10, 10);
click(500, 500);
type("u");
enter();
expect(spans()).toEqual([[40, 60]]);
type("h");
click(10, 10);
key("Escape");
idle();
expect(spans()).toHaveLength(1);
type("bh");
type("t");
expect(noticeLine()).toBe("「설정(T)」 옵션은 이 CAD 에 없음.");
enter(); // 경계 없이 Enter = 그냥 끝
idle();
expect(spans()).toHaveLength(1);
});
it("S — 객체 선택 → Enter = 고른 객체 경계로 만듦 · 고름 풂", () => {
type("h");
type("s");
expect(promptLine()).toBe("객체 선택:");
click(0, 50);
enter();
idle();
expect(spans()).toEqual([[0, 100]]);
expect(getSelectedEntityIds()).toEqual([]);
});
});