mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
feat: Orthogonal (elbow) arrows for diagramming (#8299)
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
parent
a133a70e87
commit
15e019706d
69 changed files with 5415 additions and 1144 deletions
|
@ -21,10 +21,11 @@ import type { AppClassProperties, AppProps, UIAppState, Zoom } from "../types";
|
|||
import { capitalizeString, isTransparent } from "../utils";
|
||||
import Stack from "./Stack";
|
||||
import { ToolButton } from "./ToolButton";
|
||||
import { hasStrokeColor } from "../scene/comparisons";
|
||||
import { hasStrokeColor, toolIsArrow } from "../scene/comparisons";
|
||||
import { trackEvent } from "../analytics";
|
||||
import {
|
||||
hasBoundTextElement,
|
||||
isElbowArrow,
|
||||
isLinearElement,
|
||||
isTextElement,
|
||||
} from "../element/typeChecks";
|
||||
|
@ -121,7 +122,8 @@ export const SelectedShapeActions = ({
|
|||
const showLineEditorAction =
|
||||
!appState.editingLinearElement &&
|
||||
targetElements.length === 1 &&
|
||||
isLinearElement(targetElements[0]);
|
||||
isLinearElement(targetElements[0]) &&
|
||||
!isElbowArrow(targetElements[0]);
|
||||
|
||||
return (
|
||||
<div className="panelColumn">
|
||||
|
@ -155,6 +157,11 @@ export const SelectedShapeActions = ({
|
|||
<>{renderAction("changeRoundness")}</>
|
||||
)}
|
||||
|
||||
{(toolIsArrow(appState.activeTool.type) ||
|
||||
targetElements.some((element) => toolIsArrow(element.type))) && (
|
||||
<>{renderAction("changeArrowType")}</>
|
||||
)}
|
||||
|
||||
{(appState.activeTool.type === "text" ||
|
||||
targetElements.some(isTextElement)) && (
|
||||
<>
|
||||
|
|
|
@ -48,7 +48,7 @@ import {
|
|||
} from "../appState";
|
||||
import type { PastedMixedContent } from "../clipboard";
|
||||
import { copyTextToSystemClipboard, parseClipboard } from "../clipboard";
|
||||
import type { EXPORT_IMAGE_TYPES } from "../constants";
|
||||
import { ARROW_TYPE, type EXPORT_IMAGE_TYPES } from "../constants";
|
||||
import {
|
||||
APP_NAME,
|
||||
CURSOR_TYPE,
|
||||
|
@ -142,6 +142,7 @@ import {
|
|||
newEmbeddableElement,
|
||||
newMagicFrameElement,
|
||||
newIframeElement,
|
||||
newArrowElement,
|
||||
} from "../element/newElement";
|
||||
import {
|
||||
hasBoundTextElement,
|
||||
|
@ -160,6 +161,7 @@ import {
|
|||
isIframeLikeElement,
|
||||
isMagicFrameElement,
|
||||
isTextBindableContainer,
|
||||
isElbowArrow,
|
||||
} from "../element/typeChecks";
|
||||
import type {
|
||||
ExcalidrawBindableElement,
|
||||
|
@ -181,6 +183,7 @@ import type {
|
|||
ExcalidrawIframeElement,
|
||||
ExcalidrawEmbeddableElement,
|
||||
Ordered,
|
||||
ExcalidrawArrowElement,
|
||||
} from "../element/types";
|
||||
import { getCenter, getDistance } from "../gesture";
|
||||
import {
|
||||
|
@ -425,6 +428,7 @@ import { getShortcutFromShortcutName } from "../actions/shortcuts";
|
|||
import { actionTextAutoResize } from "../actions/actionTextAutoResize";
|
||||
import { getVisibleSceneBounds } from "../element/bounds";
|
||||
import { isMaybeMermaidDefinition } from "../mermaid";
|
||||
import { mutateElbowArrow } from "../element/routing";
|
||||
|
||||
const AppContext = React.createContext<AppClassProperties>(null!);
|
||||
const AppPropsContext = React.createContext<AppProps>(null!);
|
||||
|
@ -2112,6 +2116,14 @@ class App extends React.Component<AppProps, AppState> {
|
|||
});
|
||||
};
|
||||
|
||||
public dismissLinearEditor = () => {
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
editingLinearElement: null,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
public syncActionResult = withBatchedUpdates((actionResult: ActionResult) => {
|
||||
if (this.unmounted || actionResult === false) {
|
||||
return;
|
||||
|
@ -2803,6 +2815,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
),
|
||||
),
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.scene.getNonDeletedElements(),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -3947,14 +3960,27 @@ class App extends React.Component<AppProps, AppState> {
|
|||
}
|
||||
|
||||
if (isArrowKey(event.key)) {
|
||||
const step =
|
||||
(this.state.gridSize &&
|
||||
const selectedElements = this.scene.getSelectedElements({
|
||||
selectedElementIds: this.state.selectedElementIds,
|
||||
includeBoundTextElement: true,
|
||||
includeElementsInFrames: true,
|
||||
});
|
||||
|
||||
const elbowArrow = selectedElements.find(isElbowArrow) as
|
||||
| ExcalidrawArrowElement
|
||||
| undefined;
|
||||
|
||||
const step = elbowArrow
|
||||
? elbowArrow.startBinding || elbowArrow.endBinding
|
||||
? 0
|
||||
: ELEMENT_TRANSLATE_AMOUNT
|
||||
: (this.state.gridSize &&
|
||||
(event.shiftKey
|
||||
? ELEMENT_TRANSLATE_AMOUNT
|
||||
: this.state.gridSize)) ||
|
||||
(event.shiftKey
|
||||
? ELEMENT_TRANSLATE_AMOUNT
|
||||
: this.state.gridSize)) ||
|
||||
(event.shiftKey
|
||||
? ELEMENT_SHIFT_TRANSLATE_AMOUNT
|
||||
: ELEMENT_TRANSLATE_AMOUNT);
|
||||
? ELEMENT_SHIFT_TRANSLATE_AMOUNT
|
||||
: ELEMENT_TRANSLATE_AMOUNT);
|
||||
|
||||
let offsetX = 0;
|
||||
let offsetY = 0;
|
||||
|
@ -3969,26 +3995,27 @@ class App extends React.Component<AppProps, AppState> {
|
|||
offsetY = step;
|
||||
}
|
||||
|
||||
const selectedElements = this.scene.getSelectedElements({
|
||||
selectedElementIds: this.state.selectedElementIds,
|
||||
includeBoundTextElement: true,
|
||||
includeElementsInFrames: true,
|
||||
});
|
||||
|
||||
selectedElements.forEach((element) => {
|
||||
mutateElement(element, {
|
||||
x: element.x + offsetX,
|
||||
y: element.y + offsetY,
|
||||
});
|
||||
|
||||
updateBoundElements(element, this.scene.getNonDeletedElementsMap(), {
|
||||
simultaneouslyUpdated: selectedElements,
|
||||
});
|
||||
updateBoundElements(
|
||||
element,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.scene,
|
||||
{
|
||||
simultaneouslyUpdated: selectedElements,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
this.setState({
|
||||
suggestedBindings: getSuggestedBindingsForArrows(
|
||||
selectedElements,
|
||||
selectedElements.filter(
|
||||
(element) => element.id !== elbowArrow?.id || step !== 0,
|
||||
),
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
),
|
||||
});
|
||||
|
@ -4006,11 +4033,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||
selectedElements[0].id
|
||||
) {
|
||||
this.store.shouldCaptureIncrement();
|
||||
this.setState({
|
||||
editingLinearElement: new LinearElementEditor(
|
||||
selectedElement,
|
||||
),
|
||||
});
|
||||
if (!isElbowArrow(selectedElement)) {
|
||||
this.setState({
|
||||
editingLinearElement: new LinearElementEditor(
|
||||
selectedElement,
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
|
@ -4058,6 +4087,16 @@ class App extends React.Component<AppProps, AppState> {
|
|||
})`,
|
||||
);
|
||||
}
|
||||
if (shape === "arrow" && this.state.activeTool.type === "arrow") {
|
||||
this.setState((prevState) => ({
|
||||
currentItemArrowType:
|
||||
prevState.currentItemArrowType === ARROW_TYPE.sharp
|
||||
? ARROW_TYPE.round
|
||||
: prevState.currentItemArrowType === ARROW_TYPE.round
|
||||
? ARROW_TYPE.elbow
|
||||
: ARROW_TYPE.sharp,
|
||||
}));
|
||||
}
|
||||
this.setActiveTool({ type: shape });
|
||||
event.stopPropagation();
|
||||
} else if (event.key === KEYS.Q) {
|
||||
|
@ -4191,6 +4230,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||
bindOrUnbindLinearElements(
|
||||
this.scene.getSelectedElements(this.state).filter(isLinearElement),
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.scene,
|
||||
isBindingEnabled(this.state),
|
||||
this.state.selectedLinearElement?.selectedPointsIndices ?? [],
|
||||
);
|
||||
|
@ -4422,7 +4463,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
onChange: withBatchedUpdates((nextOriginalText) => {
|
||||
updateElement(nextOriginalText, false);
|
||||
if (isNonDeletedElement(element)) {
|
||||
updateBoundElements(element, elementsMap);
|
||||
updateBoundElements(element, elementsMap, this.scene);
|
||||
}
|
||||
}),
|
||||
onSubmit: withBatchedUpdates(({ viaKeyboard, nextOriginalText }) => {
|
||||
|
@ -4871,7 +4912,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||
if (
|
||||
event[KEYS.CTRL_OR_CMD] &&
|
||||
(!this.state.editingLinearElement ||
|
||||
this.state.editingLinearElement.elementId !== selectedElements[0].id)
|
||||
this.state.editingLinearElement.elementId !==
|
||||
selectedElements[0].id) &&
|
||||
!isElbowArrow(selectedElements[0])
|
||||
) {
|
||||
this.store.shouldCaptureIncrement();
|
||||
this.setState({
|
||||
|
@ -5214,7 +5257,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
scenePointerX,
|
||||
scenePointerY,
|
||||
this.state,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.scene,
|
||||
);
|
||||
|
||||
if (
|
||||
|
@ -5301,7 +5344,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||
const [gridX, gridY] = getGridPoint(
|
||||
scenePointerX,
|
||||
scenePointerY,
|
||||
event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
|
||||
event[KEYS.CTRL_OR_CMD] || isElbowArrow(multiElement)
|
||||
? null
|
||||
: this.state.gridSize,
|
||||
);
|
||||
|
||||
const [lastCommittedX, lastCommittedY] =
|
||||
|
@ -5325,16 +5370,35 @@ class App extends React.Component<AppProps, AppState> {
|
|||
if (isPathALoop(points, this.state.zoom.value)) {
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
||||
}
|
||||
// update last uncommitted point
|
||||
mutateElement(multiElement, {
|
||||
points: [
|
||||
...points.slice(0, -1),
|
||||
if (isElbowArrow(multiElement)) {
|
||||
mutateElbowArrow(
|
||||
multiElement,
|
||||
this.scene,
|
||||
[
|
||||
lastCommittedX + dxFromLastCommitted,
|
||||
lastCommittedY + dyFromLastCommitted,
|
||||
...points.slice(0, -1),
|
||||
[
|
||||
lastCommittedX + dxFromLastCommitted,
|
||||
lastCommittedY + dyFromLastCommitted,
|
||||
],
|
||||
],
|
||||
],
|
||||
});
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
isDragging: true,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// update last uncommitted point
|
||||
mutateElement(multiElement, {
|
||||
points: [
|
||||
...points.slice(0, -1),
|
||||
[
|
||||
lastCommittedX + dxFromLastCommitted,
|
||||
lastCommittedY + dyFromLastCommitted,
|
||||
],
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
|
@ -5369,8 +5433,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||
}
|
||||
|
||||
if (
|
||||
!this.state.selectedLinearElement ||
|
||||
this.state.selectedLinearElement.hoverPointIndex === -1
|
||||
(!this.state.selectedLinearElement ||
|
||||
this.state.selectedLinearElement.hoverPointIndex === -1) &&
|
||||
!(selectedElements.length === 1 && isElbowArrow(selectedElements[0]))
|
||||
) {
|
||||
const elementWithTransformHandleType =
|
||||
getElementWithTransformHandleType(
|
||||
|
@ -5658,7 +5723,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
||||
}
|
||||
} else if (this.hitElement(scenePointerX, scenePointerY, element)) {
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
||||
if (
|
||||
!isElbowArrow(element) ||
|
||||
!(element.startBinding || element.endBinding)
|
||||
) {
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
|
@ -6232,6 +6302,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
const origin = viewportCoordsToSceneCoords(event, this.state);
|
||||
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||
const [minX, minY, maxX, maxY] = getCommonBounds(selectedElements);
|
||||
const isElbowArrowOnly = selectedElements.findIndex(isElbowArrow) === 0;
|
||||
|
||||
return {
|
||||
origin,
|
||||
|
@ -6240,7 +6311,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||
getGridPoint(
|
||||
origin.x,
|
||||
origin.y,
|
||||
event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
|
||||
event[KEYS.CTRL_OR_CMD] || isElbowArrowOnly
|
||||
? null
|
||||
: this.state.gridSize,
|
||||
),
|
||||
),
|
||||
scrollbars: isOverScrollBars(
|
||||
|
@ -6421,7 +6494,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.store,
|
||||
pointerDownState.origin,
|
||||
linearElementEditor,
|
||||
this,
|
||||
this.scene,
|
||||
);
|
||||
if (ret.hitElement) {
|
||||
pointerDownState.hit.element = ret.hitElement;
|
||||
|
@ -6753,6 +6826,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
|
||||
const boundElement = getHoveredElementForBinding(
|
||||
pointerDownState.origin,
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
this.scene.insertElement(element);
|
||||
|
@ -6923,6 +6997,17 @@ class App extends React.Component<AppProps, AppState> {
|
|||
return;
|
||||
}
|
||||
|
||||
// Elbow arrows cannot be created by putting down points
|
||||
// only the start and end points can be defined
|
||||
if (isElbowArrow(multiElement) && multiElement.points.length > 1) {
|
||||
mutateElement(multiElement, {
|
||||
lastCommittedPoint:
|
||||
multiElement.points[multiElement.points.length - 1],
|
||||
});
|
||||
this.actionManager.executeAction(actionFinalize);
|
||||
return;
|
||||
}
|
||||
|
||||
const { x: rx, y: ry, lastCommittedPoint } = multiElement;
|
||||
|
||||
// clicking inside commit zone → finalize arrow
|
||||
|
@ -6978,26 +7063,50 @@ class App extends React.Component<AppProps, AppState> {
|
|||
? [currentItemStartArrowhead, currentItemEndArrowhead]
|
||||
: [null, null];
|
||||
|
||||
const element = newLinearElement({
|
||||
type: elementType,
|
||||
x: gridX,
|
||||
y: gridY,
|
||||
strokeColor: this.state.currentItemStrokeColor,
|
||||
backgroundColor: this.state.currentItemBackgroundColor,
|
||||
fillStyle: this.state.currentItemFillStyle,
|
||||
strokeWidth: this.state.currentItemStrokeWidth,
|
||||
strokeStyle: this.state.currentItemStrokeStyle,
|
||||
roughness: this.state.currentItemRoughness,
|
||||
opacity: this.state.currentItemOpacity,
|
||||
roundness:
|
||||
this.state.currentItemRoundness === "round"
|
||||
? { type: ROUNDNESS.PROPORTIONAL_RADIUS }
|
||||
: null,
|
||||
startArrowhead,
|
||||
endArrowhead,
|
||||
locked: false,
|
||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||
});
|
||||
const element =
|
||||
elementType === "arrow"
|
||||
? newArrowElement({
|
||||
type: elementType,
|
||||
x: gridX,
|
||||
y: gridY,
|
||||
strokeColor: this.state.currentItemStrokeColor,
|
||||
backgroundColor: this.state.currentItemBackgroundColor,
|
||||
fillStyle: this.state.currentItemFillStyle,
|
||||
strokeWidth: this.state.currentItemStrokeWidth,
|
||||
strokeStyle: this.state.currentItemStrokeStyle,
|
||||
roughness: this.state.currentItemRoughness,
|
||||
opacity: this.state.currentItemOpacity,
|
||||
roundness:
|
||||
this.state.currentItemArrowType === ARROW_TYPE.round
|
||||
? { type: ROUNDNESS.PROPORTIONAL_RADIUS }
|
||||
: // note, roundness doesn't have any effect for elbow arrows,
|
||||
// but it's best to set it to null as well
|
||||
null,
|
||||
startArrowhead,
|
||||
endArrowhead,
|
||||
locked: false,
|
||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||
elbowed: this.state.currentItemArrowType === ARROW_TYPE.elbow,
|
||||
})
|
||||
: newLinearElement({
|
||||
type: elementType,
|
||||
x: gridX,
|
||||
y: gridY,
|
||||
strokeColor: this.state.currentItemStrokeColor,
|
||||
backgroundColor: this.state.currentItemBackgroundColor,
|
||||
fillStyle: this.state.currentItemFillStyle,
|
||||
strokeWidth: this.state.currentItemStrokeWidth,
|
||||
strokeStyle: this.state.currentItemStrokeStyle,
|
||||
roughness: this.state.currentItemRoughness,
|
||||
opacity: this.state.currentItemOpacity,
|
||||
roundness:
|
||||
this.state.currentItemRoundness === "round"
|
||||
? { type: ROUNDNESS.PROPORTIONAL_RADIUS }
|
||||
: null,
|
||||
locked: false,
|
||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||
});
|
||||
|
||||
this.setState((prevState) => {
|
||||
const nextSelectedElementIds = {
|
||||
...prevState.selectedElementIds,
|
||||
|
@ -7015,7 +7124,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||
});
|
||||
const boundElement = getHoveredElementForBinding(
|
||||
pointerDownState.origin,
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
isElbowArrow(element),
|
||||
);
|
||||
|
||||
this.scene.insertElement(element);
|
||||
|
@ -7352,7 +7463,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
);
|
||||
},
|
||||
linearElementEditor,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.scene,
|
||||
);
|
||||
if (didDrag) {
|
||||
pointerDownState.lastCoords.x = pointerCoords.x;
|
||||
|
@ -7476,18 +7587,24 @@ class App extends React.Component<AppProps, AppState> {
|
|||
pointerDownState,
|
||||
selectedElements,
|
||||
dragOffset,
|
||||
this.state,
|
||||
this.scene,
|
||||
snapOffset,
|
||||
event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
|
||||
);
|
||||
|
||||
this.setState({
|
||||
suggestedBindings: getSuggestedBindingsForArrows(
|
||||
selectedElements,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
),
|
||||
});
|
||||
if (
|
||||
selectedElements.length !== 1 ||
|
||||
!isElbowArrow(selectedElements[0])
|
||||
) {
|
||||
this.setState({
|
||||
suggestedBindings: getSuggestedBindingsForArrows(
|
||||
selectedElements,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
//}
|
||||
|
||||
// We duplicate the selected element if alt is pressed on pointer move
|
||||
if (event.altKey && !pointerDownState.hit.hasBeenDuplicated) {
|
||||
|
@ -7627,6 +7744,17 @@ class App extends React.Component<AppProps, AppState> {
|
|||
mutateElement(draggingElement, {
|
||||
points: [...points, [dx, dy]],
|
||||
});
|
||||
} else if (points.length > 1 && isElbowArrow(draggingElement)) {
|
||||
mutateElbowArrow(
|
||||
draggingElement,
|
||||
this.scene,
|
||||
[...points.slice(0, -1), [dx, dy]],
|
||||
[0, 0],
|
||||
undefined,
|
||||
{
|
||||
isDragging: true,
|
||||
},
|
||||
);
|
||||
} else if (points.length === 2) {
|
||||
mutateElement(draggingElement, {
|
||||
points: [...points.slice(0, -1), [dx, dy]],
|
||||
|
@ -7832,7 +7960,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
childEvent,
|
||||
this.state.editingLinearElement,
|
||||
this.state,
|
||||
this,
|
||||
this.scene,
|
||||
);
|
||||
if (editingLinearElement !== this.state.editingLinearElement) {
|
||||
this.setState({
|
||||
|
@ -7856,7 +7984,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
childEvent,
|
||||
this.state.selectedLinearElement,
|
||||
this.state,
|
||||
this,
|
||||
this.scene,
|
||||
);
|
||||
|
||||
const { startBindingElement, endBindingElement } =
|
||||
|
@ -7868,6 +7996,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
startBindingElement,
|
||||
endBindingElement,
|
||||
elementsMap,
|
||||
this.scene,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -8007,6 +8136,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.state,
|
||||
pointerCoords,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.scene.getNonDeletedElements(),
|
||||
);
|
||||
}
|
||||
this.setState({ suggestedBindings: [], startBoundElement: null });
|
||||
|
@ -8568,6 +8698,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||
bindOrUnbindLinearElements(
|
||||
linearElements,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.scene,
|
||||
isBindingEnabled(this.state),
|
||||
this.state.selectedLinearElement?.selectedPointsIndices ?? [],
|
||||
);
|
||||
|
@ -9055,6 +9187,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
}): void => {
|
||||
const hoveredBindableElement = getHoveredElementForBinding(
|
||||
pointerCoords,
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
this.setState({
|
||||
|
@ -9082,7 +9215,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||
(acc: NonDeleted<ExcalidrawBindableElement>[], coords) => {
|
||||
const hoveredBindableElement = getHoveredElementForBinding(
|
||||
coords,
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
isArrowElement(linearElement) && isElbowArrow(linearElement),
|
||||
);
|
||||
if (
|
||||
hoveredBindableElement != null &&
|
||||
|
@ -9610,6 +9745,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
resizeY,
|
||||
pointerDownState.resize.center.x,
|
||||
pointerDownState.resize.center.y,
|
||||
this.scene,
|
||||
)
|
||||
) {
|
||||
const suggestedBindings = getSuggestedBindingsForArrows(
|
||||
|
@ -9926,6 +10062,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
declare global {
|
||||
interface Window {
|
||||
h: {
|
||||
scene: Scene;
|
||||
elements: readonly ExcalidrawElement[];
|
||||
state: AppState;
|
||||
setState: React.Component<any, AppState>["setState"];
|
||||
|
@ -9952,6 +10089,12 @@ export const createTestHook = () => {
|
|||
);
|
||||
},
|
||||
},
|
||||
scene: {
|
||||
configurable: true,
|
||||
get() {
|
||||
return this.app?.scene;
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -30,10 +30,13 @@ const getHints = ({ appState, isMobile, device, app }: HintViewerProps) => {
|
|||
return t("hints.eraserRevert");
|
||||
}
|
||||
if (activeTool.type === "arrow" || activeTool.type === "line") {
|
||||
if (!multiMode) {
|
||||
return t("hints.linearElement");
|
||||
if (multiMode) {
|
||||
return t("hints.linearElementMulti");
|
||||
}
|
||||
return t("hints.linearElementMulti");
|
||||
if (activeTool.type === "arrow") {
|
||||
return t("hints.arrowTool", { arrowShortcut: getShortcutKey("A") });
|
||||
}
|
||||
return t("hints.linearElement");
|
||||
}
|
||||
|
||||
if (activeTool.type === "freedraw") {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { mutateElement } from "../../element/mutateElement";
|
||||
import { getBoundTextElement } from "../../element/textElement";
|
||||
import { isArrowElement } from "../../element/typeChecks";
|
||||
import { isArrowElement, isElbowArrow } from "../../element/typeChecks";
|
||||
import type { ExcalidrawElement } from "../../element/types";
|
||||
import { degreeToRadian, radianToDegree } from "../../math";
|
||||
import { angleIcon } from "../icons";
|
||||
|
@ -27,8 +27,9 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
|||
scene,
|
||||
}) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const elements = scene.getNonDeletedElements();
|
||||
const origElement = originalElements[0];
|
||||
if (origElement) {
|
||||
if (origElement && !isElbowArrow(origElement)) {
|
||||
const latestElement = elementsMap.get(origElement.id);
|
||||
if (!latestElement) {
|
||||
return;
|
||||
|
@ -39,7 +40,7 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
|||
mutateElement(latestElement, {
|
||||
angle: nextAngle,
|
||||
});
|
||||
updateBindings(latestElement, elementsMap);
|
||||
updateBindings(latestElement, elementsMap, elements, scene);
|
||||
|
||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||
if (boundTextElement && !isArrowElement(latestElement)) {
|
||||
|
@ -65,7 +66,7 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
|||
mutateElement(latestElement, {
|
||||
angle: nextAngle,
|
||||
});
|
||||
updateBindings(latestElement, elementsMap);
|
||||
updateBindings(latestElement, elementsMap, elements, scene);
|
||||
|
||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||
if (boundTextElement && !isArrowElement(latestElement)) {
|
||||
|
|
|
@ -31,6 +31,7 @@ const handleDimensionChange: DragInputCallbackType<
|
|||
scene,
|
||||
}) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const elements = scene.getNonDeletedElements();
|
||||
const origElement = originalElements[0];
|
||||
if (origElement) {
|
||||
const keepAspectRatio =
|
||||
|
@ -61,6 +62,8 @@ const handleDimensionChange: DragInputCallbackType<
|
|||
keepAspectRatio,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
);
|
||||
|
||||
return;
|
||||
|
@ -103,6 +106,8 @@ const handleDimensionChange: DragInputCallbackType<
|
|||
keepAspectRatio,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -25,9 +25,9 @@ export type DragInputCallbackType<
|
|||
originalElementsMap: ElementsMap;
|
||||
shouldKeepAspectRatio: boolean;
|
||||
shouldChangeByStepSize: boolean;
|
||||
scene: Scene;
|
||||
nextValue?: number;
|
||||
property: P;
|
||||
scene: Scene;
|
||||
originalAppState: AppState;
|
||||
}) => void;
|
||||
|
||||
|
@ -122,9 +122,9 @@ const StatsDragInput = <
|
|||
originalElementsMap: app.scene.getNonDeletedElementsMap(),
|
||||
shouldKeepAspectRatio: shouldKeepAspectRatio!!,
|
||||
shouldChangeByStepSize: false,
|
||||
scene,
|
||||
nextValue: rounded,
|
||||
property,
|
||||
scene,
|
||||
originalAppState: appState,
|
||||
});
|
||||
app.syncActionResult({ storeAction: StoreAction.CAPTURE });
|
||||
|
|
|
@ -66,8 +66,10 @@ const resizeElementInGroup = (
|
|||
origElement: ExcalidrawElement,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
originalElementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
) => {
|
||||
const updates = getResizedUpdates(anchorX, anchorY, scale, origElement);
|
||||
const { width: oldWidth, height: oldHeight } = latestElement;
|
||||
|
||||
mutateElement(latestElement, updates, false);
|
||||
const boundTextElement = getBoundTextElement(
|
||||
|
@ -76,8 +78,8 @@ const resizeElementInGroup = (
|
|||
);
|
||||
if (boundTextElement) {
|
||||
const newFontSize = boundTextElement.fontSize * scale;
|
||||
updateBoundElements(latestElement, elementsMap, {
|
||||
newSize: { width: updates.width, height: updates.height },
|
||||
updateBoundElements(latestElement, elementsMap, scene, {
|
||||
oldSize: { width: oldWidth, height: oldHeight },
|
||||
});
|
||||
const latestBoundTextElement = elementsMap.get(boundTextElement.id);
|
||||
if (latestBoundTextElement && isTextElement(latestBoundTextElement)) {
|
||||
|
@ -109,6 +111,7 @@ const resizeGroup = (
|
|||
originalElements: ExcalidrawElement[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
originalElementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
) => {
|
||||
// keep aspect ratio for groups
|
||||
if (property === "width") {
|
||||
|
@ -132,6 +135,7 @@ const resizeGroup = (
|
|||
origElement,
|
||||
elementsMap,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -149,6 +153,7 @@ const handleDimensionChange: DragInputCallbackType<
|
|||
property,
|
||||
}) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const elements = scene.getNonDeletedElements();
|
||||
const atomicUnits = getAtomicUnits(originalElements, originalAppState);
|
||||
if (nextValue !== undefined) {
|
||||
for (const atomicUnit of atomicUnits) {
|
||||
|
@ -185,6 +190,7 @@ const handleDimensionChange: DragInputCallbackType<
|
|||
originalElements,
|
||||
elementsMap,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
);
|
||||
} else {
|
||||
const [el] = elementsInUnit;
|
||||
|
@ -227,6 +233,8 @@ const handleDimensionChange: DragInputCallbackType<
|
|||
false,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
@ -288,6 +296,7 @@ const handleDimensionChange: DragInputCallbackType<
|
|||
originalElements,
|
||||
elementsMap,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
);
|
||||
} else {
|
||||
const [el] = elementsInUnit;
|
||||
|
@ -320,7 +329,15 @@ const handleDimensionChange: DragInputCallbackType<
|
|||
nextWidth = Math.max(MIN_WIDTH_OR_HEIGHT, nextWidth);
|
||||
nextHeight = Math.max(MIN_WIDTH_OR_HEIGHT, nextHeight);
|
||||
|
||||
resizeElement(nextWidth, nextHeight, false, origElement, elementsMap);
|
||||
resizeElement(
|
||||
nextWidth,
|
||||
nextHeight,
|
||||
false,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
NonDeletedSceneElementsMap,
|
||||
} from "../../element/types";
|
||||
import { rotate } from "../../math";
|
||||
|
@ -33,6 +34,7 @@ const moveElements = (
|
|||
originalElements: readonly ExcalidrawElement[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
originalElementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
) => {
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const origElement = originalElements[i];
|
||||
|
@ -60,6 +62,8 @@ const moveElements = (
|
|||
newTopLeftY,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
originalElementsMap,
|
||||
false,
|
||||
);
|
||||
|
@ -71,6 +75,7 @@ const moveGroupTo = (
|
|||
nextY: number,
|
||||
originalElements: ExcalidrawElement[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
originalElementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
) => {
|
||||
|
@ -106,6 +111,8 @@ const moveGroupTo = (
|
|||
topLeftY + offsetY,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
originalElementsMap,
|
||||
false,
|
||||
);
|
||||
|
@ -126,6 +133,7 @@ const handlePositionChange: DragInputCallbackType<
|
|||
originalAppState,
|
||||
}) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const elements = scene.getNonDeletedElements();
|
||||
|
||||
if (nextValue !== undefined) {
|
||||
for (const atomicUnit of getAtomicUnits(
|
||||
|
@ -150,6 +158,7 @@ const handlePositionChange: DragInputCallbackType<
|
|||
newTopLeftY,
|
||||
elementsInUnit.map((el) => el.original),
|
||||
elementsMap,
|
||||
elements,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
);
|
||||
|
@ -180,6 +189,8 @@ const handlePositionChange: DragInputCallbackType<
|
|||
newTopLeftY,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
originalElementsMap,
|
||||
false,
|
||||
);
|
||||
|
@ -206,6 +217,7 @@ const handlePositionChange: DragInputCallbackType<
|
|||
originalElements,
|
||||
elementsMap,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
);
|
||||
|
||||
scene.triggerUpdate();
|
||||
|
|
|
@ -26,6 +26,7 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
|||
scene,
|
||||
}) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const elements = scene.getNonDeletedElements();
|
||||
const origElement = originalElements[0];
|
||||
const [cx, cy] = [
|
||||
origElement.x + origElement.width / 2,
|
||||
|
@ -47,6 +48,8 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
|||
newTopLeftY,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
originalElementsMap,
|
||||
);
|
||||
return;
|
||||
|
@ -78,6 +81,8 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
|||
newTopLeftY,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
originalElementsMap,
|
||||
);
|
||||
};
|
||||
|
@ -104,9 +109,9 @@ const Position = ({
|
|||
label={property === "x" ? "X" : "Y"}
|
||||
elements={[element]}
|
||||
dragInputCallback={handlePositionChange}
|
||||
scene={scene}
|
||||
value={value}
|
||||
property={property}
|
||||
scene={scene}
|
||||
appState={appState}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -21,6 +21,7 @@ import type Scene from "../../scene/Scene";
|
|||
import { useExcalidrawAppState, useExcalidrawSetAppState } from "../App";
|
||||
import { getAtomicUnits } from "./utils";
|
||||
import { STATS_PANELS } from "../../constants";
|
||||
import { isElbowArrow } from "../../element/typeChecks";
|
||||
|
||||
interface StatsProps {
|
||||
scene: Scene;
|
||||
|
@ -209,12 +210,14 @@ export const StatsInner = memo(
|
|||
scene={scene}
|
||||
appState={appState}
|
||||
/>
|
||||
<Angle
|
||||
property="angle"
|
||||
element={singleElement}
|
||||
scene={scene}
|
||||
appState={appState}
|
||||
/>
|
||||
{!isElbowArrow(singleElement) && (
|
||||
<Angle
|
||||
property="angle"
|
||||
element={singleElement}
|
||||
scene={scene}
|
||||
appState={appState}
|
||||
/>
|
||||
)}
|
||||
<FontSize
|
||||
property="fontSize"
|
||||
element={singleElement}
|
||||
|
|
|
@ -31,6 +31,7 @@ import {
|
|||
isInGroup,
|
||||
} from "../../groups";
|
||||
import { rotate } from "../../math";
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type { AppState } from "../../types";
|
||||
import { getFontString } from "../../utils";
|
||||
|
||||
|
@ -124,6 +125,8 @@ export const resizeElement = (
|
|||
keepAspectRatio: boolean,
|
||||
origElement: ExcalidrawElement,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
scene: Scene,
|
||||
shouldInformMutation = true,
|
||||
) => {
|
||||
const latestElement = elementsMap.get(origElement.id);
|
||||
|
@ -146,6 +149,8 @@ export const resizeElement = (
|
|||
nextHeight = Math.max(nextHeight, minHeight);
|
||||
}
|
||||
|
||||
const { width: oldWidth, height: oldHeight } = latestElement;
|
||||
|
||||
mutateElement(
|
||||
latestElement,
|
||||
{
|
||||
|
@ -164,7 +169,7 @@ export const resizeElement = (
|
|||
},
|
||||
shouldInformMutation,
|
||||
);
|
||||
updateBindings(latestElement, elementsMap, {
|
||||
updateBindings(latestElement, elementsMap, elements, scene, {
|
||||
newSize: {
|
||||
width: nextWidth,
|
||||
height: nextHeight,
|
||||
|
@ -193,6 +198,10 @@ export const resizeElement = (
|
|||
}
|
||||
}
|
||||
|
||||
updateBoundElements(latestElement, elementsMap, scene, {
|
||||
oldSize: { width: oldWidth, height: oldHeight },
|
||||
});
|
||||
|
||||
if (boundTextElement && boundTextFont) {
|
||||
mutateElement(boundTextElement, {
|
||||
fontSize: boundTextFont.fontSize,
|
||||
|
@ -206,6 +215,8 @@ export const moveElement = (
|
|||
newTopLeftY: number,
|
||||
originalElement: ExcalidrawElement,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
scene: Scene,
|
||||
originalElementsMap: ElementsMap,
|
||||
shouldInformMutation = true,
|
||||
) => {
|
||||
|
@ -244,7 +255,7 @@ export const moveElement = (
|
|||
},
|
||||
shouldInformMutation,
|
||||
);
|
||||
updateBindings(latestElement, elementsMap);
|
||||
updateBindings(latestElement, elementsMap, elements, scene);
|
||||
|
||||
const boundTextElement = getBoundTextElement(
|
||||
originalElement,
|
||||
|
@ -288,14 +299,23 @@ export const getAtomicUnits = (
|
|||
export const updateBindings = (
|
||||
latestElement: ExcalidrawElement,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
scene: Scene,
|
||||
options?: {
|
||||
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
||||
newSize?: { width: number; height: number };
|
||||
},
|
||||
) => {
|
||||
if (isLinearElement(latestElement)) {
|
||||
bindOrUnbindLinearElements([latestElement], elementsMap, true, []);
|
||||
bindOrUnbindLinearElements(
|
||||
[latestElement],
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
true,
|
||||
[],
|
||||
);
|
||||
} else {
|
||||
updateBoundElements(latestElement, elementsMap, options);
|
||||
updateBoundElements(latestElement, elementsMap, scene, options);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -2095,6 +2095,35 @@ export const lineEditorIcon = createIcon(
|
|||
tablerIconProps,
|
||||
);
|
||||
|
||||
// arrow-up-right (modified)
|
||||
export const sharpArrowIcon = createIcon(
|
||||
<g>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M6 18l12 -12" />
|
||||
<path d="M18 10v-4h-4" />
|
||||
</g>,
|
||||
tablerIconProps,
|
||||
);
|
||||
|
||||
// arrow-guide (modified)
|
||||
export const elbowArrowIcon = createIcon(
|
||||
<g>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M4,19L10,19C11.097,19 12,18.097 12,17L12,9C12,7.903 12.903,7 14,7L21,7" />
|
||||
<path d="M18 4l3 3l-3 3" />
|
||||
</g>,
|
||||
tablerIconProps,
|
||||
);
|
||||
|
||||
// arrow-ramp-right-2 (heavily modified)
|
||||
export const roundArrowIcon = createIcon(
|
||||
<g>
|
||||
<path d="M16,12L20,9L16,6" />
|
||||
<path d="M6 20c0 -6.075 4.925 -11 11 -11h3" />
|
||||
</g>,
|
||||
tablerIconProps,
|
||||
);
|
||||
|
||||
export const collapseDownIcon = createIcon(
|
||||
<g>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue