|  | @@ -25,11 +25,19 @@ export class LinearElementEditor {
 | 
	
		
			
				|  |  |    public elementId: ExcalidrawElement["id"] & {
 | 
	
		
			
				|  |  |      _brand: "excalidrawLinearElementId";
 | 
	
		
			
				|  |  |    };
 | 
	
		
			
				|  |  | -  public activePointIndex: number | null;
 | 
	
		
			
				|  |  | +  /** indices */
 | 
	
		
			
				|  |  | +  public selectedPointsIndices: readonly number[] | null;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  public pointerDownState: Readonly<{
 | 
	
		
			
				|  |  | +    prevSelectedPointsIndices: readonly number[] | null;
 | 
	
		
			
				|  |  | +    /** index */
 | 
	
		
			
				|  |  | +    lastClickedPoint: number;
 | 
	
		
			
				|  |  | +  }>;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |    /** whether you're dragging a point */
 | 
	
		
			
				|  |  |    public isDragging: boolean;
 | 
	
		
			
				|  |  |    public lastUncommittedPoint: Point | null;
 | 
	
		
			
				|  |  | -  public pointerOffset: { x: number; y: number };
 | 
	
		
			
				|  |  | +  public pointerOffset: Readonly<{ x: number; y: number }>;
 | 
	
		
			
				|  |  |    public startBindingElement: ExcalidrawBindableElement | null | "keep";
 | 
	
		
			
				|  |  |    public endBindingElement: ExcalidrawBindableElement | null | "keep";
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -40,12 +48,16 @@ export class LinearElementEditor {
 | 
	
		
			
				|  |  |      Scene.mapElementToScene(this.elementId, scene);
 | 
	
		
			
				|  |  |      LinearElementEditor.normalizePoints(element);
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    this.activePointIndex = null;
 | 
	
		
			
				|  |  | +    this.selectedPointsIndices = null;
 | 
	
		
			
				|  |  |      this.lastUncommittedPoint = null;
 | 
	
		
			
				|  |  |      this.isDragging = false;
 | 
	
		
			
				|  |  |      this.pointerOffset = { x: 0, y: 0 };
 | 
	
		
			
				|  |  |      this.startBindingElement = "keep";
 | 
	
		
			
				|  |  |      this.endBindingElement = "keep";
 | 
	
		
			
				|  |  | +    this.pointerDownState = {
 | 
	
		
			
				|  |  | +      prevSelectedPointsIndices: null,
 | 
	
		
			
				|  |  | +      lastClickedPoint: -1,
 | 
	
		
			
				|  |  | +    };
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |    // ---------------------------------------------------------------------------
 | 
	
	
		
			
				|  | @@ -66,6 +78,58 @@ export class LinearElementEditor {
 | 
	
		
			
				|  |  |      return null;
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +  static handleBoxSelection(
 | 
	
		
			
				|  |  | +    event: PointerEvent,
 | 
	
		
			
				|  |  | +    appState: AppState,
 | 
	
		
			
				|  |  | +    setState: React.Component<any, AppState>["setState"],
 | 
	
		
			
				|  |  | +  ) {
 | 
	
		
			
				|  |  | +    if (
 | 
	
		
			
				|  |  | +      !appState.editingLinearElement ||
 | 
	
		
			
				|  |  | +      appState.draggingElement?.type !== "selection"
 | 
	
		
			
				|  |  | +    ) {
 | 
	
		
			
				|  |  | +      return false;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +    const { editingLinearElement } = appState;
 | 
	
		
			
				|  |  | +    const { selectedPointsIndices, elementId } = editingLinearElement;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    const element = LinearElementEditor.getElement(elementId);
 | 
	
		
			
				|  |  | +    if (!element) {
 | 
	
		
			
				|  |  | +      return false;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    const [selectionX1, selectionY1, selectionX2, selectionY2] =
 | 
	
		
			
				|  |  | +      getElementAbsoluteCoords(appState.draggingElement);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    const pointsSceneCoords =
 | 
	
		
			
				|  |  | +      LinearElementEditor.getPointsGlobalCoordinates(element);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    const nextSelectedPoints = pointsSceneCoords.reduce(
 | 
	
		
			
				|  |  | +      (acc: number[], point, index) => {
 | 
	
		
			
				|  |  | +        if (
 | 
	
		
			
				|  |  | +          (point[0] >= selectionX1 &&
 | 
	
		
			
				|  |  | +            point[0] <= selectionX2 &&
 | 
	
		
			
				|  |  | +            point[1] >= selectionY1 &&
 | 
	
		
			
				|  |  | +            point[1] <= selectionY2) ||
 | 
	
		
			
				|  |  | +          (event.shiftKey && selectedPointsIndices?.includes(index))
 | 
	
		
			
				|  |  | +        ) {
 | 
	
		
			
				|  |  | +          acc.push(index);
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        return acc;
 | 
	
		
			
				|  |  | +      },
 | 
	
		
			
				|  |  | +      [],
 | 
	
		
			
				|  |  | +    );
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    setState({
 | 
	
		
			
				|  |  | +      editingLinearElement: {
 | 
	
		
			
				|  |  | +        ...editingLinearElement,
 | 
	
		
			
				|  |  | +        selectedPointsIndices: nextSelectedPoints.length
 | 
	
		
			
				|  |  | +          ? nextSelectedPoints
 | 
	
		
			
				|  |  | +          : null,
 | 
	
		
			
				|  |  | +      },
 | 
	
		
			
				|  |  | +    });
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |    /** @returns whether point was dragged */
 | 
	
		
			
				|  |  |    static handlePointDragging(
 | 
	
		
			
				|  |  |      appState: AppState,
 | 
	
	
		
			
				|  | @@ -74,21 +138,27 @@ export class LinearElementEditor {
 | 
	
		
			
				|  |  |      scenePointerY: number,
 | 
	
		
			
				|  |  |      maybeSuggestBinding: (
 | 
	
		
			
				|  |  |        element: NonDeleted<ExcalidrawLinearElement>,
 | 
	
		
			
				|  |  | -      startOrEnd: "start" | "end",
 | 
	
		
			
				|  |  | +      pointSceneCoords: { x: number; y: number }[],
 | 
	
		
			
				|  |  |      ) => void,
 | 
	
		
			
				|  |  |    ): boolean {
 | 
	
		
			
				|  |  |      if (!appState.editingLinearElement) {
 | 
	
		
			
				|  |  |        return false;
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |      const { editingLinearElement } = appState;
 | 
	
		
			
				|  |  | -    const { activePointIndex, elementId, isDragging } = editingLinearElement;
 | 
	
		
			
				|  |  | +    const { selectedPointsIndices, elementId, isDragging } =
 | 
	
		
			
				|  |  | +      editingLinearElement;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      const element = LinearElementEditor.getElement(elementId);
 | 
	
		
			
				|  |  |      if (!element) {
 | 
	
		
			
				|  |  |        return false;
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    if (activePointIndex != null && activePointIndex > -1) {
 | 
	
		
			
				|  |  | +    // point that's being dragged (out of all selected points)
 | 
	
		
			
				|  |  | +    const draggingPoint = element.points[
 | 
	
		
			
				|  |  | +      editingLinearElement.pointerDownState.lastClickedPoint
 | 
	
		
			
				|  |  | +    ] as [number, number] | undefined;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    if (selectedPointsIndices && draggingPoint) {
 | 
	
		
			
				|  |  |        if (isDragging === false) {
 | 
	
		
			
				|  |  |          setState({
 | 
	
		
			
				|  |  |            editingLinearElement: {
 | 
	
	
		
			
				|  | @@ -98,18 +168,79 @@ export class LinearElementEditor {
 | 
	
		
			
				|  |  |          });
 | 
	
		
			
				|  |  |        }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -      const newPoint = LinearElementEditor.createPointAt(
 | 
	
		
			
				|  |  | +      const newDraggingPointPosition = LinearElementEditor.createPointAt(
 | 
	
		
			
				|  |  |          element,
 | 
	
		
			
				|  |  |          scenePointerX - editingLinearElement.pointerOffset.x,
 | 
	
		
			
				|  |  |          scenePointerY - editingLinearElement.pointerOffset.y,
 | 
	
		
			
				|  |  |          appState.gridSize,
 | 
	
		
			
				|  |  |        );
 | 
	
		
			
				|  |  | -      LinearElementEditor.movePoint(element, activePointIndex, newPoint);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      const deltaX = newDraggingPointPosition[0] - draggingPoint[0];
 | 
	
		
			
				|  |  | +      const deltaY = newDraggingPointPosition[1] - draggingPoint[1];
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      LinearElementEditor.movePoints(
 | 
	
		
			
				|  |  | +        element,
 | 
	
		
			
				|  |  | +        selectedPointsIndices.map((pointIndex) => {
 | 
	
		
			
				|  |  | +          const newPointPosition =
 | 
	
		
			
				|  |  | +            pointIndex ===
 | 
	
		
			
				|  |  | +            editingLinearElement.pointerDownState.lastClickedPoint
 | 
	
		
			
				|  |  | +              ? LinearElementEditor.createPointAt(
 | 
	
		
			
				|  |  | +                  element,
 | 
	
		
			
				|  |  | +                  scenePointerX - editingLinearElement.pointerOffset.x,
 | 
	
		
			
				|  |  | +                  scenePointerY - editingLinearElement.pointerOffset.y,
 | 
	
		
			
				|  |  | +                  appState.gridSize,
 | 
	
		
			
				|  |  | +                )
 | 
	
		
			
				|  |  | +              : ([
 | 
	
		
			
				|  |  | +                  element.points[pointIndex][0] + deltaX,
 | 
	
		
			
				|  |  | +                  element.points[pointIndex][1] + deltaY,
 | 
	
		
			
				|  |  | +                ] as const);
 | 
	
		
			
				|  |  | +          return {
 | 
	
		
			
				|  |  | +            index: pointIndex,
 | 
	
		
			
				|  |  | +            point: newPointPosition,
 | 
	
		
			
				|  |  | +            isDragging:
 | 
	
		
			
				|  |  | +              pointIndex ===
 | 
	
		
			
				|  |  | +              editingLinearElement.pointerDownState.lastClickedPoint,
 | 
	
		
			
				|  |  | +          };
 | 
	
		
			
				|  |  | +        }),
 | 
	
		
			
				|  |  | +      );
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      // suggest bindings for first and last point if selected
 | 
	
		
			
				|  |  |        if (isBindingElement(element)) {
 | 
	
		
			
				|  |  | -        maybeSuggestBinding(element, activePointIndex === 0 ? "start" : "end");
 | 
	
		
			
				|  |  | +        const coords: { x: number; y: number }[] = [];
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        const firstSelectedIndex = selectedPointsIndices[0];
 | 
	
		
			
				|  |  | +        if (firstSelectedIndex === 0) {
 | 
	
		
			
				|  |  | +          coords.push(
 | 
	
		
			
				|  |  | +            tupleToCoors(
 | 
	
		
			
				|  |  | +              LinearElementEditor.getPointGlobalCoordinates(
 | 
	
		
			
				|  |  | +                element,
 | 
	
		
			
				|  |  | +                element.points[0],
 | 
	
		
			
				|  |  | +              ),
 | 
	
		
			
				|  |  | +            ),
 | 
	
		
			
				|  |  | +          );
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        const lastSelectedIndex =
 | 
	
		
			
				|  |  | +          selectedPointsIndices[selectedPointsIndices.length - 1];
 | 
	
		
			
				|  |  | +        if (lastSelectedIndex === element.points.length - 1) {
 | 
	
		
			
				|  |  | +          coords.push(
 | 
	
		
			
				|  |  | +            tupleToCoors(
 | 
	
		
			
				|  |  | +              LinearElementEditor.getPointGlobalCoordinates(
 | 
	
		
			
				|  |  | +                element,
 | 
	
		
			
				|  |  | +                element.points[lastSelectedIndex],
 | 
	
		
			
				|  |  | +              ),
 | 
	
		
			
				|  |  | +            ),
 | 
	
		
			
				|  |  | +          );
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        if (coords.length) {
 | 
	
		
			
				|  |  | +          maybeSuggestBinding(element, coords);
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  |        }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |        return true;
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |      return false;
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -118,45 +249,79 @@ export class LinearElementEditor {
 | 
	
		
			
				|  |  |      editingLinearElement: LinearElementEditor,
 | 
	
		
			
				|  |  |      appState: AppState,
 | 
	
		
			
				|  |  |    ): LinearElementEditor {
 | 
	
		
			
				|  |  | -    const { elementId, activePointIndex, isDragging } = editingLinearElement;
 | 
	
		
			
				|  |  | +    const { elementId, selectedPointsIndices, isDragging, pointerDownState } =
 | 
	
		
			
				|  |  | +      editingLinearElement;
 | 
	
		
			
				|  |  |      const element = LinearElementEditor.getElement(elementId);
 | 
	
		
			
				|  |  |      if (!element) {
 | 
	
		
			
				|  |  |        return editingLinearElement;
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    let binding = {};
 | 
	
		
			
				|  |  | -    if (
 | 
	
		
			
				|  |  | -      isDragging &&
 | 
	
		
			
				|  |  | -      (activePointIndex === 0 || activePointIndex === element.points.length - 1)
 | 
	
		
			
				|  |  | -    ) {
 | 
	
		
			
				|  |  | -      if (isPathALoop(element.points, appState.zoom.value)) {
 | 
	
		
			
				|  |  | -        LinearElementEditor.movePoint(
 | 
	
		
			
				|  |  | -          element,
 | 
	
		
			
				|  |  | -          activePointIndex,
 | 
	
		
			
				|  |  | -          activePointIndex === 0
 | 
	
		
			
				|  |  | -            ? element.points[element.points.length - 1]
 | 
	
		
			
				|  |  | -            : element.points[0],
 | 
	
		
			
				|  |  | -        );
 | 
	
		
			
				|  |  | +    const bindings: Partial<
 | 
	
		
			
				|  |  | +      Pick<
 | 
	
		
			
				|  |  | +        InstanceType<typeof LinearElementEditor>,
 | 
	
		
			
				|  |  | +        "startBindingElement" | "endBindingElement"
 | 
	
		
			
				|  |  | +      >
 | 
	
		
			
				|  |  | +    > = {};
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    if (isDragging && selectedPointsIndices) {
 | 
	
		
			
				|  |  | +      for (const selectedPoint of selectedPointsIndices) {
 | 
	
		
			
				|  |  | +        if (
 | 
	
		
			
				|  |  | +          selectedPoint === 0 ||
 | 
	
		
			
				|  |  | +          selectedPoint === element.points.length - 1
 | 
	
		
			
				|  |  | +        ) {
 | 
	
		
			
				|  |  | +          if (isPathALoop(element.points, appState.zoom.value)) {
 | 
	
		
			
				|  |  | +            LinearElementEditor.movePoints(element, [
 | 
	
		
			
				|  |  | +              {
 | 
	
		
			
				|  |  | +                index: selectedPoint,
 | 
	
		
			
				|  |  | +                point:
 | 
	
		
			
				|  |  | +                  selectedPoint === 0
 | 
	
		
			
				|  |  | +                    ? element.points[element.points.length - 1]
 | 
	
		
			
				|  |  | +                    : element.points[0],
 | 
	
		
			
				|  |  | +              },
 | 
	
		
			
				|  |  | +            ]);
 | 
	
		
			
				|  |  | +          }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +          const bindingElement = isBindingEnabled(appState)
 | 
	
		
			
				|  |  | +            ? getHoveredElementForBinding(
 | 
	
		
			
				|  |  | +                tupleToCoors(
 | 
	
		
			
				|  |  | +                  LinearElementEditor.getPointAtIndexGlobalCoordinates(
 | 
	
		
			
				|  |  | +                    element,
 | 
	
		
			
				|  |  | +                    selectedPoint!,
 | 
	
		
			
				|  |  | +                  ),
 | 
	
		
			
				|  |  | +                ),
 | 
	
		
			
				|  |  | +                Scene.getScene(element)!,
 | 
	
		
			
				|  |  | +              )
 | 
	
		
			
				|  |  | +            : null;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +          bindings[
 | 
	
		
			
				|  |  | +            selectedPoint === 0 ? "startBindingElement" : "endBindingElement"
 | 
	
		
			
				|  |  | +          ] = bindingElement;
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  |        }
 | 
	
		
			
				|  |  | -      const bindingElement = isBindingEnabled(appState)
 | 
	
		
			
				|  |  | -        ? getHoveredElementForBinding(
 | 
	
		
			
				|  |  | -            tupleToCoors(
 | 
	
		
			
				|  |  | -              LinearElementEditor.getPointAtIndexGlobalCoordinates(
 | 
	
		
			
				|  |  | -                element,
 | 
	
		
			
				|  |  | -                activePointIndex!,
 | 
	
		
			
				|  |  | -              ),
 | 
	
		
			
				|  |  | -            ),
 | 
	
		
			
				|  |  | -            Scene.getScene(element)!,
 | 
	
		
			
				|  |  | -          )
 | 
	
		
			
				|  |  | -        : null;
 | 
	
		
			
				|  |  | -      binding = {
 | 
	
		
			
				|  |  | -        [activePointIndex === 0 ? "startBindingElement" : "endBindingElement"]:
 | 
	
		
			
				|  |  | -          bindingElement,
 | 
	
		
			
				|  |  | -      };
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |      return {
 | 
	
		
			
				|  |  |        ...editingLinearElement,
 | 
	
		
			
				|  |  | -      ...binding,
 | 
	
		
			
				|  |  | +      ...bindings,
 | 
	
		
			
				|  |  | +      // if clicking without previously dragging a point(s), and not holding
 | 
	
		
			
				|  |  | +      // shift, deselect all points except the one clicked. If holding shift,
 | 
	
		
			
				|  |  | +      // toggle the point.
 | 
	
		
			
				|  |  | +      selectedPointsIndices:
 | 
	
		
			
				|  |  | +        isDragging || event.shiftKey
 | 
	
		
			
				|  |  | +          ? !isDragging &&
 | 
	
		
			
				|  |  | +            event.shiftKey &&
 | 
	
		
			
				|  |  | +            pointerDownState.prevSelectedPointsIndices?.includes(
 | 
	
		
			
				|  |  | +              pointerDownState.lastClickedPoint,
 | 
	
		
			
				|  |  | +            )
 | 
	
		
			
				|  |  | +            ? selectedPointsIndices &&
 | 
	
		
			
				|  |  | +              selectedPointsIndices.filter(
 | 
	
		
			
				|  |  | +                (pointIndex) =>
 | 
	
		
			
				|  |  | +                  pointIndex !== pointerDownState.lastClickedPoint,
 | 
	
		
			
				|  |  | +              )
 | 
	
		
			
				|  |  | +            : selectedPointsIndices
 | 
	
		
			
				|  |  | +          : selectedPointsIndices?.includes(pointerDownState.lastClickedPoint)
 | 
	
		
			
				|  |  | +          ? [pointerDownState.lastClickedPoint]
 | 
	
		
			
				|  |  | +          : selectedPointsIndices,
 | 
	
		
			
				|  |  |        isDragging: false,
 | 
	
		
			
				|  |  |        pointerOffset: { x: 0, y: 0 },
 | 
	
		
			
				|  |  |      };
 | 
	
	
		
			
				|  | @@ -206,7 +371,12 @@ export class LinearElementEditor {
 | 
	
		
			
				|  |  |        setState({
 | 
	
		
			
				|  |  |          editingLinearElement: {
 | 
	
		
			
				|  |  |            ...appState.editingLinearElement,
 | 
	
		
			
				|  |  | -          activePointIndex: element.points.length - 1,
 | 
	
		
			
				|  |  | +          pointerDownState: {
 | 
	
		
			
				|  |  | +            prevSelectedPointsIndices:
 | 
	
		
			
				|  |  | +              appState.editingLinearElement.selectedPointsIndices,
 | 
	
		
			
				|  |  | +            lastClickedPoint: -1,
 | 
	
		
			
				|  |  | +          },
 | 
	
		
			
				|  |  | +          selectedPointsIndices: [element.points.length - 1],
 | 
	
		
			
				|  |  |            lastUncommittedPoint: null,
 | 
	
		
			
				|  |  |            endBindingElement: getHoveredElementForBinding(
 | 
	
		
			
				|  |  |              scenePointer,
 | 
	
	
		
			
				|  | @@ -259,10 +429,28 @@ export class LinearElementEditor {
 | 
	
		
			
				|  |  |          element.angle,
 | 
	
		
			
				|  |  |        );
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +    const nextSelectedPointsIndices =
 | 
	
		
			
				|  |  | +      clickedPointIndex > -1 || event.shiftKey
 | 
	
		
			
				|  |  | +        ? event.shiftKey ||
 | 
	
		
			
				|  |  | +          appState.editingLinearElement.selectedPointsIndices?.includes(
 | 
	
		
			
				|  |  | +            clickedPointIndex,
 | 
	
		
			
				|  |  | +          )
 | 
	
		
			
				|  |  | +          ? normalizeSelectedPoints([
 | 
	
		
			
				|  |  | +              ...(appState.editingLinearElement.selectedPointsIndices || []),
 | 
	
		
			
				|  |  | +              clickedPointIndex,
 | 
	
		
			
				|  |  | +            ])
 | 
	
		
			
				|  |  | +          : [clickedPointIndex]
 | 
	
		
			
				|  |  | +        : null;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |      setState({
 | 
	
		
			
				|  |  |        editingLinearElement: {
 | 
	
		
			
				|  |  |          ...appState.editingLinearElement,
 | 
	
		
			
				|  |  | -        activePointIndex: clickedPointIndex > -1 ? clickedPointIndex : null,
 | 
	
		
			
				|  |  | +        pointerDownState: {
 | 
	
		
			
				|  |  | +          prevSelectedPointsIndices:
 | 
	
		
			
				|  |  | +            appState.editingLinearElement.selectedPointsIndices,
 | 
	
		
			
				|  |  | +          lastClickedPoint: clickedPointIndex,
 | 
	
		
			
				|  |  | +        },
 | 
	
		
			
				|  |  | +        selectedPointsIndices: nextSelectedPointsIndices,
 | 
	
		
			
				|  |  |          pointerOffset: targetPoint
 | 
	
		
			
				|  |  |            ? {
 | 
	
		
			
				|  |  |                x: scenePointer.x - targetPoint[0],
 | 
	
	
		
			
				|  | @@ -292,7 +480,7 @@ export class LinearElementEditor {
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      if (!event.altKey) {
 | 
	
		
			
				|  |  |        if (lastPoint === lastUncommittedPoint) {
 | 
	
		
			
				|  |  | -        LinearElementEditor.movePoint(element, points.length - 1, "delete");
 | 
	
		
			
				|  |  | +        LinearElementEditor.deletePoints(element, [points.length - 1]);
 | 
	
		
			
				|  |  |        }
 | 
	
		
			
				|  |  |        return { ...editingLinearElement, lastUncommittedPoint: null };
 | 
	
		
			
				|  |  |      }
 | 
	
	
		
			
				|  | @@ -305,13 +493,14 @@ export class LinearElementEditor {
 | 
	
		
			
				|  |  |      );
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      if (lastPoint === lastUncommittedPoint) {
 | 
	
		
			
				|  |  | -      LinearElementEditor.movePoint(
 | 
	
		
			
				|  |  | -        element,
 | 
	
		
			
				|  |  | -        element.points.length - 1,
 | 
	
		
			
				|  |  | -        newPoint,
 | 
	
		
			
				|  |  | -      );
 | 
	
		
			
				|  |  | +      LinearElementEditor.movePoints(element, [
 | 
	
		
			
				|  |  | +        {
 | 
	
		
			
				|  |  | +          index: element.points.length - 1,
 | 
	
		
			
				|  |  | +          point: newPoint,
 | 
	
		
			
				|  |  | +        },
 | 
	
		
			
				|  |  | +      ]);
 | 
	
		
			
				|  |  |      } else {
 | 
	
		
			
				|  |  | -      LinearElementEditor.movePoint(element, "new", newPoint);
 | 
	
		
			
				|  |  | +      LinearElementEditor.addPoints(element, [{ point: newPoint }]);
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      return {
 | 
	
	
		
			
				|  | @@ -320,6 +509,21 @@ export class LinearElementEditor {
 | 
	
		
			
				|  |  |      };
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +  /** scene coords */
 | 
	
		
			
				|  |  | +  static getPointGlobalCoordinates(
 | 
	
		
			
				|  |  | +    element: NonDeleted<ExcalidrawLinearElement>,
 | 
	
		
			
				|  |  | +    point: Point,
 | 
	
		
			
				|  |  | +  ) {
 | 
	
		
			
				|  |  | +    const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
 | 
	
		
			
				|  |  | +    const cx = (x1 + x2) / 2;
 | 
	
		
			
				|  |  | +    const cy = (y1 + y2) / 2;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    let { x, y } = element;
 | 
	
		
			
				|  |  | +    [x, y] = rotate(x + point[0], y + point[1], cx, cy, element.angle);
 | 
	
		
			
				|  |  | +    return [x, y] as const;
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /** scene coords */
 | 
	
		
			
				|  |  |    static getPointsGlobalCoordinates(
 | 
	
		
			
				|  |  |      element: NonDeleted<ExcalidrawLinearElement>,
 | 
	
		
			
				|  |  |    ) {
 | 
	
	
		
			
				|  | @@ -439,22 +643,122 @@ export class LinearElementEditor {
 | 
	
		
			
				|  |  |      mutateElement(element, LinearElementEditor.getNormalizedPoints(element));
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -  static movePointByOffset(
 | 
	
		
			
				|  |  | +  static duplicateSelectedPoints(appState: AppState) {
 | 
	
		
			
				|  |  | +    if (!appState.editingLinearElement) {
 | 
	
		
			
				|  |  | +      return false;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    const { selectedPointsIndices, elementId } = appState.editingLinearElement;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    const element = LinearElementEditor.getElement(elementId);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    if (!element || selectedPointsIndices === null) {
 | 
	
		
			
				|  |  | +      return false;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    const { points } = element;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    const nextSelectedIndices: number[] = [];
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    let pointAddedToEnd = false;
 | 
	
		
			
				|  |  | +    let indexCursor = -1;
 | 
	
		
			
				|  |  | +    const nextPoints = points.reduce((acc: Point[], point, index) => {
 | 
	
		
			
				|  |  | +      ++indexCursor;
 | 
	
		
			
				|  |  | +      acc.push(point);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      const isSelected = selectedPointsIndices.includes(index);
 | 
	
		
			
				|  |  | +      if (isSelected) {
 | 
	
		
			
				|  |  | +        const nextPoint = points[index + 1];
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        if (!nextPoint) {
 | 
	
		
			
				|  |  | +          pointAddedToEnd = true;
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +        acc.push(
 | 
	
		
			
				|  |  | +          nextPoint
 | 
	
		
			
				|  |  | +            ? [(point[0] + nextPoint[0]) / 2, (point[1] + nextPoint[1]) / 2]
 | 
	
		
			
				|  |  | +            : [point[0], point[1]],
 | 
	
		
			
				|  |  | +        );
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        nextSelectedIndices.push(indexCursor + 1);
 | 
	
		
			
				|  |  | +        ++indexCursor;
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      return acc;
 | 
	
		
			
				|  |  | +    }, []);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    mutateElement(element, { points: nextPoints });
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    // temp hack to ensure the line doesn't move when adding point to the end,
 | 
	
		
			
				|  |  | +    // potentially expanding the bounding box
 | 
	
		
			
				|  |  | +    if (pointAddedToEnd) {
 | 
	
		
			
				|  |  | +      const lastPoint = element.points[element.points.length - 1];
 | 
	
		
			
				|  |  | +      LinearElementEditor.movePoints(element, [
 | 
	
		
			
				|  |  | +        {
 | 
	
		
			
				|  |  | +          index: element.points.length - 1,
 | 
	
		
			
				|  |  | +          point: [lastPoint[0] + 30, lastPoint[1] + 30],
 | 
	
		
			
				|  |  | +        },
 | 
	
		
			
				|  |  | +      ]);
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    return {
 | 
	
		
			
				|  |  | +      appState: {
 | 
	
		
			
				|  |  | +        ...appState,
 | 
	
		
			
				|  |  | +        editingLinearElement: {
 | 
	
		
			
				|  |  | +          ...appState.editingLinearElement,
 | 
	
		
			
				|  |  | +          selectedPointsIndices: nextSelectedIndices,
 | 
	
		
			
				|  |  | +        },
 | 
	
		
			
				|  |  | +      },
 | 
	
		
			
				|  |  | +    };
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  static deletePoints(
 | 
	
		
			
				|  |  |      element: NonDeleted<ExcalidrawLinearElement>,
 | 
	
		
			
				|  |  | -    pointIndex: number,
 | 
	
		
			
				|  |  | -    offset: { x: number; y: number },
 | 
	
		
			
				|  |  | +    pointIndices: readonly number[],
 | 
	
		
			
				|  |  |    ) {
 | 
	
		
			
				|  |  | -    const [x, y] = element.points[pointIndex];
 | 
	
		
			
				|  |  | -    LinearElementEditor.movePoint(element, pointIndex, [
 | 
	
		
			
				|  |  | -      x + offset.x,
 | 
	
		
			
				|  |  | -      y + offset.y,
 | 
	
		
			
				|  |  | -    ]);
 | 
	
		
			
				|  |  | +    let offsetX = 0;
 | 
	
		
			
				|  |  | +    let offsetY = 0;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    const isDeletingOriginPoint = pointIndices.includes(0);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    // if deleting first point, make the next to be [0,0] and recalculate
 | 
	
		
			
				|  |  | +    // positions of the rest with respect to it
 | 
	
		
			
				|  |  | +    if (isDeletingOriginPoint) {
 | 
	
		
			
				|  |  | +      const firstNonDeletedPoint = element.points.find((point, idx) => {
 | 
	
		
			
				|  |  | +        return !pointIndices.includes(idx);
 | 
	
		
			
				|  |  | +      });
 | 
	
		
			
				|  |  | +      if (firstNonDeletedPoint) {
 | 
	
		
			
				|  |  | +        offsetX = firstNonDeletedPoint[0];
 | 
	
		
			
				|  |  | +        offsetY = firstNonDeletedPoint[1];
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    const nextPoints = element.points.reduce((acc: Point[], point, idx) => {
 | 
	
		
			
				|  |  | +      if (!pointIndices.includes(idx)) {
 | 
	
		
			
				|  |  | +        acc.push(
 | 
	
		
			
				|  |  | +          !acc.length ? [0, 0] : [point[0] - offsetX, point[1] - offsetY],
 | 
	
		
			
				|  |  | +        );
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +      return acc;
 | 
	
		
			
				|  |  | +    }, []);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    LinearElementEditor._updatePoints(element, nextPoints, offsetX, offsetY);
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -  static movePoint(
 | 
	
		
			
				|  |  | +  static addPoints(
 | 
	
		
			
				|  |  |      element: NonDeleted<ExcalidrawLinearElement>,
 | 
	
		
			
				|  |  | -    pointIndex: number | "new",
 | 
	
		
			
				|  |  | -    targetPosition: Point | "delete",
 | 
	
		
			
				|  |  | +    targetPoints: { point: Point }[],
 | 
	
		
			
				|  |  | +  ) {
 | 
	
		
			
				|  |  | +    const offsetX = 0;
 | 
	
		
			
				|  |  | +    const offsetY = 0;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    const nextPoints = [...element.points, ...targetPoints.map((x) => x.point)];
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    LinearElementEditor._updatePoints(element, nextPoints, offsetX, offsetY);
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  static movePoints(
 | 
	
		
			
				|  |  | +    element: NonDeleted<ExcalidrawLinearElement>,
 | 
	
		
			
				|  |  | +    targetPoints: { index: number; point: Point; isDragging?: boolean }[],
 | 
	
		
			
				|  |  |      otherUpdates?: { startBinding?: PointBinding; endBinding?: PointBinding },
 | 
	
		
			
				|  |  |    ) {
 | 
	
		
			
				|  |  |      const { points } = element;
 | 
	
	
		
			
				|  | @@ -467,49 +771,50 @@ export class LinearElementEditor {
 | 
	
		
			
				|  |  |      let offsetX = 0;
 | 
	
		
			
				|  |  |      let offsetY = 0;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    let nextPoints: (readonly [number, number])[];
 | 
	
		
			
				|  |  | -    if (targetPosition === "delete") {
 | 
	
		
			
				|  |  | -      // remove point
 | 
	
		
			
				|  |  | -      if (pointIndex === "new") {
 | 
	
		
			
				|  |  | -        throw new Error("invalid args in movePoint");
 | 
	
		
			
				|  |  | -      }
 | 
	
		
			
				|  |  | -      nextPoints = points.slice();
 | 
	
		
			
				|  |  | -      nextPoints.splice(pointIndex, 1);
 | 
	
		
			
				|  |  | -      if (pointIndex === 0) {
 | 
	
		
			
				|  |  | -        // if deleting first point, make the next to be [0,0] and recalculate
 | 
	
		
			
				|  |  | -        // positions of the rest with respect to it
 | 
	
		
			
				|  |  | -        offsetX = nextPoints[0][0];
 | 
	
		
			
				|  |  | -        offsetY = nextPoints[0][1];
 | 
	
		
			
				|  |  | -        nextPoints = nextPoints.map((point, idx) => {
 | 
	
		
			
				|  |  | -          if (idx === 0) {
 | 
	
		
			
				|  |  | -            return [0, 0];
 | 
	
		
			
				|  |  | -          }
 | 
	
		
			
				|  |  | -          return [point[0] - offsetX, point[1] - offsetY];
 | 
	
		
			
				|  |  | -        });
 | 
	
		
			
				|  |  | -      }
 | 
	
		
			
				|  |  | -    } else if (pointIndex === "new") {
 | 
	
		
			
				|  |  | -      nextPoints = [...points, targetPosition];
 | 
	
		
			
				|  |  | -    } else {
 | 
	
		
			
				|  |  | -      const deltaX = targetPosition[0] - points[pointIndex][0];
 | 
	
		
			
				|  |  | -      const deltaY = targetPosition[1] - points[pointIndex][1];
 | 
	
		
			
				|  |  | -      nextPoints = points.map((point, idx) => {
 | 
	
		
			
				|  |  | -        if (idx === pointIndex) {
 | 
	
		
			
				|  |  | -          if (idx === 0) {
 | 
	
		
			
				|  |  | -            offsetX = deltaX;
 | 
	
		
			
				|  |  | -            offsetY = deltaY;
 | 
	
		
			
				|  |  | -            return point;
 | 
	
		
			
				|  |  | -          }
 | 
	
		
			
				|  |  | -          offsetX = 0;
 | 
	
		
			
				|  |  | -          offsetY = 0;
 | 
	
		
			
				|  |  | +    const selectedOriginPoint = targetPoints.find(({ index }) => index === 0);
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -          return [point[0] + deltaX, point[1] + deltaY] as const;
 | 
	
		
			
				|  |  | -        }
 | 
	
		
			
				|  |  | -        return offsetX || offsetY
 | 
	
		
			
				|  |  | -          ? ([point[0] - offsetX, point[1] - offsetY] as const)
 | 
	
		
			
				|  |  | -          : point;
 | 
	
		
			
				|  |  | -      });
 | 
	
		
			
				|  |  | +    if (selectedOriginPoint) {
 | 
	
		
			
				|  |  | +      offsetX =
 | 
	
		
			
				|  |  | +        selectedOriginPoint.point[0] - points[selectedOriginPoint.index][0];
 | 
	
		
			
				|  |  | +      offsetY =
 | 
	
		
			
				|  |  | +        selectedOriginPoint.point[1] - points[selectedOriginPoint.index][1];
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +    const nextPoints = points.map((point, idx) => {
 | 
	
		
			
				|  |  | +      const selectedPointData = targetPoints.find((p) => p.index === idx);
 | 
	
		
			
				|  |  | +      if (selectedPointData) {
 | 
	
		
			
				|  |  | +        if (selectedOriginPoint) {
 | 
	
		
			
				|  |  | +          return point;
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        const deltaX =
 | 
	
		
			
				|  |  | +          selectedPointData.point[0] - points[selectedPointData.index][0];
 | 
	
		
			
				|  |  | +        const deltaY =
 | 
	
		
			
				|  |  | +          selectedPointData.point[1] - points[selectedPointData.index][1];
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        return [point[0] + deltaX, point[1] + deltaY] as const;
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +      return offsetX || offsetY
 | 
	
		
			
				|  |  | +        ? ([point[0] - offsetX, point[1] - offsetY] as const)
 | 
	
		
			
				|  |  | +        : point;
 | 
	
		
			
				|  |  | +    });
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    LinearElementEditor._updatePoints(
 | 
	
		
			
				|  |  | +      element,
 | 
	
		
			
				|  |  | +      nextPoints,
 | 
	
		
			
				|  |  | +      offsetX,
 | 
	
		
			
				|  |  | +      offsetY,
 | 
	
		
			
				|  |  | +      otherUpdates,
 | 
	
		
			
				|  |  | +    );
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  private static _updatePoints(
 | 
	
		
			
				|  |  | +    element: NonDeleted<ExcalidrawLinearElement>,
 | 
	
		
			
				|  |  | +    nextPoints: readonly Point[],
 | 
	
		
			
				|  |  | +    offsetX: number,
 | 
	
		
			
				|  |  | +    offsetY: number,
 | 
	
		
			
				|  |  | +    otherUpdates?: { startBinding?: PointBinding; endBinding?: PointBinding },
 | 
	
		
			
				|  |  | +  ) {
 | 
	
		
			
				|  |  |      const nextCoords = getElementPointsCoords(
 | 
	
		
			
				|  |  |        element,
 | 
	
		
			
				|  |  |        nextPoints,
 | 
	
	
		
			
				|  | @@ -517,7 +822,7 @@ export class LinearElementEditor {
 | 
	
		
			
				|  |  |      );
 | 
	
		
			
				|  |  |      const prevCoords = getElementPointsCoords(
 | 
	
		
			
				|  |  |        element,
 | 
	
		
			
				|  |  | -      points,
 | 
	
		
			
				|  |  | +      element.points,
 | 
	
		
			
				|  |  |        element.strokeSharpness || "round",
 | 
	
		
			
				|  |  |      );
 | 
	
		
			
				|  |  |      const nextCenterX = (nextCoords[0] + nextCoords[2]) / 2;
 | 
	
	
		
			
				|  | @@ -536,3 +841,13 @@ export class LinearElementEditor {
 | 
	
		
			
				|  |  |      });
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +const normalizeSelectedPoints = (
 | 
	
		
			
				|  |  | +  points: (number | null)[],
 | 
	
		
			
				|  |  | +): number[] | null => {
 | 
	
		
			
				|  |  | +  let nextPoints = [
 | 
	
		
			
				|  |  | +    ...new Set(points.filter((p) => p !== null && p !== -1)),
 | 
	
		
			
				|  |  | +  ] as number[];
 | 
	
		
			
				|  |  | +  nextPoints = nextPoints.sort((a, b) => a - b);
 | 
	
		
			
				|  |  | +  return nextPoints.length ? nextPoints : null;
 | 
	
		
			
				|  |  | +};
 |