Merge remote-tracking branch 'origin/sub_laptop_3' into main_laptop_1
This commit is contained in:
@@ -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([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user