조사표 4·7절 검토 결과를 반영한다. - 블록 라이브러리: 선택한 객체를 이름 붙여 저장(BLOCK)하고 팔레트에서 골라 클릭 한 번으로 놓는다(INSERT). 정의-참조 링크 없는 클론 방식이라 새 엔티티 타입 없이 기존 직렬화·groupId를 그대로 쓴다. 기준점은 선택 영역 중심. 라이브러리는 도면과 분리해 브라우저에 두고, 유실 대비로 파일 내보내기· 가져오기를 붙였다. - 사진 넣기: IMAGEATTACH가 파일 선택창을 띄우지 않아 도구가 FILE_SELECTED를 영원히 기다리고 있었다. 도구형 명령도 run()을 함께 부르게 하고 파일 선택을 붙여 되살렸다. - 조사표 4·7절의 반영 등급·반영 열을 실제 코드에 맞춰 정정하고, 반영하지 않기로 한 항목의 사유를 절마다 남겼다. 7절은 신규 구현 없음. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
119 lines
4.1 KiB
TypeScript
119 lines
4.1 KiB
TypeScript
/**
|
|
* 블록 삽입 도구 — 라이브러리에서 고른 블록을 커서에 달고 다니다 클릭 한 번으로 놓는다.
|
|
* 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<void> {
|
|
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));
|
|
},
|
|
},
|
|
}
|
|
);
|