- B07_wf4_DesignDetail(8파일+openwebcad) -> B08_DesignDetail로 git mv (이력 보존) - B08_wf5_Quantity -> B07_Quantity (구 수량 백업 zip 보관 폴더) - 파생 문자열 일괄 전환: /b07-cad -> /b08-cad 정적 서빙, CAD 레이어 b07-* -> b08-*, postMessage aislo:b07:* -> aislo:b08:*, 패키지명 aislo-b08-cad, CSS .b08-*, 라우트 키/슬러그(B08_DESIGN_DETAIL/b08-design-detail, B07_QUANTITY/b07-quantity) - 상세설계 워크플로우 stage 4 -> 5 (확정/무효화 전이 3곳), 확정 완료 시 B09로 이동 - WORKFLOW_STEP_ROUTES 순서 재배열: index4=수량(B07), index5=상세설계(B08) - [임시] B08 이동 테스트 버튼 제거, openwebcad dist 재빌드 - 주석 의미 정렬: 수량 인계 주석 B08->B07, 도면 참조 B07->B08 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
299 lines
10 KiB
TypeScript
299 lines
10 KiB
TypeScript
import type {Point} from '@flatten-js/core';
|
|
import {
|
|
addEntities,
|
|
deleteEntities,
|
|
getActiveLayerId,
|
|
getSelectedEntities,
|
|
getSelectedEntityIds,
|
|
setAngleGuideOriginPoint,
|
|
setGhostHelperEntities,
|
|
setSelectedEntityIds,
|
|
setShouldDrawHelpers,
|
|
} from '../state';
|
|
import {Tool} from '../tools';
|
|
import type {DrawEvent, MouseClickEvent, StateEvent, ToolContext,} from './tool.types';
|
|
import {assign, createMachine, sendTo} from 'xstate';
|
|
import {selectToolStateMachine} from './select-tool';
|
|
import type {Entity} from '../entities/Entity';
|
|
import {compact} from 'es-toolkit';
|
|
import {moveEntities} from './move-tool.helpers';
|
|
import {LineEntity} from '../entities/LineEntity';
|
|
import {GUIDE_LINE_COLOR, GUIDE_LINE_STYLE, GUIDE_LINE_WIDTH,} from '../App.consts';
|
|
|
|
export interface MoveContext extends ToolContext {
|
|
startPoint: Point | null;
|
|
originalSelectedEntities: Entity[];
|
|
movedEntities: Entity[];
|
|
lastDrawLocation: Point | null;
|
|
}
|
|
|
|
export enum MoveState {
|
|
INIT = 'INIT',
|
|
CHECK_SELECTION = 'CHECK_SELECTION',
|
|
WAITING_FOR_SELECTION = 'WAITING_FOR_SELECTION',
|
|
WAITING_FOR_START_MOVE_POINT = 'WAITING_FOR_START_MOVE_POINT',
|
|
WAITING_FOR_END_MOVE_POINT = 'WAITING_FOR_END_MOVE_POINT',
|
|
}
|
|
|
|
export enum MoveAction {
|
|
INIT_MOVE_TOOL = 'INIT_MOVE_TOOL',
|
|
ENABLE_HELPERS = 'ENABLE_HELPERS',
|
|
RECORD_START_POINT = 'RECORD_START_POINT',
|
|
COPY_SELECTION_BEFORE_MOVE = 'COPY_SELECTION_BEFORE_MOVE',
|
|
DRAW_TEMP_MOVE_ENTITIES = 'DRAW_TEMP_MOVE_ENTITIES',
|
|
MOVE_SELECTION = 'MOVE_SELECTION',
|
|
DESELECT_ENTITIES = 'DESELECT_ENTITIES',
|
|
RESTORE_ORIGINAL_ENTITIES = 'RESTORE_ORIGINAL_ENTITIES',
|
|
}
|
|
|
|
/**
|
|
* Move tool state machine
|
|
* This state machine is responsible for moving entities around the canvas
|
|
* It uses the select tool state machine to select entities to move
|
|
* When the user presses enter, the selected entities are marked for moving
|
|
* The user can then select a start point
|
|
* When the user moves their mouse, the selected entities will be updated to move according to the vector: startpoint => mouse location
|
|
* When the user clicks again, the end point is selected and the entities are moved to the new location
|
|
*/
|
|
export const moveToolStateMachine = createMachine(
|
|
{
|
|
types: {} as {
|
|
context: MoveContext;
|
|
events: StateEvent;
|
|
},
|
|
context: {
|
|
startPoint: null,
|
|
originalSelectedEntities: [],
|
|
movedEntities: [],
|
|
lastDrawLocation: null,
|
|
type: Tool.MOVE,
|
|
},
|
|
initial: MoveState.INIT,
|
|
states: {
|
|
[MoveState.INIT]: {
|
|
description: 'Initializing the move tool',
|
|
always: {
|
|
actions: MoveAction.INIT_MOVE_TOOL,
|
|
target: MoveState.CHECK_SELECTION,
|
|
},
|
|
},
|
|
[MoveState.CHECK_SELECTION]: {
|
|
description: 'Check if there is something selected',
|
|
always: [
|
|
{
|
|
guard: () => {
|
|
return getSelectedEntityIds().length > 0;
|
|
},
|
|
target: MoveState.WAITING_FOR_START_MOVE_POINT,
|
|
},
|
|
{
|
|
guard: () => {
|
|
return getSelectedEntityIds().length === 0;
|
|
},
|
|
target: MoveState.WAITING_FOR_SELECTION,
|
|
},
|
|
],
|
|
},
|
|
[MoveState.WAITING_FOR_SELECTION]: {
|
|
description: 'Select what you want to move',
|
|
meta: {
|
|
instructions: 'Select what you want to move, then ENTER',
|
|
},
|
|
invoke: {
|
|
id: 'selectToolInsideTheMoveTool',
|
|
src: selectToolStateMachine,
|
|
onDone: {
|
|
actions: assign(() => {
|
|
return {
|
|
startPoint: null,
|
|
};
|
|
}),
|
|
target: MoveState.CHECK_SELECTION,
|
|
},
|
|
},
|
|
on: {
|
|
MOUSE_CLICK: {
|
|
// Forward the event to the select tool
|
|
actions: sendTo('selectToolInsideTheMoveTool', ({ event }) => {
|
|
return event;
|
|
}),
|
|
},
|
|
ESC: {
|
|
actions: [MoveAction.DESELECT_ENTITIES, MoveAction.INIT_MOVE_TOOL],
|
|
},
|
|
ENTER: {
|
|
// Forward the event to the select tool
|
|
actions: sendTo('selectToolInsideTheMoveTool', ({ event }) => {
|
|
return event;
|
|
}),
|
|
},
|
|
DRAW: {
|
|
// Forward the event to the select tool
|
|
actions: sendTo('selectToolInsideTheMoveTool', ({ event }) => {
|
|
return event;
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
[MoveState.WAITING_FOR_START_MOVE_POINT]: {
|
|
description: 'Select the start of the move line',
|
|
meta: {
|
|
instructions: 'Select the start of the move line',
|
|
},
|
|
always: {
|
|
actions: MoveAction.ENABLE_HELPERS,
|
|
},
|
|
on: {
|
|
MOUSE_CLICK: {
|
|
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,
|
|
},
|
|
},
|
|
},
|
|
[MoveState.WAITING_FOR_END_MOVE_POINT]: {
|
|
description: 'Select the end of the move line',
|
|
meta: {
|
|
instructions: 'Select the end of the move line',
|
|
},
|
|
on: {
|
|
DRAW: {
|
|
actions: [MoveAction.DRAW_TEMP_MOVE_ENTITIES],
|
|
},
|
|
MOUSE_CLICK: {
|
|
actions: [MoveAction.MOVE_SELECTION, MoveAction.DESELECT_ENTITIES],
|
|
target: MoveState.WAITING_FOR_SELECTION,
|
|
},
|
|
ESC: {
|
|
actions: MoveAction.RESTORE_ORIGINAL_ENTITIES,
|
|
target: MoveState.INIT,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
actions: {
|
|
[MoveAction.INIT_MOVE_TOOL]: assign(() => {
|
|
setShouldDrawHelpers(false);
|
|
setGhostHelperEntities([]);
|
|
setAngleGuideOriginPoint(null);
|
|
return {
|
|
startPoint: null,
|
|
originalSelectedEntities: [],
|
|
movedEntities: [],
|
|
lastDrawLocation: null,
|
|
};
|
|
}),
|
|
[MoveAction.ENABLE_HELPERS]: () => {
|
|
setShouldDrawHelpers(true);
|
|
},
|
|
[MoveAction.RECORD_START_POINT]: assign(({ event }) => {
|
|
setAngleGuideOriginPoint((event as MouseClickEvent).worldMouseLocation);
|
|
return {
|
|
startPoint: (event as MouseClickEvent).worldMouseLocation,
|
|
};
|
|
}),
|
|
[MoveAction.COPY_SELECTION_BEFORE_MOVE]: assign(({ context }) => {
|
|
const selectedEntities = getSelectedEntities();
|
|
|
|
// Move the selected entities to the ghost helper entities, so they are drawn on the canvas, but do not interact with the snap points / angle guides
|
|
setGhostHelperEntities(selectedEntities);
|
|
// Remove the selected entities from the regular entity list, so they do not get used for determining snap points / angle guides
|
|
deleteEntities(selectedEntities, false);
|
|
|
|
// TODO keep a copy of the original entities in the entities list, but set their line color to grey, so the user can see where the entities were before being moved and the original entities also are used for snap points / angle guides
|
|
|
|
setSelectedEntityIds([]);
|
|
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 => entity.clone()),
|
|
),
|
|
movedEntities: selectedEntities,
|
|
};
|
|
}),
|
|
[MoveAction.DRAW_TEMP_MOVE_ENTITIES]: ({ context, event }) => {
|
|
if (!context.startPoint) {
|
|
throw new Error(
|
|
'[MOVE] Calling draw temp move line without a start point',
|
|
);
|
|
}
|
|
|
|
const endPointTemp = (
|
|
event as DrawEvent
|
|
).drawController.getWorldMouseLocation();
|
|
|
|
// 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 =>
|
|
entity.clone(),
|
|
);
|
|
moveEntities(
|
|
movedEntities,
|
|
endPointTemp.x - context.startPoint.x,
|
|
endPointTemp.y - context.startPoint.y,
|
|
);
|
|
|
|
// // Draw a dashed line between the start move point and the current mouse location
|
|
const activeMoveLine = new LineEntity(
|
|
getActiveLayerId(),
|
|
context.startPoint as Point,
|
|
endPointTemp,
|
|
);
|
|
activeMoveLine.lineColor = GUIDE_LINE_COLOR;
|
|
activeMoveLine.lineWidth = GUIDE_LINE_WIDTH;
|
|
activeMoveLine.lineDash = GUIDE_LINE_STYLE;
|
|
setGhostHelperEntities([activeMoveLine, ...movedEntities]);
|
|
},
|
|
[MoveAction.MOVE_SELECTION]: ({ context, event }) => {
|
|
if (!context.startPoint) {
|
|
throw new Error(
|
|
'[MOVE] Calling move selection without a start point',
|
|
);
|
|
}
|
|
|
|
// Move the entities one final time
|
|
const currentEndPoint = (event as MouseClickEvent).worldMouseLocation;
|
|
moveEntities(
|
|
context.originalSelectedEntities,
|
|
currentEndPoint.x - context.startPoint.x,
|
|
currentEndPoint.y - context.startPoint.y,
|
|
);
|
|
|
|
// Switch the moved entities back from the ghost helper entities to the real entities
|
|
addEntities(context.originalSelectedEntities, true);
|
|
setGhostHelperEntities([]);
|
|
setSelectedEntityIds([]);
|
|
},
|
|
[MoveAction.DESELECT_ENTITIES]: assign(() => {
|
|
setGhostHelperEntities([]);
|
|
setSelectedEntityIds([]);
|
|
return {
|
|
startPoint: null,
|
|
originalSelectedEntities: [],
|
|
lastDrawLocation: null,
|
|
};
|
|
}),
|
|
[MoveAction.RESTORE_ORIGINAL_ENTITIES]: assign(({ context }) => {
|
|
addEntities(context.originalSelectedEntities, false); // This should already be the last state of the undo stack
|
|
setGhostHelperEntities([]);
|
|
setSelectedEntityIds([]);
|
|
return {
|
|
startPoint: null,
|
|
originalSelectedEntities: [],
|
|
lastDrawLocation: null,
|
|
};
|
|
}),
|
|
...selectToolStateMachine.implementations.actions,
|
|
},
|
|
},
|
|
);
|