From b1261eea70e3e93073e25c3b96098d80f77d30d6 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Tue, 7 Jul 2020 13:53:44 +0200 Subject: [PATCH] duplicate point on cmd+d (#1831) --- src/actions/actionDuplicateSelection.tsx | 37 ++++++++++ src/actions/types.ts | 15 +++-- src/components/App.tsx | 86 ++++++++++++------------ src/locales/en.json | 2 +- 4 files changed, 91 insertions(+), 49 deletions(-) diff --git a/src/actions/actionDuplicateSelection.tsx b/src/actions/actionDuplicateSelection.tsx index c0b15764..dce00b79 100644 --- a/src/actions/actionDuplicateSelection.tsx +++ b/src/actions/actionDuplicateSelection.tsx @@ -8,10 +8,47 @@ import { ToolButton } from "../components/ToolButton"; import { clone } from "../components/icons"; import { t } from "../i18n"; import { getShortcutKey } from "../utils"; +import { LinearElementEditor } from "../element/linearElementEditor"; +import { mutateElement } from "../element/mutateElement"; export const actionDuplicateSelection = register({ name: "duplicateSelection", perform: (elements, appState) => { + // duplicate point if selected while editing multi-point element + if (appState.editingLinearElement) { + const { activePointIndex, elementId } = appState.editingLinearElement; + const element = LinearElementEditor.getElement(elementId); + if (!element || activePointIndex === null) { + return false; + } + const { points } = element; + const selectedPoint = points[activePointIndex]; + const nextPoint = points[activePointIndex + 1]; + mutateElement(element, { + points: [ + ...points.slice(0, activePointIndex + 1), + nextPoint + ? [ + (selectedPoint[0] + nextPoint[0]) / 2, + (selectedPoint[1] + nextPoint[1]) / 2, + ] + : [selectedPoint[0] + 30, selectedPoint[1] + 30], + ...points.slice(activePointIndex + 1), + ], + }); + return { + appState: { + ...appState, + editingLinearElement: { + ...appState.editingLinearElement, + activePointIndex: activePointIndex + 1, + }, + }, + elements, + commitToHistory: true, + }; + } + const groupIdMap = new Map(); return { appState, diff --git a/src/actions/types.ts b/src/actions/types.ts index 49b6426e..4f160eee 100644 --- a/src/actions/types.ts +++ b/src/actions/types.ts @@ -2,12 +2,15 @@ import React from "react"; import { ExcalidrawElement } from "../element/types"; import { AppState } from "../types"; -export type ActionResult = { - elements?: readonly ExcalidrawElement[] | null; - appState?: AppState | null; - commitToHistory: boolean; - syncHistory?: boolean; -}; +/** if false, the action should be prevented */ +export type ActionResult = + | { + elements?: readonly ExcalidrawElement[] | null; + appState?: AppState | null; + commitToHistory: boolean; + syncHistory?: boolean; + } + | false; type ActionFn = ( elements: readonly ExcalidrawElement[], diff --git a/src/components/App.tsx b/src/components/App.tsx index 35b84fee..53dbe8e8 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -269,51 +269,53 @@ class App extends React.Component { ); } - private syncActionResult = withBatchedUpdates((res: ActionResult) => { - if (this.unmounted) { - return; - } - - let editingElement: AppState["editingElement"] | null = null; - if (res.elements) { - res.elements.forEach((element) => { - if ( - this.state.editingElement?.id === element.id && - this.state.editingElement !== element && - isNonDeletedElement(element) - ) { - editingElement = element; - } - }); - globalSceneState.replaceAllElements(res.elements); - if (res.commitToHistory) { - history.resumeRecording(); + private syncActionResult = withBatchedUpdates( + (actionResult: ActionResult) => { + if (this.unmounted || actionResult === false) { + return; } - } - if (res.appState || editingElement) { - if (res.commitToHistory) { - history.resumeRecording(); - } - this.setState( - (state) => ({ - ...res.appState, - editingElement: - editingElement || res.appState?.editingElement || null, - isCollaborating: state.isCollaborating, - collaborators: state.collaborators, - }), - () => { - if (res.syncHistory) { - history.setCurrentState( - this.state, - globalSceneState.getElementsIncludingDeleted(), - ); + let editingElement: AppState["editingElement"] | null = null; + if (actionResult.elements) { + actionResult.elements.forEach((element) => { + if ( + this.state.editingElement?.id === element.id && + this.state.editingElement !== element && + isNonDeletedElement(element) + ) { + editingElement = element; } - }, - ); - } - }); + }); + globalSceneState.replaceAllElements(actionResult.elements); + if (actionResult.commitToHistory) { + history.resumeRecording(); + } + } + + if (actionResult.appState || editingElement) { + if (actionResult.commitToHistory) { + history.resumeRecording(); + } + this.setState( + (state) => ({ + ...actionResult.appState, + editingElement: + editingElement || actionResult.appState?.editingElement || null, + isCollaborating: state.isCollaborating, + collaborators: state.collaborators, + }), + () => { + if (actionResult.syncHistory) { + history.setCurrentState( + this.state, + globalSceneState.getElementsIncludingDeleted(), + ); + } + }, + ); + } + }, + ); // Lifecycle diff --git a/src/locales/en.json b/src/locales/en.json index 0a6cfa43..f7f6fdb3 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -128,7 +128,7 @@ "resize": "You can constrain proportions by holding SHIFT while resizing,\nhold ALT to resize from the center", "rotate": "You can constrain angles by holding SHIFT while rotating", "lineEditor_info": "Double-click or press Enter to edit points", - "lineEditor_pointSelected": "Press Delete to remove point or drag to move", + "lineEditor_pointSelected": "Press Delete to remove point, CtrlOrCmd+D to duplicate, or drag to move", "lineEditor_nothingSelected": "Select a point to move or remove, or hold Alt and click to add new points" }, "errorSplash": {