feat(CAD): 337-5 라 옮기기 무리 2 단계 — COPY · MIRROR · STRETCH · ARRAY · PEDIT 고르기
- COPY: 기준점 [변위(D)/모드(O)/다중(M)] · 두 번째 점 [배열(A)] → [배열(A)/종료(E)/명령 취소(U)] <종료> 되풀이 · 첫 빈 Enter = 변위 · COPYMODE 기억 · A 항목 수 · F 맞춤 - MIRROR: 대칭선 두 점 → 원본 객체를 지우시겠습니까? [예(Y)/아니오(N)] <아니오> · Esc = 안 만듦 - STRETCH: MOVE 틀 + 걸침 창 안 꼭짓점만(선 · 폴리선 토막 · 사각형) · 창 안 통째 · 하나씩 고른 것 = 이동 · STRETCH.DISP - ARRAY: 유형 [직사각형(R)/원형(PO)] <직사각형>(ARRAYTYPE) → 편집 물음 되풀이(COU · S · COL · R · X / I · A · F · ROT · X) · 미리보기 4 × 3 · 6 개 360° · 처음 간격 범위 1.5 배 · Esc = 안 만듦(N17 없어짐) - PEDIT: 공용 고르기 상태(객체 선택 · 빈 Enter · Esc = 끝 · 묶은 뒤 끝) - 시험 transform-acad 29 + ALL · L · P 1(선택 도구 처리 뒤 돎) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TteK8mfYZetk5P3bEZkyVZ
This commit is contained in:
@@ -1,369 +1,293 @@
|
||||
import { type Point, Vector } from '@flatten-js/core';
|
||||
import { assign, createMachine, sendTo } from 'xstate';
|
||||
import { GUIDE_LINE_COLOR, GUIDE_LINE_STYLE, GUIDE_LINE_WIDTH, TO_RADIANS } from '../App.consts.ts';
|
||||
import type { Entity } from '../entities/Entity';
|
||||
import { LineEntity } from '../entities/LineEntity.ts';
|
||||
import { getPointFromEvent } from '../helpers/get-point-from-event.ts';
|
||||
import {
|
||||
addEntities,
|
||||
getActiveLayerId,
|
||||
getSelectedEntities,
|
||||
getSelectedEntityIds,
|
||||
setAngleGuideOriginPoint,
|
||||
setGhostHelperEntities,
|
||||
setSelectedEntityIds,
|
||||
setShouldDrawHelpers,
|
||||
} from '../state';
|
||||
/**
|
||||
* ARRAY(337 라 · 설계 1-3) — 「객체 선택:」 → 「배열 유형 입력 [직사각형(R)/원형(PO)] <직사각형>:」(ARRAYTYPE 기억) →
|
||||
* 직사각형: 미리보기 4열 × 3행 → 「그립을 선택하여 배열을 편집하거나 [개수(COU)/간격두기(S)/열(COL)/행(R)/종료(X)] <종료>:」 되풀이 ·
|
||||
* 원형: 「배열의 중심점 지정:」 → 미리보기 6개 360° → 「… [항목(I)/사이의 각도(A)/채울 각도(F)/항목 회전(ROT)/종료(X)] <종료>:」 되풀이.
|
||||
* 종료(X) · 빈 Enter = 만듦 · Esc = 안 만듦 · 처음 간격 = 고른 것 범위 폭 · 높이의 1.5 배.
|
||||
*/
|
||||
import type { Point } from '@flatten-js/core';
|
||||
import { assign, createMachine } from 'xstate';
|
||||
import { recall, remember } from '../commands/prompt';
|
||||
import { getPointFromEvent } from '../helpers/get-point-from-event';
|
||||
import { addEntities, setGhostHelperEntities, setShouldDrawHelpers } from '../state';
|
||||
import { Tool } from '../tools';
|
||||
import { CopyAction } from './copy-tool.ts';
|
||||
import { cloneKeepingLook } from './move-tool.helpers.ts';
|
||||
import { selectToolStateMachine } from './select-tool.ts';
|
||||
import type {
|
||||
AbsolutePointInputEvent,
|
||||
DrawEvent,
|
||||
MouseClickEvent,
|
||||
NumberInputEvent,
|
||||
StateEvent,
|
||||
TextInputEvent,
|
||||
ToolContext,
|
||||
import {
|
||||
ARRAY_OPT,
|
||||
ARRAY_PROMPT,
|
||||
type ArrayValues,
|
||||
angleBetween,
|
||||
initialSpacing,
|
||||
polarCopies,
|
||||
rectangularCopies,
|
||||
} from './array-tool.helpers';
|
||||
import {
|
||||
type DrawEvent,
|
||||
END_STATE,
|
||||
type NumberInputEvent,
|
||||
type PointInputEvent,
|
||||
type StateEvent,
|
||||
type ToolContext,
|
||||
} from './tool.types';
|
||||
import { endState, selectionState, startState } from './transform.helpers';
|
||||
|
||||
export enum ArrayState {
|
||||
INIT = 'INIT',
|
||||
WAITING_FOR_SELECTION = 'WAITING_FOR_SELECTION',
|
||||
CHECK_SELECTION = 'CHECK_SELECTION',
|
||||
ASK_COPY_MODE = 'ASK_COPY_MODE',
|
||||
ASK_NUMBER_OF_COPIES = 'ASK_NUMBER_OF_COPIES',
|
||||
LINEAR_WAITING_FOR_START_POINT = 'LINEAR_WAITING_FOR_START_POINT',
|
||||
LINEAR_WAITING_FOR_END_POINT = 'LINEAR_WAITING_FOR_END_POINT',
|
||||
RADIAL_WAITING_FOR_PIVOT_POINT = 'RADIAL_WAITING_FOR_PIVOT_POINT',
|
||||
RADIAL_WAITING_FOR_ANGLE = 'RADIAL_WAITING_FOR_ANGLE',
|
||||
EXECUTE_COPY = 'EXECUTE_COPY',
|
||||
WAITING_FOR_TYPE = 'WAITING_FOR_TYPE',
|
||||
WAITING_FOR_CENTER = 'WAITING_FOR_CENTER',
|
||||
EDIT_RECTANGULAR = 'EDIT_RECTANGULAR',
|
||||
EDIT_POLAR = 'EDIT_POLAR',
|
||||
WAITING_FOR_COLUMNS = 'WAITING_FOR_COLUMNS',
|
||||
WAITING_FOR_ROWS = 'WAITING_FOR_ROWS',
|
||||
WAITING_FOR_COLUMN_SPACING = 'WAITING_FOR_COLUMN_SPACING',
|
||||
WAITING_FOR_ROW_SPACING = 'WAITING_FOR_ROW_SPACING',
|
||||
WAITING_FOR_ITEMS = 'WAITING_FOR_ITEMS',
|
||||
WAITING_FOR_ANGLE = 'WAITING_FOR_ANGLE',
|
||||
WAITING_FOR_FILL = 'WAITING_FOR_FILL',
|
||||
WAITING_FOR_ROTATE_ITEMS = 'WAITING_FOR_ROTATE_ITEMS',
|
||||
}
|
||||
|
||||
export enum ArrayAction {
|
||||
INIT_ARRAY_TOOL = 'INIT_ARRAY_TOOL',
|
||||
PERFORM_COPY = 'PERFORM_COPY',
|
||||
DESELECT_ENTITIES = 'DESELECT_ENTITIES',
|
||||
DRAW_TEMP_DISTANCE_LINE = 'DRAW_TEMP_DISTANCE_LINE',
|
||||
/** 편집 옵션이 이어 묻는 차례 — 개수 = 열 → 행 · 간격두기 = 열 거리 → 행 거리 · 열 = 수 → 거리 · 행 = 수 → 거리 */
|
||||
type Flow = 'COUNT' | 'SPACING' | 'COLUMNS' | 'ROWS' | null;
|
||||
|
||||
export interface ArrayContext extends ToolContext, ArrayValues {
|
||||
flow: Flow;
|
||||
}
|
||||
|
||||
export interface ArrayContext extends ToolContext {
|
||||
copyMode: CopyMode | null;
|
||||
startDistanceVector: Point | null;
|
||||
endDistanceVector: Point | null;
|
||||
pivotPoint: Point | null;
|
||||
angleStep: number | null;
|
||||
numberOfCopies: number;
|
||||
}
|
||||
type Ctx = { context: ArrayContext; event: StateEvent };
|
||||
|
||||
enum CopyMode {
|
||||
LINEAR = 'LINEAR',
|
||||
RADIAL = 'RADIAL',
|
||||
}
|
||||
const S = ArrayState;
|
||||
const O = ARRAY_OPT;
|
||||
const esc = { ESC: { target: END_STATE } };
|
||||
const numberOf = (event: StateEvent) => (event as NumberInputEvent).value;
|
||||
|
||||
const initialArrayContext: ArrayContext = {
|
||||
copyMode: null,
|
||||
startDistanceVector: null,
|
||||
endDistanceVector: null,
|
||||
pivotPoint: null,
|
||||
angleStep: null,
|
||||
numberOfCopies: 0,
|
||||
type: Tool.ARRAY,
|
||||
/** 물음 기본값 = 지금 값(빈 Enter 가 지금 값을 다시 보냄) */
|
||||
const syncRect = (values: ArrayValues) => {
|
||||
remember('ARRAY.COLS', values.columns);
|
||||
remember('ARRAY.ROWS', values.rows);
|
||||
remember('ARRAY.COLDIST', values.columnSpacing);
|
||||
remember('ARRAY.ROWDIST', values.rowSpacing);
|
||||
};
|
||||
const syncPolar = (values: ArrayValues) => {
|
||||
remember('ARRAY.ITEMS', values.items);
|
||||
remember('ARRAY.FILL', values.fill);
|
||||
remember('ARRAY.ANGLE', angleBetween(values));
|
||||
};
|
||||
|
||||
export const arrayToolStateMachine = createMachine(
|
||||
{
|
||||
types: {} as {
|
||||
context: ArrayContext;
|
||||
events: StateEvent;
|
||||
const showRect = ({ context }: Ctx) => {
|
||||
syncRect(context);
|
||||
setGhostHelperEntities(rectangularCopies(context));
|
||||
};
|
||||
const showPolar = ({ context }: Ctx) => {
|
||||
syncPolar(context);
|
||||
setGhostHelperEntities(polarCopies(context));
|
||||
};
|
||||
const create = (copies: (values: ArrayValues) => unknown[]) => ({
|
||||
actions: ({ context }: Ctx) => {
|
||||
const made = copies(context) as Parameters<typeof addEntities>[0];
|
||||
if (made.length) addEntities(made, true);
|
||||
},
|
||||
target: END_STATE,
|
||||
});
|
||||
|
||||
/** 숫자 물음 — 받아서 assign · 흐름 따라 다음 */
|
||||
const numberStep = (
|
||||
prompt: unknown,
|
||||
take: (value: number, context: ArrayContext) => Partial<ArrayContext> | null,
|
||||
next: { flow?: Flow; target: string }[]
|
||||
) => ({
|
||||
meta: { kind: 'number', prompt },
|
||||
on: {
|
||||
NUMBER_INPUT: [
|
||||
...next.map(({ flow, target }) => ({
|
||||
guard: ({ context, event }: Ctx) =>
|
||||
!!take(numberOf(event), context) && (flow === undefined || context.flow === flow),
|
||||
actions: assign(({ context, event }: Ctx) => take(numberOf(event), context) ?? {}),
|
||||
target,
|
||||
})),
|
||||
{ actions: () => noticeLater('값이 범위를 벗어남.') },
|
||||
],
|
||||
...esc,
|
||||
},
|
||||
});
|
||||
|
||||
const noticeLater = (text: string) => {
|
||||
void import('../inputController/prompt-input').then(({ notice }) => notice(text));
|
||||
};
|
||||
|
||||
const fillOk = (fill: number) => fill !== 0 && Math.abs(fill) <= 360;
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 공용 상태 조각(여러 도구 공통)을 섞어 만든다
|
||||
const states: Record<string, any> = {
|
||||
[S.INIT]: startState(S.WAITING_FOR_TYPE),
|
||||
[S.WAITING_FOR_SELECTION]: selectionState(S.WAITING_FOR_TYPE),
|
||||
[S.WAITING_FOR_TYPE]: {
|
||||
meta: {
|
||||
kind: 'keyword',
|
||||
prompt: () => ARRAY_PROMPT.type(recall<string>('ARRAYTYPE') ?? O.RECTANGULAR.key),
|
||||
},
|
||||
context: initialArrayContext,
|
||||
initial: ArrayState.INIT,
|
||||
states: {
|
||||
[ArrayState.INIT]: {
|
||||
description: 'Initializing the array copy tool',
|
||||
always: {
|
||||
actions: ArrayState.INIT,
|
||||
target: ArrayState.CHECK_SELECTION,
|
||||
},
|
||||
},
|
||||
[ArrayState.WAITING_FOR_SELECTION]: {
|
||||
description: 'Select what you want to copy',
|
||||
meta: {
|
||||
instructions: '배열할 객체 선택 뒤 Enter',
|
||||
},
|
||||
invoke: {
|
||||
id: 'selectToolInsideTheCopyTool',
|
||||
src: selectToolStateMachine,
|
||||
onDone: {
|
||||
actions: assign(() => {
|
||||
return initialArrayContext;
|
||||
}),
|
||||
target: ArrayState.CHECK_SELECTION,
|
||||
},
|
||||
},
|
||||
on: {
|
||||
MOUSE_CLICK: {
|
||||
// Forward the event to the select tool
|
||||
actions: sendTo('selectToolInsideTheCopyTool', ({ event }) => {
|
||||
return event;
|
||||
}),
|
||||
},
|
||||
ESC: {
|
||||
actions: [ArrayAction.DESELECT_ENTITIES, ArrayAction.INIT_ARRAY_TOOL],
|
||||
},
|
||||
ENTER: {
|
||||
// Forward the event to the select tool
|
||||
actions: sendTo('selectToolInsideTheCopyTool', ({ event }) => {
|
||||
return event;
|
||||
}),
|
||||
},
|
||||
DRAW: {
|
||||
// Forward the event to the select tool
|
||||
actions: sendTo('selectToolInsideTheCopyTool', ({ event }) => {
|
||||
return event;
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
[ArrayState.CHECK_SELECTION]: {
|
||||
description: 'Check if there is something selected',
|
||||
always: [
|
||||
{
|
||||
guard: () => {
|
||||
return getSelectedEntityIds().length > 0;
|
||||
},
|
||||
target: ArrayState.ASK_COPY_MODE,
|
||||
},
|
||||
{
|
||||
guard: () => {
|
||||
return getSelectedEntityIds().length === 0;
|
||||
},
|
||||
target: ArrayState.WAITING_FOR_SELECTION,
|
||||
},
|
||||
on: {
|
||||
'OPTION.RECTANGULAR': {
|
||||
actions: [
|
||||
() => remember('ARRAYTYPE', O.RECTANGULAR.key),
|
||||
assign(({ context }: Ctx) => {
|
||||
const spacing = initialSpacing(context.originals);
|
||||
return {
|
||||
columns: recall<number>('ARRAY.COLS') ?? 4,
|
||||
rows: recall<number>('ARRAY.ROWS') ?? 3,
|
||||
columnSpacing: spacing.column,
|
||||
rowSpacing: spacing.row,
|
||||
};
|
||||
}),
|
||||
],
|
||||
target: S.EDIT_RECTANGULAR,
|
||||
},
|
||||
[ArrayState.ASK_COPY_MODE]: {
|
||||
meta: { instructions: '배열 방식 [직선(L)/원형(R)] <직선>:' },
|
||||
on: {
|
||||
TEXT_INPUT: {
|
||||
actions: assign(({ event }) => {
|
||||
return {
|
||||
copyMode:
|
||||
(event as TextInputEvent).value === 'R' ? CopyMode.RADIAL : CopyMode.LINEAR,
|
||||
};
|
||||
}),
|
||||
target: ArrayState.ASK_NUMBER_OF_COPIES,
|
||||
},
|
||||
ENTER: {
|
||||
actions: assign(() => ({
|
||||
copyMode: CopyMode.LINEAR,
|
||||
})),
|
||||
target: ArrayState.ASK_NUMBER_OF_COPIES,
|
||||
},
|
||||
ESC: {
|
||||
actions: ArrayAction.INIT_ARRAY_TOOL,
|
||||
},
|
||||
},
|
||||
},
|
||||
[ArrayState.ASK_NUMBER_OF_COPIES]: {
|
||||
meta: { instructions: '복사 개수 입력:' },
|
||||
on: {
|
||||
NUMBER_INPUT: [
|
||||
{
|
||||
guard: ({ context }) => {
|
||||
return context.copyMode === CopyMode.LINEAR;
|
||||
},
|
||||
actions: assign(({ event }) => ({
|
||||
numberOfCopies: (event as NumberInputEvent).value,
|
||||
})),
|
||||
target: ArrayState.LINEAR_WAITING_FOR_START_POINT,
|
||||
},
|
||||
{
|
||||
guard: ({ context }) => {
|
||||
return context.copyMode === CopyMode.RADIAL;
|
||||
},
|
||||
actions: assign(({ event }) => ({
|
||||
numberOfCopies: (event as NumberInputEvent).value,
|
||||
})),
|
||||
target: ArrayState.RADIAL_WAITING_FOR_PIVOT_POINT,
|
||||
},
|
||||
],
|
||||
ESC: {
|
||||
actions: ArrayAction.INIT_ARRAY_TOOL,
|
||||
},
|
||||
},
|
||||
},
|
||||
[ArrayState.LINEAR_WAITING_FOR_START_POINT]: {
|
||||
meta: { instructions: '간격 시작점 지정:' },
|
||||
on: {
|
||||
ABSOLUTE_POINT_INPUT: {
|
||||
actions: assign(({ event }) => ({
|
||||
startDistanceVector: (event as AbsolutePointInputEvent).value,
|
||||
})),
|
||||
target: ArrayState.EXECUTE_COPY,
|
||||
},
|
||||
MOUSE_CLICK: {
|
||||
actions: assign(({ event }) => ({
|
||||
startDistanceVector: (event as MouseClickEvent).worldMouseLocation,
|
||||
})),
|
||||
target: ArrayState.LINEAR_WAITING_FOR_END_POINT,
|
||||
},
|
||||
ESC: {
|
||||
actions: ArrayAction.INIT_ARRAY_TOOL,
|
||||
},
|
||||
},
|
||||
},
|
||||
[ArrayState.LINEAR_WAITING_FOR_END_POINT]: {
|
||||
meta: { instructions: '간격 끝점 지정:' },
|
||||
on: {
|
||||
RELATIVE_POINT_INPUT: {
|
||||
actions: assign(({ event, context }) => ({
|
||||
endDistanceVector: getPointFromEvent(context.startDistanceVector, event),
|
||||
})),
|
||||
target: ArrayState.EXECUTE_COPY,
|
||||
},
|
||||
ABSOLUTE_POINT_INPUT: {
|
||||
actions: assign(({ event, context }) => ({
|
||||
endDistanceVector: getPointFromEvent(context.startDistanceVector, event),
|
||||
})),
|
||||
target: ArrayState.EXECUTE_COPY,
|
||||
},
|
||||
MOUSE_CLICK: {
|
||||
actions: assign(({ event, context }) => ({
|
||||
endDistanceVector: getPointFromEvent(context.startDistanceVector, event),
|
||||
})),
|
||||
target: ArrayState.EXECUTE_COPY,
|
||||
},
|
||||
ESC: {
|
||||
actions: ArrayAction.INIT_ARRAY_TOOL,
|
||||
},
|
||||
DRAW: {
|
||||
actions: ArrayAction.DRAW_TEMP_DISTANCE_LINE,
|
||||
},
|
||||
},
|
||||
},
|
||||
[ArrayState.RADIAL_WAITING_FOR_PIVOT_POINT]: {
|
||||
meta: { instructions: '회전 중심점 지정:' },
|
||||
on: {
|
||||
ABSOLUTE_POINT_INPUT: {
|
||||
actions: assign(({ event }) => ({
|
||||
pivotPoint: (event as AbsolutePointInputEvent).value,
|
||||
})),
|
||||
target: ArrayState.RADIAL_WAITING_FOR_ANGLE,
|
||||
},
|
||||
MOUSE_CLICK: {
|
||||
actions: assign(({ event }) => ({
|
||||
pivotPoint: (event as MouseClickEvent).worldMouseLocation,
|
||||
})),
|
||||
target: ArrayState.RADIAL_WAITING_FOR_ANGLE,
|
||||
},
|
||||
ESC: {
|
||||
actions: ArrayAction.INIT_ARRAY_TOOL,
|
||||
},
|
||||
},
|
||||
},
|
||||
[ArrayState.RADIAL_WAITING_FOR_ANGLE]: {
|
||||
meta: { instructions: '한 칸 각도(도) 입력:' },
|
||||
on: {
|
||||
NUMBER_INPUT: {
|
||||
target: ArrayState.EXECUTE_COPY,
|
||||
actions: assign(({ event }) => ({
|
||||
angleStep: (event as NumberInputEvent).value,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
[ArrayState.EXECUTE_COPY]: {
|
||||
entry: ArrayAction.PERFORM_COPY,
|
||||
always: ArrayState.INIT,
|
||||
'OPTION.POLAR': {
|
||||
actions: [
|
||||
() => remember('ARRAYTYPE', O.POLAR.key),
|
||||
assign(() => ({
|
||||
items: recall<number>('ARRAY.ITEMS') ?? 6,
|
||||
fill: recall<number>('ARRAY.FILL') ?? 360,
|
||||
rotateItems: true,
|
||||
})),
|
||||
],
|
||||
target: S.WAITING_FOR_CENTER,
|
||||
},
|
||||
...esc,
|
||||
},
|
||||
},
|
||||
{
|
||||
actions: {
|
||||
[ArrayAction.INIT_ARRAY_TOOL]: assign(() => {
|
||||
setShouldDrawHelpers(true);
|
||||
setGhostHelperEntities([]);
|
||||
setSelectedEntityIds([]);
|
||||
setAngleGuideOriginPoint(null);
|
||||
return {
|
||||
copyMode: null,
|
||||
distanceVector: null,
|
||||
pivotPoint: null,
|
||||
angleStep: null,
|
||||
numberOfCopies: 0,
|
||||
selectedEntities: [],
|
||||
};
|
||||
}),
|
||||
|
||||
[ArrayAction.DRAW_TEMP_DISTANCE_LINE]: ({ context, event }) => {
|
||||
if (!context.startDistanceVector) {
|
||||
throw new Error('[ARRAY] Calling draw temp distance line without a start point');
|
||||
}
|
||||
|
||||
const endPointTemp = (event as DrawEvent).drawController.getWorldMouseLocation();
|
||||
|
||||
// Draw the array entities to show the result
|
||||
// Draw all selected entities according to distance vector and number of copies, so the user gets visual feedback of where the entities will be copied;
|
||||
// TODO
|
||||
|
||||
// // Draw a dashed line between the start move point and the current mouse location
|
||||
const activeDistanceLine = new LineEntity(
|
||||
getActiveLayerId(),
|
||||
context.startDistanceVector,
|
||||
endPointTemp
|
||||
);
|
||||
activeDistanceLine.lineColor = GUIDE_LINE_COLOR;
|
||||
activeDistanceLine.lineWidth = GUIDE_LINE_WIDTH;
|
||||
activeDistanceLine.lineDash = GUIDE_LINE_STYLE;
|
||||
setGhostHelperEntities([activeDistanceLine]);
|
||||
[S.WAITING_FOR_CENTER]: {
|
||||
meta: { kind: 'point', prompt: ARRAY_PROMPT.center },
|
||||
entry: () => setShouldDrawHelpers(true),
|
||||
on: {
|
||||
DRAW: {
|
||||
actions: ({ context, event }: Ctx) =>
|
||||
setGhostHelperEntities(polarCopies(context, getPointFromEvent(null, event as DrawEvent))),
|
||||
},
|
||||
|
||||
[ArrayAction.PERFORM_COPY]: ({ context }) => {
|
||||
const resultEntities: Entity[] = [];
|
||||
|
||||
for (let i = 1; i <= context.numberOfCopies; i++) {
|
||||
if (
|
||||
context.copyMode === 'LINEAR' &&
|
||||
context.startDistanceVector &&
|
||||
context.endDistanceVector
|
||||
) {
|
||||
const distanceVector = new Vector(
|
||||
context.endDistanceVector.x - context.startDistanceVector.x,
|
||||
context.endDistanceVector.y - context.startDistanceVector.y
|
||||
);
|
||||
const x = distanceVector.x * i;
|
||||
const y = distanceVector.y * i;
|
||||
resultEntities.push(
|
||||
...getSelectedEntities().map((entity) => {
|
||||
const clone = cloneKeepingLook(entity);
|
||||
clone.move(x, y);
|
||||
return clone;
|
||||
})
|
||||
);
|
||||
} else if (context.copyMode === 'RADIAL' && context.pivotPoint && context.angleStep) {
|
||||
const angleRad = context.angleStep * i * TO_RADIANS;
|
||||
resultEntities.push(
|
||||
...getSelectedEntities().map((entity) => {
|
||||
const clone = cloneKeepingLook(entity);
|
||||
clone.rotate(context.pivotPoint as Point, angleRad);
|
||||
return clone;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
addEntities(resultEntities, true);
|
||||
setGhostHelperEntities([]);
|
||||
setSelectedEntityIds([]);
|
||||
},
|
||||
[CopyAction.DESELECT_ENTITIES]: assign(() => {
|
||||
setGhostHelperEntities([]);
|
||||
setSelectedEntityIds([]);
|
||||
return initialArrayContext;
|
||||
}),
|
||||
...Object.fromEntries(
|
||||
['MOUSE_CLICK', 'ABSOLUTE_POINT_INPUT'].map((type) => [
|
||||
type,
|
||||
{
|
||||
actions: assign(({ event }: Ctx) => ({
|
||||
center: getPointFromEvent(null, event as PointInputEvent) as Point,
|
||||
})),
|
||||
target: S.EDIT_POLAR,
|
||||
},
|
||||
])
|
||||
),
|
||||
...esc,
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
[S.EDIT_RECTANGULAR]: {
|
||||
meta: { kind: 'keyword', prompt: ARRAY_PROMPT.editRect },
|
||||
entry: [() => setShouldDrawHelpers(false), assign({ flow: null }), showRect],
|
||||
on: {
|
||||
'OPTION.COUNT': {
|
||||
actions: assign({ flow: 'COUNT' as Flow }),
|
||||
target: S.WAITING_FOR_COLUMNS,
|
||||
},
|
||||
'OPTION.SPACING': {
|
||||
actions: assign({ flow: 'SPACING' as Flow }),
|
||||
target: S.WAITING_FOR_COLUMN_SPACING,
|
||||
},
|
||||
'OPTION.COLUMNS': {
|
||||
actions: assign({ flow: 'COLUMNS' as Flow }),
|
||||
target: S.WAITING_FOR_COLUMNS,
|
||||
},
|
||||
'OPTION.ROWS': { actions: assign({ flow: 'ROWS' as Flow }), target: S.WAITING_FOR_ROWS },
|
||||
'OPTION.EXIT': create(rectangularCopies),
|
||||
...esc,
|
||||
},
|
||||
},
|
||||
[S.WAITING_FOR_COLUMNS]: numberStep(
|
||||
ARRAY_PROMPT.columns,
|
||||
(columns) => {
|
||||
remember('ARRAY.COLS', columns);
|
||||
return { columns };
|
||||
},
|
||||
[{ flow: 'COUNT', target: S.WAITING_FOR_ROWS }, { target: S.WAITING_FOR_COLUMN_SPACING }]
|
||||
),
|
||||
[S.WAITING_FOR_ROWS]: numberStep(
|
||||
ARRAY_PROMPT.rows,
|
||||
(rows) => {
|
||||
remember('ARRAY.ROWS', rows);
|
||||
return { rows };
|
||||
},
|
||||
[{ flow: 'ROWS', target: S.WAITING_FOR_ROW_SPACING }, { target: S.EDIT_RECTANGULAR }]
|
||||
),
|
||||
[S.WAITING_FOR_COLUMN_SPACING]: numberStep(
|
||||
ARRAY_PROMPT.columnSpacing,
|
||||
(columnSpacing) => ({ columnSpacing }),
|
||||
[{ flow: 'SPACING', target: S.WAITING_FOR_ROW_SPACING }, { target: S.EDIT_RECTANGULAR }]
|
||||
),
|
||||
[S.WAITING_FOR_ROW_SPACING]: numberStep(
|
||||
ARRAY_PROMPT.rowSpacing,
|
||||
(rowSpacing) => ({ rowSpacing }),
|
||||
[{ target: S.EDIT_RECTANGULAR }]
|
||||
),
|
||||
[S.EDIT_POLAR]: {
|
||||
meta: { kind: 'keyword', prompt: ARRAY_PROMPT.editPolar },
|
||||
entry: [() => setShouldDrawHelpers(false), showPolar],
|
||||
on: {
|
||||
'OPTION.ITEMS': { target: S.WAITING_FOR_ITEMS },
|
||||
'OPTION.ANGLE': { target: S.WAITING_FOR_ANGLE },
|
||||
'OPTION.FILL': { target: S.WAITING_FOR_FILL },
|
||||
'OPTION.ROTATE': { target: S.WAITING_FOR_ROTATE_ITEMS },
|
||||
'OPTION.EXIT': create(polarCopies),
|
||||
...esc,
|
||||
},
|
||||
},
|
||||
[S.WAITING_FOR_ITEMS]: numberStep(
|
||||
ARRAY_PROMPT.items,
|
||||
(items) => {
|
||||
remember('ARRAY.ITEMS', items);
|
||||
return { items };
|
||||
},
|
||||
[{ target: S.EDIT_POLAR }]
|
||||
),
|
||||
// 사이 각 → 채울 각 = 사이 각 × (항목 − 1)(같은 자리)
|
||||
[S.WAITING_FOR_ANGLE]: numberStep(
|
||||
ARRAY_PROMPT.angle,
|
||||
(angle, context) => {
|
||||
const fill = angle * Math.max(context.items - 1, 1);
|
||||
return fillOk(fill) ? { fill } : null;
|
||||
},
|
||||
[{ target: S.EDIT_POLAR }]
|
||||
),
|
||||
[S.WAITING_FOR_FILL]: numberStep(
|
||||
ARRAY_PROMPT.fill,
|
||||
(fill) => {
|
||||
if (!fillOk(fill)) return null;
|
||||
remember('ARRAY.FILL', fill);
|
||||
return { fill };
|
||||
},
|
||||
[{ target: S.EDIT_POLAR }]
|
||||
),
|
||||
[S.WAITING_FOR_ROTATE_ITEMS]: {
|
||||
meta: { kind: 'keyword', prompt: ARRAY_PROMPT.rotateItems },
|
||||
on: {
|
||||
'OPTION.YES': { actions: assign({ rotateItems: true }), target: S.EDIT_POLAR },
|
||||
'OPTION.NO': { actions: assign({ rotateItems: false }), target: S.EDIT_POLAR },
|
||||
...esc,
|
||||
},
|
||||
},
|
||||
[END_STATE]: endState,
|
||||
};
|
||||
|
||||
export const arrayToolStateMachine = createMachine({
|
||||
types: {} as { context: ArrayContext; events: StateEvent },
|
||||
context: {
|
||||
originals: [],
|
||||
columns: 4,
|
||||
rows: 3,
|
||||
columnSpacing: 1,
|
||||
rowSpacing: 1,
|
||||
center: null,
|
||||
items: 6,
|
||||
fill: 360,
|
||||
rotateItems: true,
|
||||
flow: null,
|
||||
type: Tool.ARRAY,
|
||||
},
|
||||
initial: S.INIT,
|
||||
states,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user