/** * 직사각형(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 }; }), }, } );