mirror of
				https://github.com/excalidraw/excalidraw.git
				synced 2025-10-31 10:54:33 +01:00 
			
		
		
		
	duplicate point on cmd+d (#1831)
This commit is contained in:
		| @@ -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, | ||||
|   | ||||
| @@ -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[], | ||||
|   | ||||
| @@ -269,51 +269,53 @@ class App extends React.Component<any, AppState> { | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   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 | ||||
|  | ||||
|   | ||||
| @@ -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": { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 David Luzar
					David Luzar