/** * 337 다-2 — 늘이기 · 재기 · 치수 오토캐드 조작(설계 1-2 · 6-3 다 줄 중 LENGTHEN · DIVIDE · MEASURE · DIST · DIM). * 물음 글 · 옵션 글자 · 기본값 · 빈 Enter · Esc · 끝난 뒤를 키 · 마우스로 넣어 숫자로 판정. * 객체는 앱 코드(fromJson)로만 만듦 — 시험 쪽 flatten-js 는 다른 사본. * 실행: cd B07_DesignDetail/openwebcad && npx vitest run --dir ../../resources/tester/cad measure-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 { getCommandHistory } from "../../../B07_DesignDetail/openwebcad/src/commands/run-command"; import { getDimDecimals } from "../../../B07_DesignDetail/openwebcad/src/commands/dim-settings"; import { ArcEntity } from "../../../B07_DesignDetail/openwebcad/src/entities/ArcEntity"; import { CircleEntity } from "../../../B07_DesignDetail/openwebcad/src/entities/CircleEntity"; import { EntityName, type Entity } from "../../../B07_DesignDetail/openwebcad/src/entities/Entity"; import { LineEntity } from "../../../B07_DesignDetail/openwebcad/src/entities/LineEntity"; import type { MeasurementEntity } from "../../../B07_DesignDetail/openwebcad/src/entities/MeasurementEntity"; import { getActiveLayerId, setEntities } from "../../../B07_DesignDetail/openwebcad/src/state"; import { Tool } from "../../../B07_DesignDetail/openwebcad/src/tools"; import { activeTool, click, entities, initIdle, key, noticeLine, promptLine, type, typeEnter, } from "./helpers/cad-drive"; type Xy = { x: number; y: number }; const enter = () => key("Enter"); const esc = () => key("Escape"); const base = () => ({ lineColor: "#fff", lineWidth: 1, layerId: getActiveLayerId() }); async function addLine(x1: number, y1: number, x2: number, y2: number): Promise { const line = await LineEntity.fromJson({ id: `line-${x1}-${y1}-${x2}-${y2}`, type: EntityName.Line, ...base(), shapeData: { startPoint: { x: x1, y: y1 }, endPoint: { x: x2, y: y2 } }, } as never); setEntities([...entities(), line], true); // 그린 것처럼 되돌림 기록에 올림 return line; } async function addArc(): Promise { // 중심 (0, 0) · 반지름 50 · 0° → 90° 반시계(길이 25π) const arc = await ArcEntity.fromJson({ id: "arc", type: EntityName.Arc, ...base(), shapeData: { center: { x: 0, y: 0 }, radius: 50, startAngle: 0, endAngle: Math.PI / 2, counterClockwise: true }, } as never); setEntities([...entities(), arc], true); // 그린 것처럼 되돌림 기록에 올림 return arc; } async function addCircle(): Promise { const circle = await CircleEntity.fromJson({ id: "circle", type: EntityName.Circle, ...base(), shapeData: { center: { x: 300, y: 300 }, radius: 10 }, } as never); setEntities([...entities(), circle], true); // 그린 것처럼 되돌림 기록에 올림 return circle; } const ends = (entity: Entity): [Xy, Xy] => { const shape = entity.getShape() as unknown as { start: Xy; end: Xy }; return [ { x: shape.start.x, y: shape.start.y }, { x: shape.end.x, y: shape.end.y }, ]; }; const onlyLine = () => entities("Line")[0]; const lineLength = () => { const [a, b] = ends(onlyLine()); return Math.hypot(b.x - a.x, b.y - a.y); }; const points = () => entities("Point") .map((entity) => { const p = entity.getShape() as unknown as Xy; return { x: Math.round(p.x * 1000) / 1000, y: Math.round(p.y * 1000) / 1000 }; }) .sort((a, b) => a.x - b.x); const dims = () => entities("Measurement") as MeasurementEntity[]; const lastLog = (n = 1) => getCommandHistory().slice(-n); /** 선형 치수 글(반올림 · 끝 0 뗌) · 반지름 치수 글(소수 자릿수 그대로) */ const fmt = (value: number) => String(Number(value.toFixed(getDimDecimals()))); const fixed = (value: number) => value.toFixed(getDimDecimals()); beforeEach(() => { initIdle(); }); describe("LENGTHEN(LEN)", () => { const FIRST = "측정할 객체 선택 또는 [증분(DE)/퍼센트(P)/전체(T)]"; it("누름 = 현재 길이 · DE → 되풀이 · U = 이 명령 안 바꿈만 · 빈 Enter = 끝", async () => { await addLine(0, 0, 100, 0); typeEnter("len"); expect(activeTool()).toBe(Tool.LENGTHEN); expect(promptLine()).toBe(`${FIRST} <전체(T)>:`); click(50, 0); expect(noticeLine()).toBe("현재 길이: 100.0000"); expect(promptLine()).toBe(`${FIRST} <전체(T)>:`); type("d"); // DE 보다 짧음 — 안 맞음 expect(noticeLine()).toBe("*선택이 잘못됨*"); type("dy"); expect(noticeLine()).toBe("「동적(DY)」 옵션은 이 CAD 에 없음."); type("de"); expect(promptLine()).toBe("증분 길이 입력 <0.0000>:"); type("10"); expect(promptLine()).toBe("변경할 객체 선택 또는 [명령 취소(U)]:"); click(95, 0); // 끝 쪽 expect(ends(onlyLine())[1].x).toBeCloseTo(110); click(105, 0); expect(ends(onlyLine())[1].x).toBeCloseTo(120); expect(activeTool()).toBe(Tool.LENGTHEN); // 되풀이 type("u"); expect(ends(onlyLine())[1].x).toBeCloseTo(110); type("u"); type("u"); // 이 명령 안 바꿈 다 되돌린 뒤 = 아무 일 없음(앞 객체 지키기) expect(entities("Line")).toHaveLength(1); expect(lineLength()).toBeCloseTo(100); click(5, 0); // 시작 쪽 expect(ends(onlyLine())[0].x).toBeCloseTo(-10); enter(); expect(activeTool()).toBe(Tool.SELECT); }); it("마지막 방식 · 값 기억 — 첫 물음 빈 Enter = 그 방식 · 값 빈 Enter = 앞 값", async () => { await addLine(0, 0, 100, 0); typeEnter("len"); type("de"); type("10"); esc(); typeEnter("len"); expect(promptLine()).toBe(`${FIRST} <증분(DE)>:`); enter(); expect(promptLine()).toBe("증분 길이 입력 <10.0000>:"); enter(); click(95, 0); expect(lineLength()).toBeCloseTo(110); esc(); // Esc = 끝 · 바꾼 것 남음 expect(activeTool()).toBe(Tool.SELECT); expect(lineLength()).toBeCloseTo(110); }); it("P · T — 퍼센트 · 전체 · 양수 거절 · 호", async () => { await addLine(0, 0, 100, 0); typeEnter("len"); type("p"); expect(promptLine()).toBe("퍼센트 길이 입력 <100.0000>:"); type("-5"); expect(noticeLine()).toBe("값은 양수이고 0이 아니어야 함."); type("50"); click(90, 0); expect(ends(onlyLine())).toEqual([ { x: 0, y: 0 }, { x: 50, y: 0 }, ]); esc(); typeEnter("len"); expect(promptLine()).toBe(`${FIRST} <퍼센트(P)>:`); type("t"); expect(promptLine()).toBe("전체 길이 지정 <1.0000>:"); type("a"); expect(noticeLine()).toBe("「각도(A)」 옵션은 이 CAD 에 없음."); type("200"); click(45, 0); expect(lineLength()).toBeCloseTo(200); await addArc(); click(0, 50); // 호 끝(90°) 쪽 const arc = entities("Arc")[0].getShape() as unknown as { length: number; endAngle: number }; expect(arc.length).toBeCloseTo(200); expect(arc.endAngle).toBeCloseTo(4); // 200 / 50 rad esc(); }); }); describe("DIVIDE(DIV) · MEASURE(ME)", () => { it("DIVIDE — 기본 없음 · 정수 2~32767 · 블록 없음 · 한 번 뒤 끝", async () => { await addLine(0, 0, 100, 0); typeEnter("div"); expect(promptLine()).toBe("등분할 객체 선택:"); click(50, 0); expect(promptLine()).toBe("세그먼트 수 입력:"); enter(); // 기본 없음 — 그대로 expect(promptLine()).toBe("세그먼트 수 입력:"); type("2.5"); expect(noticeLine()).toBe("정수 값을 요구함."); type("1"); expect(noticeLine()).toBe("2에서 32767 사이의 정수를 요구함."); type("b"); expect(noticeLine()).toBe("「블록(B)」 옵션은 이 CAD 에 없음."); type("4"); expect(points()).toEqual([ { x: 25, y: 0 }, { x: 50, y: 0 }, { x: 75, y: 0 }, ]); expect(activeTool()).toBe(Tool.SELECT); }); it("MEASURE — 누른 자리 가까운 끝부터 · 양수", async () => { await addLine(0, 0, 100, 0); typeEnter("me"); expect(promptLine()).toBe("길이 분할 객체 선택:"); click(90, 0); // 끝 쪽 expect(promptLine()).toBe("세그먼트의 길이 지정:"); type("0"); expect(noticeLine()).toBe("값은 양수이고 0이 아니어야 함."); type("30"); expect(points().map((p) => p.x)).toEqual([10, 40, 70]); expect(activeTool()).toBe(Tool.SELECT); }); }); describe("DIST(DI)", () => { it("두 점 → 오토캐드 꼴 두 줄 기록 · 한 번 뒤 끝 · 다중 점 없음", () => { typeEnter("di"); expect(promptLine()).toBe("첫 번째 점 지정:"); type("0,0"); expect(promptLine()).toBe("두 번째 점 지정:"); type("m"); expect(noticeLine()).toBe("「다중 점(M)」 옵션은 이 CAD 에 없음."); type("30,40"); expect(lastLog(2)).toEqual([ "거리 = 50.0000, XY 평면에서의 각도 = 53, XY 평면으로부터의 각도 = 0", "X 증분 = 30.0000, Y 증분 = 40.0000, Z 증분 = 0.0000", ]); expect(activeTool()).toBe(Tool.SELECT); }); }); describe("DIMLINEAR(DLI) · DIMALIGNED(DAL)", () => { const PLACE = "치수선의 위치 지정 또는 [문자(T)/수평(H)/수직(V)]:"; it("원점 둘 → V 고정 → T 문자(`<>` = 잰 값) → 놓음 · 기록 · 한 번 뒤 끝", () => { typeEnter("dli"); expect(promptLine()).toBe("첫 번째 치수보조선 원점 지정 또는 <객체 선택>:"); type("0,0"); expect(promptLine()).toBe("두 번째 치수보조선 원점 지정:"); type("100,30"); expect(promptLine()).toBe(PLACE); type("m"); expect(noticeLine()).toBe("「여러 줄 문자(M)」 옵션은 이 CAD 에 없음."); type("v"); expect(promptLine()).toBe(PLACE); type("t"); expect(promptLine()).toBe(`치수 문자 입력 <${fmt(30)}>:`); typeEnter("L=<>"); expect(promptLine()).toBe(PLACE); type("120,15"); const [dim] = dims(); expect(dim.getLabel()).toBe(`L=${fmt(30)}`); expect(dim.getEndPoint().x).toBeCloseTo(0); // 수직 — 끝점 x 를 시작점에 맞춤 expect(dim.getEndPoint().y).toBeCloseTo(30); expect(lastLog()).toEqual([`치수 문자 = L=${fmt(30)}`]); expect(activeTool()).toBe(Tool.SELECT); }); it("첫 물음 빈 Enter = 객체 선택 → H 고정 · T 빈 Enter = 잰 값 그대로", async () => { await addLine(0, 0, 80, 60); typeEnter("dli"); enter(); expect(promptLine()).toBe("치수 기입할 객체 선택:"); click(40, 30); expect(promptLine()).toBe(PLACE); type("h"); type("t"); expect(promptLine()).toBe(`치수 문자 입력 <${fmt(80)}>:`); enter(); type("40,-20"); // 수평 고정이라 옆에 놓아도 가로 const [dim] = dims(); expect(dim.getLabel()).toBe(fmt(80)); expect(dim.textOverride).toBeUndefined(); expect(activeTool()).toBe(Tool.SELECT); }); it("Esc = 끝 · 안 만듦 · DIMALIGNED 는 [문자(T)] 만", () => { typeEnter("dli"); type("0,0"); type("10,0"); esc(); expect(activeTool()).toBe(Tool.SELECT); expect(dims()).toHaveLength(0); typeEnter("dal"); type("0,0"); type("30,40"); expect(promptLine()).toBe("치수선의 위치 지정 또는 [문자(T)]:"); type("h"); expect(noticeLine()).toBe("점 또는 옵션 키워드를 요구함."); type("0,50"); expect(dims()[0].getLabel()).toBe(fmt(50)); }); }); describe("DIMCONTINUE(DCO) · DIMBASELINE(DBA)", () => { const CHAIN = "두 번째 치수보조선 원점 지정 또는 [명령 취소(U)]:"; it("되풀이 · U = 이 명령 안 마지막 치수 · 빈 Enter = 끝", () => { typeEnter("dli"); type("0,0"); type("100,0"); type("50,20"); expect(dims()).toHaveLength(1); typeEnter("dco"); expect(promptLine()).toBe(CHAIN); type("s"); expect(noticeLine()).toBe("「선택(S)」 옵션은 이 CAD 에 없음."); type("150,0"); type("200,0"); expect(dims()).toHaveLength(3); expect(dims()[2].getStartPoint().x).toBeCloseTo(150); // 앞 치수 끝에서 이어짐 expect(promptLine()).toBe(CHAIN); type("u"); expect(dims()).toHaveLength(2); type("u"); type("u"); // 이 명령 밖 치수는 안 지움 expect(dims()).toHaveLength(1); type("130,0"); expect(dims()[1].getStartPoint().x).toBeCloseTo(100); // U 뒤 앞 치수에서 다시 이음 enter(); expect(activeTool()).toBe(Tool.SELECT); typeEnter("dba"); expect(promptLine()).toBe(CHAIN); type("180,0"); expect(dims()).toHaveLength(3); expect(dims()[2].getStartPoint().x).toBeCloseTo(100); // 기준선 = 앞 치수 시작점 esc(); expect(activeTool()).toBe(Tool.SELECT); }); }); describe("DIMRADIUS(DRA) · DIMDIAMETER(DDI)", () => { it("호 또는 원 → [문자(T)] → 위치 · 기록 · 한 번 뒤 끝", async () => { await addCircle(); typeEnter("dra"); expect(promptLine()).toBe("호 또는 원 선택:"); click(310, 300); expect(promptLine()).toBe("치수선의 위치 지정 또는 [문자(T)]:"); type("a"); expect(noticeLine()).toBe("「각도(A)」 옵션은 이 CAD 에 없음."); type("t"); expect(promptLine()).toBe(`치수 문자 입력 :`); typeEnter("<> BASE"); type("330,300"); expect(lastLog()).toEqual([`치수 문자 = R${fixed(10)} BASE`]); expect(activeTool()).toBe(Tool.SELECT); typeEnter("ddi"); click(310, 300); type("330,300"); expect(lastLog()).toEqual([`치수 문자 = Ø${fixed(20)}`]); }); });