- 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
160 lines
5.4 KiB
TypeScript
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 };
|
|
}),
|
|
},
|
|
}
|
|
);
|