/** * 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); }); });