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