/** * 그립 편집 도구 — 선택 도구에서 그립을 집으면 켜진다. * 클릭 한 번으로 그 그립을 옮긴다 (AutoCAD처럼 끌지 않고 집어서 놓는 방식). */ import type { Point } from '@flatten-js/core'; import { Actor, assign, createMachine } from 'xstate'; 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'; 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: { instructions: '그립을 옮길 위치를 지정하십시오' }, 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.ESC]: { 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)); }, }, } );