Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f59095395 | |||
| c11532ba5f | |||
| 67a75a9542 |
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
pointFrom,
|
||||
pointFromPair,
|
||||
type GlobalPoint,
|
||||
type LocalPoint,
|
||||
@@ -70,12 +69,12 @@ export const getGridPoint = (
|
||||
x: number,
|
||||
y: number,
|
||||
gridSize: NullableGridSize,
|
||||
): GlobalPoint => {
|
||||
): [number, number] => {
|
||||
if (gridSize) {
|
||||
return pointFrom<GlobalPoint>(
|
||||
return [
|
||||
Math.round(x / gridSize) * gridSize,
|
||||
Math.round(y / gridSize) * gridSize,
|
||||
);
|
||||
];
|
||||
}
|
||||
return pointFrom<GlobalPoint>(x, y);
|
||||
return [x, y];
|
||||
};
|
||||
|
||||
@@ -125,7 +125,7 @@ const focusPointUpdate = (
|
||||
if (switchToInsideBinding || boundToSameElement) {
|
||||
currentBinding = {
|
||||
...currentBinding,
|
||||
mode: "inside",
|
||||
mode: "fixed",
|
||||
};
|
||||
} else {
|
||||
currentBinding = {
|
||||
@@ -166,7 +166,7 @@ const focusPointUpdate = (
|
||||
if (switchToInsideBinding || boundToSameElementAfterUpdate) {
|
||||
adjacentBinding = {
|
||||
...adjacentBinding,
|
||||
mode: "inside",
|
||||
mode: "fixed",
|
||||
};
|
||||
} else {
|
||||
adjacentBinding = {
|
||||
@@ -256,8 +256,8 @@ export const handleFocusPointDrag = (
|
||||
// Handle binding mode switch
|
||||
const newMode =
|
||||
switchToInsideBinding && arrow[bindingField]?.mode === "orbit"
|
||||
? "inside"
|
||||
: !switchToInsideBinding && arrow[bindingField]?.mode === "inside"
|
||||
? "fixed"
|
||||
: !switchToInsideBinding && arrow[bindingField]?.mode === "fixed"
|
||||
? "orbit"
|
||||
: null;
|
||||
|
||||
|
||||
+28
-151
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
arrayToMap,
|
||||
getFeatureFlag,
|
||||
getGridPoint,
|
||||
invariant,
|
||||
isTransparent,
|
||||
} from "@excalidraw/common";
|
||||
@@ -23,7 +22,7 @@ import {
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import type { LineSegment, LocalPoint, Radians } from "@excalidraw/math";
|
||||
import type { AppState, NullableGridSize } from "@excalidraw/excalidraw/types";
|
||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||
import type { MapEntry, Mutable } from "@excalidraw/common/utility-types";
|
||||
import type { Bounds } from "@excalidraw/common";
|
||||
|
||||
@@ -155,7 +154,6 @@ export const bindOrUnbindBindingElement = (
|
||||
altKey?: boolean;
|
||||
angleLocked?: boolean;
|
||||
initialBinding?: boolean;
|
||||
gridSize?: NullableGridSize;
|
||||
},
|
||||
) => {
|
||||
const { start, end } = getBindingStrategyForDraggingBindingElementEndpoints(
|
||||
@@ -172,16 +170,12 @@ export const bindOrUnbindBindingElement = (
|
||||
},
|
||||
);
|
||||
|
||||
const isMidpointSnappingEnabled =
|
||||
appState.isMidpointSnappingEnabled && !appState.gridModeEnabled;
|
||||
|
||||
bindOrUnbindBindingElementEdge(
|
||||
arrow,
|
||||
start,
|
||||
"start",
|
||||
scene,
|
||||
appState.isBindingEnabled,
|
||||
isMidpointSnappingEnabled,
|
||||
);
|
||||
bindOrUnbindBindingElementEdge(
|
||||
arrow,
|
||||
@@ -189,7 +183,6 @@ export const bindOrUnbindBindingElement = (
|
||||
"end",
|
||||
scene,
|
||||
appState.isBindingEnabled,
|
||||
isMidpointSnappingEnabled,
|
||||
);
|
||||
if (start.focusPoint || end.focusPoint) {
|
||||
// If the strategy dictates a focus point override, then
|
||||
@@ -234,7 +227,6 @@ const bindOrUnbindBindingElementEdge = (
|
||||
startOrEnd: "start" | "end",
|
||||
scene: Scene,
|
||||
shouldSnapToOutline = true,
|
||||
isMidpointSnappingEnabled = true,
|
||||
): void => {
|
||||
if (mode === null) {
|
||||
// null means break the binding
|
||||
@@ -248,7 +240,6 @@ const bindOrUnbindBindingElementEdge = (
|
||||
scene,
|
||||
focusPoint,
|
||||
shouldSnapToOutline,
|
||||
isMidpointSnappingEnabled,
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -337,7 +328,7 @@ const bindingStrategyForNewSimpleArrowEndpointDragging = (
|
||||
if (hit) {
|
||||
start = {
|
||||
element: hit,
|
||||
mode: "inside",
|
||||
mode: "fixed",
|
||||
focusPoint: point,
|
||||
};
|
||||
} else {
|
||||
@@ -362,13 +353,13 @@ const bindingStrategyForNewSimpleArrowEndpointDragging = (
|
||||
start: isMultiPoint
|
||||
? { mode: undefined }
|
||||
: {
|
||||
mode: "inside",
|
||||
mode: "fixed",
|
||||
element: hit,
|
||||
focusPoint: origin ?? center,
|
||||
},
|
||||
end: isMultiPoint
|
||||
? { mode: "orbit", element: hit, focusPoint: point }
|
||||
: { mode: "inside", element: hit, focusPoint: point },
|
||||
: { mode: "fixed", element: hit, focusPoint: point },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -388,7 +379,7 @@ const bindingStrategyForNewSimpleArrowEndpointDragging = (
|
||||
start: isMultiPoint
|
||||
? { mode: undefined }
|
||||
: {
|
||||
mode: otherElement.id !== hit.id ? "orbit" : "inside",
|
||||
mode: otherElement.id !== hit.id ? "orbit" : "fixed",
|
||||
element: otherElement,
|
||||
focusPoint: origin ?? pointFrom<GlobalPoint>(arrow.x, arrow.y),
|
||||
},
|
||||
@@ -411,7 +402,7 @@ const bindingStrategyForNewSimpleArrowEndpointDragging = (
|
||||
const otherIsInsideBinding =
|
||||
!!appState.selectedLinearElement?.initialState.arrowStartIsInside;
|
||||
const other: BindingStrategy = {
|
||||
mode: otherIsInsideBinding ? "inside" : "orbit",
|
||||
mode: otherIsInsideBinding ? "fixed" : "orbit",
|
||||
element: otherElement,
|
||||
focusPoint: shiftKey
|
||||
? elementCenterPoint(otherElement, elementsMap)
|
||||
@@ -425,9 +416,9 @@ const bindingStrategyForNewSimpleArrowEndpointDragging = (
|
||||
let current: BindingStrategy;
|
||||
if (hit) {
|
||||
const isInsideBinding =
|
||||
globalBindMode === "inside" || globalBindMode === "skip";
|
||||
globalBindMode === "fixed" || globalBindMode === "skip";
|
||||
current = {
|
||||
mode: isInsideBinding && !isNested ? "inside" : "orbit",
|
||||
mode: isInsideBinding && !isNested ? "fixed" : "orbit",
|
||||
element: hit,
|
||||
focusPoint: isInsideBinding || isNested ? point : point,
|
||||
};
|
||||
@@ -445,10 +436,10 @@ const bindingStrategyForNewSimpleArrowEndpointDragging = (
|
||||
if (!arrow.startBinding) {
|
||||
if (hit) {
|
||||
const isInsideBinding =
|
||||
globalBindMode === "inside" || globalBindMode === "skip";
|
||||
globalBindMode === "fixed" || globalBindMode === "skip";
|
||||
|
||||
end = {
|
||||
mode: isInsideBinding ? "inside" : "orbit",
|
||||
mode: isInsideBinding ? "fixed" : "orbit",
|
||||
element: hit,
|
||||
focusPoint: point,
|
||||
};
|
||||
@@ -497,7 +488,7 @@ const bindingStrategyForSimpleArrowEndpointDragging_complex = (
|
||||
|
||||
// If the global bind mode is in free binding mode, just bind
|
||||
// where the pointer is and keep the other end intact
|
||||
if (globalBindMode === "inside" || globalBindMode === "skip") {
|
||||
if (globalBindMode === "fixed" || globalBindMode === "skip") {
|
||||
current = hit
|
||||
? {
|
||||
element:
|
||||
@@ -505,7 +496,7 @@ const bindingStrategyForSimpleArrowEndpointDragging_complex = (
|
||||
? hit
|
||||
: oppositeElement,
|
||||
focusPoint: point,
|
||||
mode: "inside",
|
||||
mode: "fixed",
|
||||
}
|
||||
: { mode: null };
|
||||
other =
|
||||
@@ -523,12 +514,9 @@ const bindingStrategyForSimpleArrowEndpointDragging_complex = (
|
||||
}
|
||||
|
||||
// Already inside binding over the same hit element should remain inside bound
|
||||
if (
|
||||
hit.id === currentBinding?.elementId &&
|
||||
currentBinding.mode === "inside"
|
||||
) {
|
||||
if (hit.id === currentBinding?.elementId && currentBinding.mode === "fixed") {
|
||||
return {
|
||||
current: { mode: "inside", focusPoint: point, element: hit },
|
||||
current: { mode: "fixed", focusPoint: point, element: hit },
|
||||
other,
|
||||
};
|
||||
}
|
||||
@@ -547,7 +535,7 @@ const bindingStrategyForSimpleArrowEndpointDragging_complex = (
|
||||
// The opposite binding is inside the same element
|
||||
// eslint-disable-next-line no-else-return
|
||||
else {
|
||||
current = { element: hit, mode: "inside", focusPoint: point };
|
||||
current = { element: hit, mode: "fixed", focusPoint: point };
|
||||
|
||||
return { current, other: isMultiPoint ? { mode: undefined } : other };
|
||||
}
|
||||
@@ -559,7 +547,7 @@ const bindingStrategyForSimpleArrowEndpointDragging_complex = (
|
||||
if (isOverlapping && oppositeElement && !otherIsTransparent) {
|
||||
current = {
|
||||
element: oppositeElement,
|
||||
mode: "inside",
|
||||
mode: "fixed",
|
||||
focusPoint: point,
|
||||
};
|
||||
} else {
|
||||
@@ -602,7 +590,6 @@ export const getBindingStrategyForDraggingBindingElementEndpoints = (
|
||||
finalize?: boolean;
|
||||
initialBinding?: boolean;
|
||||
zoom?: AppState["zoom"];
|
||||
gridSize?: NullableGridSize;
|
||||
},
|
||||
): { start: BindingStrategy; end: BindingStrategy } => {
|
||||
if (getFeatureFlag("COMPLEX_BINDINGS")) {
|
||||
@@ -643,7 +630,6 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
|
||||
finalize?: boolean;
|
||||
initialBinding?: boolean;
|
||||
zoom?: AppState["zoom"];
|
||||
gridSize?: NullableGridSize;
|
||||
},
|
||||
): { start: BindingStrategy; end: BindingStrategy } => {
|
||||
const startIdx = 0;
|
||||
@@ -706,9 +692,7 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
|
||||
elementsMap,
|
||||
);
|
||||
const hit = getHoveredElementForBinding(
|
||||
opts?.angleLocked || appState.gridModeEnabled
|
||||
? pointFrom<GlobalPoint>(scenePointerX, scenePointerY)
|
||||
: globalPoint,
|
||||
globalPoint,
|
||||
elements,
|
||||
elementsMap,
|
||||
maxBindingDistance_simple(appState.zoom),
|
||||
@@ -754,17 +738,13 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
|
||||
) {
|
||||
return {
|
||||
start: {
|
||||
mode: "inside",
|
||||
mode: "fixed",
|
||||
element: hit,
|
||||
focusPoint: startDragged
|
||||
? globalPoint
|
||||
: // NOTE: Can only affect the start point because new arrows always drag the end point
|
||||
opts?.newArrow
|
||||
? getGridPoint(
|
||||
appState.selectedLinearElement!.initialState.origin![0],
|
||||
appState.selectedLinearElement!.initialState.origin![1],
|
||||
opts.gridSize as NullableGridSize,
|
||||
)
|
||||
? appState.selectedLinearElement!.initialState.origin!
|
||||
: LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||
arrow,
|
||||
0,
|
||||
@@ -772,7 +752,7 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
|
||||
), // startFixedPoint,
|
||||
},
|
||||
end: {
|
||||
mode: "inside",
|
||||
mode: "fixed",
|
||||
element: hit,
|
||||
focusPoint: endDragged
|
||||
? globalPoint
|
||||
@@ -791,7 +771,7 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
|
||||
start: startDragged
|
||||
? hit
|
||||
? {
|
||||
mode: "inside",
|
||||
mode: "fixed",
|
||||
element: hit,
|
||||
focusPoint: globalPoint,
|
||||
}
|
||||
@@ -800,7 +780,7 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
|
||||
end: endDragged
|
||||
? hit
|
||||
? {
|
||||
mode: "inside",
|
||||
mode: "fixed",
|
||||
element: hit,
|
||||
focusPoint: globalPoint,
|
||||
}
|
||||
@@ -813,7 +793,7 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
|
||||
const current: BindingStrategy = hit
|
||||
? pointInElement
|
||||
? {
|
||||
mode: "inside",
|
||||
mode: "fixed",
|
||||
element: hit,
|
||||
focusPoint: globalPoint,
|
||||
}
|
||||
@@ -823,27 +803,12 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
|
||||
focusPoint:
|
||||
projectFixedPointOntoDiagonal(
|
||||
arrow,
|
||||
opts?.angleLocked || appState.gridModeEnabled
|
||||
? snapBoundPointToGrid(
|
||||
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
|
||||
hit,
|
||||
elementsMap,
|
||||
appState.gridSize as NullableGridSize,
|
||||
arrow,
|
||||
LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||
arrow,
|
||||
startDragged ? 1 : -2,
|
||||
elementsMap,
|
||||
),
|
||||
)
|
||||
: globalPoint,
|
||||
globalPoint,
|
||||
hit,
|
||||
startDragged ? "start" : "end",
|
||||
elementsMap,
|
||||
appState.zoom,
|
||||
appState.isMidpointSnappingEnabled &&
|
||||
!opts?.angleLocked &&
|
||||
!appState.gridModeEnabled,
|
||||
appState.isMidpointSnappingEnabled,
|
||||
) || globalPoint,
|
||||
}
|
||||
: { mode: null };
|
||||
@@ -865,7 +830,7 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
|
||||
});
|
||||
const otherNeverOverride = opts?.newArrow
|
||||
? appState.selectedLinearElement?.initialState.arrowStartIsInside
|
||||
: otherBinding?.mode === "inside";
|
||||
: otherBinding?.mode === "fixed";
|
||||
const other: BindingStrategy = !otherNeverOverride
|
||||
? otherBindableElement &&
|
||||
!otherFocusPointIsInElement &&
|
||||
@@ -888,7 +853,7 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
|
||||
startDragged ? "end" : "start",
|
||||
elementsMap,
|
||||
appState.zoom,
|
||||
false,
|
||||
appState.isMidpointSnappingEnabled,
|
||||
) || otherEndpoint,
|
||||
}
|
||||
: { mode: undefined }
|
||||
@@ -1053,7 +1018,6 @@ export const bindBindingElement = (
|
||||
scene: Scene,
|
||||
focusPoint?: GlobalPoint,
|
||||
shouldSnapToOutline = true,
|
||||
isMidpointSnappingEnabled = true,
|
||||
): void => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
|
||||
@@ -1069,7 +1033,6 @@ export const bindBindingElement = (
|
||||
startOrEnd,
|
||||
elementsMap,
|
||||
shouldSnapToOutline,
|
||||
isMidpointSnappingEnabled,
|
||||
),
|
||||
};
|
||||
} else {
|
||||
@@ -1774,92 +1737,6 @@ const extractBinding = (
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Snaps a bound arrow endpoint to the grid on the axis parallel to the
|
||||
* bindable element's side, while preserving the binding gap distance on the
|
||||
* perpendicular axis. In other words, the grid axis closest to the side's
|
||||
* perpendicular (normal) is used as the snap axis and the other axis is kept at
|
||||
* the binding gap distance.
|
||||
*/
|
||||
const snapBoundPointToGrid = (
|
||||
outlinePoint: GlobalPoint,
|
||||
bindableElement: ExcalidrawBindableElement,
|
||||
elementsMap: ElementsMap,
|
||||
gridSize: NullableGridSize,
|
||||
arrowElement: ExcalidrawArrowElement,
|
||||
adjacentPoint?: GlobalPoint,
|
||||
): GlobalPoint => {
|
||||
if (!gridSize) {
|
||||
return outlinePoint;
|
||||
}
|
||||
|
||||
const aabb = aabbForElement(bindableElement, elementsMap);
|
||||
// For ellipses and diamonds use the arrow's incoming direction instead of
|
||||
// the position-based heading, which can give the wrong axis when the
|
||||
// outline point is near a cardinal zone or an angled diamond face.
|
||||
const heading =
|
||||
adjacentPoint &&
|
||||
(bindableElement.type === "ellipse" || bindableElement.type === "diamond")
|
||||
? vectorToHeading(vectorFromPoint(adjacentPoint, outlinePoint))
|
||||
: headingForPointFromElement(bindableElement, aabb, outlinePoint);
|
||||
|
||||
const normalLocal = pointFrom<GlobalPoint>(heading[0], heading[1]);
|
||||
const normalGlobal = pointRotateRads(
|
||||
normalLocal,
|
||||
pointFrom<GlobalPoint>(0, 0),
|
||||
bindableElement.angle,
|
||||
);
|
||||
|
||||
const bindingGap = getBindingGap(bindableElement, arrowElement);
|
||||
const extent =
|
||||
Math.max(bindableElement.width, bindableElement.height) + bindingGap * 2;
|
||||
const center = getCenterForBounds(aabb);
|
||||
|
||||
const absNX = Math.abs(normalGlobal[0]);
|
||||
const absNY = Math.abs(normalGlobal[1]);
|
||||
if (absNX >= absNY) {
|
||||
// Global X is closest to the perpendicular so snap Y, intersect horizontal line
|
||||
const [, snappedY] = getGridPoint(
|
||||
outlinePoint[0],
|
||||
outlinePoint[1],
|
||||
gridSize,
|
||||
);
|
||||
const intersector = lineSegment<GlobalPoint>(
|
||||
pointFrom<GlobalPoint>(center[0] - extent, snappedY),
|
||||
pointFrom<GlobalPoint>(center[0] + extent, snappedY),
|
||||
);
|
||||
const intersection = intersectElementWithLineSegment(
|
||||
bindableElement,
|
||||
elementsMap,
|
||||
intersector,
|
||||
bindingGap,
|
||||
).sort(
|
||||
(a, b) =>
|
||||
pointDistanceSq(a, outlinePoint) - pointDistanceSq(b, outlinePoint),
|
||||
)[0];
|
||||
|
||||
return intersection ?? pointFrom<GlobalPoint>(outlinePoint[0], snappedY);
|
||||
}
|
||||
|
||||
// Global Y is closest to the perpendicular so snap X, intersect vertical line
|
||||
const [snappedX] = getGridPoint(outlinePoint[0], outlinePoint[1], gridSize);
|
||||
const intersector = lineSegment<GlobalPoint>(
|
||||
pointFrom<GlobalPoint>(snappedX, center[1] - extent),
|
||||
pointFrom<GlobalPoint>(snappedX, center[1] + extent),
|
||||
);
|
||||
const intersection = intersectElementWithLineSegment(
|
||||
bindableElement,
|
||||
elementsMap,
|
||||
intersector,
|
||||
bindingGap,
|
||||
).sort(
|
||||
(a, b) =>
|
||||
pointDistanceSq(a, outlinePoint) - pointDistanceSq(b, outlinePoint),
|
||||
)[0];
|
||||
|
||||
return intersection ?? pointFrom<GlobalPoint>(snappedX, outlinePoint[1]);
|
||||
};
|
||||
|
||||
const elementArea = (element: ExcalidrawBindableElement) =>
|
||||
element.width * element.height;
|
||||
|
||||
@@ -1892,7 +1769,7 @@ export const updateBoundPoint = (
|
||||
|
||||
// 0. Short-circuit for inside binding as it doesn't require any
|
||||
// calculations and is not affected by other bindings
|
||||
if (binding.mode === "inside") {
|
||||
if (binding.mode === "fixed") {
|
||||
return LinearElementEditor.createPointAt(
|
||||
arrow,
|
||||
elementsMap,
|
||||
|
||||
@@ -762,7 +762,7 @@ export const isPointInElement = (
|
||||
(isLinearElement(element) || isFreeDrawElement(element)) &&
|
||||
!isPathALoop(element.points)
|
||||
) {
|
||||
// There isn't any "inside" for a non-looping path
|
||||
// There isn't any "fixed" for a non-looping path
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -359,7 +359,6 @@ export class LinearElementEditor {
|
||||
linearElementEditor,
|
||||
);
|
||||
|
||||
const angleLocked = shouldRotateWithDiscreteAngle(event);
|
||||
LinearElementEditor.movePoints(
|
||||
element,
|
||||
app.scene,
|
||||
@@ -371,10 +370,7 @@ export class LinearElementEditor {
|
||||
},
|
||||
{
|
||||
isBindingEnabled: app.state.isBindingEnabled,
|
||||
isMidpointSnappingEnabled:
|
||||
app.state.isMidpointSnappingEnabled &&
|
||||
!angleLocked &&
|
||||
!app.state.gridModeEnabled,
|
||||
isMidpointSnappingEnabled: app.state.isMidpointSnappingEnabled,
|
||||
},
|
||||
);
|
||||
// Set the suggested binding from the updates if available
|
||||
@@ -431,9 +427,7 @@ export class LinearElementEditor {
|
||||
"start",
|
||||
elementsMap,
|
||||
app.state.zoom,
|
||||
app.state.isMidpointSnappingEnabled &&
|
||||
!angleLocked &&
|
||||
!app.state.gridModeEnabled,
|
||||
app.state.isMidpointSnappingEnabled,
|
||||
)
|
||||
: linearElementEditor.initialState.altFocusPoint,
|
||||
},
|
||||
@@ -560,8 +554,6 @@ export class LinearElementEditor {
|
||||
linearElementEditor,
|
||||
);
|
||||
|
||||
const angleLocked =
|
||||
shouldRotateWithDiscreteAngle(event) && singlePointDragged;
|
||||
LinearElementEditor.movePoints(
|
||||
element,
|
||||
app.scene,
|
||||
@@ -573,10 +565,7 @@ export class LinearElementEditor {
|
||||
},
|
||||
{
|
||||
isBindingEnabled: app.state.isBindingEnabled,
|
||||
isMidpointSnappingEnabled:
|
||||
app.state.isMidpointSnappingEnabled &&
|
||||
!angleLocked &&
|
||||
!app.state.gridModeEnabled,
|
||||
isMidpointSnappingEnabled: app.state.isMidpointSnappingEnabled,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -672,9 +661,7 @@ export class LinearElementEditor {
|
||||
"start",
|
||||
elementsMap,
|
||||
app.state.zoom,
|
||||
app.state.isMidpointSnappingEnabled &&
|
||||
!angleLocked &&
|
||||
!app.state.gridModeEnabled,
|
||||
app.state.isMidpointSnappingEnabled,
|
||||
)
|
||||
: linearElementEditor.initialState.altFocusPoint,
|
||||
},
|
||||
@@ -1096,7 +1083,7 @@ export class LinearElementEditor {
|
||||
},
|
||||
arrowStartIsInside:
|
||||
!!app.state.newElement &&
|
||||
(app.state.bindMode === "inside" || app.state.bindMode === "skip"),
|
||||
(app.state.bindMode === "fixed" || app.state.bindMode === "skip"),
|
||||
altFocusPoint: null,
|
||||
},
|
||||
selectedPointsIndices: [element.points.length - 1],
|
||||
@@ -1158,7 +1145,7 @@ export class LinearElementEditor {
|
||||
},
|
||||
arrowStartIsInside:
|
||||
!!app.state.newElement &&
|
||||
(app.state.bindMode === "inside" || app.state.bindMode === "skip"),
|
||||
(app.state.bindMode === "fixed" || app.state.bindMode === "skip"),
|
||||
altFocusPoint: null,
|
||||
},
|
||||
selectedPointsIndices: nextSelectedPointsIndices,
|
||||
@@ -2189,7 +2176,6 @@ const pointDraggingUpdates = (
|
||||
newArrow: !!app.state.newElement,
|
||||
angleLocked,
|
||||
altKey,
|
||||
gridSize: app.getEffectiveGridSize(),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -2426,7 +2412,7 @@ const pointDraggingUpdates = (
|
||||
const endLocalPoint = startIsDraggingOverEndElement
|
||||
? nextArrow.points[nextArrow.points.length - 1]
|
||||
: endIsDraggingOverStartElement &&
|
||||
app.state.bindMode !== "inside" &&
|
||||
app.state.bindMode !== "fixed" &&
|
||||
getFeatureFlag("COMPLEX_BINDINGS")
|
||||
? nextArrow.points[0]
|
||||
: endBindable
|
||||
@@ -2457,7 +2443,7 @@ const pointDraggingUpdates = (
|
||||
endIsDraggingOverStartElement && getFeatureFlag("COMPLEX_BINDINGS")
|
||||
? nextArrow.points[0]
|
||||
: startIsDraggingOverEndElement &&
|
||||
app.state.bindMode !== "inside" &&
|
||||
app.state.bindMode !== "fixed" &&
|
||||
getFeatureFlag("COMPLEX_BINDINGS")
|
||||
? endLocalPoint
|
||||
: startBindable
|
||||
@@ -2475,7 +2461,7 @@ const pointDraggingUpdates = (
|
||||
!startIsDraggingOverEndElement &&
|
||||
!(
|
||||
endIsDraggingOverStartElement &&
|
||||
app.state.bindMode !== "inside" &&
|
||||
app.state.bindMode !== "fixed" &&
|
||||
getFeatureFlag("COMPLEX_BINDINGS")
|
||||
) &&
|
||||
!!endBindable;
|
||||
|
||||
@@ -279,7 +279,7 @@ export type ExcalidrawTextElementWithContainer = {
|
||||
|
||||
export type FixedPoint = [number, number];
|
||||
|
||||
export type BindMode = "inside" | "orbit" | "skip";
|
||||
export type BindMode = "fixed" | "orbit" | "skip";
|
||||
|
||||
export type FixedPointBinding = {
|
||||
elementId: ExcalidrawBindableElement["id"];
|
||||
|
||||
@@ -72,14 +72,14 @@ describe("binding for simple arrows", () => {
|
||||
expect(startBinding.fixedPoint[0]).toBeLessThanOrEqual(1);
|
||||
expect(startBinding.fixedPoint[1]).toBeGreaterThanOrEqual(0);
|
||||
expect(startBinding.fixedPoint[1]).toBeLessThanOrEqual(1);
|
||||
expect(startBinding.mode).toBe("inside");
|
||||
expect(startBinding.mode).toBe("fixed");
|
||||
|
||||
const endBinding = arrow.endBinding as FixedPointBinding;
|
||||
expect(endBinding.fixedPoint[0]).toBeGreaterThanOrEqual(0);
|
||||
expect(endBinding.fixedPoint[0]).toBeLessThanOrEqual(1);
|
||||
expect(endBinding.fixedPoint[1]).toBeGreaterThanOrEqual(0);
|
||||
expect(endBinding.fixedPoint[1]).toBeLessThanOrEqual(1);
|
||||
expect(endBinding.mode).toBe("inside");
|
||||
expect(endBinding.mode).toBe("fixed");
|
||||
|
||||
// Move the bindable
|
||||
mouse.downAt(100, 150);
|
||||
|
||||
@@ -27,7 +27,7 @@ import { isInvisiblySmallElement } from "@excalidraw/element";
|
||||
|
||||
import { CaptureUpdateAction } from "@excalidraw/element";
|
||||
|
||||
import type { LocalPoint } from "@excalidraw/math";
|
||||
import type { GlobalPoint, LocalPoint } from "@excalidraw/math";
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawLinearElement,
|
||||
@@ -93,40 +93,32 @@ export const actionFinalize = register<FormData>({
|
||||
? [element.points.length - 1] // New arrow creation
|
||||
: appState.selectedLinearElement.selectedPointsIndices;
|
||||
|
||||
const angleLocked = shouldRotateWithDiscreteAngle(event);
|
||||
const effectiveGridSize = event[KEYS.CTRL_OR_CMD]
|
||||
? null
|
||||
: app.getEffectiveGridSize();
|
||||
|
||||
const draggedPoints: PointsPositionUpdates =
|
||||
selectedPointsIndices.reduce((map, index) => {
|
||||
map.set(index, {
|
||||
point: angleLocked
|
||||
? element.points[index]
|
||||
: LinearElementEditor.createPointAt(
|
||||
element,
|
||||
elementsMap,
|
||||
sceneCoords.x - linearElementEditor.pointerOffset.x,
|
||||
sceneCoords.y - linearElementEditor.pointerOffset.y,
|
||||
effectiveGridSize,
|
||||
),
|
||||
point: LinearElementEditor.pointFromAbsoluteCoords(
|
||||
element,
|
||||
pointFrom<GlobalPoint>(
|
||||
sceneCoords.x - linearElementEditor.pointerOffset.x,
|
||||
sceneCoords.y - linearElementEditor.pointerOffset.y,
|
||||
),
|
||||
elementsMap,
|
||||
),
|
||||
});
|
||||
|
||||
return map;
|
||||
}, new Map()) ?? new Map();
|
||||
|
||||
bindOrUnbindBindingElement(
|
||||
element,
|
||||
draggedPoints,
|
||||
sceneCoords.x,
|
||||
sceneCoords.y,
|
||||
sceneCoords.x - linearElementEditor.pointerOffset.x,
|
||||
sceneCoords.y - linearElementEditor.pointerOffset.y,
|
||||
scene,
|
||||
appState,
|
||||
{
|
||||
newArrow,
|
||||
altKey: event.altKey,
|
||||
angleLocked,
|
||||
gridSize: app.getEffectiveGridSize(),
|
||||
angleLocked: shouldRotateWithDiscreteAngle(event),
|
||||
},
|
||||
);
|
||||
} else if (isLineElement(element)) {
|
||||
|
||||
@@ -1940,7 +1940,7 @@ export const actionChangeArrowType = register<keyof typeof ARROW_TYPE>({
|
||||
bindBindingElement(
|
||||
newElement,
|
||||
startElement,
|
||||
appState.bindMode === "inside" ? "inside" : "orbit",
|
||||
appState.bindMode === "fixed" ? "fixed" : "orbit",
|
||||
"start",
|
||||
app.scene,
|
||||
);
|
||||
@@ -1954,7 +1954,7 @@ export const actionChangeArrowType = register<keyof typeof ARROW_TYPE>({
|
||||
bindBindingElement(
|
||||
newElement,
|
||||
endElement,
|
||||
appState.bindMode === "inside" ? "inside" : "orbit",
|
||||
appState.bindMode === "fixed" ? "fixed" : "orbit",
|
||||
"end",
|
||||
app.scene,
|
||||
);
|
||||
|
||||
@@ -1095,7 +1095,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
);
|
||||
|
||||
this.setState({
|
||||
bindMode: "inside",
|
||||
bindMode: "fixed",
|
||||
selectedLinearElement: {
|
||||
...this.state.selectedLinearElement,
|
||||
initialState: {
|
||||
@@ -1176,16 +1176,16 @@ class App extends React.Component<AppProps, AppState> {
|
||||
: null;
|
||||
const isAlreadyInsideBindingToSameElement =
|
||||
(otherBinding &&
|
||||
arrow[otherBinding]?.mode === "inside" &&
|
||||
arrow[otherBinding]?.mode === "fixed" &&
|
||||
arrow[otherBinding]?.elementId === hoveredElement?.id) ||
|
||||
(currentBinding &&
|
||||
arrow[currentBinding]?.mode === "inside" &&
|
||||
arrow[currentBinding]?.mode === "fixed" &&
|
||||
hoveredElement?.id === arrow[currentBinding]?.elementId);
|
||||
|
||||
if (
|
||||
currentBinding &&
|
||||
otherBinding &&
|
||||
arrow[currentBinding]?.mode === "inside" &&
|
||||
arrow[currentBinding]?.mode === "fixed" &&
|
||||
hoveredElement?.id !== arrow[currentBinding]?.elementId &&
|
||||
arrow[otherBinding]?.elementId !== arrow[currentBinding]?.elementId
|
||||
) {
|
||||
@@ -1217,7 +1217,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
|
||||
// Clear the inside binding mode too
|
||||
if (this.state.bindMode === "inside") {
|
||||
if (this.state.bindMode === "fixed") {
|
||||
flushSync(() => {
|
||||
this.setState({
|
||||
bindMode: "orbit",
|
||||
@@ -7263,16 +7263,14 @@ class App extends React.Component<AppProps, AppState> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set suggested binding if we're hovering with an arrow tool
|
||||
// and not dragging out a new element
|
||||
if (this.state.activeTool.type === "arrow" && !this.state.newElement) {
|
||||
const scenePointer = pointFrom<GlobalPoint>(scenePointerX, scenePointerY);
|
||||
if (this.state.activeTool.type === "arrow") {
|
||||
const hit = getHoveredElementForBinding(
|
||||
scenePointer,
|
||||
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
maxBindingDistance_simple(this.state.zoom),
|
||||
);
|
||||
const scenePointer = pointFrom<GlobalPoint>(scenePointerX, scenePointerY);
|
||||
const elementsMap = this.scene.getNonDeletedElementsMap();
|
||||
if (hit && !isPointInElement(scenePointer, hit, elementsMap)) {
|
||||
this.setState({
|
||||
|
||||
@@ -253,7 +253,6 @@ const getRelevantAppStateProps = (
|
||||
newElement: appState.newElement,
|
||||
isBindingEnabled: appState.isBindingEnabled,
|
||||
isMidpointSnappingEnabled: appState.isMidpointSnappingEnabled,
|
||||
gridModeEnabled: appState.gridModeEnabled,
|
||||
suggestedBinding: appState.suggestedBinding,
|
||||
isRotating: appState.isRotating,
|
||||
elementsToHighlight: appState.elementsToHighlight,
|
||||
|
||||
@@ -239,8 +239,8 @@ const repairBinding = <T extends ExcalidrawArrowElement>(
|
||||
if (binding.elementId) {
|
||||
return {
|
||||
elementId: binding.elementId,
|
||||
mode: binding.mode,
|
||||
fixedPoint: normalizeFixedPoint(binding.fixedPoint),
|
||||
mode: (binding.mode as string) === "inside" ? "fixed" : binding.mode,
|
||||
fixedPoint: normalizeFixedPoint(binding.fixedPoint || [0.5, 0.5]),
|
||||
} as FixedPointBinding | null;
|
||||
}
|
||||
return null;
|
||||
@@ -269,7 +269,7 @@ const repairBinding = <T extends ExcalidrawArrowElement>(
|
||||
elementsMap,
|
||||
);
|
||||
const mode = isPointInElement(p, boundElement, elementsMap)
|
||||
? "inside"
|
||||
? "fixed"
|
||||
: "orbit";
|
||||
const safeElement = {
|
||||
...element,
|
||||
@@ -289,7 +289,7 @@ const repairBinding = <T extends ExcalidrawArrowElement>(
|
||||
: null,
|
||||
};
|
||||
const focusPoint =
|
||||
mode === "inside"
|
||||
mode === "fixed"
|
||||
? p
|
||||
: projectFixedPointOntoDiagonal(
|
||||
safeElement,
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
FRAME_STYLE,
|
||||
getFeatureFlag,
|
||||
invariant,
|
||||
shouldRotateWithDiscreteAngle,
|
||||
THEME,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
@@ -230,7 +229,6 @@ const renderBindingHighlightForBindableElement_simple = (
|
||||
elementsMap: ElementsMap,
|
||||
appState: InteractiveCanvasAppState,
|
||||
pointerCoords: GlobalPoint | null,
|
||||
angleLocked = false,
|
||||
) => {
|
||||
const enclosingFrame =
|
||||
suggestedBinding.element.frameId &&
|
||||
@@ -417,8 +415,6 @@ const renderBindingHighlightForBindableElement_simple = (
|
||||
|
||||
if (
|
||||
appState.isMidpointSnappingEnabled &&
|
||||
!appState.gridModeEnabled &&
|
||||
!angleLocked &&
|
||||
(isFrameLikeElement(suggestedBinding.element) ||
|
||||
isBindableElement(suggestedBinding.element))
|
||||
) {
|
||||
@@ -811,12 +807,7 @@ const renderBindingHighlightForBindableElement_complex = (
|
||||
|
||||
context.restore();
|
||||
|
||||
if (
|
||||
appState.isMidpointSnappingEnabled &&
|
||||
!appState.gridModeEnabled &&
|
||||
(!app.lastPointerMoveEvent ||
|
||||
!shouldRotateWithDiscreteAngle(app.lastPointerMoveEvent))
|
||||
) {
|
||||
if (appState.isMidpointSnappingEnabled) {
|
||||
// Draw midpoint indicators
|
||||
context.save();
|
||||
context.translate(
|
||||
@@ -929,16 +920,12 @@ const renderBindingHighlightForBindableElement = (
|
||||
app.lastPointerMoveCoords.y,
|
||||
)
|
||||
: null;
|
||||
const angleLocked =
|
||||
!!app.lastPointerMoveEvent &&
|
||||
shouldRotateWithDiscreteAngle(app.lastPointerMoveEvent);
|
||||
renderBindingHighlightForBindableElement_simple(
|
||||
context,
|
||||
suggestedBinding,
|
||||
allElementsMap,
|
||||
appState,
|
||||
pointerCoords,
|
||||
angleLocked,
|
||||
);
|
||||
context.restore();
|
||||
};
|
||||
|
||||
@@ -857,7 +857,7 @@ describe("repairing bindings", () => {
|
||||
endBinding: {
|
||||
elementId: container.id,
|
||||
fixedPoint: [0.5, 0.5],
|
||||
mode: "inside",
|
||||
mode: "fixed",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -224,7 +224,6 @@ export type InteractiveCanvasAppState = Readonly<
|
||||
newElement: AppState["newElement"];
|
||||
isBindingEnabled: AppState["isBindingEnabled"];
|
||||
isMidpointSnappingEnabled: AppState["isMidpointSnappingEnabled"];
|
||||
gridModeEnabled: AppState["gridModeEnabled"];
|
||||
suggestedBinding: AppState["suggestedBinding"];
|
||||
isRotating: AppState["isRotating"];
|
||||
elementsToHighlight: AppState["elementsToHighlight"];
|
||||
@@ -846,7 +845,6 @@ export type AppClassProperties = {
|
||||
onStateChange: App["onStateChange"];
|
||||
|
||||
lastPointerMoveCoords: App["lastPointerMoveCoords"];
|
||||
lastPointerMoveEvent: App["lastPointerMoveEvent"];
|
||||
bindModeHandler: App["bindModeHandler"];
|
||||
|
||||
setAppState: App["setAppState"];
|
||||
|
||||
Reference in New Issue
Block a user