|
@@ -31,6 +31,7 @@ import {
|
|
dragSelectedElements,
|
|
dragSelectedElements,
|
|
getDragOffsetXY,
|
|
getDragOffsetXY,
|
|
dragNewElement,
|
|
dragNewElement,
|
|
|
|
+ hitTest,
|
|
} from "../element";
|
|
} from "../element";
|
|
import {
|
|
import {
|
|
getElementsWithinSelection,
|
|
getElementsWithinSelection,
|
|
@@ -60,6 +61,8 @@ import {
|
|
ExcalidrawTextElement,
|
|
ExcalidrawTextElement,
|
|
NonDeleted,
|
|
NonDeleted,
|
|
ExcalidrawGenericElement,
|
|
ExcalidrawGenericElement,
|
|
|
|
+ ExcalidrawLinearElement,
|
|
|
|
+ ExcalidrawBindableElement,
|
|
} from "../element/types";
|
|
} from "../element/types";
|
|
|
|
|
|
import { distance2d, isPathALoop, getGridPoint } from "../math";
|
|
import { distance2d, isPathALoop, getGridPoint } from "../math";
|
|
@@ -136,7 +139,13 @@ import { generateCollaborationLink, getCollaborationLinkData } from "../data";
|
|
import { mutateElement, newElementWith } from "../element/mutateElement";
|
|
import { mutateElement, newElementWith } from "../element/mutateElement";
|
|
import { invalidateShapeForElement } from "../renderer/renderElement";
|
|
import { invalidateShapeForElement } from "../renderer/renderElement";
|
|
import { unstable_batchedUpdates } from "react-dom";
|
|
import { unstable_batchedUpdates } from "react-dom";
|
|
-import { isLinearElement } from "../element/typeChecks";
|
|
|
|
|
|
+import {
|
|
|
|
+ isLinearElement,
|
|
|
|
+ isLinearElementType,
|
|
|
|
+ isBindingElement,
|
|
|
|
+ isBindingElementType,
|
|
|
|
+ isBindableElement,
|
|
|
|
+} from "../element/typeChecks";
|
|
import { actionFinalize, actionDeleteSelected } from "../actions";
|
|
import { actionFinalize, actionDeleteSelected } from "../actions";
|
|
import {
|
|
import {
|
|
restoreUsernameFromLocalStorage,
|
|
restoreUsernameFromLocalStorage,
|
|
@@ -154,6 +163,19 @@ import {
|
|
} from "../groups";
|
|
} from "../groups";
|
|
import { Library } from "../data/library";
|
|
import { Library } from "../data/library";
|
|
import Scene from "../scene/Scene";
|
|
import Scene from "../scene/Scene";
|
|
|
|
+import {
|
|
|
|
+ getHoveredElementForBinding,
|
|
|
|
+ maybeBindLinearElement,
|
|
|
|
+ getEligibleElementsForBinding,
|
|
|
|
+ bindOrUnbindSelectedElements,
|
|
|
|
+ unbindLinearElements,
|
|
|
|
+ fixBindingsAfterDuplication,
|
|
|
|
+ maybeBindBindableElement,
|
|
|
|
+ getElligibleElementForBindingElementAtCoors,
|
|
|
|
+ fixBindingsAfterDeletion,
|
|
|
|
+ isLinearElementSimpleAndAlreadyBound,
|
|
|
|
+ isBindingEnabled,
|
|
|
|
+} from "../element/binding";
|
|
|
|
|
|
/**
|
|
/**
|
|
* @param func handler taking at most single parameter (event).
|
|
* @param func handler taking at most single parameter (event).
|
|
@@ -407,6 +429,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
|
|
|
|
private onBlur = withBatchedUpdates(() => {
|
|
private onBlur = withBatchedUpdates(() => {
|
|
isHoldingSpace = false;
|
|
isHoldingSpace = false;
|
|
|
|
+ this.setState({ isBindingEnabled: true });
|
|
this.saveDebounced();
|
|
this.saveDebounced();
|
|
this.saveDebounced.flush();
|
|
this.saveDebounced.flush();
|
|
});
|
|
});
|
|
@@ -690,7 +713,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
this.broadcastScene(SCENE.UPDATE, /* syncAll */ true);
|
|
this.broadcastScene(SCENE.UPDATE, /* syncAll */ true);
|
|
}, SYNC_FULL_SCENE_INTERVAL_MS);
|
|
}, SYNC_FULL_SCENE_INTERVAL_MS);
|
|
|
|
|
|
- componentDidUpdate(prevProps: ExcalidrawProps) {
|
|
|
|
|
|
+ componentDidUpdate(prevProps: ExcalidrawProps, prevState: AppState) {
|
|
const { width: prevWidth, height: prevHeight } = prevProps;
|
|
const { width: prevWidth, height: prevHeight } = prevProps;
|
|
const { width: currentWidth, height: currentHeight } = this.props;
|
|
const { width: currentWidth, height: currentHeight } = this.props;
|
|
if (prevWidth !== currentWidth || prevHeight !== currentHeight) {
|
|
if (prevWidth !== currentWidth || prevHeight !== currentHeight) {
|
|
@@ -714,6 +737,25 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
this.actionManager.executeAction(actionFinalize);
|
|
this.actionManager.executeAction(actionFinalize);
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
+ const { multiElement } = prevState;
|
|
|
|
+ if (
|
|
|
|
+ prevState.elementType !== this.state.elementType &&
|
|
|
|
+ multiElement != null &&
|
|
|
|
+ isBindingEnabled(this.state) &&
|
|
|
|
+ isBindingElement(multiElement)
|
|
|
|
+ ) {
|
|
|
|
+ maybeBindLinearElement(
|
|
|
|
+ multiElement,
|
|
|
|
+ this.state,
|
|
|
|
+ this.scene,
|
|
|
|
+ tupleToCoors(
|
|
|
|
+ LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
|
|
|
+ multiElement,
|
|
|
|
+ -1,
|
|
|
|
+ ),
|
|
|
|
+ ),
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
|
|
const cursorButton: {
|
|
const cursorButton: {
|
|
[id: string]: string | undefined;
|
|
[id: string]: string | undefined;
|
|
@@ -950,16 +992,31 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
const dy = y - elementsCenterY;
|
|
const dy = y - elementsCenterY;
|
|
const groupIdMap = new Map();
|
|
const groupIdMap = new Map();
|
|
|
|
|
|
|
|
+ const oldIdToDuplicatedId = new Map();
|
|
const newElements = clipboardElements.map((element) => {
|
|
const newElements = clipboardElements.map((element) => {
|
|
- return duplicateElement(this.state.editingGroupId, groupIdMap, element, {
|
|
|
|
- x: element.x + dx - minX,
|
|
|
|
- y: element.y + dy - minY,
|
|
|
|
- });
|
|
|
|
|
|
+ const newElement = duplicateElement(
|
|
|
|
+ this.state.editingGroupId,
|
|
|
|
+ groupIdMap,
|
|
|
|
+ element,
|
|
|
|
+ {
|
|
|
|
+ x: element.x + dx - minX,
|
|
|
|
+ y: element.y + dy - minY,
|
|
|
|
+ },
|
|
|
|
+ );
|
|
|
|
+ oldIdToDuplicatedId.set(element.id, newElement.id);
|
|
|
|
+ return newElement;
|
|
});
|
|
});
|
|
- this.scene.replaceAllElements([
|
|
|
|
|
|
+ const nextElements = [
|
|
...this.scene.getElementsIncludingDeleted(),
|
|
...this.scene.getElementsIncludingDeleted(),
|
|
...newElements,
|
|
...newElements,
|
|
- ]);
|
|
|
|
|
|
+ ];
|
|
|
|
+ fixBindingsAfterDuplication(
|
|
|
|
+ nextElements,
|
|
|
|
+ clipboardElements,
|
|
|
|
+ oldIdToDuplicatedId,
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ this.scene.replaceAllElements(nextElements);
|
|
history.resumeRecording();
|
|
history.resumeRecording();
|
|
this.setState(
|
|
this.setState(
|
|
selectGroupsForSelectedElements(
|
|
selectGroupsForSelectedElements(
|
|
@@ -1403,6 +1460,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
if (event[KEYS.CTRL_OR_CMD] && event.keyCode === KEYS.GRID_KEY_CODE) {
|
|
if (event[KEYS.CTRL_OR_CMD] && event.keyCode === KEYS.GRID_KEY_CODE) {
|
|
this.toggleGridMode();
|
|
this.toggleGridMode();
|
|
}
|
|
}
|
|
|
|
+ if (event[KEYS.CTRL_OR_CMD]) {
|
|
|
|
+ this.setState({ isBindingEnabled: false });
|
|
|
|
+ }
|
|
|
|
|
|
if (event.code === "KeyC" && event.altKey && event.shiftKey) {
|
|
if (event.code === "KeyC" && event.altKey && event.shiftKey) {
|
|
this.copyToClipboardAsPng();
|
|
this.copyToClipboardAsPng();
|
|
@@ -1511,6 +1571,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
}
|
|
}
|
|
isHoldingSpace = false;
|
|
isHoldingSpace = false;
|
|
}
|
|
}
|
|
|
|
+ if (!event[KEYS.CTRL_OR_CMD] && !this.state.isBindingEnabled) {
|
|
|
|
+ this.setState({ isBindingEnabled: true });
|
|
|
|
+ }
|
|
});
|
|
});
|
|
|
|
|
|
private selectShapeTool(elementType: AppState["elementType"]) {
|
|
private selectShapeTool(elementType: AppState["elementType"]) {
|
|
@@ -1520,6 +1583,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
if (isToolIcon(document.activeElement)) {
|
|
if (isToolIcon(document.activeElement)) {
|
|
document.activeElement.blur();
|
|
document.activeElement.blur();
|
|
}
|
|
}
|
|
|
|
+ if (!isLinearElementType(elementType)) {
|
|
|
|
+ this.setState({ suggestedBindings: [] });
|
|
|
|
+ }
|
|
if (elementType !== "selection") {
|
|
if (elementType !== "selection") {
|
|
this.setState({
|
|
this.setState({
|
|
elementType,
|
|
elementType,
|
|
@@ -1558,10 +1624,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
gesture.initialScale = null;
|
|
gesture.initialScale = null;
|
|
});
|
|
});
|
|
|
|
|
|
- private setElements = (elements: readonly ExcalidrawElement[]) => {
|
|
|
|
- this.scene.replaceAllElements(elements);
|
|
|
|
- };
|
|
|
|
-
|
|
|
|
private handleTextWysiwyg(
|
|
private handleTextWysiwyg(
|
|
element: ExcalidrawTextElement,
|
|
element: ExcalidrawTextElement,
|
|
{
|
|
{
|
|
@@ -1612,6 +1674,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
[element.id]: true,
|
|
[element.id]: true,
|
|
},
|
|
},
|
|
}));
|
|
}));
|
|
|
|
+ } else {
|
|
|
|
+ fixBindingsAfterDeletion(this.scene.getElements(), [element]);
|
|
}
|
|
}
|
|
if (!isDeleted || isExistingElement) {
|
|
if (!isDeleted || isExistingElement) {
|
|
history.resumeRecording();
|
|
history.resumeRecording();
|
|
@@ -1643,13 +1707,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
x: number,
|
|
x: number,
|
|
y: number,
|
|
y: number,
|
|
): NonDeleted<ExcalidrawTextElement> | null {
|
|
): NonDeleted<ExcalidrawTextElement> | null {
|
|
- const element = getElementAtPosition(
|
|
|
|
- this.scene.getElements(),
|
|
|
|
- this.state,
|
|
|
|
- x,
|
|
|
|
- y,
|
|
|
|
- this.state.zoom,
|
|
|
|
- );
|
|
|
|
|
|
+ const element = this.getElementAtPosition(x, y);
|
|
|
|
|
|
if (element && isTextElement(element) && !element.isDeleted) {
|
|
if (element && isTextElement(element) && !element.isDeleted) {
|
|
return element;
|
|
return element;
|
|
@@ -1657,6 +1715,15 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
return null;
|
|
return null;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ private getElementAtPosition(
|
|
|
|
+ x: number,
|
|
|
|
+ y: number,
|
|
|
|
+ ): NonDeleted<ExcalidrawElement> | null {
|
|
|
|
+ return getElementAtPosition(this.scene.getElements(), (element) =>
|
|
|
|
+ hitTest(element, this.state, x, y),
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
+
|
|
private startTextEditing = ({
|
|
private startTextEditing = ({
|
|
sceneX,
|
|
sceneX,
|
|
sceneY,
|
|
sceneY,
|
|
@@ -1786,14 +1853,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
const selectedGroupIds = getSelectedGroupIds(this.state);
|
|
const selectedGroupIds = getSelectedGroupIds(this.state);
|
|
|
|
|
|
if (selectedGroupIds.length > 0) {
|
|
if (selectedGroupIds.length > 0) {
|
|
- const elements = this.scene.getElements();
|
|
|
|
- const hitElement = getElementAtPosition(
|
|
|
|
- elements,
|
|
|
|
- this.state,
|
|
|
|
- sceneX,
|
|
|
|
- sceneY,
|
|
|
|
- this.state.zoom,
|
|
|
|
- );
|
|
|
|
|
|
+ const hitElement = this.getElementAtPosition(sceneX, sceneY);
|
|
|
|
|
|
const selectedGroupId =
|
|
const selectedGroupId =
|
|
hitElement &&
|
|
hitElement &&
|
|
@@ -1873,12 +1933,13 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- const { x: scenePointerX, y: scenePointerY } = viewportCoordsToSceneCoords(
|
|
|
|
|
|
+ const scenePointer = viewportCoordsToSceneCoords(
|
|
event,
|
|
event,
|
|
this.state,
|
|
this.state,
|
|
this.canvas,
|
|
this.canvas,
|
|
window.devicePixelRatio,
|
|
window.devicePixelRatio,
|
|
);
|
|
);
|
|
|
|
+ const { x: scenePointerX, y: scenePointerY } = scenePointer;
|
|
|
|
|
|
if (
|
|
if (
|
|
this.state.editingLinearElement &&
|
|
this.state.editingLinearElement &&
|
|
@@ -1894,6 +1955,27 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
if (editingLinearElement !== this.state.editingLinearElement) {
|
|
if (editingLinearElement !== this.state.editingLinearElement) {
|
|
this.setState({ editingLinearElement });
|
|
this.setState({ editingLinearElement });
|
|
}
|
|
}
|
|
|
|
+ if (editingLinearElement.lastUncommittedPoint != null) {
|
|
|
|
+ this.maybeSuggestBindingAtCursor(scenePointer);
|
|
|
|
+ } else {
|
|
|
|
+ this.setState({ suggestedBindings: [] });
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (isBindingElementType(this.state.elementType)) {
|
|
|
|
+ // Hovering with a selected tool or creating new linear element via click
|
|
|
|
+ // and point
|
|
|
|
+ const { draggingElement } = this.state;
|
|
|
|
+ if (isBindingElement(draggingElement)) {
|
|
|
|
+ this.maybeSuggestBindingForLinearElementAtCursor(
|
|
|
|
+ draggingElement,
|
|
|
|
+ "end",
|
|
|
|
+ scenePointer,
|
|
|
|
+ this.state.startBoundElement,
|
|
|
|
+ );
|
|
|
|
+ } else {
|
|
|
|
+ this.maybeSuggestBindingAtCursor(scenePointer);
|
|
|
|
+ }
|
|
}
|
|
}
|
|
|
|
|
|
if (this.state.multiElement) {
|
|
if (this.state.multiElement) {
|
|
@@ -1954,6 +2036,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
});
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
+
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
@@ -2003,13 +2086,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
- const hitElement = getElementAtPosition(
|
|
|
|
- elements,
|
|
|
|
- this.state,
|
|
|
|
- scenePointerX,
|
|
|
|
- scenePointerY,
|
|
|
|
- this.state.zoom,
|
|
|
|
- );
|
|
|
|
|
|
+ const hitElement = this.getElementAtPosition(scenePointerX, scenePointerY);
|
|
if (this.state.elementType === "text") {
|
|
if (this.state.elementType === "text") {
|
|
document.documentElement.style.cursor = isTextElement(hitElement)
|
|
document.documentElement.style.cursor = isTextElement(hitElement)
|
|
? CURSOR_TYPE.TEXT
|
|
? CURSOR_TYPE.TEXT
|
|
@@ -2328,24 +2405,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
- if (pointerDownState.scrollbars.isOverHorizontal) {
|
|
|
|
- const x = event.clientX;
|
|
|
|
- const dx = x - pointerDownState.lastCoords.x;
|
|
|
|
- this.setState({
|
|
|
|
- scrollX: normalizeScroll(this.state.scrollX - dx / this.state.zoom),
|
|
|
|
- });
|
|
|
|
- pointerDownState.lastCoords.x = x;
|
|
|
|
- return;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- if (pointerDownState.scrollbars.isOverVertical) {
|
|
|
|
- const y = event.clientY;
|
|
|
|
- const dy = y - pointerDownState.lastCoords.y;
|
|
|
|
- this.setState({
|
|
|
|
- scrollY: normalizeScroll(this.state.scrollY - dy / this.state.zoom),
|
|
|
|
- });
|
|
|
|
- pointerDownState.lastCoords.y = y;
|
|
|
|
- }
|
|
|
|
|
|
+ this.handlePointerMoveOverScrollbars(event, pointerDownState);
|
|
});
|
|
});
|
|
|
|
|
|
const onPointerUp = withBatchedUpdates(() => {
|
|
const onPointerUp = withBatchedUpdates(() => {
|
|
@@ -2440,8 +2500,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
this.state,
|
|
this.state,
|
|
(appState) => this.setState(appState),
|
|
(appState) => this.setState(appState),
|
|
history,
|
|
history,
|
|
- pointerDownState.origin.x,
|
|
|
|
- pointerDownState.origin.y,
|
|
|
|
|
|
+ pointerDownState.origin,
|
|
);
|
|
);
|
|
if (ret.hitElement) {
|
|
if (ret.hitElement) {
|
|
pointerDownState.hit.element = ret.hitElement;
|
|
pointerDownState.hit.element = ret.hitElement;
|
|
@@ -2454,12 +2513,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
// hitElement may already be set above, so check first
|
|
// hitElement may already be set above, so check first
|
|
pointerDownState.hit.element =
|
|
pointerDownState.hit.element =
|
|
pointerDownState.hit.element ??
|
|
pointerDownState.hit.element ??
|
|
- getElementAtPosition(
|
|
|
|
- elements,
|
|
|
|
- this.state,
|
|
|
|
|
|
+ this.getElementAtPosition(
|
|
pointerDownState.origin.x,
|
|
pointerDownState.origin.x,
|
|
pointerDownState.origin.y,
|
|
pointerDownState.origin.y,
|
|
- this.state.zoom,
|
|
|
|
);
|
|
);
|
|
|
|
|
|
this.maybeClearSelectionWhenHittingElement(
|
|
this.maybeClearSelectionWhenHittingElement(
|
|
@@ -2544,7 +2600,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
|
|
|
|
private handleLinearElementOnPointerDown = (
|
|
private handleLinearElementOnPointerDown = (
|
|
event: React.PointerEvent<HTMLCanvasElement>,
|
|
event: React.PointerEvent<HTMLCanvasElement>,
|
|
- elementType: "draw" | "line" | "arrow",
|
|
|
|
|
|
+ elementType: ExcalidrawLinearElement["type"],
|
|
pointerDownState: PointerDownState,
|
|
pointerDownState: PointerDownState,
|
|
): void => {
|
|
): void => {
|
|
if (this.state.multiElement) {
|
|
if (this.state.multiElement) {
|
|
@@ -2616,6 +2672,10 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
mutateElement(element, {
|
|
mutateElement(element, {
|
|
points: [...element.points, [0, 0]],
|
|
points: [...element.points, [0, 0]],
|
|
});
|
|
});
|
|
|
|
+ const boundElement = getHoveredElementForBinding(
|
|
|
|
+ pointerDownState.origin,
|
|
|
|
+ this.scene,
|
|
|
|
+ );
|
|
this.scene.replaceAllElements([
|
|
this.scene.replaceAllElements([
|
|
...this.scene.getElementsIncludingDeleted(),
|
|
...this.scene.getElementsIncludingDeleted(),
|
|
element,
|
|
element,
|
|
@@ -2623,6 +2683,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
this.setState({
|
|
this.setState({
|
|
draggingElement: element,
|
|
draggingElement: element,
|
|
editingElement: element,
|
|
editingElement: element,
|
|
|
|
+ startBoundElement: boundElement,
|
|
|
|
+ suggestedBindings: [],
|
|
});
|
|
});
|
|
}
|
|
}
|
|
};
|
|
};
|
|
@@ -2690,33 +2752,21 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
- if (pointerDownState.scrollbars.isOverHorizontal) {
|
|
|
|
- const x = event.clientX;
|
|
|
|
- const dx = x - pointerDownState.lastCoords.x;
|
|
|
|
- this.setState({
|
|
|
|
- scrollX: normalizeScroll(this.state.scrollX - dx / this.state.zoom),
|
|
|
|
- });
|
|
|
|
- pointerDownState.lastCoords.x = x;
|
|
|
|
|
|
+ if (this.handlePointerMoveOverScrollbars(event, pointerDownState)) {
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
- if (pointerDownState.scrollbars.isOverVertical) {
|
|
|
|
- const y = event.clientY;
|
|
|
|
- const dy = y - pointerDownState.lastCoords.y;
|
|
|
|
- this.setState({
|
|
|
|
- scrollY: normalizeScroll(this.state.scrollY - dy / this.state.zoom),
|
|
|
|
- });
|
|
|
|
- pointerDownState.lastCoords.y = y;
|
|
|
|
- return;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- const { x, y } = viewportCoordsToSceneCoords(
|
|
|
|
|
|
+ const pointerCoords = viewportCoordsToSceneCoords(
|
|
event,
|
|
event,
|
|
this.state,
|
|
this.state,
|
|
this.canvas,
|
|
this.canvas,
|
|
window.devicePixelRatio,
|
|
window.devicePixelRatio,
|
|
);
|
|
);
|
|
- const [gridX, gridY] = getGridPoint(x, y, this.state.gridSize);
|
|
|
|
|
|
+ const [gridX, gridY] = getGridPoint(
|
|
|
|
+ pointerCoords.x,
|
|
|
|
+ pointerCoords.y,
|
|
|
|
+ this.state.gridSize,
|
|
|
|
+ );
|
|
|
|
|
|
// for arrows/lines, don't start dragging until a given threshold
|
|
// for arrows/lines, don't start dragging until a given threshold
|
|
// to ensure we don't create a 2-point arrow by mistake when
|
|
// to ensure we don't create a 2-point arrow by mistake when
|
|
@@ -2729,8 +2779,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
) {
|
|
) {
|
|
if (
|
|
if (
|
|
distance2d(
|
|
distance2d(
|
|
- x,
|
|
|
|
- y,
|
|
|
|
|
|
+ pointerCoords.x,
|
|
|
|
+ pointerCoords.y,
|
|
pointerDownState.origin.x,
|
|
pointerDownState.origin.x,
|
|
pointerDownState.origin.y,
|
|
pointerDownState.origin.y,
|
|
) < DRAGGING_THRESHOLD
|
|
) < DRAGGING_THRESHOLD
|
|
@@ -2753,8 +2803,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
isRotating: resizeHandle === "rotation",
|
|
isRotating: resizeHandle === "rotation",
|
|
});
|
|
});
|
|
const [resizeX, resizeY] = getGridPoint(
|
|
const [resizeX, resizeY] = getGridPoint(
|
|
- x - pointerDownState.resize.offset.x,
|
|
|
|
- y - pointerDownState.resize.offset.y,
|
|
|
|
|
|
+ pointerCoords.x - pointerDownState.resize.offset.x,
|
|
|
|
+ pointerCoords.y - pointerDownState.resize.offset.y,
|
|
this.state.gridSize,
|
|
this.state.gridSize,
|
|
);
|
|
);
|
|
if (
|
|
if (
|
|
@@ -2775,6 +2825,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
pointerDownState.resize.originalElements,
|
|
pointerDownState.resize.originalElements,
|
|
)
|
|
)
|
|
) {
|
|
) {
|
|
|
|
+ this.maybeSuggestBindingForAll(selectedElements);
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
@@ -2783,13 +2834,20 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
const didDrag = LinearElementEditor.handlePointDragging(
|
|
const didDrag = LinearElementEditor.handlePointDragging(
|
|
this.state,
|
|
this.state,
|
|
(appState) => this.setState(appState),
|
|
(appState) => this.setState(appState),
|
|
- x,
|
|
|
|
- y,
|
|
|
|
|
|
+ pointerCoords.x,
|
|
|
|
+ pointerCoords.y,
|
|
|
|
+ (element, startOrEnd) => {
|
|
|
|
+ this.maybeSuggestBindingForLinearElementAtCursor(
|
|
|
|
+ element,
|
|
|
|
+ startOrEnd,
|
|
|
|
+ pointerCoords,
|
|
|
|
+ );
|
|
|
|
+ },
|
|
);
|
|
);
|
|
|
|
|
|
if (didDrag) {
|
|
if (didDrag) {
|
|
- pointerDownState.lastCoords.x = x;
|
|
|
|
- pointerDownState.lastCoords.y = y;
|
|
|
|
|
|
+ pointerDownState.lastCoords.x = pointerCoords.x;
|
|
|
|
+ pointerDownState.lastCoords.y = pointerCoords.y;
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
@@ -2805,11 +2863,12 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
);
|
|
);
|
|
if (selectedElements.length > 0) {
|
|
if (selectedElements.length > 0) {
|
|
const [dragX, dragY] = getGridPoint(
|
|
const [dragX, dragY] = getGridPoint(
|
|
- x - pointerDownState.drag.offset.x,
|
|
|
|
- y - pointerDownState.drag.offset.y,
|
|
|
|
|
|
+ pointerCoords.x - pointerDownState.drag.offset.x,
|
|
|
|
+ pointerCoords.y - pointerDownState.drag.offset.y,
|
|
this.state.gridSize,
|
|
this.state.gridSize,
|
|
);
|
|
);
|
|
- dragSelectedElements(selectedElements, dragX, dragY);
|
|
|
|
|
|
+ dragSelectedElements(selectedElements, dragX, dragY, this.scene);
|
|
|
|
+ this.maybeSuggestBindingForAll(selectedElements);
|
|
|
|
|
|
// We duplicate the selected element if alt is pressed on pointer move
|
|
// We duplicate the selected element if alt is pressed on pointer move
|
|
if (event.altKey && !pointerDownState.hit.hasBeenDuplicated) {
|
|
if (event.altKey && !pointerDownState.hit.hasBeenDuplicated) {
|
|
@@ -2822,6 +2881,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
const nextElements = [];
|
|
const nextElements = [];
|
|
const elementsToAppend = [];
|
|
const elementsToAppend = [];
|
|
const groupIdMap = new Map();
|
|
const groupIdMap = new Map();
|
|
|
|
+ const oldIdToDuplicatedId = new Map();
|
|
for (const element of this.scene.getElementsIncludingDeleted()) {
|
|
for (const element of this.scene.getElementsIncludingDeleted()) {
|
|
if (
|
|
if (
|
|
this.state.selectedElementIds[element.id] ||
|
|
this.state.selectedElementIds[element.id] ||
|
|
@@ -2846,14 +2906,19 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
});
|
|
});
|
|
nextElements.push(duplicatedElement);
|
|
nextElements.push(duplicatedElement);
|
|
elementsToAppend.push(element);
|
|
elementsToAppend.push(element);
|
|
|
|
+ oldIdToDuplicatedId.set(element.id, duplicatedElement.id);
|
|
} else {
|
|
} else {
|
|
nextElements.push(element);
|
|
nextElements.push(element);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
- this.scene.replaceAllElements([
|
|
|
|
- ...nextElements,
|
|
|
|
- ...elementsToAppend,
|
|
|
|
- ]);
|
|
|
|
|
|
+ const nextSceneElements = [...nextElements, ...elementsToAppend];
|
|
|
|
+ fixBindingsAfterDuplication(
|
|
|
|
+ nextSceneElements,
|
|
|
|
+ elementsToAppend,
|
|
|
|
+ oldIdToDuplicatedId,
|
|
|
|
+ "duplicatesServeAsOld",
|
|
|
|
+ );
|
|
|
|
+ this.scene.replaceAllElements(nextSceneElements);
|
|
}
|
|
}
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
@@ -2872,8 +2937,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
let dx: number;
|
|
let dx: number;
|
|
let dy: number;
|
|
let dy: number;
|
|
if (draggingElement.type === "draw") {
|
|
if (draggingElement.type === "draw") {
|
|
- dx = x - draggingElement.x;
|
|
|
|
- dy = y - draggingElement.y;
|
|
|
|
|
|
+ dx = pointerCoords.x - draggingElement.x;
|
|
|
|
+ dy = pointerCoords.y - draggingElement.y;
|
|
} else {
|
|
} else {
|
|
dx = gridX - draggingElement.x;
|
|
dx = gridX - draggingElement.x;
|
|
dy = gridY - draggingElement.y;
|
|
dy = gridY - draggingElement.y;
|
|
@@ -2903,16 +2968,25 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
});
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
+ if (isBindingElement(draggingElement)) {
|
|
|
|
+ // When creating a linear element by dragging
|
|
|
|
+ this.maybeSuggestBindingForLinearElementAtCursor(
|
|
|
|
+ draggingElement,
|
|
|
|
+ "end",
|
|
|
|
+ pointerCoords,
|
|
|
|
+ this.state.startBoundElement,
|
|
|
|
+ );
|
|
|
|
+ }
|
|
} else if (draggingElement.type === "selection") {
|
|
} else if (draggingElement.type === "selection") {
|
|
dragNewElement(
|
|
dragNewElement(
|
|
draggingElement,
|
|
draggingElement,
|
|
this.state.elementType,
|
|
this.state.elementType,
|
|
pointerDownState.origin.x,
|
|
pointerDownState.origin.x,
|
|
pointerDownState.origin.y,
|
|
pointerDownState.origin.y,
|
|
- x,
|
|
|
|
- y,
|
|
|
|
- distance(pointerDownState.origin.x, x),
|
|
|
|
- distance(pointerDownState.origin.y, y),
|
|
|
|
|
|
+ pointerCoords.x,
|
|
|
|
+ pointerCoords.y,
|
|
|
|
+ distance(pointerDownState.origin.x, pointerCoords.x),
|
|
|
|
+ distance(pointerDownState.origin.y, pointerCoords.y),
|
|
getResizeWithSidesSameLengthKey(event),
|
|
getResizeWithSidesSameLengthKey(event),
|
|
getResizeCenterPointKey(event),
|
|
getResizeCenterPointKey(event),
|
|
);
|
|
);
|
|
@@ -2929,6 +3003,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
getResizeWithSidesSameLengthKey(event),
|
|
getResizeWithSidesSameLengthKey(event),
|
|
getResizeCenterPointKey(event),
|
|
getResizeCenterPointKey(event),
|
|
);
|
|
);
|
|
|
|
+ this.maybeSuggestBindingForAll([draggingElement]);
|
|
}
|
|
}
|
|
|
|
|
|
if (this.state.elementType === "selection") {
|
|
if (this.state.elementType === "selection") {
|
|
@@ -2963,6 +3038,33 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ // Returns whether the pointer move happened over either scrollbar
|
|
|
|
+ private handlePointerMoveOverScrollbars(
|
|
|
|
+ event: PointerEvent,
|
|
|
|
+ pointerDownState: PointerDownState,
|
|
|
|
+ ): boolean {
|
|
|
|
+ if (pointerDownState.scrollbars.isOverHorizontal) {
|
|
|
|
+ const x = event.clientX;
|
|
|
|
+ const dx = x - pointerDownState.lastCoords.x;
|
|
|
|
+ this.setState({
|
|
|
|
+ scrollX: normalizeScroll(this.state.scrollX - dx / this.state.zoom),
|
|
|
|
+ });
|
|
|
|
+ pointerDownState.lastCoords.x = x;
|
|
|
|
+ return true;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (pointerDownState.scrollbars.isOverVertical) {
|
|
|
|
+ const y = event.clientY;
|
|
|
|
+ const dy = y - pointerDownState.lastCoords.y;
|
|
|
|
+ this.setState({
|
|
|
|
+ scrollY: normalizeScroll(this.state.scrollY - dy / this.state.zoom),
|
|
|
|
+ });
|
|
|
|
+ pointerDownState.lastCoords.y = y;
|
|
|
|
+ return true;
|
|
|
|
+ }
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+
|
|
private onPointerUpFromPointerDownHandler(
|
|
private onPointerUpFromPointerDownHandler(
|
|
pointerDownState: PointerDownState,
|
|
pointerDownState: PointerDownState,
|
|
): (event: PointerEvent) => void {
|
|
): (event: PointerEvent) => void {
|
|
@@ -2973,6 +3075,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
multiElement,
|
|
multiElement,
|
|
elementType,
|
|
elementType,
|
|
elementLocked,
|
|
elementLocked,
|
|
|
|
+ isResizing,
|
|
} = this.state;
|
|
} = this.state;
|
|
|
|
|
|
this.setState({
|
|
this.setState({
|
|
@@ -2991,14 +3094,19 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
|
|
|
|
this.savePointer(childEvent.clientX, childEvent.clientY, "up");
|
|
this.savePointer(childEvent.clientX, childEvent.clientY, "up");
|
|
|
|
|
|
- // if moving start/end point towards start/end point within threshold,
|
|
|
|
- // close the loop
|
|
|
|
|
|
+ // Handle end of dragging a point of a linear element, might close a loop
|
|
|
|
+ // and sets binding element
|
|
if (this.state.editingLinearElement) {
|
|
if (this.state.editingLinearElement) {
|
|
const editingLinearElement = LinearElementEditor.handlePointerUp(
|
|
const editingLinearElement = LinearElementEditor.handlePointerUp(
|
|
|
|
+ childEvent,
|
|
this.state.editingLinearElement,
|
|
this.state.editingLinearElement,
|
|
|
|
+ this.state,
|
|
);
|
|
);
|
|
if (editingLinearElement !== this.state.editingLinearElement) {
|
|
if (editingLinearElement !== this.state.editingLinearElement) {
|
|
- this.setState({ editingLinearElement });
|
|
|
|
|
|
+ this.setState({
|
|
|
|
+ editingLinearElement,
|
|
|
|
+ suggestedBindings: [],
|
|
|
|
+ });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
@@ -3021,21 +3129,24 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
if (draggingElement!.points.length > 1) {
|
|
if (draggingElement!.points.length > 1) {
|
|
history.resumeRecording();
|
|
history.resumeRecording();
|
|
}
|
|
}
|
|
|
|
+ const pointerCoords = viewportCoordsToSceneCoords(
|
|
|
|
+ childEvent,
|
|
|
|
+ this.state,
|
|
|
|
+ this.canvas,
|
|
|
|
+ window.devicePixelRatio,
|
|
|
|
+ );
|
|
if (
|
|
if (
|
|
!pointerDownState.drag.hasOccurred &&
|
|
!pointerDownState.drag.hasOccurred &&
|
|
draggingElement &&
|
|
draggingElement &&
|
|
!multiElement
|
|
!multiElement
|
|
) {
|
|
) {
|
|
- const { x, y } = viewportCoordsToSceneCoords(
|
|
|
|
- childEvent,
|
|
|
|
- this.state,
|
|
|
|
- this.canvas,
|
|
|
|
- window.devicePixelRatio,
|
|
|
|
- );
|
|
|
|
mutateElement(draggingElement, {
|
|
mutateElement(draggingElement, {
|
|
points: [
|
|
points: [
|
|
...draggingElement.points,
|
|
...draggingElement.points,
|
|
- [x - draggingElement.x, y - draggingElement.y],
|
|
|
|
|
|
+ [
|
|
|
|
+ pointerCoords.x - draggingElement.x,
|
|
|
|
+ pointerCoords.y - draggingElement.y,
|
|
|
|
+ ],
|
|
],
|
|
],
|
|
});
|
|
});
|
|
this.setState({
|
|
this.setState({
|
|
@@ -3043,6 +3154,18 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
editingElement: this.state.draggingElement,
|
|
editingElement: this.state.draggingElement,
|
|
});
|
|
});
|
|
} else if (pointerDownState.drag.hasOccurred && !multiElement) {
|
|
} else if (pointerDownState.drag.hasOccurred && !multiElement) {
|
|
|
|
+ if (
|
|
|
|
+ isBindingEnabled(this.state) &&
|
|
|
|
+ isBindingElement(draggingElement)
|
|
|
|
+ ) {
|
|
|
|
+ maybeBindLinearElement(
|
|
|
|
+ draggingElement,
|
|
|
|
+ this.state,
|
|
|
|
+ this.scene,
|
|
|
|
+ pointerCoords,
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
+ this.setState({ suggestedBindings: [], startBoundElement: null });
|
|
if (!elementLocked) {
|
|
if (!elementLocked) {
|
|
resetCursor();
|
|
resetCursor();
|
|
this.setState((prevState) => ({
|
|
this.setState((prevState) => ({
|
|
@@ -3086,6 +3209,13 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
draggingElement,
|
|
draggingElement,
|
|
getNormalizedDimensions(draggingElement),
|
|
getNormalizedDimensions(draggingElement),
|
|
);
|
|
);
|
|
|
|
+
|
|
|
|
+ if (
|
|
|
|
+ isBindingEnabled(this.state) &&
|
|
|
|
+ isBindableElement(draggingElement)
|
|
|
|
+ ) {
|
|
|
|
+ maybeBindBindableElement(draggingElement);
|
|
|
|
+ }
|
|
}
|
|
}
|
|
|
|
|
|
if (resizingElement) {
|
|
if (resizingElement) {
|
|
@@ -3155,20 +3285,80 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
history.resumeRecording();
|
|
history.resumeRecording();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ if (pointerDownState.drag.hasOccurred || isResizing) {
|
|
|
|
+ (isBindingEnabled(this.state)
|
|
|
|
+ ? bindOrUnbindSelectedElements
|
|
|
|
+ : unbindLinearElements)(
|
|
|
|
+ getSelectedElements(this.scene.getElements(), this.state),
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
+
|
|
if (!elementLocked) {
|
|
if (!elementLocked) {
|
|
resetCursor();
|
|
resetCursor();
|
|
this.setState({
|
|
this.setState({
|
|
draggingElement: null,
|
|
draggingElement: null,
|
|
|
|
+ suggestedBindings: [],
|
|
elementType: "selection",
|
|
elementType: "selection",
|
|
});
|
|
});
|
|
} else {
|
|
} else {
|
|
this.setState({
|
|
this.setState({
|
|
draggingElement: null,
|
|
draggingElement: null,
|
|
|
|
+ suggestedBindings: [],
|
|
});
|
|
});
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ private maybeSuggestBindingAtCursor = (pointerCoords: {
|
|
|
|
+ x: number;
|
|
|
|
+ y: number;
|
|
|
|
+ }): void => {
|
|
|
|
+ const hoveredBindableElement = getHoveredElementForBinding(
|
|
|
|
+ pointerCoords,
|
|
|
|
+ this.scene,
|
|
|
|
+ );
|
|
|
|
+ this.setState({
|
|
|
|
+ suggestedBindings:
|
|
|
|
+ hoveredBindableElement != null ? [hoveredBindableElement] : [],
|
|
|
|
+ });
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ private maybeSuggestBindingForLinearElementAtCursor = (
|
|
|
|
+ linearElement: NonDeleted<ExcalidrawLinearElement>,
|
|
|
|
+ startOrEnd: "start" | "end",
|
|
|
|
+ pointerCoords: {
|
|
|
|
+ x: number;
|
|
|
|
+ y: number;
|
|
|
|
+ },
|
|
|
|
+ // During line creation the start binding hasn't been written yet
|
|
|
|
+ // into `linearElement`
|
|
|
|
+ oppositeBindingBoundElement?: ExcalidrawBindableElement | null,
|
|
|
|
+ ): void => {
|
|
|
|
+ const hoveredBindableElement = getElligibleElementForBindingElementAtCoors(
|
|
|
|
+ linearElement,
|
|
|
|
+ startOrEnd,
|
|
|
|
+ pointerCoords,
|
|
|
|
+ );
|
|
|
|
+ this.setState({
|
|
|
|
+ suggestedBindings:
|
|
|
|
+ hoveredBindableElement != null &&
|
|
|
|
+ !isLinearElementSimpleAndAlreadyBound(
|
|
|
|
+ linearElement,
|
|
|
|
+ oppositeBindingBoundElement?.id,
|
|
|
|
+ hoveredBindableElement,
|
|
|
|
+ )
|
|
|
|
+ ? [hoveredBindableElement]
|
|
|
|
+ : [],
|
|
|
|
+ });
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ private maybeSuggestBindingForAll(
|
|
|
|
+ selectedElements: NonDeleted<ExcalidrawElement>[],
|
|
|
|
+ ): void {
|
|
|
|
+ const suggestedBindings = getEligibleElementsForBinding(selectedElements);
|
|
|
|
+ this.setState({ suggestedBindings });
|
|
|
|
+ }
|
|
|
|
+
|
|
private maybeClearSelectionWhenHittingElement(
|
|
private maybeClearSelectionWhenHittingElement(
|
|
event: React.PointerEvent<HTMLCanvasElement>,
|
|
event: React.PointerEvent<HTMLCanvasElement>,
|
|
hitElement: ExcalidrawElement | null,
|
|
hitElement: ExcalidrawElement | null,
|
|
@@ -3291,13 +3481,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
);
|
|
);
|
|
|
|
|
|
const elements = this.scene.getElements();
|
|
const elements = this.scene.getElements();
|
|
- const element = getElementAtPosition(
|
|
|
|
- elements,
|
|
|
|
- this.state,
|
|
|
|
- x,
|
|
|
|
- y,
|
|
|
|
- this.state.zoom,
|
|
|
|
- );
|
|
|
|
|
|
+ const element = this.getElementAtPosition(x, y);
|
|
if (!element) {
|
|
if (!element) {
|
|
ContextMenu.push({
|
|
ContextMenu.push({
|
|
options: [
|
|
options: [
|