From 8cfb1e75a894bc67fba94de8dfbfc2540eb12719 Mon Sep 17 00:00:00 2001 From: "Andy Lu (Lu, Yu-An)" Date: Tue, 26 Aug 2025 23:45:43 +0800 Subject: [PATCH] feat(eraser): implement eraser feature (#221) * feat(eraser): implement eraser feature Introduces an eraser tool to the freehand drawing plugin, including a new Erase Icon, toolbar button, and hotkey (E). Implements erasing logic in with-freehand-delete.ts and integrates it into the freehand plugin. Updates FreehandShape enum and pointer utilities to support the eraser. * fix(eraser): lint for better readability * fix(eraser): switch the order of eraser icon and freehand icon * fix(eraser): fix multiple suggestions during code review * fix(eraser): make all the freehand elements being erased at the same time in a eraser perform * fix(eraser): fix type declaration and remove unused imports --- packages/drawnix/src/components/icons.tsx | 6 ++ .../components/toolbar/creation-toolbar.tsx | 6 ++ packages/drawnix/src/plugins/freehand/type.ts | 1 + .../drawnix/src/plugins/freehand/utils.ts | 2 +- .../plugins/freehand/with-freehand-erase.ts | 98 +++++++++++++++++++ .../src/plugins/freehand/with-freehand.ts | 3 +- packages/drawnix/src/plugins/with-hotkey.ts | 5 + 7 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 packages/drawnix/src/plugins/freehand/with-freehand-erase.ts diff --git a/packages/drawnix/src/components/icons.tsx b/packages/drawnix/src/components/icons.tsx index 995f2eb6..6ec07b1c 100644 --- a/packages/drawnix/src/components/icons.tsx +++ b/packages/drawnix/src/components/icons.tsx @@ -44,6 +44,12 @@ export const TextIcon = createIcon( ); +export const EraseIcon = createIcon( + + + +); + export const StraightArrowLineIcon = createIcon( diff --git a/packages/drawnix/src/components/toolbar/creation-toolbar.tsx b/packages/drawnix/src/components/toolbar/creation-toolbar.tsx index c78d7a7c..012ef4b7 100644 --- a/packages/drawnix/src/components/toolbar/creation-toolbar.tsx +++ b/packages/drawnix/src/components/toolbar/creation-toolbar.tsx @@ -8,6 +8,7 @@ import { SelectionIcon, ShapeIcon, TextIcon, + EraseIcon, StraightArrowLineIcon, FeltTipPenIcon, ImageIcon, @@ -87,6 +88,11 @@ export const BUTTONS: AppToolButtonProps[] = [ pointer: FreehandShape.feltTipPen, titleKey: 'toolbar.pen', }, + { + icon: EraseIcon, + pointer: FreehandShape.eraser, + title: 'Erase — E', + }, { icon: StraightArrowLineIcon, titleKey: 'toolbar.arrow', diff --git a/packages/drawnix/src/plugins/freehand/type.ts b/packages/drawnix/src/plugins/freehand/type.ts index 67d25ded..d674bc9c 100644 --- a/packages/drawnix/src/plugins/freehand/type.ts +++ b/packages/drawnix/src/plugins/freehand/type.ts @@ -29,6 +29,7 @@ export const FreehandThemeColors = { }; export enum FreehandShape { + eraser = 'eraser', nibPen = 'nibPen', feltTipPen = 'feltTipPen', artisticBrush = 'artisticBrush', diff --git a/packages/drawnix/src/plugins/freehand/utils.ts b/packages/drawnix/src/plugins/freehand/utils.ts index ff44483f..f29f4aed 100644 --- a/packages/drawnix/src/plugins/freehand/utils.ts +++ b/packages/drawnix/src/plugins/freehand/utils.ts @@ -20,7 +20,7 @@ import { } from '@plait/draw'; export function getFreehandPointers() { - return [FreehandShape.feltTipPen]; + return [FreehandShape.feltTipPen, FreehandShape.eraser]; } export const createFreehandElement = ( diff --git a/packages/drawnix/src/plugins/freehand/with-freehand-erase.ts b/packages/drawnix/src/plugins/freehand/with-freehand-erase.ts new file mode 100644 index 00000000..4fadd631 --- /dev/null +++ b/packages/drawnix/src/plugins/freehand/with-freehand-erase.ts @@ -0,0 +1,98 @@ +import { + PlaitBoard, + Point, + throttleRAF, + toHostPoint, + toViewBoxPoint, +} from '@plait/core'; +import { isDrawingMode } from '@plait/common'; +import { isHitFreehand } from './utils'; +import { Freehand, FreehandShape } from './type'; +import { CoreTransforms } from '@plait/core'; + +export const withFreehandErase = (board: PlaitBoard) => { + const { pointerDown, pointerMove, pointerUp, globalPointerUp } = board; + + let isErasing = false; + const elementsToDelete = new Set(); + + const checkAndMarkFreehandElementsForDeletion = (point: Point) => { + const viewBoxPoint = toViewBoxPoint(board, toHostPoint(board, point[0], point[1])); + + const freehandElements = board.children.filter((element) => + Freehand.isFreehand(element) + ) as Freehand[]; + + freehandElements.forEach((element) => { + if (!elementsToDelete.has(element.id) && isHitFreehand(board, element, viewBoxPoint)) { + elementsToDelete.add(element.id); + } + }); + }; + + const deleteMarkedElements = () => { + if (elementsToDelete.size > 0) { + const elementsToRemove = board.children.filter((element) => + elementsToDelete.has(element.id) + ); + + if (elementsToRemove.length > 0) { + CoreTransforms.removeElements(board, elementsToRemove); + } + } + }; + + const complete = () => { + if (isErasing) { + deleteMarkedElements(); + isErasing = false; + elementsToDelete.clear(); + } + }; + + board.pointerDown = (event: PointerEvent) => { + const isEraserPointer = PlaitBoard.isInPointer(board, [FreehandShape.eraser]); + + if (isEraserPointer && isDrawingMode(board)) { + isErasing = true; + elementsToDelete.clear(); + const currentPoint: Point = [event.x, event.y]; + checkAndMarkFreehandElementsForDeletion(currentPoint); + return; + } + + pointerDown(event); + }; + + board.pointerMove = (event: PointerEvent) => { + if (isErasing) { + throttleRAF(board, 'with-freehand-erase', () => { + const currentPoint: Point = [event.x, event.y]; + checkAndMarkFreehandElementsForDeletion(currentPoint); + }); + return; + } + + pointerMove(event); + }; + + board.pointerUp = (event: PointerEvent) => { + if (isErasing) { + complete(); + return; + } + + pointerUp(event); + }; + + board.globalPointerUp = (event: PointerEvent) => { + if (isErasing) { + complete(); + return; + } + + globalPointerUp(event); + }; + + return board; +}; \ No newline at end of file diff --git a/packages/drawnix/src/plugins/freehand/with-freehand.ts b/packages/drawnix/src/plugins/freehand/with-freehand.ts index 0fd38d38..1b26a43e 100644 --- a/packages/drawnix/src/plugins/freehand/with-freehand.ts +++ b/packages/drawnix/src/plugins/freehand/with-freehand.ts @@ -16,6 +16,7 @@ import { WithDrawOptions, WithDrawPluginKey, } from '@plait/draw'; +import { withFreehandErase } from './with-freehand-erase'; export const withFreehand = (board: PlaitBoard) => { const { @@ -83,5 +84,5 @@ export const withFreehand = (board: PlaitBoard) => { { customGeometryTypes: [FREEHAND_TYPE] } ); - return withFreehandFragment(withFreehandCreate(board)); + return withFreehandErase(withFreehandFragment(withFreehandCreate(board))); }; diff --git a/packages/drawnix/src/plugins/with-hotkey.ts b/packages/drawnix/src/plugins/with-hotkey.ts index 1780811a..412a0c72 100644 --- a/packages/drawnix/src/plugins/with-hotkey.ts +++ b/packages/drawnix/src/plugins/with-hotkey.ts @@ -61,6 +61,11 @@ export const buildDrawnixHotkeyPlugin = ( BoardTransforms.updatePointerType(board, MindPointerType.mind); updateAppState({ pointer: MindPointerType.mind }); } + if (event.key === 'e') { + setCreationMode(board, BoardCreationMode.drawing); + BoardTransforms.updatePointerType(board, FreehandShape.eraser); + updateAppState({ pointer: FreehandShape.eraser }); + } if (event.key === 'p') { setCreationMode(board, BoardCreationMode.drawing); BoardTransforms.updatePointerType(board, FreehandShape.feltTipPen);