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

This commit is contained in:
2026-10-16 00:15:12 +09:00
7 changed files with 1216 additions and 141 deletions
+374
View File
@@ -0,0 +1,374 @@
/**
* 337 다-1 고치기 7 — OFFSET · FILLET · CHAMFER · TRIM · EXTEND · BREAK · JOIN 을 오토캐드 물음 차례 · 옵션 · 기본값 ·
* 빈 Enter · Esc · 끝난 뒤 동작으로(설계 `docs/raw/조사/2026-10-15_337_CAD오토캐드조작_설계.md` 1-2 · 6-3 다 줄).
* 기억(OFFSETDIST · FILLETRAD · CHAMFERA/B · TRIMMODE)은 파일 안 세션 하나 — 시험 차례대로 이어짐.
* 실행: cd B07_DesignDetail/openwebcad && npx vitest run --dir ../../resources/tester/cad modify-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 { Tool } from "../../../B07_DesignDetail/openwebcad/src/tools";
import { activeTool, click, entities, initIdle, key, promptLine, type } from "./helpers/cad-drive";
type Shape = { start: { x: number; y: number }; end: { x: number; y: number } };
const enter = () => key("Enter");
const r = (value: number) => Math.round(value * 1e4) / 1e4;
/** 선 하나 — 쉼에서 그리고 Esc 로 끝 */
const line = (a: string, b: string) => {
type("l");
type(a);
type(b);
key("Escape");
};
/** 선마다 [x1, y1, x2, y2] — 차례 무관(정렬) */
const segs = () =>
entities("Line")
.map((entity) => {
const { start, end } = entity.getShape() as unknown as Shape;
return [r(start.x), r(start.y), r(end.x), r(end.y)];
})
.sort((a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2] || a[3] - b[3]);
const ys = () =>
segs()
.map((s) => s[1])
.sort((a, b) => a - b);
beforeEach(() => {
initIdle();
});
describe("OFFSET", () => {
it("처음 <통과점> · 통과점 · 되풀이 · 거리 기억 · T · 빈 Enter 끝", () => {
line("0,0", "100,0");
type("o");
expect(activeTool()).toBe(Tool.OFFSET);
expect(promptLine()).toBe("간격띄우기 거리 지정 또는 [통과점(T)/지우기(E)] <통과점>:");
enter(); // <통과점>
expect(promptLine()).toBe("간격띄우기할 객체 선택 또는 [종료(E)/명령 취소(U)] <종료>:");
click(50, 0);
expect(promptLine()).toBe("통과점 지정 또는 [종료(E)/다중(M)/명령 취소(U)] <종료>:");
type("30,20");
expect(ys()).toEqual([0, 20]);
expect(promptLine()).toBe("간격띄우기할 객체 선택 또는 [종료(E)/명령 취소(U)] <종료>:"); // 되풀이
key("Escape");
expect(activeTool()).toBe(Tool.SELECT);
type("o");
expect(promptLine()).toBe("간격띄우기 거리 지정 또는 [통과점(T)/지우기(E)] <통과점>:"); // 통과점 기억
type("0");
expect(activeTool()).toBe(Tool.OFFSET); // 0 거절
type("5");
click(50, 0);
expect(promptLine()).toBe(
"간격띄우기할 면의 점 지정 또는 [종료(E)/다중(M)/명령 취소(U)] <종료>:",
);
type("50,-10");
click(50, 20);
type("50,30");
expect(ys()).toEqual([-5, 0, 20, 25]);
enter(); // <종료>
expect(activeTool()).toBe(Tool.SELECT);
type("o");
expect(promptLine()).toBe("간격띄우기 거리 지정 또는 [통과점(T)/지우기(E)] <5.0000>:");
type("t");
click(50, 25);
expect(promptLine()).toBe("통과점 지정 또는 [종료(E)/다중(M)/명령 취소(U)] <종료>:");
type("e"); // 종료
expect(activeTool()).toBe(Tool.SELECT);
type("o");
expect(promptLine()).toBe("간격띄우기 거리 지정 또는 [통과점(T)/지우기(E)] <통과점>:");
key("Escape");
});
it("지우기(E) 예 · 다중(M) · 명령 취소(U) · 없는 옵션 L 안내", () => {
line("0,0", "100,0");
type("o");
type("l");
expect(activeTool()).toBe(Tool.OFFSET); // 도면층(L) = 이 CAD 에 없음
type("e");
expect(promptLine()).toBe(
"간격띄우기 후 원본 객체를 지우시겠습니까? [예(Y)/아니오(N)] <아니오>:",
);
type("y");
type("10");
click(50, 0);
type("50,5");
expect(ys()).toEqual([10]); // 원본 지움
key("Escape");
type("o");
type("e");
expect(promptLine()).toBe("간격띄우기 후 원본 객체를 지우시겠습니까? [예(Y)/아니오(N)] <예>:");
type("n");
enter(); // <10.0000>
click(50, 10);
type("m");
expect(promptLine()).toBe("간격띄우기할 면의 점 지정 또는 [종료(E)/명령 취소(U)] <다음 객체>:");
type("50,100");
type("50,100");
expect(ys()).toEqual([10, 20, 30]); // 방금 만든 것에서 되풀이 · 원본 둠
type("u");
expect(ys()).toEqual([10, 20]);
expect(promptLine()).toBe("간격띄우기할 객체 선택 또는 [종료(E)/명령 취소(U)] <종료>:");
type("u");
type("u"); // 이 명령에서 만든 것 없음 = 그대로
expect(ys()).toEqual([10]);
enter();
expect(activeTool()).toBe(Tool.SELECT);
});
});
/** ㄱ 자 — 가로 (0,0)~(100,0) · 세로 (80,−20)~(80,100) · 구석 (80, 0) */
const corner = () => {
line("0,0", "100,0");
line("80,-20", "80,100");
};
describe("FILLET", () => {
it("물음 글 · R 기억 · 쌍 하나 뒤 끝", () => {
corner();
type("f");
expect(promptLine()).toBe("첫 번째 객체 선택 또는 [명령 취소(U)/반지름(R)/자르기(T)/다중(M)]:");
type("r");
expect(promptLine()).toBe("모깎기 반지름 지정 <0.0000>:");
type("10");
expect(promptLine()).toBe("첫 번째 객체 선택 또는 [명령 취소(U)/반지름(R)/자르기(T)/다중(M)]:");
click(30, 0);
expect(promptLine()).toBe(
"두 번째 객체 선택 또는 Shift 키를 누른 채 선택하여 구석 적용 또는 [반지름(R)]:",
);
click(80, 60);
expect(segs()).toEqual([
[0, 0, 70, 0],
[80, 100, 80, 10],
]);
expect(entities("Arc")).toHaveLength(1);
expect(activeTool()).toBe(Tool.SELECT);
type("f");
type("r");
expect(promptLine()).toBe("모깎기 반지름 지정 <10.0000>:");
key("Escape");
expect(activeTool()).toBe(Tool.SELECT);
});
it("다중(M) · Shift = 반지름 0 구석 · 명령 취소(U) · 자르기(T) 자르지 않기 · 빈 Enter 끝", () => {
corner();
const before = segs();
type("f");
type("m");
click(30, 0);
click(80, 60, { shiftKey: true });
expect(segs()).toEqual([
[0, 0, 80, 0],
[80, 100, 80, 0],
]);
expect(entities("Arc")).toHaveLength(0);
expect(promptLine()).toBe("첫 번째 객체 선택 또는 [명령 취소(U)/반지름(R)/자르기(T)/다중(M)]:"); // 다중 되풀이
type("u");
expect(segs()).toEqual(before);
type("t");
expect(promptLine()).toBe("자르기 모드 옵션 입력 [자르기(T)/자르지 않기(N)] <자르기>:");
type("n");
click(30, 0);
click(80, 60);
expect(segs()).toEqual(before); // 안 자름
expect(entities("Arc")).toHaveLength(1);
enter();
expect(activeTool()).toBe(Tool.SELECT);
type("f");
type("t");
expect(promptLine()).toBe("자르기 모드 옵션 입력 [자르기(T)/자르지 않기(N)] <자르지 않기>:");
type("t");
key("Escape");
});
});
describe("CHAMFER", () => {
it("D 기억(둘째 기본 = 첫째) · 모따기 선 · 한 번 뒤 끝", () => {
corner();
type("cha");
expect(activeTool()).toBe(Tool.CHAMFER);
expect(promptLine()).toBe("첫 번째 선 선택 또는 [명령 취소(U)/거리(D)/자르기(T)/다중(M)]:");
type("d");
expect(promptLine()).toBe("첫 번째 모따기 거리 지정 <0.0000>:");
type("10");
expect(promptLine()).toBe("두 번째 모따기 거리 지정 <10.0000>:");
enter();
click(30, 0);
expect(promptLine()).toBe(
"두 번째 선 선택 또는 Shift 키를 누른 채 선택하여 구석 적용 또는 [거리(D)]:",
);
click(80, 60);
expect(segs()).toEqual([
[0, 0, 70, 0],
[70, 0, 80, 10],
[80, 100, 80, 10],
]);
expect(activeTool()).toBe(Tool.SELECT);
type("cha");
type("d");
expect(promptLine()).toBe("첫 번째 모따기 거리 지정 <10.0000>:");
type("4");
expect(promptLine()).toBe("두 번째 모따기 거리 지정 <4.0000>:");
key("Escape");
});
it("Shift = 거리 0 구석", () => {
corner();
type("cha");
click(30, 0);
click(80, 60, { shiftKey: true });
expect(segs()).toEqual([
[0, 0, 80, 0],
[80, 100, 80, 0],
]);
});
});
describe("TRIM", () => {
/** 가로 (0,0)~(100,0) · 세로 둘 x = 30 · 70 */
const fence = () => {
line("0,0", "100,0");
line("30,-20", "30,20");
line("70,-20", "70,20");
};
it("되풀이 · 반경 밖 무시 · U · 지우기(R) · 빈 Enter 끝", () => {
fence();
type("tr");
expect(activeTool()).toBe(Tool.ERASER);
expect(promptLine()).toBe(
"자를 객체 선택 또는 Shift 키를 누른 채 선택하여 연장 또는 [지우기(R)]:",
);
click(50, 0);
expect(segs()).toContainEqual([0, 0, 30, 0]);
expect(segs()).toContainEqual([70, 0, 100, 0]);
expect(segs()).toHaveLength(4);
click(50, 50); // 집기 반경 밖
expect(segs()).toHaveLength(4);
type("u");
expect(segs()).toContainEqual([0, 0, 100, 0]);
click(50, 0);
click(30, 15);
expect(segs()).toContainEqual([30, -20, 30, 0]);
expect(activeTool()).toBe(Tool.ERASER); // 되풀이
type("r");
expect(promptLine()).toBe("지울 객체 선택:");
click(70, 10);
enter();
expect(segs().some((s) => s[0] === 70 && s[2] === 70)).toBe(false);
expect(promptLine()).toBe(
"자를 객체 선택 또는 Shift 키를 누른 채 선택하여 연장 또는 [지우기(R)]:",
);
type("c");
expect(activeTool()).toBe(Tool.ERASER); // 걸치기(C) = 이 CAD 에 없음
enter();
expect(activeTool()).toBe(Tool.SELECT);
});
it("Esc 한 번 = 끝 · Shift = 연장", () => {
line("0,0", "50,0");
line("80,-20", "80,20");
type("tr");
click(40, 0, { shiftKey: true });
expect(segs()).toContainEqual([0, 0, 80, 0]);
key("Escape");
expect(activeTool()).toBe(Tool.SELECT);
});
it("사각형 변 = 원래 도면층 · 꼴(N9)", () => {
type("rec");
type("0,0");
type("100,40");
key("Escape");
const rectangle = entities("Rectangle")[0];
rectangle.layerId = "L2";
rectangle.lineColor = "#ff0000";
line("20,-10", "20,50");
type("tr");
click(60, 40);
const sides = entities("Line").filter((entity) => entity.layerId === "L2");
expect(sides.length).toBe(4);
expect(sides.every((entity) => entity.lineColor === "#ff0000")).toBe(true);
expect(segs()).toContainEqual([0, 40, 20, 40]);
});
});
describe("EXTEND", () => {
it("빠른 연장 · 되풀이 · Shift = 자르기 · 빈 Enter 끝", () => {
line("0,0", "50,0");
line("80,-20", "80,20");
type("ex");
expect(activeTool()).toBe(Tool.EXTEND);
expect(promptLine()).toBe("연장할 객체 선택 또는 Shift 키를 누른 채 선택하여 자르기:");
click(40, 0);
expect(segs()).toContainEqual([0, 0, 80, 0]);
click(80, 10, { shiftKey: true });
expect(segs()).toContainEqual([80, -20, 80, 0]);
expect(activeTool()).toBe(Tool.EXTEND);
enter();
expect(activeTool()).toBe(Tool.SELECT);
});
});
describe("BREAK", () => {
it("누른 점 = 첫 끊기점 · 둘째 점 · 한 번 뒤 끝", () => {
line("0,0", "100,0");
type("br");
expect(promptLine()).toBe("객체 선택:");
click(20, 0);
expect(promptLine()).toBe("두 번째 끊기점 지정 또는 [첫 번째 점(F)]:");
type("60,0");
expect(segs()).toEqual([
[0, 0, 20, 0],
[60, 0, 100, 0],
]);
expect(activeTool()).toBe(Tool.SELECT);
});
it("첫 번째 점(F) · `@0,0` = 같은 점(틈 없이 둘)", () => {
line("0,0", "100,0");
type("br");
click(50, 0);
type("f");
expect(promptLine()).toBe("첫 번째 끊기점 지정:");
type("10,0");
expect(promptLine()).toBe("두 번째 끊기점 지정:");
type("30,0");
expect(segs()).toEqual([
[0, 0, 10, 0],
[30, 0, 100, 0],
]);
type("br");
click(60, 0);
type("@0,0");
expect(segs()).toEqual([
[0, 0, 10, 0],
[30, 0, 60, 0],
[60, 0, 100, 0],
]);
});
});
describe("JOIN", () => {
it("고르기 물음 글 · 빈 Enter = 결합 · 한 번 뒤 끝", () => {
line("0,0", "40,0");
line("40,0", "40,40");
type("j");
expect(promptLine()).toBe("한 번에 결합할 원본 객체 또는 여러 객체 선택:");
click(20, 0);
click(40, 20);
enter();
expect(entities("Line")).toHaveLength(0);
expect(entities("PolyLine")).toHaveLength(1);
expect(activeTool()).toBe(Tool.SELECT);
});
});