mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
feat: Elbow arrow segment fixing & positioning (#8952)
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com> Co-authored-by: David Luzar <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
parent
8551823da9
commit
91ebf8b0ea
33 changed files with 3282 additions and 1716 deletions
|
@ -165,6 +165,7 @@ import {
|
|||
isTextBindableContainer,
|
||||
isElbowArrow,
|
||||
isFlowchartNodeElement,
|
||||
isBindableElement,
|
||||
} from "../element/typeChecks";
|
||||
import type {
|
||||
ExcalidrawBindableElement,
|
||||
|
@ -189,7 +190,6 @@ import type {
|
|||
MagicGenerationData,
|
||||
ExcalidrawNonSelectionElement,
|
||||
ExcalidrawArrowElement,
|
||||
NonDeletedSceneElementsMap,
|
||||
} from "../element/types";
|
||||
import { getCenter, getDistance } from "../gesture";
|
||||
import {
|
||||
|
@ -292,7 +292,6 @@ import {
|
|||
getDateTime,
|
||||
isShallowEqual,
|
||||
arrayToMap,
|
||||
toBrandedType,
|
||||
} from "../utils";
|
||||
import {
|
||||
createSrcDoc,
|
||||
|
@ -443,7 +442,6 @@ import { actionTextAutoResize } from "../actions/actionTextAutoResize";
|
|||
import { getVisibleSceneBounds } from "../element/bounds";
|
||||
import { isMaybeMermaidDefinition } from "../mermaid";
|
||||
import NewElementCanvas from "./canvases/NewElementCanvas";
|
||||
import { mutateElbowArrow, updateElbowArrow } from "../element/routing";
|
||||
import {
|
||||
FlowChartCreator,
|
||||
FlowChartNavigator,
|
||||
|
@ -3184,49 +3182,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
retainSeed?: boolean;
|
||||
fitToContent?: boolean;
|
||||
}) => {
|
||||
let elements = opts.elements.map((el, _, elements) => {
|
||||
if (isElbowArrow(el)) {
|
||||
const startEndElements = [
|
||||
el.startBinding &&
|
||||
elements.find((l) => l.id === el.startBinding?.elementId),
|
||||
el.endBinding &&
|
||||
elements.find((l) => l.id === el.endBinding?.elementId),
|
||||
];
|
||||
const startBinding = startEndElements[0] ? el.startBinding : null;
|
||||
const endBinding = startEndElements[1] ? el.endBinding : null;
|
||||
return {
|
||||
...el,
|
||||
...updateElbowArrow(
|
||||
{
|
||||
...el,
|
||||
startBinding,
|
||||
endBinding,
|
||||
},
|
||||
toBrandedType<NonDeletedSceneElementsMap>(
|
||||
new Map(
|
||||
startEndElements
|
||||
.filter((x) => x != null)
|
||||
.map(
|
||||
(el) =>
|
||||
[el!.id, el] as [
|
||||
string,
|
||||
Ordered<NonDeletedExcalidrawElement>,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
[el.points[0], el.points[el.points.length - 1]],
|
||||
undefined,
|
||||
{
|
||||
zoom: this.state.zoom,
|
||||
},
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return el;
|
||||
});
|
||||
elements = restoreElements(elements, null, undefined);
|
||||
const elements = restoreElements(opts.elements, null, undefined);
|
||||
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
||||
|
||||
const elementsCenterX = distance(minX, maxX) / 2;
|
||||
|
@ -4377,7 +4333,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||
|
||||
updateBoundElements(element, this.scene.getNonDeletedElementsMap(), {
|
||||
simultaneouslyUpdated: selectedElements,
|
||||
zoom: this.state.zoom,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -5365,6 +5320,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||
|
||||
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||
|
||||
let { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
|
||||
event,
|
||||
this.state,
|
||||
);
|
||||
|
||||
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
|
||||
if (
|
||||
event[KEYS.CTRL_OR_CMD] &&
|
||||
|
@ -5378,6 +5338,64 @@ class App extends React.Component<AppProps, AppState> {
|
|||
editingLinearElement: new LinearElementEditor(selectedElements[0]),
|
||||
});
|
||||
return;
|
||||
} else if (
|
||||
this.state.selectedLinearElement &&
|
||||
isElbowArrow(selectedElements[0])
|
||||
) {
|
||||
const hitCoords = LinearElementEditor.getSegmentMidpointHitCoords(
|
||||
this.state.selectedLinearElement,
|
||||
{ x: sceneX, y: sceneY },
|
||||
this.state,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
const midPoint = hitCoords
|
||||
? LinearElementEditor.getSegmentMidPointIndex(
|
||||
this.state.selectedLinearElement,
|
||||
this.state,
|
||||
hitCoords,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
)
|
||||
: -1;
|
||||
|
||||
if (midPoint && midPoint > -1) {
|
||||
this.store.shouldCaptureIncrement();
|
||||
LinearElementEditor.deleteFixedSegment(selectedElements[0], midPoint);
|
||||
|
||||
const nextCoords = LinearElementEditor.getSegmentMidpointHitCoords(
|
||||
{
|
||||
...this.state.selectedLinearElement,
|
||||
segmentMidPointHoveredCoords: null,
|
||||
},
|
||||
{ x: sceneX, y: sceneY },
|
||||
this.state,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
const nextIndex = nextCoords
|
||||
? LinearElementEditor.getSegmentMidPointIndex(
|
||||
this.state.selectedLinearElement,
|
||||
this.state,
|
||||
nextCoords,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
)
|
||||
: null;
|
||||
|
||||
this.setState({
|
||||
selectedLinearElement: {
|
||||
...this.state.selectedLinearElement,
|
||||
pointerDownState: {
|
||||
...this.state.selectedLinearElement.pointerDownState,
|
||||
segmentMidpoint: {
|
||||
index: nextIndex,
|
||||
value: hitCoords,
|
||||
added: false,
|
||||
},
|
||||
},
|
||||
segmentMidPointHoveredCoords: nextCoords,
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5388,11 +5406,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||
|
||||
resetCursor(this.interactiveCanvas);
|
||||
|
||||
let { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
|
||||
event,
|
||||
this.state,
|
||||
);
|
||||
|
||||
const selectedGroupIds = getSelectedGroupIds(this.state);
|
||||
|
||||
if (selectedGroupIds.length > 0) {
|
||||
|
@ -5849,41 +5862,23 @@ class App extends React.Component<AppProps, AppState> {
|
|||
if (isPathALoop(points, this.state.zoom.value)) {
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
||||
}
|
||||
if (isElbowArrow(multiElement)) {
|
||||
mutateElbowArrow(
|
||||
multiElement,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
[
|
||||
// update last uncommitted point
|
||||
mutateElement(
|
||||
multiElement,
|
||||
{
|
||||
points: [
|
||||
...points.slice(0, -1),
|
||||
pointFrom<LocalPoint>(
|
||||
lastCommittedX + dxFromLastCommitted,
|
||||
lastCommittedY + dyFromLastCommitted,
|
||||
),
|
||||
],
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
isDragging: true,
|
||||
informMutation: false,
|
||||
zoom: this.state.zoom,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// update last uncommitted point
|
||||
mutateElement(
|
||||
multiElement,
|
||||
{
|
||||
points: [
|
||||
...points.slice(0, -1),
|
||||
pointFrom<LocalPoint>(
|
||||
lastCommittedX + dxFromLastCommitted,
|
||||
lastCommittedY + dyFromLastCommitted,
|
||||
),
|
||||
],
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
},
|
||||
false,
|
||||
{
|
||||
isDragging: true,
|
||||
},
|
||||
);
|
||||
|
||||
// in this path, we're mutating multiElement to reflect
|
||||
// how it will be after adding pointer position as the next point
|
||||
|
@ -6049,7 +6044,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.setState({
|
||||
activeEmbeddable: { element: hitElement, state: "hover" },
|
||||
});
|
||||
} else {
|
||||
} else if (!hitElement || !isElbowArrow(hitElement)) {
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
||||
if (this.state.activeEmbeddable?.state === "hover") {
|
||||
this.setState({ activeEmbeddable: null });
|
||||
|
@ -6235,14 +6230,18 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.state,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
|
||||
if (hoverPointIndex >= 0 || segmentMidPointHoveredCoords) {
|
||||
const isHoveringAPointHandle = isElbowArrow(element)
|
||||
? hoverPointIndex === 0 ||
|
||||
hoverPointIndex === element.points.length - 1
|
||||
: hoverPointIndex >= 0;
|
||||
if (isHoveringAPointHandle || segmentMidPointHoveredCoords) {
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
||||
} else if (this.hitElement(scenePointerX, scenePointerY, element)) {
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
||||
}
|
||||
} else if (this.hitElement(scenePointerX, scenePointerY, element)) {
|
||||
if (
|
||||
// Ebow arrows can only be moved when unconnected
|
||||
!isElbowArrow(element) ||
|
||||
!(element.startBinding || element.endBinding)
|
||||
) {
|
||||
|
@ -6972,6 +6971,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
if (
|
||||
selectedElements.length === 1 &&
|
||||
!this.state.editingLinearElement &&
|
||||
!isElbowArrow(selectedElements[0]) &&
|
||||
!(
|
||||
this.state.selectedLinearElement &&
|
||||
this.state.selectedLinearElement.hoverPointIndex !== -1
|
||||
|
@ -7673,6 +7673,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||
locked: false,
|
||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||
elbowed: this.state.currentItemArrowType === ARROW_TYPE.elbow,
|
||||
fixedSegments:
|
||||
this.state.currentItemArrowType === ARROW_TYPE.elbow
|
||||
? []
|
||||
: null,
|
||||
})
|
||||
: newLinearElement({
|
||||
type: elementType,
|
||||
|
@ -7913,6 +7917,63 @@ class App extends React.Component<AppProps, AppState> {
|
|||
return;
|
||||
}
|
||||
const pointerCoords = viewportCoordsToSceneCoords(event, this.state);
|
||||
|
||||
if (
|
||||
this.state.selectedLinearElement &&
|
||||
this.state.selectedLinearElement.elbowed &&
|
||||
this.state.selectedLinearElement.pointerDownState.segmentMidpoint.index
|
||||
) {
|
||||
const [gridX, gridY] = getGridPoint(
|
||||
pointerCoords.x,
|
||||
pointerCoords.y,
|
||||
event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(),
|
||||
);
|
||||
|
||||
let index =
|
||||
this.state.selectedLinearElement.pointerDownState.segmentMidpoint
|
||||
.index;
|
||||
if (index < 0) {
|
||||
const nextCoords = LinearElementEditor.getSegmentMidpointHitCoords(
|
||||
{
|
||||
...this.state.selectedLinearElement,
|
||||
segmentMidPointHoveredCoords: null,
|
||||
},
|
||||
{ x: gridX, y: gridY },
|
||||
this.state,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
index = nextCoords
|
||||
? LinearElementEditor.getSegmentMidPointIndex(
|
||||
this.state.selectedLinearElement,
|
||||
this.state,
|
||||
nextCoords,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
)
|
||||
: -1;
|
||||
}
|
||||
|
||||
const ret = LinearElementEditor.moveFixedSegment(
|
||||
this.state.selectedLinearElement,
|
||||
index,
|
||||
gridX,
|
||||
gridY,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
|
||||
flushSync(() => {
|
||||
if (this.state.selectedLinearElement) {
|
||||
this.setState({
|
||||
selectedLinearElement: {
|
||||
...this.state.selectedLinearElement,
|
||||
segmentMidPointHoveredCoords: ret.segmentMidPointHoveredCoords,
|
||||
pointerDownState: ret.pointerDownState,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const lastPointerCoords =
|
||||
this.lastPointerMoveCoords ?? pointerDownState.origin;
|
||||
this.lastPointerMoveCoords = pointerCoords;
|
||||
|
@ -8265,7 +8326,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
|
||||
// when we're editing the name of a frame, we want the user to be
|
||||
// able to select and interact with the text input
|
||||
!this.state.editingFrame &&
|
||||
if (!this.state.editingFrame) {
|
||||
dragSelectedElements(
|
||||
pointerDownState,
|
||||
selectedElements,
|
||||
|
@ -8274,6 +8335,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
snapOffset,
|
||||
event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(),
|
||||
);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
selectedElementsAreBeingDragged: true,
|
||||
|
@ -8449,26 +8511,17 @@ class App extends React.Component<AppProps, AppState> {
|
|||
},
|
||||
false,
|
||||
);
|
||||
} else if (points.length > 1 && isElbowArrow(newElement)) {
|
||||
mutateElbowArrow(
|
||||
newElement,
|
||||
elementsMap,
|
||||
[...points.slice(0, -1), pointFrom<LocalPoint>(dx, dy)],
|
||||
vector(0, 0),
|
||||
undefined,
|
||||
{
|
||||
isDragging: true,
|
||||
informMutation: false,
|
||||
zoom: this.state.zoom,
|
||||
},
|
||||
);
|
||||
} else if (points.length === 2) {
|
||||
} else if (
|
||||
points.length === 2 ||
|
||||
(points.length > 1 && isElbowArrow(newElement))
|
||||
) {
|
||||
mutateElement(
|
||||
newElement,
|
||||
{
|
||||
points: [...points.slice(0, -1), pointFrom<LocalPoint>(dx, dy)],
|
||||
},
|
||||
false,
|
||||
{ isDragging: true },
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -8663,6 +8716,24 @@ class App extends React.Component<AppProps, AppState> {
|
|||
selectedElementsAreBeingDragged: false,
|
||||
});
|
||||
const elementsMap = this.scene.getNonDeletedElementsMap();
|
||||
|
||||
if (
|
||||
pointerDownState.drag.hasOccurred &&
|
||||
pointerDownState.hit?.element?.id
|
||||
) {
|
||||
const element = elementsMap.get(pointerDownState.hit.element.id);
|
||||
if (isBindableElement(element)) {
|
||||
// Renormalize elbow arrows when they are changed via indirect move
|
||||
element.boundElements
|
||||
?.filter((e) => e.type === "arrow")
|
||||
.map((e) => elementsMap.get(e.id))
|
||||
.filter((e) => isElbowArrow(e))
|
||||
.forEach((e) => {
|
||||
!!e && mutateElement(e, {}, true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Handle end of dragging a point of a linear element, might close a loop
|
||||
// and sets binding element
|
||||
if (this.state.editingLinearElement) {
|
||||
|
@ -8687,6 +8758,17 @@ class App extends React.Component<AppProps, AppState> {
|
|||
}
|
||||
}
|
||||
} else if (this.state.selectedLinearElement) {
|
||||
// Normalize elbow arrow points, remove close parallel segments
|
||||
if (this.state.selectedLinearElement.elbowed) {
|
||||
const element = LinearElementEditor.getElement(
|
||||
this.state.selectedLinearElement.elementId,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
if (element) {
|
||||
mutateElement(element, {}, true);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
pointerDownState.hit?.element?.id !==
|
||||
this.state.selectedLinearElement.elementId
|
||||
|
@ -9126,10 +9208,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.state.selectedLinearElement?.elementId !== hitElement?.id &&
|
||||
isLinearElement(hitElement)
|
||||
) {
|
||||
const selectedELements = this.scene.getSelectedElements(this.state);
|
||||
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||
// set selectedLinearElement when no other element selected except
|
||||
// the one we've hit
|
||||
if (selectedELements.length === 1) {
|
||||
if (selectedElements.length === 1) {
|
||||
this.setState({
|
||||
selectedLinearElement: new LinearElementEditor(hitElement),
|
||||
});
|
||||
|
@ -9337,6 +9419,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||
}
|
||||
|
||||
if (
|
||||
// not elbow midpoint dragged
|
||||
!(hitElement && isElbowArrow(hitElement)) &&
|
||||
// not dragged
|
||||
!pointerDownState.drag.hasOccurred &&
|
||||
// not resized
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue