/** * 그립 편집 도구 — 선택 도구에서 그립을 집으면 켜진다(뜨거운 그립). * 오토캐드처럼 누르고-옮기고-누르기 · 끌어 놓기 둘 다(누름 · 뗌 가름은 input-controller) · * 거리 숫자 = 그립 점에서 커서 쪽(N8) · 좌표 · Esc · 종료(X) = 그립 놓음. */ import type { Point } from '@flatten-js/core'; import { Actor, assign, createMachine } from 'xstate'; import type { OptionSpec, PromptSpec } from '../commands/prompt'; import type { Entity } from '../entities/Entity'; import { getPointFromEvent } from '../helpers/get-point-from-event'; import { type Grip, applyGrip } from '../helpers/grips'; import { getEntities, setActiveToolActor, setAngleGuideOriginPoint, setEntities, setGhostHelperEntities, setShouldDrawHelpers, } from '../state'; import { Tool } from '../tools'; import { selectToolStateMachine } from './select-tool'; import { ActorEvent, type PointInputEvent, type StateEvent, type ToolContext } from './tool.types'; const gripOption = (key: string, kw: string, label: string): OptionSpec => ({ key, kw, en: key, label, }); /** 오토캐드 그립 신축 줄 — 종료(X)만 · 기준점(B) · 복사(C) · 명령 취소(U) 는 뺌 */ const GRIP_PROMPT: PromptSpec = { text: '** 신축 ** 신축점 지정', options: [gripOption('EXIT', 'X', '종료')], absent: [ gripOption('BASE', 'B', '기준점'), gripOption('COPY', 'C', '복사'), gripOption('UNDO', 'U', '명령 취소'), ], }; let editedEntity: Entity | null = null; let editedGrip: Grip | null = null; /** 선택 도구가 그립을 집었을 때 부른다 */ export function startGripEdit(entity: Entity, grip: Grip): void { editedEntity = entity; editedGrip = grip; setActiveToolActor(new Actor(gripEditToolStateMachine)); } /** 편집 중인 객체 — 그립을 그릴 때 원본 대신 미리보기를 보여주려고 읽는다 */ export const getGripEditTargetId = (): string | null => editedEntity?.id ?? null; function previewAt(point: Point): Entity | null { if (!editedEntity || !editedGrip) return null; return applyGrip(editedEntity, editedGrip, point); } export enum GripEditState { INIT = 'INIT', WAITING_FOR_TARGET_POINT = 'WAITING_FOR_TARGET_POINT', } export enum GripEditAction { INIT_GRIP_EDIT_TOOL = 'INIT_GRIP_EDIT_TOOL', DRAW_TEMP_GRIP_EDIT = 'DRAW_TEMP_GRIP_EDIT', APPLY_GRIP_EDIT = 'APPLY_GRIP_EDIT', SWITCH_TO_SELECT_TOOL = 'SWITCH_TO_SELECT_TOOL', } export const gripEditToolStateMachine = createMachine( { types: {} as { context: ToolContext; events: StateEvent }, context: { type: Tool.GRIP_EDIT }, initial: GripEditState.INIT, states: { [GripEditState.INIT]: { always: { actions: GripEditAction.INIT_GRIP_EDIT_TOOL, target: GripEditState.WAITING_FOR_TARGET_POINT, }, }, [GripEditState.WAITING_FOR_TARGET_POINT]: { description: '그립을 옮길 위치를 지정한다', meta: { kind: 'point', prompt: GRIP_PROMPT }, on: { [ActorEvent.DRAW]: { actions: GripEditAction.DRAW_TEMP_GRIP_EDIT }, [ActorEvent.MOUSE_CLICK]: { actions: [GripEditAction.APPLY_GRIP_EDIT, GripEditAction.SWITCH_TO_SELECT_TOOL], }, [ActorEvent.ABSOLUTE_POINT_INPUT]: { actions: [GripEditAction.APPLY_GRIP_EDIT, GripEditAction.SWITCH_TO_SELECT_TOOL], }, [ActorEvent.RELATIVE_POINT_INPUT]: { actions: [GripEditAction.APPLY_GRIP_EDIT, GripEditAction.SWITCH_TO_SELECT_TOOL], }, [ActorEvent.NUMBER_INPUT]: { actions: [GripEditAction.APPLY_GRIP_EDIT, GripEditAction.SWITCH_TO_SELECT_TOOL], }, [ActorEvent.ESC]: { actions: GripEditAction.SWITCH_TO_SELECT_TOOL }, 'OPTION.EXIT': { actions: GripEditAction.SWITCH_TO_SELECT_TOOL }, }, }, }, }, { actions: { [GripEditAction.INIT_GRIP_EDIT_TOOL]: assign(() => { setShouldDrawHelpers(true); setGhostHelperEntities([]); setAngleGuideOriginPoint(editedGrip?.point ?? null); return {}; }), [GripEditAction.DRAW_TEMP_GRIP_EDIT]: ({ event }) => { const point = getPointFromEvent(editedGrip?.point ?? null, event as PointInputEvent); const preview = previewAt(point); setGhostHelperEntities(preview ? [preview] : []); }, [GripEditAction.APPLY_GRIP_EDIT]: ({ event }) => { const point = getPointFromEvent(editedGrip?.point ?? null, event as PointInputEvent); const edited = previewAt(point); if (!edited || !editedEntity) return; const targetId = editedEntity.id; setEntities( getEntities().map((entity) => (entity.id === targetId ? edited : entity)), true ); }, [GripEditAction.SWITCH_TO_SELECT_TOOL]: () => { editedEntity = null; editedGrip = null; setGhostHelperEntities([]); setAngleGuideOriginPoint(null); setActiveToolActor(new Actor(selectToolStateMachine)); }, }, } );