feat(editor): visualize binding midpoints + support for simple arrows (#10611)
* feat: Force exact center focus point When the projected point is close to center snap it to the exact center. Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Tests Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Snap to center around side mid point. Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * Trigger CI * fix: Midpoint outline focus point Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Tests Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Dragging existing arrow reset focus point on outline Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Tests Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: Midpoint indicator Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Rotated mid points Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: No hole Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: Cache hits and scene lookups Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * chore: Remove debug Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Consider hit threshold and inside override too Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Increase outline midpoint sticky distance Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Don't show midpoint indicator when no snapping is possible Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: Indicate lock-in Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * chore: Remove Map caching Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: incorrect threshold Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: threshold setting Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Hit caching Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Simple arrow mid point selection inconsistency Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: cache override Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Precise know dragging with midpoint refactor Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fear: Frame support Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Crossing arrow won't trigger mid point Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Arrow creation point highlight Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Restore types & tests Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * chore: Restore restore.ts Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: restore.ts Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Elbow arrows reliably highlight center point Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Highlight point ordering Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: Bind with focus point across shape Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Lint * fix: Midpoint and binding alignment Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * chore: Indicator color Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * chore: More knob tuning Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Radius Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Tests Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * simplify point indicators --------- Signed-off-by: Mark Tolmacs <mark@lazycat.hu> Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
@@ -800,6 +800,7 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
|
||||
hit,
|
||||
startDragged ? "start" : "end",
|
||||
elementsMap,
|
||||
appState.zoom,
|
||||
) || globalPoint,
|
||||
}
|
||||
: { mode: null };
|
||||
@@ -833,6 +834,7 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
|
||||
otherBindableElement,
|
||||
startDragged ? "end" : "start",
|
||||
elementsMap,
|
||||
appState.zoom,
|
||||
) || otherEndpoint,
|
||||
}
|
||||
: { mode: undefined }
|
||||
@@ -1389,14 +1391,16 @@ export const bindPointToSnapToElementOutline = (
|
||||
headingForPointFromElement(bindableElement, aabb, point),
|
||||
);
|
||||
const snapPoint = snapToMid(
|
||||
arrowElement,
|
||||
bindableElement,
|
||||
elementsMap,
|
||||
edgePoint,
|
||||
0.05,
|
||||
arrowElement,
|
||||
);
|
||||
const resolved = snapPoint || point;
|
||||
const otherPoint = pointFrom<GlobalPoint>(
|
||||
isHorizontal ? bindableCenter[0] : snapPoint[0],
|
||||
!isHorizontal ? bindableCenter[1] : snapPoint[1],
|
||||
isHorizontal ? bindableCenter[0] : resolved[0],
|
||||
!isHorizontal ? bindableCenter[1] : resolved[1],
|
||||
);
|
||||
const intersector =
|
||||
customIntersector ??
|
||||
@@ -1404,7 +1408,7 @@ export const bindPointToSnapToElementOutline = (
|
||||
otherPoint,
|
||||
pointFromVector(
|
||||
vectorScale(
|
||||
vectorNormalize(vectorFromPoint(snapPoint, otherPoint)),
|
||||
vectorNormalize(vectorFromPoint(resolved, otherPoint)),
|
||||
Math.max(bindableElement.width, bindableElement.height) * 2,
|
||||
),
|
||||
otherPoint,
|
||||
@@ -1419,14 +1423,14 @@ export const bindPointToSnapToElementOutline = (
|
||||
|
||||
if (!intersection) {
|
||||
const anotherPoint = pointFrom<GlobalPoint>(
|
||||
!isHorizontal ? bindableCenter[0] : snapPoint[0],
|
||||
isHorizontal ? bindableCenter[1] : snapPoint[1],
|
||||
!isHorizontal ? bindableCenter[0] : resolved[0],
|
||||
isHorizontal ? bindableCenter[1] : resolved[1],
|
||||
);
|
||||
const anotherIntersector = lineSegment(
|
||||
anotherPoint,
|
||||
pointFromVector(
|
||||
vectorScale(
|
||||
vectorNormalize(vectorFromPoint(snapPoint, anotherPoint)),
|
||||
vectorNormalize(vectorFromPoint(resolved, anotherPoint)),
|
||||
Math.max(bindableElement.width, bindableElement.height) * 2,
|
||||
),
|
||||
anotherPoint,
|
||||
@@ -1573,18 +1577,18 @@ export const avoidRectangularCorner = (
|
||||
return p;
|
||||
};
|
||||
|
||||
const snapToMid = (
|
||||
arrowElement: ExcalidrawArrowElement,
|
||||
export const snapToMid = (
|
||||
bindTarget: ExcalidrawBindableElement,
|
||||
elementsMap: ElementsMap,
|
||||
p: GlobalPoint,
|
||||
tolerance: number = 0.05,
|
||||
): GlobalPoint => {
|
||||
arrowElement?: ExcalidrawArrowElement,
|
||||
): GlobalPoint | undefined => {
|
||||
const { x, y, width, height, angle } = bindTarget;
|
||||
const center = elementCenterPoint(bindTarget, elementsMap, -0.1, -0.1);
|
||||
const nonRotated = pointRotateRads(p, center, -angle as Radians);
|
||||
|
||||
const bindingGap = getBindingGap(bindTarget, arrowElement);
|
||||
const bindingGap = arrowElement ? getBindingGap(bindTarget, arrowElement) : 0;
|
||||
|
||||
// snap-to-center point is adaptive to element size, but we don't want to go
|
||||
// above and below certain px distance
|
||||
@@ -1593,7 +1597,7 @@ const snapToMid = (
|
||||
|
||||
// Too close to the center makes it hard to resolve direction precisely
|
||||
if (pointDistance(center, nonRotated) < bindingGap) {
|
||||
return p;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -1602,8 +1606,8 @@ const snapToMid = (
|
||||
nonRotated[1] < center[1] + verticalThreshold
|
||||
) {
|
||||
// LEFT
|
||||
return pointRotateRads<GlobalPoint>(
|
||||
pointFrom(x - bindingGap, center[1]),
|
||||
return pointRotateRads(
|
||||
pointFrom<GlobalPoint>(x - bindingGap, center[1]),
|
||||
center,
|
||||
angle,
|
||||
);
|
||||
@@ -1613,7 +1617,11 @@ const snapToMid = (
|
||||
nonRotated[0] < center[0] + horizontalThreshold
|
||||
) {
|
||||
// TOP
|
||||
return pointRotateRads(pointFrom(center[0], y - bindingGap), center, angle);
|
||||
return pointRotateRads(
|
||||
pointFrom<GlobalPoint>(center[0], y - bindingGap),
|
||||
center,
|
||||
angle,
|
||||
);
|
||||
} else if (
|
||||
nonRotated[0] >= x + width / 2 &&
|
||||
nonRotated[1] > center[1] - verticalThreshold &&
|
||||
@@ -1621,7 +1629,7 @@ const snapToMid = (
|
||||
) {
|
||||
// RIGHT
|
||||
return pointRotateRads(
|
||||
pointFrom(x + width + bindingGap, center[1]),
|
||||
pointFrom<GlobalPoint>(x + width + bindingGap, center[1]),
|
||||
center,
|
||||
angle,
|
||||
);
|
||||
@@ -1632,7 +1640,7 @@ const snapToMid = (
|
||||
) {
|
||||
// DOWN
|
||||
return pointRotateRads(
|
||||
pointFrom(center[0], y + height + bindingGap),
|
||||
pointFrom<GlobalPoint>(center[0], y + height + bindingGap),
|
||||
center,
|
||||
angle,
|
||||
);
|
||||
@@ -1681,7 +1689,7 @@ const snapToMid = (
|
||||
}
|
||||
}
|
||||
|
||||
return p;
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const compareElementArea = (
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
|
||||
import {
|
||||
deconstructLinearOrFreeDrawElement,
|
||||
getSnapOutlineMidPoint,
|
||||
isPathALoop,
|
||||
moveArrowAboveBindable,
|
||||
projectFixedPointOntoDiagonal,
|
||||
@@ -48,6 +49,7 @@ import {
|
||||
calculateFixedPointForNonElbowArrowBinding,
|
||||
getBindingStrategyForDraggingBindingElementEndpoints,
|
||||
isBindingEnabled,
|
||||
snapToMid,
|
||||
updateBoundPoint,
|
||||
} from "./binding";
|
||||
import {
|
||||
@@ -355,6 +357,7 @@ export class LinearElementEditor {
|
||||
app,
|
||||
shouldRotateWithDiscreteAngle(event),
|
||||
event.altKey,
|
||||
linearElementEditor,
|
||||
);
|
||||
|
||||
LinearElementEditor.movePoints(element, app.scene, positions, {
|
||||
@@ -408,13 +411,14 @@ export class LinearElementEditor {
|
||||
altFocusPoint:
|
||||
!linearElementEditor.initialState.altFocusPoint &&
|
||||
startBindingElement &&
|
||||
updates?.suggestedBinding?.id !== startBindingElement.id
|
||||
updates?.suggestedBinding?.element.id !== startBindingElement.id
|
||||
? projectFixedPointOntoDiagonal(
|
||||
element,
|
||||
pointFrom<GlobalPoint>(element.x, element.y),
|
||||
startBindingElement,
|
||||
"start",
|
||||
elementsMap,
|
||||
app.state.zoom,
|
||||
)
|
||||
: linearElementEditor.initialState.altFocusPoint,
|
||||
},
|
||||
@@ -532,6 +536,7 @@ export class LinearElementEditor {
|
||||
app,
|
||||
shouldRotateWithDiscreteAngle(event) && singlePointDragged,
|
||||
event.altKey,
|
||||
linearElementEditor,
|
||||
);
|
||||
|
||||
LinearElementEditor.movePoints(element, app.scene, positions, {
|
||||
@@ -607,11 +612,11 @@ export class LinearElementEditor {
|
||||
const altFocusPointBindableElement =
|
||||
endIsSelected && // The "other" end (i.e. "end") is dragged
|
||||
startBindingElement &&
|
||||
updates?.suggestedBinding?.id !== startBindingElement.id // The end point is not hovering the start bindable + it's binding gap
|
||||
updates?.suggestedBinding?.element.id !== startBindingElement.id // The end point is not hovering the start bindable + it's binding gap
|
||||
? startBindingElement
|
||||
: startIsSelected && // The "other" end (i.e. "start") is dragged
|
||||
endBindingElement &&
|
||||
updates?.suggestedBinding?.id !== endBindingElement.id // The start point is not hovering the end bindable + it's binding gap
|
||||
updates?.suggestedBinding?.element.id !== endBindingElement.id // The start point is not hovering the end bindable + it's binding gap
|
||||
? endBindingElement
|
||||
: null;
|
||||
|
||||
@@ -631,6 +636,7 @@ export class LinearElementEditor {
|
||||
altFocusPointBindableElement,
|
||||
"start",
|
||||
elementsMap,
|
||||
app.state.zoom,
|
||||
)
|
||||
: linearElementEditor.initialState.altFocusPoint,
|
||||
},
|
||||
@@ -2080,6 +2086,7 @@ const pointDraggingUpdates = (
|
||||
app: AppClassProperties,
|
||||
angleLocked: boolean,
|
||||
altKey: boolean,
|
||||
linearElementEditor: LinearElementEditor,
|
||||
): {
|
||||
positions: PointsPositionUpdates;
|
||||
updates?: PointMoveOtherUpdates;
|
||||
@@ -2127,13 +2134,27 @@ const pointDraggingUpdates = (
|
||||
);
|
||||
|
||||
if (isElbowArrow(element)) {
|
||||
const suggestedBindingElement = startIsDragged
|
||||
? start.element
|
||||
: endIsDragged
|
||||
? end.element
|
||||
: null;
|
||||
|
||||
return {
|
||||
positions: naiveDraggingPoints,
|
||||
updates: {
|
||||
suggestedBinding: startIsDragged
|
||||
? start.element
|
||||
: endIsDragged
|
||||
? end.element
|
||||
suggestedBinding: suggestedBindingElement
|
||||
? {
|
||||
element: suggestedBindingElement,
|
||||
midPoint: snapToMid(
|
||||
suggestedBindingElement,
|
||||
elementsMap,
|
||||
pointFrom<GlobalPoint>(
|
||||
scenePointerX - linearElementEditor.pointerOffset.x,
|
||||
scenePointerY - linearElementEditor.pointerOffset.y,
|
||||
),
|
||||
),
|
||||
}
|
||||
: null,
|
||||
},
|
||||
};
|
||||
@@ -2226,7 +2247,20 @@ const pointDraggingUpdates = (
|
||||
(updates.startBinding.mode === "orbit" ||
|
||||
!getFeatureFlag("COMPLEX_BINDINGS"))
|
||||
) {
|
||||
updates.suggestedBinding = start.element;
|
||||
updates.suggestedBinding = start.element
|
||||
? {
|
||||
element: start.element,
|
||||
midPoint: getSnapOutlineMidPoint(
|
||||
pointFrom<GlobalPoint>(
|
||||
scenePointerX - linearElementEditor.pointerOffset.x,
|
||||
scenePointerY - linearElementEditor.pointerOffset.y,
|
||||
),
|
||||
start.element,
|
||||
elementsMap,
|
||||
app.state.zoom,
|
||||
),
|
||||
}
|
||||
: null;
|
||||
}
|
||||
} else if (startIsDragged) {
|
||||
updates.suggestedBinding = app.state.suggestedBinding;
|
||||
@@ -2252,7 +2286,20 @@ const pointDraggingUpdates = (
|
||||
(updates.endBinding.mode === "orbit" ||
|
||||
!getFeatureFlag("COMPLEX_BINDINGS"))
|
||||
) {
|
||||
updates.suggestedBinding = end.element;
|
||||
updates.suggestedBinding = end.element
|
||||
? {
|
||||
element: end.element,
|
||||
midPoint: getSnapOutlineMidPoint(
|
||||
pointFrom<GlobalPoint>(
|
||||
scenePointerX - linearElementEditor.pointerOffset.x,
|
||||
scenePointerY - linearElementEditor.pointerOffset.y,
|
||||
),
|
||||
end.element,
|
||||
elementsMap,
|
||||
app.state.zoom,
|
||||
),
|
||||
}
|
||||
: null;
|
||||
}
|
||||
} else if (endIsDragged) {
|
||||
updates.suggestedBinding = app.state.suggestedBinding;
|
||||
|
||||
+119
-25
@@ -7,6 +7,7 @@ import {
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import {
|
||||
bezierEquation,
|
||||
curve,
|
||||
curveCatmullRomCubicApproxPoints,
|
||||
curveOffsetPoints,
|
||||
@@ -27,19 +28,25 @@ import {
|
||||
|
||||
import type { Curve, LineSegment, LocalPoint } from "@excalidraw/math";
|
||||
|
||||
import type { NormalizedZoomValue, Zoom } from "@excalidraw/excalidraw/types";
|
||||
import type {
|
||||
AppState,
|
||||
NormalizedZoomValue,
|
||||
Zoom,
|
||||
} from "@excalidraw/excalidraw/types";
|
||||
|
||||
import { elementCenterPoint, getDiamondPoints } from "./bounds";
|
||||
|
||||
import { generateLinearCollisionShape } from "./shape";
|
||||
|
||||
import { isPointInElement } from "./collision";
|
||||
import { hitElementItself, isPointInElement } from "./collision";
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
import { isRectangularElement } from "./typeChecks";
|
||||
import { maxBindingDistance_simple } from "./binding";
|
||||
|
||||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawArrowElement,
|
||||
ExcalidrawBindableElement,
|
||||
ExcalidrawDiamondElement,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawFreeDrawElement,
|
||||
@@ -329,24 +336,10 @@ export function deconstructRectanguloidElement(
|
||||
return shape;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the **unrotated** building components of a diamond element
|
||||
* in the form of line segments and curves as a tuple, in this order.
|
||||
*
|
||||
* @param element The element to deconstruct
|
||||
* @param offset An optional offset
|
||||
* @returns Tuple of line **unrotated** segments (0) and curves (1)
|
||||
*/
|
||||
export function deconstructDiamondElement(
|
||||
export function getDiamondBaseCorners(
|
||||
element: ExcalidrawDiamondElement,
|
||||
offset: number = 0,
|
||||
): [LineSegment<GlobalPoint>[], Curve<GlobalPoint>[]] {
|
||||
const cachedShape = getElementShapesCacheEntry(element, offset);
|
||||
|
||||
if (cachedShape) {
|
||||
return cachedShape;
|
||||
}
|
||||
|
||||
): Curve<GlobalPoint>[] {
|
||||
const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] =
|
||||
getDiamondPoints(element);
|
||||
const verticalRadius = element.roundness
|
||||
@@ -363,7 +356,7 @@ export function deconstructDiamondElement(
|
||||
pointFrom(element.x + leftX, element.y + leftY),
|
||||
];
|
||||
|
||||
const baseCorners = [
|
||||
return [
|
||||
curve(
|
||||
pointFrom<GlobalPoint>(
|
||||
right[0] - verticalRadius,
|
||||
@@ -413,6 +406,27 @@ export function deconstructDiamondElement(
|
||||
),
|
||||
), // TOP
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the **unrotated** building components of a diamond element
|
||||
* in the form of line segments and curves as a tuple, in this order.
|
||||
*
|
||||
* @param element The element to deconstruct
|
||||
* @param offset An optional offset
|
||||
* @returns Tuple of line **unrotated** segments (0) and curves (1)
|
||||
*/
|
||||
export function deconstructDiamondElement(
|
||||
element: ExcalidrawDiamondElement,
|
||||
offset: number = 0,
|
||||
): [LineSegment<GlobalPoint>[], Curve<GlobalPoint>[]] {
|
||||
const cachedShape = getElementShapesCacheEntry(element, offset);
|
||||
|
||||
if (cachedShape) {
|
||||
return cachedShape;
|
||||
}
|
||||
|
||||
const baseCorners = getDiamondBaseCorners(element, offset);
|
||||
|
||||
const corners = baseCorners.map(
|
||||
(corner) =>
|
||||
@@ -570,18 +584,94 @@ const getDiagonalsForBindableElement = (
|
||||
return [diagonalOne, diagonalTwo];
|
||||
};
|
||||
|
||||
export const getSnapOutlineMidPoint = (
|
||||
point: GlobalPoint,
|
||||
element: ExcalidrawBindableElement,
|
||||
elementsMap: ElementsMap,
|
||||
zoom: AppState["zoom"],
|
||||
) => {
|
||||
const center = elementCenterPoint(element, elementsMap);
|
||||
const sideMidpoints =
|
||||
element.type === "diamond"
|
||||
? getDiamondBaseCorners(element).map((curve) => {
|
||||
const point = bezierEquation(curve, 0.5);
|
||||
const rotatedPoint = pointRotateRads(point, center, element.angle);
|
||||
|
||||
return pointFrom<GlobalPoint>(rotatedPoint[0], rotatedPoint[1]);
|
||||
})
|
||||
: [
|
||||
// RIGHT midpoint
|
||||
pointRotateRads(
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + element.width,
|
||||
element.y + element.height / 2,
|
||||
),
|
||||
center,
|
||||
element.angle,
|
||||
),
|
||||
// BOTTOM midpoint
|
||||
pointRotateRads(
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height,
|
||||
),
|
||||
center,
|
||||
element.angle,
|
||||
),
|
||||
// LEFT midpoint
|
||||
pointRotateRads(
|
||||
pointFrom<GlobalPoint>(element.x, element.y + element.height / 2),
|
||||
center,
|
||||
element.angle,
|
||||
),
|
||||
// TOP midpoint
|
||||
pointRotateRads(
|
||||
pointFrom<GlobalPoint>(element.x + element.width / 2, element.y),
|
||||
center,
|
||||
element.angle,
|
||||
),
|
||||
];
|
||||
const candidate = sideMidpoints.find(
|
||||
(midpoint) =>
|
||||
pointDistance(point, midpoint) <=
|
||||
maxBindingDistance_simple(zoom) + element.strokeWidth / 2 &&
|
||||
!hitElementItself({
|
||||
point,
|
||||
element,
|
||||
threshold: 0,
|
||||
elementsMap,
|
||||
overrideShouldTestInside: true,
|
||||
}),
|
||||
);
|
||||
|
||||
return candidate;
|
||||
};
|
||||
|
||||
export const projectFixedPointOntoDiagonal = (
|
||||
arrow: ExcalidrawArrowElement,
|
||||
point: GlobalPoint,
|
||||
element: ExcalidrawElement,
|
||||
element: ExcalidrawBindableElement,
|
||||
startOrEnd: "start" | "end",
|
||||
elementsMap: ElementsMap,
|
||||
zoom: AppState["zoom"],
|
||||
): GlobalPoint | null => {
|
||||
invariant(arrow.points.length >= 2, "Arrow must have at least two points");
|
||||
if (arrow.width < 3 && arrow.height < 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sideMidPoint = getSnapOutlineMidPoint(
|
||||
point,
|
||||
element,
|
||||
elementsMap,
|
||||
zoom,
|
||||
);
|
||||
if (sideMidPoint) {
|
||||
return sideMidPoint;
|
||||
}
|
||||
|
||||
// Do the projection onto the diagonals (or center lines
|
||||
// for non-rectangular shapes)
|
||||
const [diagonalOne, diagonalTwo] = getDiagonalsForBindableElement(
|
||||
element,
|
||||
elementsMap,
|
||||
@@ -603,18 +693,22 @@ export const projectFixedPointOntoDiagonal = (
|
||||
),
|
||||
a,
|
||||
);
|
||||
const intersector = lineSegment<GlobalPoint>(point, b);
|
||||
const intersector = lineSegment<GlobalPoint>(b, a);
|
||||
const p1 = lineSegmentIntersectionPoints(diagonalOne, intersector);
|
||||
const p2 = lineSegmentIntersectionPoints(diagonalTwo, intersector);
|
||||
const d1 = p1 && pointDistance(a, p1);
|
||||
const d2 = p2 && pointDistance(a, p2);
|
||||
|
||||
let p = null;
|
||||
let projection = null;
|
||||
if (d1 != null && d2 != null) {
|
||||
p = d1 < d2 ? p1 : p2;
|
||||
projection = d1 < d2 ? p1 : p2;
|
||||
} else {
|
||||
p = p1 || p2 || null;
|
||||
projection = p1 || p2 || null;
|
||||
}
|
||||
|
||||
return p && isPointInElement(p, element, elementsMap) ? p : null;
|
||||
if (projection && isPointInElement(projection, element, elementsMap)) {
|
||||
return projection;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -1350,8 +1350,8 @@ describe("multiple selection", () => {
|
||||
|
||||
expect(boundArrow.x).toBeCloseTo(380 * scaleX);
|
||||
expect(boundArrow.y).toBeCloseTo(240 * scaleY);
|
||||
expect(boundArrow.points[1][0]).toBeCloseTo(63.4035);
|
||||
expect(boundArrow.points[1][1]).toBeCloseTo(-84.538);
|
||||
expect(boundArrow.points[1][0]).toBeCloseTo(63.40354208105561);
|
||||
expect(boundArrow.points[1][1]).toBeCloseTo(-84.53805610807356);
|
||||
|
||||
expect(arrowLabelPos.x + arrowLabel.width / 2).toBeCloseTo(
|
||||
boundArrow.x + boundArrow.points[1][0] / 2,
|
||||
|
||||
@@ -108,7 +108,6 @@ export const actionFinalize = register<FormData>({
|
||||
|
||||
return map;
|
||||
}, new Map()) ?? new Map();
|
||||
|
||||
bindOrUnbindBindingElement(
|
||||
element,
|
||||
draggedPoints,
|
||||
|
||||
@@ -249,6 +249,7 @@ import {
|
||||
maxBindingDistance_simple,
|
||||
convertToExcalidrawElements,
|
||||
type ExcalidrawElementSkeleton,
|
||||
getSnapOutlineMidPoint,
|
||||
handleFocusPointDrag,
|
||||
handleFocusPointHover,
|
||||
handleFocusPointPointerDown,
|
||||
@@ -6400,15 +6401,28 @@ class App extends React.Component<AppProps, AppState> {
|
||||
// and point
|
||||
const { newElement } = this.state;
|
||||
if (!newElement && isBindingEnabled(this.state)) {
|
||||
const globalPoint = pointFrom<GlobalPoint>(
|
||||
scenePointerX,
|
||||
scenePointerY,
|
||||
);
|
||||
const elementsMap = this.scene.getNonDeletedElementsMap();
|
||||
const hoveredElement = getHoveredElementForBinding(
|
||||
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
|
||||
globalPoint,
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
elementsMap,
|
||||
maxBindingDistance_simple(this.state.zoom),
|
||||
);
|
||||
if (hoveredElement) {
|
||||
this.setState({
|
||||
suggestedBinding: hoveredElement,
|
||||
suggestedBinding: {
|
||||
element: hoveredElement,
|
||||
midPoint: getSnapOutlineMidPoint(
|
||||
globalPoint,
|
||||
hoveredElement,
|
||||
elementsMap,
|
||||
this.state.zoom,
|
||||
),
|
||||
},
|
||||
});
|
||||
} else if (this.state.suggestedBinding) {
|
||||
this.setState({
|
||||
@@ -6591,16 +6605,19 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
maxBindingDistance_simple(this.state.zoom),
|
||||
);
|
||||
if (
|
||||
hit &&
|
||||
!isPointInElement(
|
||||
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
|
||||
hit,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
)
|
||||
) {
|
||||
const scenePointer = pointFrom<GlobalPoint>(scenePointerX, scenePointerY);
|
||||
const elementsMap = this.scene.getNonDeletedElementsMap();
|
||||
if (hit && !isPointInElement(scenePointer, hit, elementsMap)) {
|
||||
this.setState({
|
||||
suggestedBinding: hit,
|
||||
suggestedBinding: {
|
||||
element: hit,
|
||||
midPoint: getSnapOutlineMidPoint(
|
||||
scenePointer,
|
||||
hit,
|
||||
elementsMap,
|
||||
this.state.zoom,
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -8762,7 +8779,18 @@ class App extends React.Component<AppProps, AppState> {
|
||||
bindMode: "orbit",
|
||||
newElement: element,
|
||||
startBoundElement: boundElement,
|
||||
suggestedBinding: boundElement || null,
|
||||
suggestedBinding:
|
||||
boundElement && isBindingElement(element)
|
||||
? {
|
||||
element: boundElement,
|
||||
midPoint: getSnapOutlineMidPoint(
|
||||
point,
|
||||
boundElement,
|
||||
elementsMap,
|
||||
this.state.zoom,
|
||||
),
|
||||
}
|
||||
: null,
|
||||
selectedElementIds: nextSelectedElementIds,
|
||||
selectedLinearElement: linearElementEditor,
|
||||
};
|
||||
|
||||
@@ -135,7 +135,6 @@ describe("binding with linear elements", () => {
|
||||
) as HTMLInputElement;
|
||||
expect(linear.startBinding).not.toBe(null);
|
||||
expect(inputX).not.toBeNull();
|
||||
|
||||
UI.updateInput(inputX, String("184"));
|
||||
expect(linear.startBinding).not.toBe(null);
|
||||
});
|
||||
|
||||
@@ -82,7 +82,12 @@ import {
|
||||
getNormalizedZoom,
|
||||
} from "../scene";
|
||||
|
||||
import type { AppState, BinaryFiles, LibraryItem } from "../types";
|
||||
import type {
|
||||
AppState,
|
||||
BinaryFiles,
|
||||
LibraryItem,
|
||||
NormalizedZoomValue,
|
||||
} from "../types";
|
||||
import type { ImportedDataState, LegacyAppState } from "./types";
|
||||
|
||||
type RestoredAppState = Omit<
|
||||
@@ -212,6 +217,7 @@ const repairBinding = <T extends ExcalidrawArrowElement>(
|
||||
boundElement,
|
||||
startOrEnd,
|
||||
elementsMap,
|
||||
{ value: 1 as NormalizedZoomValue },
|
||||
) || p;
|
||||
const { fixedPoint } = calculateFixedPointForNonElbowArrowBinding(
|
||||
element,
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
type GlobalPoint,
|
||||
type LocalPoint,
|
||||
type Radians,
|
||||
bezierEquation,
|
||||
pointRotateRads,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import {
|
||||
@@ -21,11 +23,13 @@ import {
|
||||
deconstructDiamondElement,
|
||||
deconstructRectanguloidElement,
|
||||
elementCenterPoint,
|
||||
getDiamondBaseCorners,
|
||||
FOCUS_POINT_SIZE,
|
||||
getOmitSidesForEditorInterface,
|
||||
getTransformHandles,
|
||||
getTransformHandlesFromCoords,
|
||||
hasBoundingBox,
|
||||
hitElementItself,
|
||||
isArrowElement,
|
||||
isBindableElement,
|
||||
isElbowArrow,
|
||||
@@ -35,6 +39,12 @@ import {
|
||||
isLineElement,
|
||||
isTextElement,
|
||||
LinearElementEditor,
|
||||
headingForPoint,
|
||||
compareHeading,
|
||||
HEADING_RIGHT,
|
||||
HEADING_DOWN,
|
||||
HEADING_LEFT,
|
||||
HEADING_UP,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import { renderSelectionElement } from "@excalidraw/element";
|
||||
@@ -88,8 +98,11 @@ import {
|
||||
strokeRectWithRotation_simple,
|
||||
} from "./helpers";
|
||||
|
||||
import type { AppClassProperties, InteractiveCanvasAppState } from "../types";
|
||||
|
||||
import type {
|
||||
AppState,
|
||||
AppClassProperties,
|
||||
InteractiveCanvasAppState,
|
||||
} from "../types";
|
||||
import type {
|
||||
InteractiveCanvasRenderConfig,
|
||||
InteractiveSceneRenderConfig,
|
||||
@@ -209,11 +222,14 @@ const renderSingleLinearPoint = <Point extends GlobalPoint | LocalPoint>(
|
||||
|
||||
const renderBindingHighlightForBindableElement_simple = (
|
||||
context: CanvasRenderingContext2D,
|
||||
element: ExcalidrawBindableElement,
|
||||
suggestedBinding: NonNullable<AppState["suggestedBinding"]>,
|
||||
elementsMap: ElementsMap,
|
||||
appState: InteractiveCanvasAppState,
|
||||
pointerCoords: GlobalPoint | null,
|
||||
) => {
|
||||
const enclosingFrame = element.frameId && elementsMap.get(element.frameId);
|
||||
const enclosingFrame =
|
||||
suggestedBinding.element.frameId &&
|
||||
elementsMap.get(suggestedBinding.element.frameId);
|
||||
if (enclosingFrame && isFrameLikeElement(enclosingFrame)) {
|
||||
context.translate(enclosingFrame.x, enclosingFrame.y);
|
||||
|
||||
@@ -236,12 +252,12 @@ const renderBindingHighlightForBindableElement_simple = (
|
||||
context.translate(-enclosingFrame.x, -enclosingFrame.y);
|
||||
}
|
||||
|
||||
switch (element.type) {
|
||||
switch (suggestedBinding.element.type) {
|
||||
case "magicframe":
|
||||
case "frame":
|
||||
context.save();
|
||||
|
||||
context.translate(element.x, element.y);
|
||||
context.translate(suggestedBinding.element.x, suggestedBinding.element.y);
|
||||
|
||||
context.lineWidth = FRAME_STYLE.strokeWidth / appState.zoom.value;
|
||||
context.strokeStyle =
|
||||
@@ -254,14 +270,19 @@ const renderBindingHighlightForBindableElement_simple = (
|
||||
context.roundRect(
|
||||
0,
|
||||
0,
|
||||
element.width,
|
||||
element.height,
|
||||
suggestedBinding.element.width,
|
||||
suggestedBinding.element.height,
|
||||
FRAME_STYLE.radius / appState.zoom.value,
|
||||
);
|
||||
context.stroke();
|
||||
context.closePath();
|
||||
} else {
|
||||
context.strokeRect(0, 0, element.width, element.height);
|
||||
context.strokeRect(
|
||||
0,
|
||||
0,
|
||||
suggestedBinding.element.width,
|
||||
suggestedBinding.element.height,
|
||||
);
|
||||
}
|
||||
|
||||
context.restore();
|
||||
@@ -269,30 +290,30 @@ const renderBindingHighlightForBindableElement_simple = (
|
||||
default:
|
||||
context.save();
|
||||
|
||||
const center = elementCenterPoint(element, elementsMap);
|
||||
const center = elementCenterPoint(suggestedBinding.element, elementsMap);
|
||||
|
||||
context.translate(center[0], center[1]);
|
||||
context.rotate(element.angle as Radians);
|
||||
context.rotate(suggestedBinding.element.angle as Radians);
|
||||
context.translate(-center[0], -center[1]);
|
||||
|
||||
context.translate(element.x, element.y);
|
||||
context.translate(suggestedBinding.element.x, suggestedBinding.element.y);
|
||||
|
||||
context.lineWidth =
|
||||
clamp(1.75, element.strokeWidth, 4) /
|
||||
clamp(1.75, suggestedBinding.element.strokeWidth, 4) /
|
||||
Math.max(0.25, appState.zoom.value);
|
||||
context.strokeStyle =
|
||||
appState.theme === THEME.DARK
|
||||
? `rgba(3, 93, 161, 1)`
|
||||
: `rgba(106, 189, 252, 1)`;
|
||||
|
||||
switch (element.type) {
|
||||
switch (suggestedBinding.element.type) {
|
||||
case "ellipse":
|
||||
context.beginPath();
|
||||
context.ellipse(
|
||||
element.width / 2,
|
||||
element.height / 2,
|
||||
element.width / 2,
|
||||
element.height / 2,
|
||||
suggestedBinding.element.width / 2,
|
||||
suggestedBinding.element.height / 2,
|
||||
suggestedBinding.element.width / 2,
|
||||
suggestedBinding.element.height / 2,
|
||||
0,
|
||||
0,
|
||||
2 * Math.PI,
|
||||
@@ -302,18 +323,20 @@ const renderBindingHighlightForBindableElement_simple = (
|
||||
break;
|
||||
case "diamond":
|
||||
{
|
||||
const [segments, curves] = deconstructDiamondElement(element);
|
||||
const [segments, curves] = deconstructDiamondElement(
|
||||
suggestedBinding.element,
|
||||
);
|
||||
|
||||
// Draw each line segment individually
|
||||
segments.forEach((segment) => {
|
||||
context.beginPath();
|
||||
context.moveTo(
|
||||
segment[0][0] - element.x,
|
||||
segment[0][1] - element.y,
|
||||
segment[0][0] - suggestedBinding.element.x,
|
||||
segment[0][1] - suggestedBinding.element.y,
|
||||
);
|
||||
context.lineTo(
|
||||
segment[1][0] - element.x,
|
||||
segment[1][1] - element.y,
|
||||
segment[1][0] - suggestedBinding.element.x,
|
||||
segment[1][1] - suggestedBinding.element.y,
|
||||
);
|
||||
context.stroke();
|
||||
});
|
||||
@@ -322,14 +345,17 @@ const renderBindingHighlightForBindableElement_simple = (
|
||||
curves.forEach((curve) => {
|
||||
const [start, control1, control2, end] = curve;
|
||||
context.beginPath();
|
||||
context.moveTo(start[0] - element.x, start[1] - element.y);
|
||||
context.moveTo(
|
||||
start[0] - suggestedBinding.element.x,
|
||||
start[1] - suggestedBinding.element.y,
|
||||
);
|
||||
context.bezierCurveTo(
|
||||
control1[0] - element.x,
|
||||
control1[1] - element.y,
|
||||
control2[0] - element.x,
|
||||
control2[1] - element.y,
|
||||
end[0] - element.x,
|
||||
end[1] - element.y,
|
||||
control1[0] - suggestedBinding.element.x,
|
||||
control1[1] - suggestedBinding.element.y,
|
||||
control2[0] - suggestedBinding.element.x,
|
||||
control2[1] - suggestedBinding.element.y,
|
||||
end[0] - suggestedBinding.element.x,
|
||||
end[1] - suggestedBinding.element.y,
|
||||
);
|
||||
context.stroke();
|
||||
});
|
||||
@@ -338,18 +364,20 @@ const renderBindingHighlightForBindableElement_simple = (
|
||||
break;
|
||||
default:
|
||||
{
|
||||
const [segments, curves] = deconstructRectanguloidElement(element);
|
||||
const [segments, curves] = deconstructRectanguloidElement(
|
||||
suggestedBinding.element,
|
||||
);
|
||||
|
||||
// Draw each line segment individually
|
||||
segments.forEach((segment) => {
|
||||
context.beginPath();
|
||||
context.moveTo(
|
||||
segment[0][0] - element.x,
|
||||
segment[0][1] - element.y,
|
||||
segment[0][0] - suggestedBinding.element.x,
|
||||
segment[0][1] - suggestedBinding.element.y,
|
||||
);
|
||||
context.lineTo(
|
||||
segment[1][0] - element.x,
|
||||
segment[1][1] - element.y,
|
||||
segment[1][0] - suggestedBinding.element.x,
|
||||
segment[1][1] - suggestedBinding.element.y,
|
||||
);
|
||||
context.stroke();
|
||||
});
|
||||
@@ -358,14 +386,17 @@ const renderBindingHighlightForBindableElement_simple = (
|
||||
curves.forEach((curve) => {
|
||||
const [start, control1, control2, end] = curve;
|
||||
context.beginPath();
|
||||
context.moveTo(start[0] - element.x, start[1] - element.y);
|
||||
context.moveTo(
|
||||
start[0] - suggestedBinding.element.x,
|
||||
start[1] - suggestedBinding.element.y,
|
||||
);
|
||||
context.bezierCurveTo(
|
||||
control1[0] - element.x,
|
||||
control1[1] - element.y,
|
||||
control2[0] - element.x,
|
||||
control2[1] - element.y,
|
||||
end[0] - element.x,
|
||||
end[1] - element.y,
|
||||
control1[0] - suggestedBinding.element.x,
|
||||
control1[1] - suggestedBinding.element.y,
|
||||
control2[0] - suggestedBinding.element.x,
|
||||
control2[1] - suggestedBinding.element.y,
|
||||
end[0] - suggestedBinding.element.x,
|
||||
end[1] - suggestedBinding.element.y,
|
||||
);
|
||||
context.stroke();
|
||||
});
|
||||
@@ -378,6 +409,133 @@ const renderBindingHighlightForBindableElement_simple = (
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
isFrameLikeElement(suggestedBinding.element) ||
|
||||
isBindableElement(suggestedBinding.element)
|
||||
) {
|
||||
// Draw midpoint indicators
|
||||
const linearElement = appState.selectedLinearElement;
|
||||
const arrow =
|
||||
linearElement?.elementId &&
|
||||
LinearElementEditor.getElement(linearElement?.elementId, elementsMap);
|
||||
const insideBindable =
|
||||
pointerCoords &&
|
||||
arrow &&
|
||||
hitElementItself({
|
||||
point: pointerCoords,
|
||||
element: suggestedBinding.element,
|
||||
elementsMap,
|
||||
threshold: 0,
|
||||
overrideShouldTestInside: true,
|
||||
});
|
||||
|
||||
if (!insideBindable || isElbowArrow(arrow)) {
|
||||
context.save();
|
||||
context.translate(suggestedBinding.element.x, suggestedBinding.element.y);
|
||||
|
||||
const midpointRadius = 5 / appState.zoom.value;
|
||||
const center = elementCenterPoint(suggestedBinding.element, elementsMap);
|
||||
|
||||
let midpoints: LocalPoint[];
|
||||
if (suggestedBinding.element.type === "diamond") {
|
||||
const center = elementCenterPoint(
|
||||
suggestedBinding.element,
|
||||
elementsMap,
|
||||
);
|
||||
midpoints = getDiamondBaseCorners(suggestedBinding.element).map(
|
||||
(curve) => {
|
||||
const point = bezierEquation(curve, 0.5);
|
||||
const rotatedPoint = pointRotateRads(
|
||||
point,
|
||||
center,
|
||||
suggestedBinding.element.angle,
|
||||
);
|
||||
|
||||
return pointFrom<LocalPoint>(
|
||||
rotatedPoint[0] - suggestedBinding.element.x,
|
||||
rotatedPoint[1] - suggestedBinding.element.y,
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
const basePoints = [
|
||||
{
|
||||
x: suggestedBinding.element.width,
|
||||
y: suggestedBinding.element.height / 2,
|
||||
}, // RIGHT
|
||||
{
|
||||
x: suggestedBinding.element.width / 2,
|
||||
y: suggestedBinding.element.height,
|
||||
}, // BOTTOM
|
||||
{ x: 0, y: suggestedBinding.element.height / 2 }, // LEFT
|
||||
{ x: suggestedBinding.element.width / 2, y: 0 }, // TOP
|
||||
];
|
||||
midpoints = basePoints.map((point) => {
|
||||
const globalPoint = pointFrom<GlobalPoint>(
|
||||
point.x + suggestedBinding.element.x,
|
||||
point.y + suggestedBinding.element.y,
|
||||
);
|
||||
const rotatedPoint = pointRotateRads(
|
||||
globalPoint,
|
||||
center,
|
||||
suggestedBinding.element.angle,
|
||||
);
|
||||
return pointFrom<LocalPoint>(
|
||||
rotatedPoint[0] - suggestedBinding.element.x,
|
||||
rotatedPoint[1] - suggestedBinding.element.y,
|
||||
);
|
||||
});
|
||||
}
|
||||
const highlightedPoint =
|
||||
suggestedBinding.midPoint &&
|
||||
pointFrom<LocalPoint>(
|
||||
suggestedBinding.midPoint[0] - suggestedBinding.element.x,
|
||||
suggestedBinding.midPoint[1] - suggestedBinding.element.y,
|
||||
);
|
||||
|
||||
const target = [HEADING_RIGHT, HEADING_DOWN, HEADING_LEFT, HEADING_UP];
|
||||
midpoints.forEach((midpoint, idx) => {
|
||||
const isHighlighted =
|
||||
highlightedPoint &&
|
||||
compareHeading(
|
||||
headingForPoint(
|
||||
pointRotateRads(
|
||||
pointFrom<GlobalPoint>(
|
||||
highlightedPoint[0] + suggestedBinding.element.x,
|
||||
highlightedPoint[1] + suggestedBinding.element.y,
|
||||
),
|
||||
center,
|
||||
suggestedBinding.element.angle as Radians,
|
||||
),
|
||||
center,
|
||||
),
|
||||
target[idx],
|
||||
);
|
||||
|
||||
if (!isHighlighted) {
|
||||
context.fillStyle =
|
||||
appState.theme === THEME.DARK
|
||||
? `rgba(0, 0, 0, 0.5)`
|
||||
: `rgba(65, 65, 65, 0.4)`;
|
||||
context.beginPath();
|
||||
context.arc(midpoint[0], midpoint[1], midpointRadius, 0, 2 * Math.PI);
|
||||
context.fill();
|
||||
} else {
|
||||
context.fillStyle =
|
||||
appState.theme === THEME.DARK
|
||||
? `rgba(3, 93, 161, 1)`
|
||||
: `rgba(106, 189, 252, 1)`;
|
||||
|
||||
context.beginPath();
|
||||
context.arc(midpoint[0], midpoint[1], midpointRadius, 0, 2 * Math.PI);
|
||||
context.fill();
|
||||
}
|
||||
});
|
||||
|
||||
context.restore();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renderBindingHighlightForBindableElement_complex = (
|
||||
@@ -631,6 +789,78 @@ const renderBindingHighlightForBindableElement_complex = (
|
||||
context.fill();
|
||||
|
||||
context.restore();
|
||||
|
||||
// Draw midpoint indicators
|
||||
context.save();
|
||||
context.translate(
|
||||
element.x + appState.scrollX,
|
||||
element.y + appState.scrollY,
|
||||
);
|
||||
|
||||
const midpointRadius = 5 / appState.zoom.value;
|
||||
const cutoutPadding = 5 / appState.zoom.value;
|
||||
const cutoutRadius = midpointRadius + cutoutPadding;
|
||||
|
||||
let midpoints;
|
||||
if (element.type === "diamond") {
|
||||
const [, curves] = deconstructDiamondElement(element);
|
||||
const center = elementCenterPoint(element, allElementsMap);
|
||||
|
||||
midpoints = curves.map((curve) => {
|
||||
const point = bezierEquation(curve, 0.5);
|
||||
const rotatedPoint = pointRotateRads(point, center, element.angle);
|
||||
return {
|
||||
x: rotatedPoint[0] - element.x,
|
||||
y: rotatedPoint[1] - element.y,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
const center = elementCenterPoint(element, allElementsMap);
|
||||
const basePoints = [
|
||||
{ x: element.width / 2, y: 0 }, // TOP
|
||||
{ x: element.width, y: element.height / 2 }, // RIGHT
|
||||
{ x: element.width / 2, y: element.height }, // BOTTOM
|
||||
{ x: 0, y: element.height / 2 }, // LEFT
|
||||
];
|
||||
midpoints = basePoints.map((point) => {
|
||||
const globalPoint = pointFrom<GlobalPoint>(
|
||||
point.x + element.x,
|
||||
point.y + element.y,
|
||||
);
|
||||
const rotatedPoint = pointRotateRads(
|
||||
globalPoint,
|
||||
center,
|
||||
element.angle,
|
||||
);
|
||||
return {
|
||||
x: rotatedPoint[0] - element.x,
|
||||
y: rotatedPoint[1] - element.y,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Clear cutouts around midpoints
|
||||
midpoints.forEach((midpoint) => {
|
||||
context.clearRect(
|
||||
midpoint.x - cutoutRadius,
|
||||
midpoint.y - cutoutRadius,
|
||||
cutoutRadius * 2,
|
||||
cutoutRadius * 2,
|
||||
);
|
||||
});
|
||||
|
||||
context.fillStyle =
|
||||
appState.theme === THEME.DARK
|
||||
? `rgba(3, 93, 161, ${opacity})`
|
||||
: `rgba(106, 189, 252, ${opacity})`;
|
||||
|
||||
midpoints.forEach((midpoint) => {
|
||||
context.beginPath();
|
||||
context.arc(midpoint.x, midpoint.y, midpointRadius, 0, 2 * Math.PI);
|
||||
context.fill();
|
||||
});
|
||||
|
||||
context.restore();
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -641,17 +871,21 @@ const renderBindingHighlightForBindableElement_complex = (
|
||||
const renderBindingHighlightForBindableElement = (
|
||||
app: AppClassProperties,
|
||||
context: CanvasRenderingContext2D,
|
||||
element: ExcalidrawBindableElement,
|
||||
suggestedBinding: AppState["suggestedBinding"],
|
||||
allElementsMap: NonDeletedSceneElementsMap,
|
||||
appState: InteractiveCanvasAppState,
|
||||
deltaTime: number,
|
||||
state?: { runtime: number },
|
||||
) => {
|
||||
if (suggestedBinding === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (getFeatureFlag("COMPLEX_BINDINGS")) {
|
||||
return renderBindingHighlightForBindableElement_complex(
|
||||
app,
|
||||
context,
|
||||
element,
|
||||
suggestedBinding.element,
|
||||
allElementsMap,
|
||||
appState,
|
||||
deltaTime,
|
||||
@@ -661,11 +895,18 @@ const renderBindingHighlightForBindableElement = (
|
||||
|
||||
context.save();
|
||||
context.translate(appState.scrollX, appState.scrollY);
|
||||
const pointerCoords = app.lastPointerMoveCoords
|
||||
? pointFrom<GlobalPoint>(
|
||||
app.lastPointerMoveCoords.x,
|
||||
app.lastPointerMoveCoords.y,
|
||||
)
|
||||
: null;
|
||||
renderBindingHighlightForBindableElement_simple(
|
||||
context,
|
||||
element,
|
||||
suggestedBinding,
|
||||
allElementsMap,
|
||||
appState,
|
||||
pointerCoords,
|
||||
);
|
||||
context.restore();
|
||||
};
|
||||
|
||||
@@ -227,7 +227,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"strokeWidth": 2,
|
||||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 34,
|
||||
"version": 33,
|
||||
"width": "94.00000",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
@@ -334,7 +334,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
],
|
||||
"mode": "orbit",
|
||||
},
|
||||
"height": "74.35962",
|
||||
"height": "105.58873",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
@@ -342,50 +342,50 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
],
|
||||
[
|
||||
88,
|
||||
"74.35962",
|
||||
"105.58873",
|
||||
],
|
||||
],
|
||||
"startBinding": {
|
||||
"elementId": "id0",
|
||||
"fixedPoint": [
|
||||
"0.63636",
|
||||
"0.63636",
|
||||
1,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
},
|
||||
"version": 33,
|
||||
"version": 32,
|
||||
"width": 88,
|
||||
},
|
||||
"inserted": {
|
||||
"endBinding": {
|
||||
"elementId": "id1",
|
||||
"fixedPoint": [
|
||||
"0.39746",
|
||||
"0.60254",
|
||||
0,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
},
|
||||
"height": "1.66245",
|
||||
"height": "0.00000",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
88,
|
||||
"-1.66245",
|
||||
"88.00000",
|
||||
"0.00000",
|
||||
],
|
||||
],
|
||||
"startBinding": {
|
||||
"elementId": "id0",
|
||||
"fixedPoint": [
|
||||
"0.63636",
|
||||
"0.63636",
|
||||
1,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
},
|
||||
"version": 30,
|
||||
"width": 88,
|
||||
"version": 29,
|
||||
"width": "88.00000",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -440,13 +440,13 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
],
|
||||
],
|
||||
"startBinding": null,
|
||||
"version": 34,
|
||||
"version": 33,
|
||||
"width": "94.00000",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"inserted": {
|
||||
"height": "74.35962",
|
||||
"height": "105.58873",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
@@ -454,21 +454,21 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
],
|
||||
[
|
||||
88,
|
||||
"74.35962",
|
||||
"105.58873",
|
||||
],
|
||||
],
|
||||
"startBinding": {
|
||||
"elementId": "id0",
|
||||
"fixedPoint": [
|
||||
"0.63636",
|
||||
"0.63636",
|
||||
1,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
},
|
||||
"version": 33,
|
||||
"version": 32,
|
||||
"width": 88,
|
||||
"x": 6,
|
||||
"y": "46.67801",
|
||||
"y": "7.09000",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -854,7 +854,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"strokeWidth": 2,
|
||||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 26,
|
||||
"version": 30,
|
||||
"width": 100,
|
||||
"x": 150,
|
||||
"y": 0,
|
||||
@@ -904,60 +904,60 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"id4": {
|
||||
"deleted": {
|
||||
"endBinding": null,
|
||||
"height": "5.28000",
|
||||
"height": "0.00293",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
-44,
|
||||
"-5.28000",
|
||||
"-44.00000",
|
||||
"-0.00293",
|
||||
],
|
||||
],
|
||||
"startBinding": {
|
||||
"elementId": "id0",
|
||||
"fixedPoint": [
|
||||
"0.63636",
|
||||
"0.63636",
|
||||
1,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
},
|
||||
"version": 25,
|
||||
"width": 44,
|
||||
"y": "5.28000",
|
||||
"version": 29,
|
||||
"width": "44.00000",
|
||||
"y": "0.00293",
|
||||
},
|
||||
"inserted": {
|
||||
"endBinding": {
|
||||
"elementId": "id1",
|
||||
"fixedPoint": [
|
||||
"0.41067",
|
||||
"0.58933",
|
||||
0,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
},
|
||||
"height": "9.80848",
|
||||
"height": 0,
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
"47.06697",
|
||||
"9.80848",
|
||||
"6.00000",
|
||||
0,
|
||||
],
|
||||
],
|
||||
"startBinding": {
|
||||
"elementId": "id0",
|
||||
"fixedPoint": [
|
||||
"0.63636",
|
||||
"0.63636",
|
||||
1,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
},
|
||||
"version": 24,
|
||||
"width": "47.06697",
|
||||
"y": "-0.87545",
|
||||
"version": 27,
|
||||
"width": "6.00000",
|
||||
"y": "0.01000",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1004,35 +1004,35 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
],
|
||||
],
|
||||
"startBinding": null,
|
||||
"version": 26,
|
||||
"version": 30,
|
||||
"width": 100,
|
||||
"x": 150,
|
||||
"y": 0,
|
||||
},
|
||||
"inserted": {
|
||||
"height": "5.28000",
|
||||
"height": "0.00293",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
-44,
|
||||
"-5.28000",
|
||||
"-44.00000",
|
||||
"-0.00293",
|
||||
],
|
||||
],
|
||||
"startBinding": {
|
||||
"elementId": "id0",
|
||||
"fixedPoint": [
|
||||
"0.63636",
|
||||
"0.63636",
|
||||
1,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
},
|
||||
"version": 25,
|
||||
"width": 44,
|
||||
"version": 29,
|
||||
"width": "44.00000",
|
||||
"x": 144,
|
||||
"y": "5.28000",
|
||||
"y": "0.00293",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -2390,7 +2390,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"endBinding": {
|
||||
"elementId": "id1",
|
||||
"fixedPoint": [
|
||||
"0.50010",
|
||||
0,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
@@ -2398,7 +2398,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": "398.76619",
|
||||
"height": "439.13521",
|
||||
"id": "id4",
|
||||
"index": "a2",
|
||||
"isDeleted": false,
|
||||
@@ -2413,7 +2413,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
],
|
||||
[
|
||||
488,
|
||||
"-398.76619",
|
||||
"-439.13521",
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
@@ -2434,16 +2434,16 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"strokeWidth": 2,
|
||||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 12,
|
||||
"version": 13,
|
||||
"width": 488,
|
||||
"x": 6,
|
||||
"y": "-4.89286",
|
||||
"y": "-5.38920",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should update bound element points when rectangle was remotely moved and arrow is added back through the history > [end of test] number of elements 1`] = `3`;
|
||||
|
||||
exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should update bound element points when rectangle was remotely moved and arrow is added back through the history > [end of test] number of renders 1`] = `11`;
|
||||
exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should update bound element points when rectangle was remotely moved and arrow is added back through the history > [end of test] number of renders 1`] = `12`;
|
||||
|
||||
exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should update bound element points when rectangle was remotely moved and arrow is added back through the history > [end of test] redo stack 1`] = `[]`;
|
||||
|
||||
@@ -2558,7 +2558,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"endBinding": {
|
||||
"elementId": "id1",
|
||||
"fixedPoint": [
|
||||
"0.50010",
|
||||
0,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
@@ -2566,7 +2566,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": "398.76619",
|
||||
"height": "439.13521",
|
||||
"index": "a2",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
@@ -2579,7 +2579,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
],
|
||||
[
|
||||
488,
|
||||
"-398.76619",
|
||||
"-439.13521",
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
@@ -2599,14 +2599,14 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "arrow",
|
||||
"version": 12,
|
||||
"version": 13,
|
||||
"width": 488,
|
||||
"x": 6,
|
||||
"y": "-4.89286",
|
||||
"y": "-5.38920",
|
||||
},
|
||||
"inserted": {
|
||||
"isDeleted": true,
|
||||
"version": 9,
|
||||
"version": 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -16375,7 +16375,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"endBinding": {
|
||||
"elementId": "id2",
|
||||
"fixedPoint": [
|
||||
"0.50010",
|
||||
0,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
@@ -16383,7 +16383,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": "0.00092",
|
||||
"height": "0.00004",
|
||||
"id": "id13",
|
||||
"index": "a3",
|
||||
"isDeleted": false,
|
||||
@@ -16398,7 +16398,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
],
|
||||
[
|
||||
"88.00000",
|
||||
"0.00092",
|
||||
"0.00004",
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
@@ -16409,7 +16409,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"startBinding": {
|
||||
"elementId": "id0",
|
||||
"fixedPoint": [
|
||||
"0.50010",
|
||||
1,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
@@ -16422,7 +16422,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"version": 12,
|
||||
"width": "88.00000",
|
||||
"x": 6,
|
||||
"y": "0.00849",
|
||||
"y": "0.00996",
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -16458,7 +16458,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"endBinding": {
|
||||
"elementId": "id2",
|
||||
"fixedPoint": [
|
||||
"0.50010",
|
||||
0,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
@@ -16467,7 +16467,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"startBinding": {
|
||||
"elementId": "id0",
|
||||
"fixedPoint": [
|
||||
"0.50010",
|
||||
1,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
@@ -16478,7 +16478,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"endBinding": {
|
||||
"elementId": "id2",
|
||||
"fixedPoint": [
|
||||
"0.50010",
|
||||
0,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
@@ -16487,7 +16487,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"startBinding": {
|
||||
"elementId": "id0",
|
||||
"fixedPoint": [
|
||||
"0.50010",
|
||||
1,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
@@ -16793,7 +16793,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"endBinding": {
|
||||
"elementId": "id2",
|
||||
"fixedPoint": [
|
||||
"0.50010",
|
||||
0,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
@@ -16801,7 +16801,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": "0.00611",
|
||||
"height": "0.00936",
|
||||
"index": "a3",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
@@ -16814,7 +16814,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
],
|
||||
[
|
||||
88,
|
||||
"0.00611",
|
||||
"0.00936",
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
@@ -16825,7 +16825,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"startBinding": {
|
||||
"elementId": "id0",
|
||||
"fixedPoint": [
|
||||
"0.50010",
|
||||
1,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
@@ -17126,7 +17126,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"endBinding": {
|
||||
"elementId": "id2",
|
||||
"fixedPoint": [
|
||||
"0.50010",
|
||||
0,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
@@ -17134,7 +17134,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": "0.00092",
|
||||
"height": "0.00004",
|
||||
"id": "id13",
|
||||
"index": "a3",
|
||||
"isDeleted": false,
|
||||
@@ -17149,7 +17149,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
],
|
||||
[
|
||||
"88.00000",
|
||||
"0.00092",
|
||||
"0.00004",
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
@@ -17160,7 +17160,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"startBinding": {
|
||||
"elementId": "id0",
|
||||
"fixedPoint": [
|
||||
"0.50010",
|
||||
1,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
@@ -17173,7 +17173,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"version": 12,
|
||||
"width": "88.00000",
|
||||
"x": 6,
|
||||
"y": "0.00849",
|
||||
"y": "0.00996",
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -17434,7 +17434,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"endBinding": {
|
||||
"elementId": "id2",
|
||||
"fixedPoint": [
|
||||
"0.50010",
|
||||
0,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
@@ -17442,7 +17442,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": "0.00092",
|
||||
"height": "0.00004",
|
||||
"index": "a3",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
@@ -17455,7 +17455,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
],
|
||||
[
|
||||
"88.00000",
|
||||
"0.00092",
|
||||
"0.00004",
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
@@ -17466,7 +17466,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"startBinding": {
|
||||
"elementId": "id0",
|
||||
"fixedPoint": [
|
||||
"0.50010",
|
||||
1,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
@@ -17478,7 +17478,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"version": 12,
|
||||
"width": "88.00000",
|
||||
"x": 6,
|
||||
"y": "0.00849",
|
||||
"y": "0.00996",
|
||||
},
|
||||
"inserted": {
|
||||
"isDeleted": true,
|
||||
@@ -17775,7 +17775,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"endBinding": {
|
||||
"elementId": "id2",
|
||||
"fixedPoint": [
|
||||
"0.50010",
|
||||
0,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
@@ -17783,7 +17783,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": "0.00092",
|
||||
"height": "0.00004",
|
||||
"id": "id13",
|
||||
"index": "a3",
|
||||
"isDeleted": false,
|
||||
@@ -17798,7 +17798,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
],
|
||||
[
|
||||
"88.00000",
|
||||
"0.00092",
|
||||
"0.00004",
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
@@ -17809,7 +17809,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"startBinding": {
|
||||
"elementId": "id0",
|
||||
"fixedPoint": [
|
||||
"0.50010",
|
||||
1,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
@@ -17822,7 +17822,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"version": 12,
|
||||
"width": "88.00000",
|
||||
"x": 6,
|
||||
"y": "0.00849",
|
||||
"y": "0.00996",
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -18083,7 +18083,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"endBinding": {
|
||||
"elementId": "id2",
|
||||
"fixedPoint": [
|
||||
"0.50010",
|
||||
0,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
@@ -18091,7 +18091,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": "0.00092",
|
||||
"height": "0.00004",
|
||||
"index": "a3",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
@@ -18104,7 +18104,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
],
|
||||
[
|
||||
"88.00000",
|
||||
"0.00092",
|
||||
"0.00004",
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
@@ -18115,7 +18115,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"startBinding": {
|
||||
"elementId": "id0",
|
||||
"fixedPoint": [
|
||||
"0.50010",
|
||||
1,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
@@ -18127,7 +18127,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"version": 12,
|
||||
"width": "88.00000",
|
||||
"x": 6,
|
||||
"y": "0.00849",
|
||||
"y": "0.00996",
|
||||
},
|
||||
"inserted": {
|
||||
"isDeleted": true,
|
||||
@@ -18422,7 +18422,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"endBinding": {
|
||||
"elementId": "id2",
|
||||
"fixedPoint": [
|
||||
"0.50010",
|
||||
0,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
@@ -18430,7 +18430,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": "0.00092",
|
||||
"height": "0.00004",
|
||||
"id": "id13",
|
||||
"index": "a3",
|
||||
"isDeleted": false,
|
||||
@@ -18445,7 +18445,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
],
|
||||
[
|
||||
"88.00000",
|
||||
"0.00092",
|
||||
"0.00004",
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
@@ -18456,7 +18456,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"startBinding": {
|
||||
"elementId": "id0",
|
||||
"fixedPoint": [
|
||||
"0.50010",
|
||||
1,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
@@ -18469,7 +18469,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"version": 12,
|
||||
"width": "88.00000",
|
||||
"x": 6,
|
||||
"y": "0.00849",
|
||||
"y": "0.00996",
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -18522,7 +18522,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"endBinding": {
|
||||
"elementId": "id2",
|
||||
"fixedPoint": [
|
||||
"0.50010",
|
||||
0,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
@@ -18530,7 +18530,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"startBinding": {
|
||||
"elementId": "id0",
|
||||
"fixedPoint": [
|
||||
"0.50010",
|
||||
1,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
@@ -18541,7 +18541,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"endBinding": {
|
||||
"elementId": "id2",
|
||||
"fixedPoint": [
|
||||
"0.50010",
|
||||
0,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
@@ -18816,7 +18816,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"endBinding": {
|
||||
"elementId": "id2",
|
||||
"fixedPoint": [
|
||||
"0.50010",
|
||||
0,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
@@ -18824,7 +18824,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": "0.00611",
|
||||
"height": "0.00936",
|
||||
"index": "a3",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
@@ -18837,7 +18837,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
],
|
||||
[
|
||||
88,
|
||||
"0.00611",
|
||||
"0.00936",
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
@@ -18848,7 +18848,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"startBinding": {
|
||||
"elementId": "id0",
|
||||
"fixedPoint": [
|
||||
"0.50010",
|
||||
1,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
@@ -19177,7 +19177,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"endBinding": {
|
||||
"elementId": "id2",
|
||||
"fixedPoint": [
|
||||
"0.50010",
|
||||
0,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
@@ -19185,7 +19185,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": "0.00092",
|
||||
"height": "0.00004",
|
||||
"id": "id13",
|
||||
"index": "a3",
|
||||
"isDeleted": false,
|
||||
@@ -19200,7 +19200,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
],
|
||||
[
|
||||
"88.00000",
|
||||
"0.00092",
|
||||
"0.00004",
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
@@ -19211,7 +19211,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"startBinding": {
|
||||
"elementId": "id0",
|
||||
"fixedPoint": [
|
||||
"0.50010",
|
||||
1,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
@@ -19224,7 +19224,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"version": 13,
|
||||
"width": "88.00000",
|
||||
"x": 6,
|
||||
"y": "0.00849",
|
||||
"y": "0.00996",
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -19288,7 +19288,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"endBinding": {
|
||||
"elementId": "id2",
|
||||
"fixedPoint": [
|
||||
"0.50010",
|
||||
0,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
@@ -19296,7 +19296,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"startBinding": {
|
||||
"elementId": "id0",
|
||||
"fixedPoint": [
|
||||
"0.50010",
|
||||
1,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
@@ -19567,7 +19567,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"endBinding": {
|
||||
"elementId": "id2",
|
||||
"fixedPoint": [
|
||||
"0.50010",
|
||||
0,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
@@ -19575,7 +19575,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": "0.00611",
|
||||
"height": "0.00936",
|
||||
"index": "a3",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
@@ -19588,7 +19588,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
],
|
||||
[
|
||||
88,
|
||||
"0.00611",
|
||||
"0.00936",
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
@@ -19599,7 +19599,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"startBinding": {
|
||||
"elementId": "id0",
|
||||
"fixedPoint": [
|
||||
"0.50010",
|
||||
1,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
|
||||
@@ -182,14 +182,14 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
||||
"elementId": "id3",
|
||||
"fixedPoint": [
|
||||
"-0.02000",
|
||||
"0.44667",
|
||||
"0.47904",
|
||||
],
|
||||
"mode": "orbit",
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": "90.00000",
|
||||
"height": "90.02554",
|
||||
"id": "id6",
|
||||
"index": "a2",
|
||||
"isDeleted": false,
|
||||
@@ -204,7 +204,7 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
||||
],
|
||||
[
|
||||
89,
|
||||
"90.00000",
|
||||
"90.02554",
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
@@ -217,7 +217,7 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
||||
"elementId": "id0",
|
||||
"fixedPoint": [
|
||||
"1.06000",
|
||||
"0.46000",
|
||||
"0.55687",
|
||||
],
|
||||
"mode": "orbit",
|
||||
},
|
||||
@@ -230,6 +230,6 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
||||
"versionNonce": 271613161,
|
||||
"width": 89,
|
||||
"x": 106,
|
||||
"y": "46.00000",
|
||||
"y": "55.68677",
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1590,7 +1590,7 @@ describe("history", () => {
|
||||
expect(API.getUndoStack().length).toBe(5);
|
||||
expect(arrow.startBinding).toEqual({
|
||||
elementId: rect1.id,
|
||||
fixedPoint: expect.arrayContaining([0.5001, 0.5001]),
|
||||
fixedPoint: expect.arrayContaining([1, 0.5001]),
|
||||
mode: "orbit",
|
||||
});
|
||||
expect(arrow.endBinding).toEqual({
|
||||
@@ -1613,7 +1613,7 @@ describe("history", () => {
|
||||
expect(API.getRedoStack().length).toBe(1);
|
||||
expect(arrow.startBinding).toEqual({
|
||||
elementId: rect1.id,
|
||||
fixedPoint: expect.arrayContaining([0.5001, 0.5001]),
|
||||
fixedPoint: expect.arrayContaining([1, 0.5001]),
|
||||
mode: "orbit",
|
||||
});
|
||||
expect(arrow.endBinding).toEqual({
|
||||
@@ -1636,7 +1636,7 @@ describe("history", () => {
|
||||
expect(API.getRedoStack().length).toBe(0);
|
||||
expect(arrow.startBinding).toEqual({
|
||||
elementId: rect1.id,
|
||||
fixedPoint: expect.arrayContaining([0.5001, 0.5001]),
|
||||
fixedPoint: expect.arrayContaining([1, 0.5001]),
|
||||
mode: "orbit",
|
||||
});
|
||||
expect(arrow.endBinding).toEqual({
|
||||
@@ -1667,7 +1667,7 @@ describe("history", () => {
|
||||
expect(API.getRedoStack().length).toBe(0);
|
||||
expect(arrow.startBinding).toEqual({
|
||||
elementId: rect1.id,
|
||||
fixedPoint: expect.arrayContaining([0.5001, 0.5001]),
|
||||
fixedPoint: expect.arrayContaining([1, 0.5001]),
|
||||
mode: "orbit",
|
||||
});
|
||||
expect(arrow.endBinding).toEqual({
|
||||
@@ -1690,7 +1690,7 @@ describe("history", () => {
|
||||
expect(API.getRedoStack().length).toBe(1);
|
||||
expect(arrow.startBinding).toEqual({
|
||||
elementId: rect1.id,
|
||||
fixedPoint: expect.arrayContaining([0.5001, 0.5001]),
|
||||
fixedPoint: expect.arrayContaining([1, 0.5001]),
|
||||
mode: "orbit",
|
||||
});
|
||||
expect(arrow.endBinding).toEqual({
|
||||
@@ -4510,20 +4510,20 @@ describe("history", () => {
|
||||
|
||||
// create start binding
|
||||
mouse.downAt(0, 0);
|
||||
mouse.moveTo(0, 10);
|
||||
mouse.moveTo(0, 10);
|
||||
mouse.moveTo(0, 3);
|
||||
mouse.moveTo(0, 3);
|
||||
mouse.up();
|
||||
|
||||
// create end binding
|
||||
mouse.downAt(100, 0);
|
||||
mouse.moveTo(100, 10);
|
||||
mouse.moveTo(100, 10);
|
||||
mouse.moveTo(100, 3);
|
||||
mouse.moveTo(100, 3);
|
||||
mouse.up();
|
||||
|
||||
expect(
|
||||
(h.elements[2] as ExcalidrawElbowArrowElement).startBinding
|
||||
?.fixedPoint,
|
||||
).not.toEqual([1, 0.5001]);
|
||||
).toEqual([1, 0.5001]);
|
||||
expect(
|
||||
(h.elements[2] as ExcalidrawElbowArrowElement).startBinding?.mode,
|
||||
).toBe("orbit");
|
||||
@@ -4623,12 +4623,12 @@ describe("history", () => {
|
||||
id: arrowId,
|
||||
startBinding: expect.objectContaining({
|
||||
elementId: rect1.id,
|
||||
fixedPoint: [0.6363636363636364, 0.6363636363636364],
|
||||
fixedPoint: [1, 0.5001],
|
||||
mode: "orbit",
|
||||
}),
|
||||
endBinding: expect.objectContaining({
|
||||
elementId: rect2.id,
|
||||
fixedPoint: [0.4106696643494564, 0.5893303356505437],
|
||||
fixedPoint: [0, 0.5001],
|
||||
mode: "orbit",
|
||||
}),
|
||||
}),
|
||||
@@ -4666,13 +4666,13 @@ describe("history", () => {
|
||||
|
||||
// create start binding
|
||||
mouse.downAt(0, 0);
|
||||
mouse.moveTo(0, 10);
|
||||
mouse.upAt(0, 10);
|
||||
mouse.moveTo(0, 3);
|
||||
mouse.upAt(0, 3);
|
||||
|
||||
// create end binding
|
||||
mouse.downAt(100, 0);
|
||||
mouse.moveTo(100, 10);
|
||||
mouse.upAt(100, 10);
|
||||
mouse.moveTo(100, 3);
|
||||
mouse.upAt(100, 3);
|
||||
|
||||
expect(h.elements).toEqual(
|
||||
expect.arrayContaining([
|
||||
@@ -4766,13 +4766,13 @@ describe("history", () => {
|
||||
id: arrowId,
|
||||
startBinding: expect.objectContaining({
|
||||
elementId: rect1.id,
|
||||
fixedPoint: [0.6363636363636364, 0.6363636363636364],
|
||||
fixedPoint: [1, 0.5001],
|
||||
mode: "orbit",
|
||||
}),
|
||||
// rebound with previous rectangle
|
||||
endBinding: expect.objectContaining({
|
||||
elementId: rect2.id,
|
||||
fixedPoint: [0.39746300211416496, 0.6025369978858351],
|
||||
fixedPoint: [0, 0.5001],
|
||||
mode: "orbit",
|
||||
}),
|
||||
}),
|
||||
@@ -5031,8 +5031,9 @@ describe("history", () => {
|
||||
// bind arrow to rect1 and rect2
|
||||
UI.clickTool("arrow");
|
||||
mouse.down(0, 0);
|
||||
mouse.moveTo(50, 0);
|
||||
mouse.up(50, 0);
|
||||
mouse.moveTo(25, 0);
|
||||
mouse.moveTo(47, 0);
|
||||
mouse.up(47, 0);
|
||||
|
||||
const arrowId = h.elements[2].id;
|
||||
|
||||
@@ -5055,7 +5056,7 @@ describe("history", () => {
|
||||
}),
|
||||
endBinding: expect.objectContaining({
|
||||
elementId: rect2.id,
|
||||
fixedPoint: expect.arrayContaining([0.5001, 0.5001]),
|
||||
fixedPoint: expect.arrayContaining([0, 0.5001]),
|
||||
}),
|
||||
isDeleted: true,
|
||||
}),
|
||||
|
||||
@@ -78,7 +78,7 @@ describe("move element", () => {
|
||||
// create elements
|
||||
const rectA = UI.createElement("rectangle", { size: 100 });
|
||||
const rectB = UI.createElement("rectangle", { x: 200, y: 0, size: 300 });
|
||||
const arrow = UI.createElement("arrow", { x: 110, y: 50, size: 80 });
|
||||
const arrow = UI.createElement("arrow", { x: 105, y: 50, size: 88 });
|
||||
|
||||
act(() => {
|
||||
// bind line to two rectangles
|
||||
@@ -110,7 +110,10 @@ describe("move element", () => {
|
||||
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
|
||||
expect([rectA.x, rectA.y]).toEqual([0, 0]);
|
||||
expect([rectB.x, rectB.y]).toEqual([200, 0]);
|
||||
expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[106, 46]], 0);
|
||||
expect([[arrow.x, arrow.y]]).toCloselyEqualPoints(
|
||||
[[106.00000000000001, 55.6867741935484]],
|
||||
0,
|
||||
);
|
||||
expect([[arrow.width, arrow.height]]).toCloselyEqualPoints([[88, 88]], 0);
|
||||
|
||||
renderInteractiveScene.mockClear();
|
||||
@@ -129,11 +132,11 @@ describe("move element", () => {
|
||||
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
|
||||
expect([rectA.x, rectA.y]).toEqual([0, 0]);
|
||||
expect([rectB.x, rectB.y]).toEqual([201, 2]);
|
||||
expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[106, 46]], 0);
|
||||
expect([[arrow.width, arrow.height]]).toCloselyEqualPoints(
|
||||
[[79, 124.1678]],
|
||||
2,
|
||||
expect([[arrow.x, arrow.y]]).toCloselyEqualPoints(
|
||||
[[106, 55.6867741935484]],
|
||||
0,
|
||||
);
|
||||
expect([[arrow.width, arrow.height]]).toCloselyEqualPoints([[89, 90]], 0);
|
||||
|
||||
h.elements.forEach((element) => expect(element).toMatchSnapshot());
|
||||
});
|
||||
|
||||
@@ -80,6 +80,6 @@ test("unselected bound arrows update when rotating their target elements", async
|
||||
expect(textArrow.x).toEqual(360);
|
||||
expect(textArrow.y).toEqual(300);
|
||||
expect(textArrow.points[0]).toEqual([0, 0]);
|
||||
expect(textArrow.points[1][0]).toBeCloseTo(-98.87, 0);
|
||||
expect(textArrow.points[1][1]).toBeCloseTo(-123.65, 0);
|
||||
expect(textArrow.points[1][0]).toBeCloseTo(-95.4635969899922, 0);
|
||||
expect(textArrow.points[1][1]).toBeCloseTo(-126.8785027399889, 0);
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
@@ -301,7 +302,10 @@ export interface AppState {
|
||||
selectionElement: NonDeletedExcalidrawElement | null;
|
||||
isBindingEnabled: boolean;
|
||||
startBoundElement: NonDeleted<ExcalidrawBindableElement> | null;
|
||||
suggestedBinding: NonDeleted<ExcalidrawBindableElement> | null;
|
||||
suggestedBinding: {
|
||||
element: NonDeleted<ExcalidrawBindableElement>;
|
||||
midPoint?: GlobalPoint;
|
||||
} | null;
|
||||
frameToHighlight: NonDeleted<ExcalidrawFrameLikeElement> | null;
|
||||
frameRendering: {
|
||||
enabled: boolean;
|
||||
@@ -759,6 +763,7 @@ export type AppClassProperties = {
|
||||
updateEditorAtom: App["updateEditorAtom"];
|
||||
onPointerDownEmitter: App["onPointerDownEmitter"];
|
||||
|
||||
lastPointerMoveCoords: App["lastPointerMoveCoords"];
|
||||
bindModeHandler: App["bindModeHandler"];
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user