- 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
364 lines
14 KiB
TypeScript
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)}`]);
|
|
});
|
|
});
|