/** * 선(LINE) — 337 설계 1-1 · 오토캐드 차례. * 「첫 번째 점 지정:」(빈 Enter = 마지막 선 · 폴리선 · 호 끝점에서) → 「다음 점 지정 또는 [명령 취소(U)]:」 되풀이 · * 둘째 토막부터 [닫기(C)/명령 취소(U)]. 토막마다 바로 도면에 · U = 마지막 토막(되돌림 한 칸) · C = 첫 점으로 닫고 끝 · * 빈 Enter · Esc = 끝(그린 토막 남음). */ import type { Point } from '@flatten-js/core'; import { assign, createMachine } from 'xstate'; import { OPT, type PromptSpec } from '../commands/prompt'; import type { LineEntity } from '../entities/LineEntity'; import { getPointFromEvent } from '../helpers/get-point-from-event.ts'; import { addEntities, setAngleGuideOriginPoint, setGhostHelperEntities, setSelectedEntityIds, setShouldDrawHelpers, undo, } from '../state'; import { Tool } from '../tools'; import { END, firstPointEvents, lastDrawEnd, pointEvents, rememberDrawEnd, } from './draw/draw-steps'; import { lineEntity } from './factories/entity-factory'; import { type DrawEvent, END_STATE, type PointInputEvent, type StateEvent, type ToolContext, } from './tool.types'; export interface LineContext extends ToolContext { startPoint: Point | null; /** 이번 이어 그리기의 첫 점 — 닫기(C) */ firstPoint: Point | null; /** 이번 이어 그리기에서 그린 구간 — 명령 취소(U) */ segments: LineEntity[]; } export enum LineState { INIT = 'INIT', WAITING_FOR_START_POINT = 'WAITING_FOR_START_POINT', WAITING_FOR_END_POINT = 'WAITING_FOR_END_POINT', } export enum LineAction { INIT_LINE_TOOL = 'INIT_LINE_TOOL', RECORD_START_POINT = 'RECORD_START_POINT', CONTINUE_FROM_LAST = 'CONTINUE_FROM_LAST', DRAW_TEMP_LINE = 'DRAW_TEMP_LINE', DRAW_FINAL_LINE = 'DRAW_FINAL_LINE', CLOSE_LINE = 'CLOSE_LINE', UNDO_SEGMENT = 'UNDO_SEGMENT', } const START_PROMPT: PromptSpec = { text: '첫 번째 점 지정' }; const canClose = (context: Partial) => (context.segments?.length ?? 0) >= 2; const nextPrompt = (context: Partial): PromptSpec => ({ text: '다음 점 지정', options: canClose(context) ? [OPT.CLOSE, OPT.UNDO] : [OPT.UNDO], }); const startAt = (startPoint: Point) => { setAngleGuideOriginPoint(startPoint); return { startPoint, firstPoint: startPoint, segments: [] }; }; export const lineToolStateMachine = createMachine( { types: {} as { context: LineContext; events: StateEvent; }, context: { startPoint: null, firstPoint: null, segments: [], type: Tool.LINE, }, initial: LineState.INIT, states: { [LineState.INIT]: { always: { actions: LineAction.INIT_LINE_TOOL, target: LineState.WAITING_FOR_START_POINT, }, }, [LineState.WAITING_FOR_START_POINT]: { meta: { kind: 'point', prompt: START_PROMPT }, on: { ...firstPointEvents(LineAction.RECORD_START_POINT, LineState.WAITING_FOR_END_POINT), ENTER: { guard: () => !!lastDrawEnd(), actions: LineAction.CONTINUE_FROM_LAST, target: LineState.WAITING_FOR_END_POINT, }, ESC: { target: END_STATE }, }, }, [LineState.WAITING_FOR_END_POINT]: { meta: { kind: 'point', prompt: nextPrompt }, on: { DRAW: { actions: LineAction.DRAW_TEMP_LINE }, ...pointEvents(LineAction.DRAW_FINAL_LINE, LineState.WAITING_FOR_END_POINT), 'OPTION.CLOSE': { guard: 'canClose', actions: LineAction.CLOSE_LINE, target: END_STATE, }, 'OPTION.UNDO': [ { guard: 'hasSegment', actions: LineAction.UNDO_SEGMENT, }, // 그린 토막 없음 — 첫 점부터 다시 { actions: LineAction.INIT_LINE_TOOL, target: LineState.WAITING_FOR_START_POINT }, ], ENTER: { target: END_STATE }, ESC: { target: END_STATE }, }, }, ...END, }, }, { // 옵션 사건(`OPTION.*`)은 이름 붙인 guard 로 — 글자 사건 꼴이라 줄 안 guard 는 타입이 안 맞음 guards: { canClose: ({ context }) => canClose(context), hasSegment: ({ context }) => context.segments.length > 0, }, actions: { [LineAction.INIT_LINE_TOOL]: assign(() => { setShouldDrawHelpers(true); setSelectedEntityIds([]); setGhostHelperEntities([]); setAngleGuideOriginPoint(null); return { startPoint: null, firstPoint: null, segments: [] }; }), [LineAction.RECORD_START_POINT]: assign(({ event }) => startAt(getPointFromEvent(null, event as PointInputEvent)) ), [LineAction.CONTINUE_FROM_LAST]: assign(() => startAt(lastDrawEnd() as Point)), [LineAction.DRAW_TEMP_LINE]: ({ context, event }) => { const start = context.startPoint as Point; setGhostHelperEntities([lineEntity(start, getPointFromEvent(start, event as DrawEvent))]); }, [LineAction.DRAW_FINAL_LINE]: assign(({ context, event }) => { const start = context.startPoint as Point; const endPoint = getPointFromEvent(start, event as PointInputEvent); const segment = lineEntity(start, endPoint); addEntities([segment], true); rememberDrawEnd(endPoint); setGhostHelperEntities([]); setAngleGuideOriginPoint(endPoint); return { startPoint: endPoint, segments: [...context.segments, segment] }; }), [LineAction.CLOSE_LINE]: ({ context }) => { const first = context.firstPoint as Point; addEntities([lineEntity(context.startPoint as Point, first)], true); rememberDrawEnd(first); }, [LineAction.UNDO_SEGMENT]: assign(({ context }) => { const last = context.segments[context.segments.length - 1]; undo(); // 토막 하나 = 되돌림 한 칸 const startPoint = last.getStartPoint(); rememberDrawEnd(startPoint); setGhostHelperEntities([]); setAngleGuideOriginPoint(startPoint); return { startPoint, segments: context.segments.slice(0, -1) }; }), }, } );