Unfinished simple arrow avoidance
This commit is contained in:
parent
898499777f
commit
6efe4c6f38
@ -3,9 +3,6 @@ import {
|
||||
arrayToMap,
|
||||
isBindingFallthroughEnabled,
|
||||
tupleToCoors,
|
||||
invariant,
|
||||
isDevEnv,
|
||||
isTestEnv,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import {
|
||||
@ -399,10 +396,15 @@ export const maybeSuggestBindingsForLinearElementAtCoords = (
|
||||
}[],
|
||||
scene: Scene,
|
||||
zoom: AppState["zoom"],
|
||||
elementsMap: ElementsMap,
|
||||
// During line creation the start binding hasn't been written yet
|
||||
// into `linearElement`
|
||||
oppositeBindingBoundElement?: ExcalidrawBindableElement | null,
|
||||
): ExcalidrawBindableElement[] =>
|
||||
Array.from(
|
||||
pointerCoords.reduce(
|
||||
(acc: Set<NonDeleted<ExcalidrawBindableElement>>, coords) => {
|
||||
const p = pointFrom<GlobalPoint>(coords.x, coords.y);
|
||||
const hoveredBindableElement = getHoveredElementForBinding(
|
||||
coords,
|
||||
scene.getNonDeletedElements(),
|
||||
@ -411,8 +413,20 @@ export const maybeSuggestBindingsForLinearElementAtCoords = (
|
||||
isElbowArrow(linearElement),
|
||||
isElbowArrow(linearElement),
|
||||
);
|
||||
const pointIsInside =
|
||||
hoveredBindableElement != null &&
|
||||
isPointInElement(p, hoveredBindableElement, elementsMap);
|
||||
|
||||
if (hoveredBindableElement != null) {
|
||||
if (
|
||||
hoveredBindableElement != null &&
|
||||
((pointIsInside &&
|
||||
oppositeBindingBoundElement?.id === hoveredBindableElement.id) ||
|
||||
!isLinearElementSimpleAndAlreadyBound(
|
||||
linearElement,
|
||||
oppositeBindingBoundElement?.id,
|
||||
hoveredBindableElement,
|
||||
))
|
||||
) {
|
||||
acc.add(hoveredBindableElement);
|
||||
}
|
||||
|
||||
@ -996,35 +1010,40 @@ const getDistanceForBinding = (
|
||||
};
|
||||
|
||||
export const bindPointToSnapToElementOutline = (
|
||||
arrow: ExcalidrawElbowArrowElement,
|
||||
linearElement: ExcalidrawLinearElement,
|
||||
bindableElement: ExcalidrawBindableElement,
|
||||
startOrEnd: "start" | "end",
|
||||
elementsMap: ElementsMap,
|
||||
): GlobalPoint => {
|
||||
if (isDevEnv() || isTestEnv()) {
|
||||
invariant(arrow.points.length > 1, "Arrow should have at least 2 points");
|
||||
}
|
||||
|
||||
const aabb = aabbForElement(bindableElement, elementsMap);
|
||||
const localP =
|
||||
arrow.points[startOrEnd === "start" ? 0 : arrow.points.length - 1];
|
||||
linearElement.points[
|
||||
startOrEnd === "start" ? 0 : linearElement.points.length - 1
|
||||
];
|
||||
const globalP = pointFrom<GlobalPoint>(
|
||||
arrow.x + localP[0],
|
||||
arrow.y + localP[1],
|
||||
linearElement.x + localP[0],
|
||||
linearElement.y + localP[1],
|
||||
);
|
||||
|
||||
if (linearElement.points.length < 2) {
|
||||
// New arrow creation, so no snapping
|
||||
return globalP;
|
||||
}
|
||||
|
||||
const edgePoint = isRectanguloidElement(bindableElement)
|
||||
? avoidRectangularCorner(bindableElement, elementsMap, globalP)
|
||||
: globalP;
|
||||
const elbowed = isElbowArrow(arrow);
|
||||
const elbowed = isElbowArrow(linearElement);
|
||||
const center = getCenterForBounds(aabb);
|
||||
const adjacentPointIdx = startOrEnd === "start" ? 1 : arrow.points.length - 2;
|
||||
const adjacentPointIdx =
|
||||
startOrEnd === "start" ? 1 : linearElement.points.length - 2;
|
||||
const adjacentPoint = pointRotateRads(
|
||||
pointFrom<GlobalPoint>(
|
||||
arrow.x + arrow.points[adjacentPointIdx][0],
|
||||
arrow.y + arrow.points[adjacentPointIdx][1],
|
||||
linearElement.x + linearElement.points[adjacentPointIdx][0],
|
||||
linearElement.y + linearElement.points[adjacentPointIdx][1],
|
||||
),
|
||||
center,
|
||||
arrow.angle ?? 0,
|
||||
linearElement.angle ?? 0,
|
||||
);
|
||||
|
||||
let intersection: GlobalPoint | null = null;
|
||||
@ -1083,7 +1102,35 @@ export const bindPointToSnapToElementOutline = (
|
||||
return edgePoint;
|
||||
}
|
||||
|
||||
return elbowed ? intersection : edgePoint;
|
||||
return intersection;
|
||||
};
|
||||
|
||||
export const getOutlineAvoidingPoint = (
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
hoveredElement: ExcalidrawBindableElement | null,
|
||||
coords: GlobalPoint,
|
||||
pointIndex: number,
|
||||
elementsMap: ElementsMap,
|
||||
): GlobalPoint => {
|
||||
if (hoveredElement) {
|
||||
const newPoints = Array.from(element.points);
|
||||
newPoints[pointIndex] = pointFrom<LocalPoint>(
|
||||
coords[0] - element.x,
|
||||
coords[1] - element.y,
|
||||
);
|
||||
|
||||
return bindPointToSnapToElementOutline(
|
||||
{
|
||||
...element,
|
||||
points: newPoints,
|
||||
},
|
||||
hoveredElement,
|
||||
pointIndex === 0 ? "start" : "end",
|
||||
elementsMap,
|
||||
);
|
||||
}
|
||||
|
||||
return coords;
|
||||
};
|
||||
|
||||
export const avoidRectangularCorner = (
|
||||
|
@ -45,6 +45,7 @@ import type { Mutable } from "@excalidraw/common/utility-types";
|
||||
import {
|
||||
bindOrUnbindLinearElement,
|
||||
getHoveredElementForBinding,
|
||||
getOutlineAvoidingPoint,
|
||||
isBindingEnabled,
|
||||
maybeSuggestBindingsForLinearElementAtCoords,
|
||||
} from "./binding";
|
||||
@ -58,6 +59,7 @@ import { headingIsHorizontal, vectorToHeading } from "./heading";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
||||
import {
|
||||
isArrowElement,
|
||||
isBindingElement,
|
||||
isElbowArrow,
|
||||
isFixedPointBinding,
|
||||
@ -290,15 +292,17 @@ export class LinearElementEditor {
|
||||
return null;
|
||||
}
|
||||
|
||||
const elbowed = isElbowArrow(element);
|
||||
|
||||
if (
|
||||
isElbowArrow(element) &&
|
||||
elbowed &&
|
||||
!linearElementEditor.pointerDownState.lastClickedIsEndPoint &&
|
||||
linearElementEditor.pointerDownState.lastClickedPoint !== 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectedPointsIndices = isElbowArrow(element)
|
||||
const selectedPointsIndices = elbowed
|
||||
? [
|
||||
!!linearElementEditor.selectedPointsIndices?.includes(0)
|
||||
? 0
|
||||
@ -308,7 +312,7 @@ export class LinearElementEditor {
|
||||
: undefined,
|
||||
].filter((idx): idx is number => idx !== undefined)
|
||||
: linearElementEditor.selectedPointsIndices;
|
||||
const lastClickedPoint = isElbowArrow(element)
|
||||
const lastClickedPoint = elbowed
|
||||
? linearElementEditor.pointerDownState.lastClickedPoint > 0
|
||||
? element.points.length - 1
|
||||
: 0
|
||||
@ -375,7 +379,7 @@ export class LinearElementEditor {
|
||||
app.scene,
|
||||
new Map(
|
||||
selectedPointsIndices.map((pointIndex) => {
|
||||
const newPointPosition: LocalPoint =
|
||||
let newPointPosition: LocalPoint =
|
||||
pointIndex === lastClickedPoint
|
||||
? LinearElementEditor.createPointAt(
|
||||
element,
|
||||
@ -390,6 +394,67 @@ export class LinearElementEditor {
|
||||
element.points[pointIndex][0] + deltaX,
|
||||
element.points[pointIndex][1] + deltaY,
|
||||
);
|
||||
|
||||
if (
|
||||
pointIndex === 0 ||
|
||||
pointIndex === element.points.length - 1
|
||||
) {
|
||||
const [, , , , cx, cy] = getElementAbsoluteCoords(
|
||||
element,
|
||||
elementsMap,
|
||||
true,
|
||||
);
|
||||
let newGlobalPointPosition = pointRotateRads(
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + newPointPosition[0],
|
||||
element.y + newPointPosition[1],
|
||||
),
|
||||
pointFrom<GlobalPoint>(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
const hoveredElement = getHoveredElementForBinding(
|
||||
{
|
||||
x: newGlobalPointPosition[0],
|
||||
y: newGlobalPointPosition[1],
|
||||
},
|
||||
app.scene.getNonDeletedElements(),
|
||||
elementsMap,
|
||||
app.state.zoom,
|
||||
true,
|
||||
isElbowArrow(element),
|
||||
);
|
||||
|
||||
const otherBinding =
|
||||
element[pointIndex === 0 ? "endBinding" : "startBinding"];
|
||||
|
||||
// Allow binding inside the element if both ends are inside
|
||||
if (
|
||||
isArrowElement(element) &&
|
||||
!(
|
||||
hoveredElement?.id === otherBinding?.elementId &&
|
||||
hoveredElement != null
|
||||
)
|
||||
) {
|
||||
newGlobalPointPosition = getOutlineAvoidingPoint(
|
||||
element,
|
||||
hoveredElement,
|
||||
newGlobalPointPosition,
|
||||
pointIndex,
|
||||
elementsMap,
|
||||
);
|
||||
}
|
||||
|
||||
newPointPosition = LinearElementEditor.createPointAt(
|
||||
element,
|
||||
elementsMap,
|
||||
newGlobalPointPosition[0] -
|
||||
linearElementEditor.pointerOffset.x,
|
||||
newGlobalPointPosition[1] -
|
||||
linearElementEditor.pointerOffset.y,
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
pointIndex,
|
||||
{
|
||||
@ -452,6 +517,7 @@ export class LinearElementEditor {
|
||||
coords,
|
||||
app.scene,
|
||||
app.state.zoom,
|
||||
elementsMap,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -124,6 +124,7 @@ export const getDefaultAppState = (): Omit<
|
||||
searchMatches: null,
|
||||
lockedMultiSelections: {},
|
||||
activeLockedId: null,
|
||||
arrowOriginalEndpoint: null,
|
||||
};
|
||||
};
|
||||
|
||||
@ -249,6 +250,7 @@ const APP_STATE_STORAGE_CONF = (<
|
||||
searchMatches: { browser: false, export: false, server: false },
|
||||
lockedMultiSelections: { browser: true, export: true, server: true },
|
||||
activeLockedId: { browser: false, export: false, server: false },
|
||||
arrowOriginalEndpoint: { browser: false, export: false, server: false },
|
||||
});
|
||||
|
||||
const _clearAppStateForStorage = <
|
||||
|
@ -232,9 +232,11 @@ import {
|
||||
hitElementBoundingBox,
|
||||
isLineElement,
|
||||
isSimpleArrow,
|
||||
getOutlineAvoidingPoint,
|
||||
mutateElement,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import type { LocalPoint, Radians } from "@excalidraw/math";
|
||||
import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math";
|
||||
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
@ -5888,6 +5890,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
[scenePointer],
|
||||
this.scene,
|
||||
this.state.zoom,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
//this.state.startBoundElement,
|
||||
),
|
||||
});
|
||||
} else {
|
||||
@ -5897,9 +5901,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
if (this.state.multiElement) {
|
||||
const { multiElement } = this.state;
|
||||
const { x: rx, y: ry } = multiElement;
|
||||
|
||||
const { points, lastCommittedPoint } = multiElement;
|
||||
const { x: rx, y: ry, points, lastCommittedPoint } = multiElement;
|
||||
const lastPoint = points[points.length - 1];
|
||||
|
||||
setCursorForShape(this.interactiveCanvas, this.state);
|
||||
@ -7757,7 +7759,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
elementType === "arrow"
|
||||
? [currentItemStartArrowhead, currentItemEndArrowhead]
|
||||
: [null, null];
|
||||
|
||||
const element =
|
||||
elementType === "arrow"
|
||||
? newArrowElement({
|
||||
@ -7805,6 +7806,28 @@ class App extends React.Component<AppProps, AppState> {
|
||||
locked: false,
|
||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||
});
|
||||
const hoveredElement = getHoveredElementForBinding(
|
||||
{
|
||||
x: gridX,
|
||||
y: gridY,
|
||||
},
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.state.zoom,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
|
||||
if (hoveredElement) {
|
||||
mutateElement(element, this.scene.getNonDeletedElementsMap(), {
|
||||
startBinding: {
|
||||
elementId: hoveredElement.id,
|
||||
focus: 0,
|
||||
gap: 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.setState((prevState) => {
|
||||
const nextSelectedElementIds = {
|
||||
...prevState.selectedElementIds,
|
||||
@ -8671,8 +8694,67 @@ class App extends React.Component<AppProps, AppState> {
|
||||
} else if (isLinearElement(newElement)) {
|
||||
pointerDownState.drag.hasOccurred = true;
|
||||
const points = newElement.points;
|
||||
const hoveredElement = getHoveredElementForBinding(
|
||||
{ x: gridX, y: gridY },
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.state.zoom,
|
||||
isElbowArrow(newElement),
|
||||
isElbowArrow(newElement),
|
||||
);
|
||||
const arrowIsInsideTheSameElement =
|
||||
newElement.startBinding &&
|
||||
hoveredElement?.id === newElement.startBinding.elementId;
|
||||
|
||||
let dx = gridX - newElement.x;
|
||||
let dy = gridY - newElement.y;
|
||||
let firstPointX = newElement.x + newElement.points[0][0];
|
||||
let firstPointY = newElement.y + newElement.points[0][1];
|
||||
|
||||
if (isBindingElement(newElement, false)) {
|
||||
if (!arrowIsInsideTheSameElement) {
|
||||
const [outlinePointX, outlinePointY] = getOutlineAvoidingPoint(
|
||||
newElement,
|
||||
hoveredElement,
|
||||
pointFrom(gridX, gridY),
|
||||
newElement.points.length - 1,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
dx = outlinePointX - newElement.x;
|
||||
dy = outlinePointY - newElement.y;
|
||||
|
||||
if (!this.state.arrowOriginalEndpoint) {
|
||||
this.setState({
|
||||
arrowOriginalEndpoint: pointFrom<GlobalPoint>(
|
||||
firstPointX,
|
||||
firstPointY,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
const otherHoveredElement = getHoveredElementForBinding(
|
||||
{ x: firstPointX, y: firstPointY },
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.state.zoom,
|
||||
isElbowArrow(newElement),
|
||||
isElbowArrow(newElement),
|
||||
);
|
||||
[firstPointX, firstPointY] = getOutlineAvoidingPoint(
|
||||
newElement,
|
||||
otherHoveredElement,
|
||||
pointFrom(firstPointX, firstPointY),
|
||||
0,
|
||||
elementsMap,
|
||||
);
|
||||
} else {
|
||||
firstPointX =
|
||||
this.state.arrowOriginalEndpoint?.[0] ?? firstPointX;
|
||||
firstPointY =
|
||||
this.state.arrowOriginalEndpoint?.[1] ?? firstPointY;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldRotateWithDiscreteAngle(event) && points.length === 2) {
|
||||
({ width: dx, height: dy } = getLockedLinearCursorAlignSize(
|
||||
@ -8687,6 +8769,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.scene.mutateElement(
|
||||
newElement,
|
||||
{
|
||||
x: firstPointX,
|
||||
y: firstPointY,
|
||||
points: [...points, pointFrom<LocalPoint>(dx, dy)],
|
||||
},
|
||||
{ informMutation: false, isDragging: false },
|
||||
@ -8698,6 +8782,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.scene.mutateElement(
|
||||
newElement,
|
||||
{
|
||||
x: firstPointX,
|
||||
y: firstPointY,
|
||||
points: [...points.slice(0, -1), pointFrom<LocalPoint>(dx, dy)],
|
||||
},
|
||||
{ isDragging: true, informMutation: false },
|
||||
@ -8716,6 +8802,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
[pointerCoords],
|
||||
this.scene,
|
||||
this.state.zoom,
|
||||
elementsMap,
|
||||
this.state.startBoundElement,
|
||||
),
|
||||
});
|
||||
}
|
||||
@ -8953,6 +9041,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
this.setState({
|
||||
selectedElementsAreBeingDragged: false,
|
||||
arrowOriginalEndpoint: null,
|
||||
});
|
||||
const elementsMap = this.scene.getNonDeletedElementsMap();
|
||||
|
||||
|
@ -47,6 +47,7 @@ import type {
|
||||
DurableIncrement,
|
||||
EphemeralIncrement,
|
||||
} from "@excalidraw/element";
|
||||
import type { GlobalPoint } from "@excalidraw/math";
|
||||
|
||||
import type { Action } from "./actions/types";
|
||||
import type { Spreadsheet } from "./charts";
|
||||
@ -444,6 +445,7 @@ export interface AppState {
|
||||
// as elements are unlocked, we remove the groupId from the elements
|
||||
// and also remove groupId from this map
|
||||
lockedMultiSelections: { [groupId: string]: true };
|
||||
arrowOriginalEndpoint: GlobalPoint | null;
|
||||
}
|
||||
|
||||
export type SearchMatch = {
|
||||
|
Loading…
x
Reference in New Issue
Block a user