/** * 블록 삽입 도구 — 라이브러리에서 고른 블록을 커서에 달고 다니다 클릭 한 번으로 놓는다. * ponytail: 1:1·0도로만 놓는다. 회전·축척은 기존 ROTATE·SCALE 명령으로 한다. */ import type { Point } from '@flatten-js/core'; import { toast } from 'react-toastify'; import { Actor, assign, createMachine } from 'xstate'; import { type BlockDefinition, deserializeBlock } from '../blocks/block-library'; import type { Entity } from '../entities/Entity'; import { getPointFromEvent } from '../helpers/get-point-from-event'; import { addEntities, setActiveToolActor, setAngleGuideOriginPoint, setGhostHelperEntities, setSelectedEntityIds, setShouldDrawHelpers, } from '../state'; import { Tool } from '../tools'; import { selectToolStateMachine } from './select-tool'; import { ActorEvent, type PointInputEvent, type StateEvent, type ToolContext } from './tool.types'; /** 삽입 대기 중인 객체와 그 객체들이 지금 놓여 있는 기준점 */ let pendingEntities: Entity[] = []; let anchor: { x: number; y: number } = { x: 0, y: 0 }; function movePendingTo(x: number, y: number): void { for (const entity of pendingEntities) entity.move(x - anchor.x, y - anchor.y); anchor = { x, y }; } /** 라이브러리 팔레트에서 블록을 고르면 부른다. 객체를 풀어 두고 삽입 도구를 켠다. */ export async function startBlockInsert(block: BlockDefinition): Promise { try { pendingEntities = await deserializeBlock(block); } catch { toast.error(`블록 '${block.name}'을 풀지 못했습니다.`); return; } if (!pendingEntities.length) { toast.error(`블록 '${block.name}'에 객체가 없습니다.`); return; } anchor = { ...block.basePoint }; setActiveToolActor(new Actor(insertBlockToolStateMachine)); } export enum InsertBlockState { INIT = 'INIT', WAITING_FOR_INSERTION_POINT = 'WAITING_FOR_INSERTION_POINT', } export enum InsertBlockAction { INIT_INSERT_BLOCK_TOOL = 'INIT_INSERT_BLOCK_TOOL', DRAW_TEMP_BLOCK = 'DRAW_TEMP_BLOCK', PLACE_BLOCK = 'PLACE_BLOCK', SWITCH_TO_SELECT_TOOL = 'SWITCH_TO_SELECT_TOOL', } export const insertBlockToolStateMachine = createMachine( { types: {} as { context: ToolContext; events: StateEvent }, context: { type: Tool.INSERT_BLOCK }, initial: InsertBlockState.INIT, states: { [InsertBlockState.INIT]: { always: { actions: InsertBlockAction.INIT_INSERT_BLOCK_TOOL, target: InsertBlockState.WAITING_FOR_INSERTION_POINT, }, }, [InsertBlockState.WAITING_FOR_INSERTION_POINT]: { description: '블록을 놓을 위치를 지정한다', meta: { instructions: '블록을 놓을 위치를 지정하십시오' }, on: { [ActorEvent.DRAW]: { actions: InsertBlockAction.DRAW_TEMP_BLOCK }, [ActorEvent.MOUSE_CLICK]: { actions: [InsertBlockAction.PLACE_BLOCK, InsertBlockAction.SWITCH_TO_SELECT_TOOL], }, [ActorEvent.ABSOLUTE_POINT_INPUT]: { actions: [InsertBlockAction.PLACE_BLOCK, InsertBlockAction.SWITCH_TO_SELECT_TOOL], }, [ActorEvent.ESC]: { actions: InsertBlockAction.SWITCH_TO_SELECT_TOOL }, }, }, }, }, { actions: { [InsertBlockAction.INIT_INSERT_BLOCK_TOOL]: assign(() => { setShouldDrawHelpers(true); setGhostHelperEntities([]); setSelectedEntityIds([]); setAngleGuideOriginPoint(null); return {}; }), [InsertBlockAction.DRAW_TEMP_BLOCK]: ({ event }) => { if (!pendingEntities.length) return; const point = getPointFromEvent(null, event as PointInputEvent); movePendingTo(point.x, point.y); setGhostHelperEntities([...pendingEntities]); }, [InsertBlockAction.PLACE_BLOCK]: ({ event }) => { if (!pendingEntities.length) return; const point: Point = getPointFromEvent(null, event as PointInputEvent); movePendingTo(point.x, point.y); addEntities(pendingEntities, true); pendingEntities = []; setGhostHelperEntities([]); }, [InsertBlockAction.SWITCH_TO_SELECT_TOOL]: () => { pendingEntities = []; setGhostHelperEntities([]); setActiveToolActor(new Actor(selectToolStateMachine)); }, }, } );