feat(CAD): 331-2 이동 · 복사 · 회전 키보드 값

- 이동 · 복사 — 기준점 `x,y` · 둘째 점 거리 · `x,y` · `@x,y` · 각도 고정 · 미리보기도 같은 점 계산(getPointFromEvent)
- 회전 — 기준점 `x,y` · 각도 숫자 바로(반시계 +)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013QyNXY2TUKXCFMwAca2APG
This commit is contained in:
2026-10-15 12:36:19 +09:00
co-authored by Claude Opus 5.5
parent 8e96880880
commit 32ddaa8064
4 changed files with 154 additions and 26 deletions
@@ -10,7 +10,8 @@ import {
setShouldDrawHelpers,
} from '../state';
import { Tool } from '../tools';
import type { DrawEvent, MouseClickEvent, StateEvent, ToolContext } from './tool.types';
import type { DrawEvent, PointInputEvent, StateEvent, ToolContext } from './tool.types';
import { getPointFromEvent } from '../helpers/get-point-from-event';
import { assign, createMachine, sendTo } from 'xstate';
import { selectToolStateMachine } from './select-tool';
import type { Entity } from '../entities/Entity';
@@ -147,6 +148,10 @@ export const copyToolStateMachine = createMachine(
actions: [CopyAction.RECORD_START_POINT, CopyAction.COPY_SELECTION_BEFORE_COPY],
target: CopyState.WAITING_FOR_END_COPY_POINT,
},
ABSOLUTE_POINT_INPUT: {
actions: [CopyAction.RECORD_START_POINT, CopyAction.COPY_SELECTION_BEFORE_COPY],
target: CopyState.WAITING_FOR_END_COPY_POINT,
},
ESC: {
actions: CopyAction.DESELECT_ENTITIES,
target: CopyState.INIT,
@@ -166,6 +171,18 @@ export const copyToolStateMachine = createMachine(
actions: [CopyAction.COPY_SELECTION],
target: CopyState.WAITING_FOR_END_COPY_POINT,
},
NUMBER_INPUT: {
actions: [CopyAction.COPY_SELECTION],
target: CopyState.WAITING_FOR_END_COPY_POINT,
},
ABSOLUTE_POINT_INPUT: {
actions: [CopyAction.COPY_SELECTION],
target: CopyState.WAITING_FOR_END_COPY_POINT,
},
RELATIVE_POINT_INPUT: {
actions: [CopyAction.COPY_SELECTION],
target: CopyState.WAITING_FOR_END_COPY_POINT,
},
ESC: {
actions: CopyAction.DESELECT_ENTITIES,
target: CopyState.INIT,
@@ -191,10 +208,9 @@ export const copyToolStateMachine = createMachine(
setShouldDrawHelpers(true);
},
[CopyAction.RECORD_START_POINT]: assign(({ event }) => {
setAngleGuideOriginPoint((event as MouseClickEvent).worldMouseLocation);
return {
startPoint: (event as MouseClickEvent).worldMouseLocation,
};
const startPoint = getPointFromEvent(null, event as PointInputEvent);
setAngleGuideOriginPoint(startPoint);
return { startPoint };
}),
[CopyAction.COPY_SELECTION_BEFORE_COPY]: assign(({ context }) => {
const selectedEntities = getSelectedEntities();
@@ -208,7 +224,9 @@ export const copyToolStateMachine = createMachine(
return {
startPoint: context.startPoint,
// Make a copy of the selected entities before copying them, so we can restore them when the user cancels the copy action
originalSelectedEntities: compact(selectedEntities.map((entity) => cloneKeepingLook(entity))),
originalSelectedEntities: compact(
selectedEntities.map((entity) => cloneKeepingLook(entity))
),
copiedEntities: selectedEntities,
};
}),
@@ -217,11 +235,13 @@ export const copyToolStateMachine = createMachine(
throw new Error('[COPY] Calling draw temp copy line without a start point');
}
const endPointTemp = (event as DrawEvent).drawController.getWorldMouseLocation();
const endPointTemp = getPointFromEvent(context.startPoint, event as DrawEvent);
// Copy the entities to the new location
// Draw all selected entities according to translation vector, so the user gets visual feedback of where the entities will be copied;
const movedEntities = context.originalSelectedEntities.map((entity) => cloneKeepingLook(entity));
const movedEntities = context.originalSelectedEntities.map((entity) =>
cloneKeepingLook(entity)
);
moveEntities(
movedEntities,
endPointTemp.x - context.startPoint.x,
@@ -245,8 +265,11 @@ export const copyToolStateMachine = createMachine(
}
// Copy the entities one final time
const currentEndPoint = (event as MouseClickEvent).worldMouseLocation;
const copiedEntities = context.originalSelectedEntities.map((entity) => cloneKeepingLook(entity));
// 클릭 · 거리 · 절대 · 상대 좌표(키보드)
const currentEndPoint = getPointFromEvent(context.startPoint, event as PointInputEvent);
const copiedEntities = context.originalSelectedEntities.map((entity) =>
cloneKeepingLook(entity)
);
moveEntities(
copiedEntities,
currentEndPoint.x - context.startPoint.x,
@@ -11,7 +11,8 @@ import {
setShouldDrawHelpers,
} from '../state';
import { Tool } from '../tools';
import type { DrawEvent, MouseClickEvent, StateEvent, ToolContext } from './tool.types';
import type { DrawEvent, PointInputEvent, StateEvent, ToolContext } from './tool.types';
import { getPointFromEvent } from '../helpers/get-point-from-event';
import { assign, createMachine, sendTo } from 'xstate';
import { selectToolStateMachine } from './select-tool';
import type { Entity } from '../entities/Entity';
@@ -148,6 +149,10 @@ export const moveToolStateMachine = createMachine(
actions: [MoveAction.RECORD_START_POINT, MoveAction.COPY_SELECTION_BEFORE_MOVE],
target: MoveState.WAITING_FOR_END_MOVE_POINT,
},
ABSOLUTE_POINT_INPUT: {
actions: [MoveAction.RECORD_START_POINT, MoveAction.COPY_SELECTION_BEFORE_MOVE],
target: MoveState.WAITING_FOR_END_MOVE_POINT,
},
ESC: {
actions: MoveAction.DESELECT_ENTITIES,
target: MoveState.INIT,
@@ -167,6 +172,18 @@ export const moveToolStateMachine = createMachine(
actions: [MoveAction.MOVE_SELECTION, MoveAction.DESELECT_ENTITIES],
target: MoveState.WAITING_FOR_SELECTION,
},
NUMBER_INPUT: {
actions: [MoveAction.MOVE_SELECTION, MoveAction.DESELECT_ENTITIES],
target: MoveState.WAITING_FOR_SELECTION,
},
ABSOLUTE_POINT_INPUT: {
actions: [MoveAction.MOVE_SELECTION, MoveAction.DESELECT_ENTITIES],
target: MoveState.WAITING_FOR_SELECTION,
},
RELATIVE_POINT_INPUT: {
actions: [MoveAction.MOVE_SELECTION, MoveAction.DESELECT_ENTITIES],
target: MoveState.WAITING_FOR_SELECTION,
},
ESC: {
actions: MoveAction.RESTORE_ORIGINAL_ENTITIES,
target: MoveState.INIT,
@@ -192,10 +209,9 @@ export const moveToolStateMachine = createMachine(
setShouldDrawHelpers(true);
},
[MoveAction.RECORD_START_POINT]: assign(({ event }) => {
setAngleGuideOriginPoint((event as MouseClickEvent).worldMouseLocation);
return {
startPoint: (event as MouseClickEvent).worldMouseLocation,
};
const startPoint = getPointFromEvent(null, event as PointInputEvent);
setAngleGuideOriginPoint(startPoint);
return { startPoint };
}),
[MoveAction.COPY_SELECTION_BEFORE_MOVE]: assign(({ context }) => {
const selectedEntities = getSelectedEntities();
@@ -211,7 +227,9 @@ export const moveToolStateMachine = createMachine(
return {
startPoint: context.startPoint,
// Make a copy of the selected entities before moving them, so we can restore them when the user cancels the move action
originalSelectedEntities: compact(selectedEntities.map((entity) => cloneKeepingLook(entity, true))),
originalSelectedEntities: compact(
selectedEntities.map((entity) => cloneKeepingLook(entity, true))
),
movedEntities: selectedEntities,
};
}),
@@ -220,11 +238,13 @@ export const moveToolStateMachine = createMachine(
throw new Error('[MOVE] Calling draw temp move line without a start point');
}
const endPointTemp = (event as DrawEvent).drawController.getWorldMouseLocation();
const endPointTemp = getPointFromEvent(context.startPoint, event as DrawEvent);
// Move the entities to the new location
// Draw all selected entities according to translation vector, so the user gets visual feedback of where the entities will be moved;
const movedEntities = context.originalSelectedEntities.map((entity) => cloneKeepingLook(entity, true));
const movedEntities = context.originalSelectedEntities.map((entity) =>
cloneKeepingLook(entity, true)
);
moveEntities(
movedEntities,
endPointTemp.x - context.startPoint.x,
@@ -248,7 +268,8 @@ export const moveToolStateMachine = createMachine(
}
// Move the entities one final time
const currentEndPoint = (event as MouseClickEvent).worldMouseLocation;
// 클릭 · 거리 · 절대 · 상대 좌표(키보드)
const currentEndPoint = getPointFromEvent(context.startPoint, event as PointInputEvent);
moveEntities(
context.originalSelectedEntities,
currentEndPoint.x - context.startPoint.x,
@@ -10,7 +10,15 @@ import {
setShouldDrawHelpers,
} from '../state';
import { Tool } from '../tools';
import type { DrawEvent, MouseClickEvent, StateEvent, ToolContext } from './tool.types';
import type {
DrawEvent,
MouseClickEvent,
NumberInputEvent,
PointInputEvent,
StateEvent,
ToolContext,
} from './tool.types';
import { getPointFromEvent } from '../helpers/get-point-from-event';
import { assign, createMachine, sendTo } from 'xstate';
import { selectToolStateMachine } from './select-tool';
import type { Entity } from '../entities/Entity';
@@ -41,6 +49,7 @@ export enum RotateAction {
COPY_SELECTION_BEFORE_ROTATE = 'COPY_SELECTION_BEFORE_ROTATE',
DRAW_TEMP_ROTATE_ENTITIES = 'DRAW_TEMP_ROTATE_ENTITIES',
ROTATE_SELECTION = 'ROTATE_SELECTION',
ROTATE_BY_ANGLE = 'ROTATE_BY_ANGLE',
DESELECT_ENTITIES = 'DESELECT_ENTITIES',
RESTORE_ORIGINAL_ENTITIES = 'RESTORE_ORIGINAL_ENTITIES',
}
@@ -147,6 +156,10 @@ export const rotateToolStateMachine = createMachine(
actions: [RotateAction.RECORD_ROTATION_ORIGIN],
target: RotateState.WAITING_FOR_ANGLE_START_POINT,
},
ABSOLUTE_POINT_INPUT: {
actions: [RotateAction.RECORD_ROTATION_ORIGIN],
target: RotateState.WAITING_FOR_ANGLE_START_POINT,
},
ESC: {
actions: RotateAction.DESELECT_ENTITIES,
target: RotateState.INIT,
@@ -166,6 +179,15 @@ export const rotateToolStateMachine = createMachine(
],
target: RotateState.WAITING_FOR_ANGLE_END_POINT,
},
NUMBER_INPUT: {
// 각도(도) 바로 — 반시계 +
actions: [
RotateAction.COPY_SELECTION_BEFORE_ROTATE,
RotateAction.ROTATE_BY_ANGLE,
RotateAction.DESELECT_ENTITIES,
],
target: RotateState.WAITING_FOR_SELECTION,
},
ESC: {
actions: RotateAction.RESTORE_ORIGINAL_ENTITIES,
target: RotateState.INIT,
@@ -204,11 +226,9 @@ export const rotateToolStateMachine = createMachine(
setShouldDrawHelpers(true);
},
[RotateAction.RECORD_ROTATION_ORIGIN]: assign(({ context, event }): RotateContext => {
setAngleGuideOriginPoint((event as MouseClickEvent).worldMouseLocation);
return {
...context,
rotationOrigin: (event as MouseClickEvent).worldMouseLocation,
};
const rotationOrigin = getPointFromEvent(null, event as PointInputEvent);
setAngleGuideOriginPoint(rotationOrigin);
return { ...context, rotationOrigin };
}),
[RotateAction.RECORD_ROTATION_ANGLE_START_POINT]: assign(
({ context, event }): RotateContext => {
@@ -232,7 +252,9 @@ export const rotateToolStateMachine = createMachine(
return {
...context,
// Make a copy of the selected entities before rotating them, so we can restore them when the user cancels the rotate action
originalSelectedEntities: compact(selectedEntities.map((entity) => cloneKeepingLook(entity, true))),
originalSelectedEntities: compact(
selectedEntities.map((entity) => cloneKeepingLook(entity, true))
),
};
}),
[RotateAction.DRAW_TEMP_ROTATE_ENTITIES]: ({ context, event }) => {
@@ -279,6 +301,19 @@ export const rotateToolStateMachine = createMachine(
setGhostHelperEntities([]);
setSelectedEntityIds([]);
},
[RotateAction.ROTATE_BY_ANGLE]: ({ context, event }) => {
if (!context.rotationOrigin) {
throw new Error('[ROTATE] Calling rotate by angle without a rotation origin');
}
const radians = ((event as NumberInputEvent).value * Math.PI) / 180;
const rotatedEntities = compact(
context.originalSelectedEntities.map((entity) => cloneKeepingLook(entity, true))
);
for (const entity of rotatedEntities) entity.rotate(context.rotationOrigin, radians);
addEntities(rotatedEntities, true);
setGhostHelperEntities([]);
setSelectedEntityIds([]);
},
[RotateAction.DESELECT_ENTITIES]: assign(({ context }): RotateContext => {
setGhostHelperEntities([]);
setSelectedEntityIds([]);