앞선 감사가 불완전했다. 같은 부류로 도각·원지반을 건드리던 곳이 더 있었다. - select-tool.helpers pickEntityAt: 클릭 선택 후보에 잠금 객체가 섞여 도각을 물면 아무 일도 안 일어나는 죽은 클릭이 됐다. - eraser-tool: 자르기의 교차점 계산이 도각 선을 절단 경계로 썼다. - property-tools OVERKILL: 선택이 없으면 전체가 대상이라 도각까지 지웠다. - text-tools 찾기/바꾸기: 도각 표제란 글자를 바꿔 버렸다. - selection-tools 유형/유사 선택: 잠금 객체까지 세어 토스트 개수가 틀렸다. 전부 getPickableEntities()로 바꾼다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
162 lines
4.4 KiB
TypeScript
162 lines
4.4 KiB
TypeScript
import type { Point, Polygon } from '@flatten-js/core';
|
|
import { assign, createMachine } from 'xstate';
|
|
import type { ArcEntity } from '../entities/ArcEntity';
|
|
import type { CircleEntity } from '../entities/CircleEntity';
|
|
import { EntityName } from '../entities/Entity';
|
|
import { LineEntity } from '../entities/LineEntity';
|
|
import type { RectangleEntity } from '../entities/RectangleEntity';
|
|
import { findClosestEntity } from '../helpers/find-closest-entity';
|
|
import { polygonToSegments } from '../helpers/polygon-to-segments';
|
|
import {
|
|
addEntities,
|
|
deleteEntities,
|
|
getActiveLayerId,
|
|
getEntities,
|
|
getPickableEntities,
|
|
setEntities,
|
|
setGhostHelperEntities,
|
|
setShouldDrawHelpers,
|
|
} from '../state';
|
|
import { Tool } from '../tools';
|
|
import {
|
|
eraseArcSegment,
|
|
eraseCircleSegment,
|
|
eraseLineSegment,
|
|
getAllIntersectionPoints,
|
|
} from './eraser-tool.helpers';
|
|
import type { MouseClickEvent, StateEvent, ToolContext } from './tool.types';
|
|
|
|
export interface EraserContext extends ToolContext {
|
|
startPoint: Point | null;
|
|
}
|
|
|
|
export enum EraserState {
|
|
INIT = 'INIT',
|
|
WAITING_FOR_FIRST_CLICK = 'WAITING_FOR_FIRST_CLICK',
|
|
}
|
|
|
|
export enum EraserAction {
|
|
INIT_ERASER_TOOL = 'INIT_ERASER_TOOL',
|
|
HANDLE_MOUSE_CLICK = 'HANDLE_MOUSE_CLICK',
|
|
}
|
|
|
|
export const eraserToolStateMachine = createMachine(
|
|
{
|
|
types: {} as {
|
|
context: EraserContext;
|
|
events: StateEvent;
|
|
},
|
|
context: {
|
|
startPoint: null,
|
|
type: Tool.ERASER,
|
|
},
|
|
initial: EraserState.INIT,
|
|
states: {
|
|
[EraserState.INIT]: {
|
|
description: 'Initializing the eraser tool',
|
|
always: {
|
|
actions: EraserAction.INIT_ERASER_TOOL,
|
|
target: EraserState.WAITING_FOR_FIRST_CLICK,
|
|
},
|
|
},
|
|
[EraserState.WAITING_FOR_FIRST_CLICK]: {
|
|
description: 'Select a line segment to delete',
|
|
meta: {
|
|
instructions: 'Select a line segment to delete',
|
|
},
|
|
on: {
|
|
// TODO implement a DRAW action to draw the segment that will be erased in dotted line
|
|
MOUSE_CLICK: {
|
|
actions: EraserAction.HANDLE_MOUSE_CLICK,
|
|
target: EraserState.WAITING_FOR_FIRST_CLICK,
|
|
},
|
|
},
|
|
},
|
|
// TODO implement rectangle selection to delete
|
|
},
|
|
},
|
|
{
|
|
actions: {
|
|
[EraserAction.INIT_ERASER_TOOL]: assign(() => {
|
|
setShouldDrawHelpers(false);
|
|
setGhostHelperEntities([]);
|
|
return {};
|
|
}),
|
|
[EraserAction.HANDLE_MOUSE_CLICK]: assign(({ context, event }) => {
|
|
handleMouseClick((event as MouseClickEvent).worldMouseLocation);
|
|
|
|
return context;
|
|
}),
|
|
},
|
|
}
|
|
);
|
|
|
|
export function handleMouseClick(worldMouseLocation: Point) {
|
|
const closestEntity = findClosestEntity(worldMouseLocation, getPickableEntities());
|
|
if (!closestEntity) {
|
|
return;
|
|
}
|
|
|
|
const clickedPointOnShape = closestEntity.segment.start;
|
|
|
|
// Find entities that intersect with the closest entity
|
|
const intersections = getAllIntersectionPoints(closestEntity.entity, getPickableEntities());
|
|
|
|
const entityType = closestEntity.entity.getType();
|
|
switch (entityType) {
|
|
case EntityName.Line: {
|
|
const line = closestEntity.entity as LineEntity;
|
|
eraseLineSegment(line, clickedPointOnShape, intersections);
|
|
break;
|
|
}
|
|
|
|
case EntityName.Circle: {
|
|
const circle = closestEntity.entity as CircleEntity;
|
|
eraseCircleSegment(circle, clickedPointOnShape, intersections);
|
|
break;
|
|
}
|
|
|
|
case EntityName.Arc: {
|
|
const arc = closestEntity.entity as ArcEntity;
|
|
eraseArcSegment(arc, clickedPointOnShape, intersections);
|
|
break;
|
|
}
|
|
|
|
case EntityName.Rectangle: {
|
|
const rectangle = closestEntity.entity as RectangleEntity;
|
|
const segments = polygonToSegments(rectangle.getShape() as Polygon);
|
|
const segmentEntities = segments.map(
|
|
(segment) => new LineEntity(getActiveLayerId(), segment)
|
|
);
|
|
|
|
// Find the closest segment to the clicked point
|
|
const closestSegmentInfo = findClosestEntity(worldMouseLocation, segmentEntities);
|
|
|
|
// Remove the rectangle
|
|
deleteEntities([rectangle], false);
|
|
|
|
// Add 4 lines instead of the rectangle
|
|
addEntities(segmentEntities, false);
|
|
|
|
// Remove (a segment) of the 4th line closest to the cursor
|
|
const lineIntersections = getAllIntersectionPoints(
|
|
closestSegmentInfo.entity,
|
|
getPickableEntities()
|
|
);
|
|
eraseLineSegment(
|
|
closestSegmentInfo.entity as LineEntity,
|
|
clickedPointOnShape,
|
|
lineIntersections
|
|
);
|
|
break;
|
|
}
|
|
|
|
case EntityName.Image: {
|
|
// TODO implement image eraser
|
|
// Switch to rectangle entity and delete the image and the line that was closest to the cursor
|
|
}
|
|
}
|
|
|
|
setEntities(getEntities(), true); // Force a new undo state entry
|
|
}
|