/** * MOVE(337 라 · 설계 1-3) — 「객체 선택:」 → 「기준점 지정 또는 [변위(D)] <변위>:」 → * 「두 번째 점 지정 또는 <첫 번째 점을 변위로 사용>:」 · D → 「변위 지정 <0.0000, 0.0000>:」. * 한 번 뒤 끝 · Esc = 제자리 끝 · 변위 기억(MOVE.DISP). STRETCH 도 이 틀(`createDisplaceTool`). */ import type { Point } from '@flatten-js/core'; import { assign, createMachine } from 'xstate'; import { OPT, type PromptSpec, remember } from '../commands/prompt'; import type { Entity } from '../entities/Entity'; import { getPointFromEvent } from '../helpers/get-point-from-event'; import { setAngleGuideOriginPoint, setGhostHelperEntities, setShouldDrawHelpers } from '../state'; import { Tool } from '../tools'; import { type DrawEvent, END_STATE, type PointInputEvent, type StateEvent, type ToolContext, } from './tool.types'; import { type PickedContext, changedCopies, endState, guideLine, replaceInPlace, selectionState, startState, } from './transform.helpers'; export enum MoveState { INIT = 'INIT', WAITING_FOR_SELECTION = 'WAITING_FOR_SELECTION', WAITING_FOR_BASE_POINT = 'WAITING_FOR_BASE_POINT', WAITING_FOR_SECOND_POINT = 'WAITING_FOR_SECOND_POINT', WAITING_FOR_DISPLACEMENT = 'WAITING_FOR_DISPLACEMENT', } export interface DisplaceContext extends ToolContext, PickedContext { base: Point | null; /** 고르기 중 마지막 걸침 창(STRETCH) */ window: [Point, Point] | null; } export interface DisplaceConfig { tool: Tool; /** 변위 기억 열쇠 */ remember: string; /** 변위만큼 바꾼 사본(id 그대로 · 안 바뀐 것은 빼도 됨) */ change: (context: DisplaceContext, dx: number, dy: number) => Entity[]; /** 고르기 누름 엿봄(걸침 창 기억) */ watch?: unknown; /** 시작 기록 줄 */ intro?: string; } type Ctx = { context: DisplaceContext; event: StateEvent }; export const BASE_PROMPT: PromptSpec = { text: '기준점 지정', options: [OPT.DISPLACEMENT], default: { kind: 'option', key: OPT.DISPLACEMENT.key }, }; export const SECOND_PROMPT: PromptSpec = { // 옵션 없이 「또는 <…>」 — 오토캐드 줄 그대로 text: '두 번째 점 지정 또는', default: { kind: 'enter', label: '첫 번째 점을 변위로 사용' }, }; export function createDisplaceTool(config: DisplaceConfig) { const S = MoveState; const DISP_PROMPT: PromptSpec = { text: '변위 지정', default: { kind: 'value', remember: config.remember, initial: { x: 0, y: 0 } }, }; const commit = (context: DisplaceContext, dx: number, dy: number) => { replaceInPlace(config.change(context, dx, dy)); remember(config.remember, { x: dx, y: dy }); }; const toward = ({ context, event }: Ctx) => getPointFromEvent(context.base, event as PointInputEvent); const commitSecond = (args: Ctx) => { const base = args.context.base as Point; const dest = toward(args); commit(args.context, dest.x - base.x, dest.y - base.y); }; // 첫 점 좌표가 곧 변위(빈 Enter) · D 변위 물음의 점 const commitPointAsVector = (point: Point, context: DisplaceContext) => commit(context, point.x, point.y); const done = (actions: unknown) => ({ actions, target: END_STATE }); const pointEvents = (actions: unknown) => ({ MOUSE_CLICK: done(actions), ABSOLUTE_POINT_INPUT: done(actions), RELATIVE_POINT_INPUT: done(actions), NUMBER_INPUT: done(actions), }); // biome-ignore lint/suspicious/noExplicitAny: 공용 상태 조각(여러 도구 공통)을 섞어 만든다 const states: Record = { [S.INIT]: { ...startState(S.WAITING_FOR_BASE_POINT), exit: () => { if (config.intro) { const intro = config.intro; void import('../commands/run-command').then(({ logLine }) => logLine(intro)); } }, }, [S.WAITING_FOR_SELECTION]: selectionState(S.WAITING_FOR_BASE_POINT, config.watch), [S.WAITING_FOR_BASE_POINT]: { meta: { kind: 'point', prompt: BASE_PROMPT }, entry: () => setShouldDrawHelpers(true), on: { MOUSE_CLICK: { actions: assign(({ event }) => { const base = getPointFromEvent(null, event as PointInputEvent); setAngleGuideOriginPoint(base); return { base }; }), target: S.WAITING_FOR_SECOND_POINT, }, ABSOLUTE_POINT_INPUT: { actions: assign(({ event }) => { const base = getPointFromEvent(null, event as PointInputEvent); setAngleGuideOriginPoint(base); return { base }; }), target: S.WAITING_FOR_SECOND_POINT, }, 'OPTION.DISPLACEMENT': { target: S.WAITING_FOR_DISPLACEMENT }, ESC: { target: END_STATE }, }, }, [S.WAITING_FOR_SECOND_POINT]: { meta: { kind: 'point', prompt: SECOND_PROMPT }, on: { DRAW: { actions: (args: Ctx) => { const base = args.context.base as Point; const dest = getPointFromEvent(base, args.event as DrawEvent); setGhostHelperEntities([ guideLine(base, dest), ...config.change(args.context, dest.x - base.x, dest.y - base.y), ]); }, }, ...pointEvents(commitSecond), ENTER: done(({ context }: Ctx) => commitPointAsVector(context.base as Point, context)), ESC: { target: END_STATE }, }, }, [S.WAITING_FOR_DISPLACEMENT]: { meta: { kind: 'point', prompt: DISP_PROMPT }, entry: () => setAngleGuideOriginPoint(null), on: { MOUSE_CLICK: done(({ context, event }: Ctx) => commitPointAsVector(getPointFromEvent(null, event as PointInputEvent), context) ), ABSOLUTE_POINT_INPUT: done(({ context, event }: Ctx) => commitPointAsVector(getPointFromEvent(null, event as PointInputEvent), context) ), ESC: { target: END_STATE }, }, }, [END_STATE]: endState, }; return createMachine({ types: {} as { context: DisplaceContext; events: StateEvent }, context: { originals: [], base: null, window: null, type: config.tool }, initial: S.INIT, states, }); } export const moveToolStateMachine = createDisplaceTool({ tool: Tool.MOVE, remember: 'MOVE.DISP', change: (context, dx, dy) => changedCopies(context.originals, (entity) => entity.move(dx, dy)), });