diff --git a/src/actions/actionFinalize.tsx b/src/actions/actionFinalize.tsx index 9eeda4b0..46d0eef8 100644 --- a/src/actions/actionFinalize.tsx +++ b/src/actions/actionFinalize.tsx @@ -18,12 +18,15 @@ export const actionFinalize = register({ if (appState.multiElement) { // pen and mouse have hover if (appState.lastPointerDownWith !== "touch") { - mutateElement(appState.multiElement, { - points: appState.multiElement.points.slice( - 0, - appState.multiElement.points.length - 1, - ), - }); + const { points, lastCommittedPoint } = appState.multiElement; + if ( + !lastCommittedPoint || + points[points.length - 1] !== lastCommittedPoint + ) { + mutateElement(appState.multiElement, { + points: appState.multiElement.points.slice(0, -1), + }); + } } if (isInvisiblySmallElement(appState.multiElement)) { newElements = newElements.slice(0, -1); diff --git a/src/actions/manager.tsx b/src/actions/manager.tsx index 52a9872c..00a09a66 100644 --- a/src/actions/manager.tsx +++ b/src/actions/manager.tsx @@ -60,6 +60,16 @@ export class ActionManager implements ActionsManagerInterface { return true; } + executeAction(action: Action) { + const commitToHistory = + action.commitToHistory && + action.commitToHistory(this.getAppState(), this.getElements()); + this.updater( + action.perform(this.getElements(), this.getAppState(), null), + commitToHistory, + ); + } + getContextMenuItems(actionFilter: ActionFilterFn = action => action) { return Object.values(this.actions) .filter(actionFilter) diff --git a/src/components/App.tsx b/src/components/App.tsx index e03eae9d..6cdec1ca 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -92,6 +92,7 @@ import { POINTER_BUTTON, DRAGGING_THRESHOLD, TEXT_TO_CENTER_SNAP_THRESHOLD, + ARROW_CONFIRM_THRESHOLD, } from "../constants"; import { LayerUI } from "./LayerUI"; import { ScrollBars } from "../scene/types"; @@ -102,6 +103,7 @@ import { unstable_batchedUpdates } from "react-dom"; import { SceneStateCallbackRemover } from "../scene/globalScene"; import { isLinearElement } from "../element/typeChecks"; import { rescalePoints } from "../points"; +import { actionFinalize } from "../actions"; function withBatchedUpdates< TFunction extends ((event: any) => void) | (() => void) @@ -1122,13 +1124,52 @@ export class App extends React.Component { ); if (this.state.multiElement) { const { multiElement } = this.state; - const originX = multiElement.x; - const originY = multiElement.y; - const points = multiElement.points; + const { x: rx, y: ry } = multiElement; - mutateElement(multiElement, { - points: [...points.slice(0, -1), [x - originX, y - originY]], - }); + const { points, lastCommittedPoint } = multiElement; + const lastPoint = points[points.length - 1]; + + setCursorForShape(this.state.elementType); + + if (lastPoint === lastCommittedPoint) { + // if we haven't yet created a temp point and we're beyond commit-zone + // threshold, add a point + if ( + distance2d(x - rx, y - ry, lastPoint[0], lastPoint[1]) >= + ARROW_CONFIRM_THRESHOLD + ) { + mutateElement(multiElement, { + points: [...points, [x - rx, y - ry]], + }); + } else { + document.documentElement.style.cursor = CURSOR_TYPE.POINTER; + // in this branch, we're inside the commit zone, and no uncommitted + // point exists. Thus do nothing (don't add/remove points). + } + } else { + // cursor moved inside commit zone, and there's uncommitted point, + // thus remove it + if ( + points.length > 2 && + lastCommittedPoint && + distance2d( + x - rx, + y - ry, + lastCommittedPoint[0], + lastCommittedPoint[1], + ) < ARROW_CONFIRM_THRESHOLD + ) { + document.documentElement.style.cursor = CURSOR_TYPE.POINTER; + mutateElement(multiElement, { + points: points.slice(0, -1), + }); + } else { + // update last uncommitted point + mutateElement(multiElement, { + points: [...points.slice(0, -1), [x - rx, y - ry]], + }); + } + } return; } @@ -1505,16 +1546,36 @@ export class App extends React.Component { ) { if (this.state.multiElement) { const { multiElement } = this.state; - const { x: rx, y: ry } = multiElement; + + const { x: rx, y: ry, lastCommittedPoint } = multiElement; + + // clicking inside commit zone → finalize arrow + if ( + multiElement.points.length > 1 && + lastCommittedPoint && + distance2d( + x - rx, + y - ry, + lastCommittedPoint[0], + lastCommittedPoint[1], + ) < ARROW_CONFIRM_THRESHOLD + ) { + this.actionManager.executeAction(actionFinalize); + return; + } this.setState(prevState => ({ selectedElementIds: { ...prevState.selectedElementIds, [multiElement.id]: true, }, })); + // clicking outside commit zone → update reference for last committed + // point mutateElement(multiElement, { - points: [...multiElement.points, [x - rx, y - ry]], + lastCommittedPoint: + multiElement.points[multiElement.points.length - 1], }); + document.documentElement.style.cursor = CURSOR_TYPE.POINTER; } else { const element = newLinearElement({ type: this.state.elementType, diff --git a/src/constants.ts b/src/constants.ts index fdbb71a1..83d7c7d8 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,4 +1,5 @@ export const DRAGGING_THRESHOLD = 10; // 10px +export const ARROW_CONFIRM_THRESHOLD = 10; // 10px export const ELEMENT_SHIFT_TRANSLATE_AMOUNT = 5; export const ELEMENT_TRANSLATE_AMOUNT = 1; export const TEXT_TO_CENTER_SNAP_THRESHOLD = 30; @@ -6,6 +7,7 @@ export const CURSOR_TYPE = { TEXT: "text", CROSSHAIR: "crosshair", GRABBING: "grabbing", + POINTER: "pointer", }; export const POINTER_BUTTON = { MAIN: 0, diff --git a/src/element/newElement.ts b/src/element/newElement.ts index 5305431d..6928d493 100644 --- a/src/element/newElement.ts +++ b/src/element/newElement.ts @@ -91,12 +91,14 @@ export function newTextElement( export function newLinearElement( opts: { - type: "arrow" | "line"; + type: ExcalidrawLinearElement["type"]; + lastCommittedPoint?: ExcalidrawLinearElement["lastCommittedPoint"]; } & ElementConstructorOpts, ): ExcalidrawLinearElement { return { ..._newElementBase(opts.type, opts), points: [], + lastCommittedPoint: opts.lastCommittedPoint || null, }; } diff --git a/src/element/types.ts b/src/element/types.ts index 9b7381c9..52117891 100644 --- a/src/element/types.ts +++ b/src/element/types.ts @@ -44,6 +44,7 @@ export type ExcalidrawLinearElement = _ExcalidrawElementBase & Readonly<{ type: "arrow" | "line"; points: Point[]; + lastCommittedPoint?: Point | null; }>; export type PointerType = "mouse" | "pen" | "touch"; diff --git a/src/locales/en.json b/src/locales/en.json index 109c6fe7..aaa501db 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -95,7 +95,7 @@ }, "hints": { "linearElement": "Click to start multiple points, drag for single line", - "linearElementMulti": "Press Escape or Enter to finish", + "linearElementMulti": "Click on last point or press Escape or Enter to finish", "resize": "You can constraint proportions by holding SHIFT while resizing" }, "errorSplash": {