Files
Aislo/B07_DesignDetail/openwebcad/src/tools/eraser-tool.ts
T
eomsangdonandClaude Opus 5 9bf6de9b10 fix(B07): 잠금 도면층을 집던 경로 다섯 곳을 마저 닫는다
앞선 감사가 불완전했다. 같은 부류로 도각·원지반을 건드리던 곳이 더 있었다.

- 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>
2026-08-31 15:48:08 +09:00

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
}