- OFFSET 새 파일(offset-tool.ts) — 거리 <통과점> · 통과점(T) · 지우기(E) 예/아니오 · 객체 → 면의 점 되풀이 · 다중(M) <다음 객체> · 명령 취소(U) · 기억 OFFSETDIST · OFFSETERASE - FILLET · CHAMFER 한 꼴 — 객체 먼저 · 반지름(R) · 거리(D) 둘(둘째 기본 = 첫째) · 자르기(T) 모드 · 다중(M) · 명령 취소(U) · Shift = 구석 · 기억 FILLETRAD · CHAMFERA/B · TRIMMODE - TRIM · EXTEND 빠른 모드 한 꼴 — 되풀이 · Shift = 반대 일 · 집기 반경 밖 무시 · 지우기(R) · 숨은 U · 사각형 변 원래 층 · 꼴(N9) · 자른 선 꼴 유지 - BREAK — 누른 점 = 첫 끊기점 · 첫 번째 점(F) · @0,0 = 같은 점 · JOIN 물음 글 - 시험 resources/tester/cad/modify-acad.test.ts 13 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013QyNXY2TUKXCFMwAca2APG
375 lines
12 KiB
TypeScript
375 lines
12 KiB
TypeScript
/**
|
||
* 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);
|
||
});
|
||
});
|