excalidraw/src/actions/actionFinalize.tsx
Michal Srb 26f67d27ec
Allow binding linear elements to other elements (#1899)
* Refactor: simplify linear element type

* Refactor: dedupe scrollbar handling

* First step towards binding - establish relationship and basic test for dragged lines

* Refactor: use zoom from appstate

* Refactor: generalize getElementAtPosition

* Only consider bindable elements in hit test

* Refactor: pull out pieces of hit test for reuse later

* Refactor: pull out diamond from hit test for reuse later

* Refactor: pull out text from hit test for reuse later

* Suggest binding when hovering

* Give shapes in regression test real size

* Give shapes in undo/redo test real size

* Keep bound element highlighted

* Show binding suggestion for multi-point elements

* Move binding to its on module with functions so that I can use it from actions, add support for binding end of multi-point elements

* Use Id instead of ID

* Improve boundary offset for non-squarish elements

* Fix localStorage for binding on linear elements

* Simplify dragging code and fix elements bound twice to the same shape

* Fix binding for rectangles

* Bind both ends at the end of the linear element creation, needed for focus points

* wip

* Refactor: Renames and reshapes for next commit

* Calculate and store focus points and gaps, but dont use them yet

* Focus points for rectangles

* Dont blow up when canceling linear element

* Stop suggesting binding when a non-compatible tool is selected

* Clean up collision code

* Using Geometric Algebra for hit tests

* Correct binding for all shapes

* Constant gap around polygon corners

* Fix rotation handling

* Generalize update and fix hit test for rotated elements

* Handle rotation realtime

* Handle scaling

* Remove vibration when moving bound and binding element together

* Handle simultenous scaling

* Allow binding and unbinding when editing linear elements

* Dont delete binding when the end point wasnt touched

* Bind on enter/escape when editing

* Support multiple suggested bindable elements in preparation for supporting linear elements dragging

* Update binding when moving linear elements

* Update binding when resizing linear elements

* Dont re-render UI on binding hints

* Update both ends when one is moved

* Use distance instead of focus point for binding

* Complicated approach for posterity, ignore this commit

* Revert the complicated approach

* Better focus point strategy, working for all shapes

* Update snapshots

* Dont break binding gap when mirroring shape

* Dont break binding gap when grid mode pushes it inside

* Dont bind draw elements

* Support alt duplication

* Fix alt duplication to

* Support cmd+D duplication

* All copy mechanisms are supported

* Allow binding shapes to arrows, having arrows created first

* Prevent arrows from disappearing for ellipses

* Better binding suggestion highlight for shapes

* Dont suggest second binding for simple elements when editing or moving them

* Dont steal already bound linear elements when moving shapes

* Fix highlighting diamonds and more precisely highlight other shapes

* Highlight linear element edges for binding

* Highlight text binding too

* Handle deletion

* Dont suggest second binding for simple linear elements when creating them

* Dont highlight bound element during creation

* Fix binding for rotated linear elements

* Fix collision check for ellipses

* Dont show suggested bindings for selected pairs

* Bind multi-point linear elements when the tool is switched - important for mobile

* Handle unbinding one of two bound edges correctly

* Rename boundElement in state to startBoundElement

* Dont double account for zoom when rendering binding highlight

* Fix rendering of edited linear element point handles

* Suggest binding when adding new point to a linear element

* Bind when adding a new point to a linear element and dont unbind when moving middle elements

* Handle deleting points

* Add cmd modifier key to disable binding

* Use state for enabling binding, fix not binding for linear elements during creation

* Drop support for binding lines, only arrows are bindable

* Reset binding mode on blur

* Fix not binding lines
2020-08-08 21:04:15 -07:00

169 lines
5.0 KiB
TypeScript

import { KEYS } from "../keys";
import { isInvisiblySmallElement } from "../element";
import { resetCursor } from "../utils";
import React from "react";
import { ToolButton } from "../components/ToolButton";
import { done } from "../components/icons";
import { t } from "../i18n";
import { register } from "./register";
import { mutateElement } from "../element/mutateElement";
import { isPathALoop } from "../math";
import { LinearElementEditor } from "../element/linearElementEditor";
import Scene from "../scene/Scene";
import {
maybeBindLinearElement,
bindOrUnbindLinearElement,
} from "../element/binding";
import { isBindingElement } from "../element/typeChecks";
export const actionFinalize = register({
name: "finalize",
perform: (elements, appState) => {
if (appState.editingLinearElement) {
const {
elementId,
startBindingElement,
endBindingElement,
} = appState.editingLinearElement;
const element = LinearElementEditor.getElement(elementId);
if (element) {
if (isBindingElement(element)) {
bindOrUnbindLinearElement(
element,
startBindingElement,
endBindingElement,
);
}
return {
elements:
element.points.length < 2 || isInvisiblySmallElement(element)
? elements.filter((el) => el.id !== element.id)
: undefined,
appState: {
...appState,
editingLinearElement: null,
},
commitToHistory: true,
};
}
}
let newElements = elements;
if (window.document.activeElement instanceof HTMLElement) {
window.document.activeElement.blur();
}
const multiPointElement = appState.multiElement
? appState.multiElement
: appState.editingElement?.type === "draw"
? appState.editingElement
: null;
if (multiPointElement) {
// pen and mouse have hover
if (
multiPointElement.type !== "draw" &&
appState.lastPointerDownWith !== "touch"
) {
const { points, lastCommittedPoint } = multiPointElement;
if (
!lastCommittedPoint ||
points[points.length - 1] !== lastCommittedPoint
) {
mutateElement(multiPointElement, {
points: multiPointElement.points.slice(0, -1),
});
}
}
if (isInvisiblySmallElement(multiPointElement)) {
newElements = newElements.slice(0, -1);
}
// If the multi point line closes the loop,
// set the last point to first point.
// This ensures that loop remains closed at different scales.
const isLoop = isPathALoop(multiPointElement.points);
if (
multiPointElement.type === "line" ||
multiPointElement.type === "draw"
) {
if (isLoop) {
const linePoints = multiPointElement.points;
const firstPoint = linePoints[0];
mutateElement(multiPointElement, {
points: linePoints.map((point, i) =>
i === linePoints.length - 1
? ([firstPoint[0], firstPoint[1]] as const)
: point,
),
});
}
}
if (
isBindingElement(multiPointElement) &&
!isLoop &&
multiPointElement.points.length > 1
) {
const [x, y] = LinearElementEditor.getPointAtIndexGlobalCoordinates(
multiPointElement,
-1,
);
maybeBindLinearElement(
multiPointElement,
appState,
Scene.getScene(multiPointElement)!,
{ x, y },
);
}
if (!appState.elementLocked) {
appState.selectedElementIds[multiPointElement.id] = true;
}
}
if (!appState.elementLocked || !multiPointElement) {
resetCursor();
}
return {
elements: newElements,
appState: {
...appState,
elementType:
appState.elementLocked && multiPointElement
? appState.elementType
: "selection",
draggingElement: null,
multiElement: null,
editingElement: null,
startBoundElement: null,
suggestedBindings: [],
selectedElementIds:
multiPointElement && !appState.elementLocked
? {
...appState.selectedElementIds,
[multiPointElement.id]: true,
}
: appState.selectedElementIds,
},
commitToHistory: appState.elementType === "draw",
};
},
keyTest: (event, appState) =>
(event.key === KEYS.ESCAPE &&
(appState.editingLinearElement !== null ||
(!appState.draggingElement && appState.multiElement === null))) ||
((event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) &&
appState.multiElement !== null),
PanelComponent: ({ appState, updateData }) => (
<ToolButton
type="button"
icon={done}
title={t("buttons.done")}
aria-label={t("buttons.done")}
onClick={updateData}
visible={appState.multiElement != null}
/>
),
});