Files
Aislo/B07_DesignDetail/openwebcad/src/tools/rectangle-tool.ts
T
eomsangdonandClaude Opus 5.5 3cebb37ed0 feat(CAD): 337-3a 나-1 그리기 8 — 오토캐드 물음 차례 · 옵션 · 기본값 · 빈 Enter · Esc · 끝난 뒤
- LINE: 「첫 번째 점 지정」 빈 Enter = 마지막 끝점에서 이음 · U = 마지막 토막(되돌림 한 칸) · 둘째 토막부터 C · Enter · Esc = 끝(토막 남음)
- PLINE(새 pline-tool): 호(A) 접선 호 · 길이(L) · 닫기(C · 호 모드 CL) · 명령 취소(U) · 선(L) · Esc · Enter = 그린 데까지 남김
- RECTANG: 치수(D) 길이 · 폭 기억(RECT.L · RECT.W) → 누른 쪽 사분면 · 없는 옵션 안내
- CIRCLE: 3P · 2P · 지름(D) <2r> · 반지름 기억(CIRCLERAD · 처음 <> 없음) · 한 번 뒤 끝
- POLYGON(새 polygon-tool): 변 수 기억 <4> · 3~1024 정수 · 모서리(E) · 내접/외접 기억 · 숫자 = 아래 변 수평
- ARC(새 arc-tool): 세 점 · 중심(C) · 각도(A) · 끝(E) · 반지름(R)
- ELLIPSE(새 ellipse-tool): 축 두 끝 · 중심(C) · 회전(R) · 다른 축 숫자 = 중심에서 반 길이(N10)
- SPLINE: 다음 점 입력 · U · 넷째 점부터 C · Esc 버림 · DONUT 중심 되풀이(loopFrom)
- 시험 draw-acad 22 · keyboard-chain 옛 선 · 폴리선 줄 두 곳 새 동작으로

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

160 lines
5.4 KiB
TypeScript

/**
* 직사각형(RECTANG) — 337 설계 1-1 · 오토캐드 차례.
* 「첫 번째 구석점 지정:」 → 「다른 구석점 지정 또는 [치수(D)]:」 ·
* D → 「직사각형의 길이 지정 <10.0000>:」 → 「직사각형의 폭 지정 <10.0000>:」(기억 RECT.L · RECT.W) →
* 다시 「다른 구석점 지정 또는 [치수(D)]:」(누른 쪽 사분면으로 놓음). 하나 그리면 끝.
*/
import { Point } from '@flatten-js/core';
import { assign, createMachine } from 'xstate';
import { type PromptSpec, remember } from '../commands/prompt';
import { RectangleEntity } from '../entities/RectangleEntity';
import { getPointFromEvent } from '../helpers/get-point-from-event.ts';
import {
addEntities,
getActiveLayerId,
setAngleGuideOriginPoint,
setGhostHelperEntities,
setSelectedEntityIds,
setShouldDrawHelpers,
} from '../state';
import { Tool } from '../tools';
import { END, firstPointEvents, option, pointEvents } from './draw/draw-steps';
import { styled } from './factories/entity-factory';
import {
type DrawEvent,
END_STATE,
type NumberInputEvent,
type PointInputEvent,
type StateEvent,
type ToolContext,
} from './tool.types';
export interface RectangleContext extends ToolContext {
startPoint: Point | null;
/** 치수(D)로 정한 길이 · 폭 — 있으면 다른 구석 = 사분면만 */
length: number | null;
width: number | null;
}
export enum RectangleState {
INIT = 'INIT',
WAITING_FOR_START_POINT = 'WAITING_FOR_START_POINT',
WAITING_FOR_END_POINT = 'WAITING_FOR_END_POINT',
WAITING_FOR_LENGTH = 'WAITING_FOR_LENGTH',
WAITING_FOR_WIDTH = 'WAITING_FOR_WIDTH',
}
const START_PROMPT: PromptSpec = {
text: '첫 번째 구석점 지정',
absent: [
option('CHAMFER', 'C', '모따기'),
option('ELEVATION', 'E', '고도'),
option('FILLET', 'F', '모깎기'),
option('THICKNESS', 'T', '두께'),
option('WIDTH', 'W', '폭'),
],
};
const END_PROMPT: PromptSpec = {
text: '다른 구석점 지정',
options: [option('DIMENSIONS', 'D', '치수')],
absent: [option('AREA', 'A', '면적'), option('ROTATION', 'R', '회전')],
};
const sizePrompt = (text: string, key: string): PromptSpec => ({
text,
default: { kind: 'value', remember: key, initial: 10 },
number: { positive: true },
});
/** 다른 구석 — 치수가 있으면 누른 쪽 사분면에 길이 × 폭 */
function cornerOf(context: RectangleContext, point: Point): Point {
const start = context.startPoint as Point;
if (context.length === null || context.width === null) return point;
const sx = point.x < start.x ? -1 : 1;
const sy = point.y < start.y ? -1 : 1;
return new Point(start.x + sx * context.length, start.y + sy * context.width);
}
const rectangle = (context: RectangleContext, point: Point) =>
styled(
new RectangleEntity(getActiveLayerId(), context.startPoint as Point, cornerOf(context, point))
);
export const rectangleToolStateMachine = createMachine(
{
types: {} as { context: RectangleContext; events: StateEvent },
context: { startPoint: null, length: null, width: null, type: Tool.RECTANGLE },
initial: RectangleState.INIT,
states: {
[RectangleState.INIT]: {
always: { actions: 'init', target: RectangleState.WAITING_FOR_START_POINT },
},
[RectangleState.WAITING_FOR_START_POINT]: {
meta: { kind: 'point', prompt: START_PROMPT },
on: {
...firstPointEvents('recordStart', RectangleState.WAITING_FOR_END_POINT),
ESC: { target: END_STATE },
},
},
[RectangleState.WAITING_FOR_END_POINT]: {
meta: { kind: 'point', prompt: END_PROMPT },
on: {
DRAW: { actions: 'preview' },
...pointEvents('draw', END_STATE),
'OPTION.DIMENSIONS': { target: RectangleState.WAITING_FOR_LENGTH },
ESC: { target: END_STATE },
},
},
[RectangleState.WAITING_FOR_LENGTH]: {
meta: { kind: 'number', prompt: sizePrompt('직사각형의 길이 지정', 'RECT.L') },
on: {
NUMBER_INPUT: { actions: 'recordLength', target: RectangleState.WAITING_FOR_WIDTH },
ESC: { target: END_STATE },
},
},
[RectangleState.WAITING_FOR_WIDTH]: {
meta: { kind: 'number', prompt: sizePrompt('직사각형의 폭 지정', 'RECT.W') },
on: {
NUMBER_INPUT: { actions: 'recordWidth', target: RectangleState.WAITING_FOR_END_POINT },
ESC: { target: END_STATE },
},
},
...END,
},
},
{
actions: {
init: assign(() => {
setShouldDrawHelpers(true);
setGhostHelperEntities([]);
setSelectedEntityIds([]);
setAngleGuideOriginPoint(null);
return { startPoint: null, length: null, width: null };
}),
recordStart: assign(({ event }) => {
const startPoint = getPointFromEvent(null, event as PointInputEvent);
setAngleGuideOriginPoint(startPoint);
return { startPoint };
}),
preview: ({ context, event }) => {
const point = getPointFromEvent(context.startPoint, event as DrawEvent);
setGhostHelperEntities([rectangle(context, point)]);
},
draw: ({ context, event }) => {
const point = getPointFromEvent(context.startPoint, event as PointInputEvent);
setGhostHelperEntities([]);
addEntities([rectangle(context, point)], true);
},
recordLength: assign(({ event }) => {
const length = (event as NumberInputEvent).value;
remember('RECT.L', length);
return { length };
}),
recordWidth: assign(({ event }) => {
const width = (event as NumberInputEvent).value;
remember('RECT.W', width);
return { width };
}),
},
}
);