Files
Aislo/resources/tester/cad/measure-acad.test.ts
T
eomsangdonandClaude Opus 5.5 c15be5116a feat(B07 CAD): 337-4b 다-2 늘이기 · 재기 · 치수 오토캐드 조작 — LENGTHEN · DIVIDE · MEASURE · DIST · DIM 류
- LENGTHEN 새 도구(lengthen-tool.ts) — 측정할 객체 [증분(DE)/퍼센트(P)/전체(T)] <마지막 방식> · 누름 = 현재 길이 알림 · 방식별 값 기억 · 변경할 객체 되풀이 · U = 이 명령 안 바꿈만 · 빈 Enter · Esc 끝 · 선 · 호
- DIVIDE · MEASURE — 기본값 없음 · 정수 2~32767 · 양수 · 블록(B) 없음 안내 · MEASURE 누른 끝부터
- DIST — 오토캐드 꼴 두 줄 기록(거리 · 각도 · 증분) · 다중 점(M) 없음 안내
- DIMLINEAR · DIMALIGNED — 빈 Enter = 객체 선택 · [문자(T)/수평(H)/수직(V)] · 치수 문자 = 기록 · 치수 객체 문자 바꿈 칸(<> = 잰 값)
- DIMCONTINUE · DIMBASELINE — 되풀이 · 명령 취소(U) · 빈 Enter 끝 · DIMRADIUS · DIMDIAMETER [문자(T)] · 그 밖 치수 물음 글
- 시험 measure-acad 11

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL
2026-10-16 00:15:35 +09:00

364 lines
14 KiB
TypeScript

/**
* 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<Entity> {
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<Entity> {
// 중심 (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<Entity> {
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(`치수 문자 입력 <R${fixed(10)}>:`);
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)}`]);
});
});