mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-11-19 20:24:49 +01:00
binding gap and distance and binding highlight tweaks
This commit is contained in:
@@ -103,32 +103,25 @@ export type BindingStrategy =
|
||||
focusPoint?: undefined;
|
||||
};
|
||||
|
||||
export const FIXED_BINDING_DISTANCE = 5;
|
||||
/** excludes element strokeWidth */
|
||||
export const BASE_BINDING_GAP = 10;
|
||||
/** scaled based on zoom when < 1 */
|
||||
const BASE_BINDING_DISTANCE = Math.max(BASE_BINDING_GAP, 15);
|
||||
|
||||
export const BINDING_HIGHLIGHT_THICKNESS = 10;
|
||||
|
||||
export const getFixedBindingDistance = (
|
||||
export const getFixedBindingGap = (
|
||||
element: ExcalidrawBindableElement,
|
||||
): number => FIXED_BINDING_DISTANCE + element.strokeWidth / 2;
|
||||
): number => BASE_BINDING_GAP + element.strokeWidth / 2;
|
||||
|
||||
export const maxBindingGap_simple = (
|
||||
element: ExcalidrawElement,
|
||||
elementWidth: number,
|
||||
elementHeight: number,
|
||||
zoom?: AppState["zoom"],
|
||||
): number => {
|
||||
export const maxBindingDistance_simple = (zoom?: AppState["zoom"]): number => {
|
||||
const zoomValue = zoom?.value && zoom.value < 1 ? zoom.value : 1;
|
||||
|
||||
// Aligns diamonds with rectangles
|
||||
const shapeRatio = element.type === "diamond" ? 1 / Math.sqrt(2) : 1;
|
||||
const smallerDimension = shapeRatio * Math.min(elementWidth, elementHeight);
|
||||
|
||||
return Math.max(
|
||||
16,
|
||||
// bigger bindable boundary for bigger elements
|
||||
Math.min(0.25 * smallerDimension, 32),
|
||||
// keep in sync with the zoomed highlight
|
||||
BINDING_HIGHLIGHT_THICKNESS / zoomValue + FIXED_BINDING_DISTANCE,
|
||||
return clamp(
|
||||
// reducing zoom impact so that the diff between binding distance and
|
||||
// binding gap is kept to minimum when possible
|
||||
BASE_BINDING_DISTANCE / (zoomValue * 1.5),
|
||||
BASE_BINDING_DISTANCE,
|
||||
BASE_BINDING_DISTANCE * 2,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -247,8 +240,7 @@ const bindingStrategyForElbowArrowEndpointDragging = (
|
||||
globalPoint,
|
||||
elements,
|
||||
elementsMap,
|
||||
(element) =>
|
||||
maxBindingGap_simple(element, element.width, element.height, zoom),
|
||||
(element) => maxBindingDistance_simple(zoom),
|
||||
);
|
||||
|
||||
const current = hit
|
||||
@@ -667,7 +659,7 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
|
||||
globalPoint,
|
||||
elements,
|
||||
elementsMap,
|
||||
(e) => maxBindingGap_simple(e, e.width, e.height, appState.zoom),
|
||||
(e) => maxBindingDistance_simple(appState.zoom),
|
||||
);
|
||||
const pointInElement = hit && isPointInElement(globalPoint, hit, elementsMap);
|
||||
const otherBindableElement = otherBinding
|
||||
@@ -1197,12 +1189,7 @@ const getDistanceForBinding = (
|
||||
zoom?: AppState["zoom"],
|
||||
) => {
|
||||
const distance = distanceToElement(bindableElement, elementsMap, point);
|
||||
const bindDistance = maxBindingGap_simple(
|
||||
bindableElement,
|
||||
bindableElement.width,
|
||||
bindableElement.height,
|
||||
zoom,
|
||||
);
|
||||
const bindDistance = maxBindingDistance_simple(zoom);
|
||||
|
||||
return distance > bindDistance ? null : distance;
|
||||
};
|
||||
@@ -1272,7 +1259,7 @@ export const bindPointToSnapToElementOutline = (
|
||||
bindableElement,
|
||||
elementsMap,
|
||||
intersector,
|
||||
getFixedBindingDistance(bindableElement),
|
||||
getFixedBindingGap(bindableElement),
|
||||
).sort(pointDistanceSq)[0];
|
||||
|
||||
if (!intersection) {
|
||||
@@ -1294,7 +1281,7 @@ export const bindPointToSnapToElementOutline = (
|
||||
bindableElement,
|
||||
elementsMap,
|
||||
anotherIntersector,
|
||||
FIXED_BINDING_DISTANCE,
|
||||
BASE_BINDING_GAP,
|
||||
).sort(pointDistanceSq)[0];
|
||||
}
|
||||
} else {
|
||||
@@ -1302,7 +1289,7 @@ export const bindPointToSnapToElementOutline = (
|
||||
vectorNormalize(vectorFromPoint(edgePoint, adjacentPoint)),
|
||||
pointDistance(edgePoint, adjacentPoint) +
|
||||
Math.max(bindableElement.width, bindableElement.height) +
|
||||
getFixedBindingDistance(bindableElement) * 2,
|
||||
getFixedBindingGap(bindableElement) * 2,
|
||||
);
|
||||
const intersector =
|
||||
customIntersector ??
|
||||
@@ -1317,7 +1304,7 @@ export const bindPointToSnapToElementOutline = (
|
||||
bindableElement,
|
||||
elementsMap,
|
||||
intersector,
|
||||
getFixedBindingDistance(bindableElement),
|
||||
getFixedBindingGap(bindableElement),
|
||||
).sort(
|
||||
(g, h) =>
|
||||
pointDistanceSq(g, adjacentPoint) -
|
||||
@@ -1346,15 +1333,15 @@ export const avoidRectangularCorner = (
|
||||
|
||||
if (nonRotatedPoint[0] < element.x && nonRotatedPoint[1] < element.y) {
|
||||
// Top left
|
||||
if (nonRotatedPoint[1] - element.y > -getFixedBindingDistance(element)) {
|
||||
if (nonRotatedPoint[1] - element.y > -getFixedBindingGap(element)) {
|
||||
return pointRotateRads<GlobalPoint>(
|
||||
pointFrom(element.x - getFixedBindingDistance(element), element.y),
|
||||
pointFrom(element.x - getFixedBindingGap(element), element.y),
|
||||
center,
|
||||
element.angle,
|
||||
);
|
||||
}
|
||||
return pointRotateRads(
|
||||
pointFrom(element.x, element.y - getFixedBindingDistance(element)),
|
||||
pointFrom(element.x, element.y - getFixedBindingGap(element)),
|
||||
center,
|
||||
element.angle,
|
||||
);
|
||||
@@ -1363,11 +1350,11 @@ export const avoidRectangularCorner = (
|
||||
nonRotatedPoint[1] > element.y + element.height
|
||||
) {
|
||||
// Bottom left
|
||||
if (nonRotatedPoint[0] - element.x > -getFixedBindingDistance(element)) {
|
||||
if (nonRotatedPoint[0] - element.x > -getFixedBindingGap(element)) {
|
||||
return pointRotateRads(
|
||||
pointFrom(
|
||||
element.x,
|
||||
element.y + element.height + getFixedBindingDistance(element),
|
||||
element.y + element.height + getFixedBindingGap(element),
|
||||
),
|
||||
center,
|
||||
element.angle,
|
||||
@@ -1375,7 +1362,7 @@ export const avoidRectangularCorner = (
|
||||
}
|
||||
return pointRotateRads(
|
||||
pointFrom(
|
||||
element.x - getFixedBindingDistance(element),
|
||||
element.x - getFixedBindingGap(element),
|
||||
element.y + element.height,
|
||||
),
|
||||
center,
|
||||
@@ -1388,12 +1375,12 @@ export const avoidRectangularCorner = (
|
||||
// Bottom right
|
||||
if (
|
||||
nonRotatedPoint[0] - element.x <
|
||||
element.width + getFixedBindingDistance(element)
|
||||
element.width + getFixedBindingGap(element)
|
||||
) {
|
||||
return pointRotateRads(
|
||||
pointFrom(
|
||||
element.x + element.width,
|
||||
element.y + element.height + getFixedBindingDistance(element),
|
||||
element.y + element.height + getFixedBindingGap(element),
|
||||
),
|
||||
center,
|
||||
element.angle,
|
||||
@@ -1401,7 +1388,7 @@ export const avoidRectangularCorner = (
|
||||
}
|
||||
return pointRotateRads(
|
||||
pointFrom(
|
||||
element.x + element.width + getFixedBindingDistance(element),
|
||||
element.x + element.width + getFixedBindingGap(element),
|
||||
element.y + element.height,
|
||||
),
|
||||
center,
|
||||
@@ -1414,12 +1401,12 @@ export const avoidRectangularCorner = (
|
||||
// Top right
|
||||
if (
|
||||
nonRotatedPoint[0] - element.x <
|
||||
element.width + getFixedBindingDistance(element)
|
||||
element.width + getFixedBindingGap(element)
|
||||
) {
|
||||
return pointRotateRads(
|
||||
pointFrom(
|
||||
element.x + element.width,
|
||||
element.y - getFixedBindingDistance(element),
|
||||
element.y - getFixedBindingGap(element),
|
||||
),
|
||||
center,
|
||||
element.angle,
|
||||
@@ -1427,7 +1414,7 @@ export const avoidRectangularCorner = (
|
||||
}
|
||||
return pointRotateRads(
|
||||
pointFrom(
|
||||
element.x + element.width + getFixedBindingDistance(element),
|
||||
element.x + element.width + getFixedBindingGap(element),
|
||||
element.y,
|
||||
),
|
||||
center,
|
||||
@@ -1454,7 +1441,7 @@ const snapToMid = (
|
||||
const horizontalThreshold = clamp(tolerance * width, 5, 80);
|
||||
|
||||
// Too close to the center makes it hard to resolve direction precisely
|
||||
if (pointDistance(center, nonRotated) < getFixedBindingDistance(element)) {
|
||||
if (pointDistance(center, nonRotated) < getFixedBindingGap(element)) {
|
||||
return p;
|
||||
}
|
||||
|
||||
@@ -1465,7 +1452,7 @@ const snapToMid = (
|
||||
) {
|
||||
// LEFT
|
||||
return pointRotateRads<GlobalPoint>(
|
||||
pointFrom(x - getFixedBindingDistance(element), center[1]),
|
||||
pointFrom(x - getFixedBindingGap(element), center[1]),
|
||||
center,
|
||||
angle,
|
||||
);
|
||||
@@ -1476,7 +1463,7 @@ const snapToMid = (
|
||||
) {
|
||||
// TOP
|
||||
return pointRotateRads(
|
||||
pointFrom(center[0], y - getFixedBindingDistance(element)),
|
||||
pointFrom(center[0], y - getFixedBindingGap(element)),
|
||||
center,
|
||||
angle,
|
||||
);
|
||||
@@ -1487,7 +1474,7 @@ const snapToMid = (
|
||||
) {
|
||||
// RIGHT
|
||||
return pointRotateRads(
|
||||
pointFrom(x + width + getFixedBindingDistance(element), center[1]),
|
||||
pointFrom(x + width + getFixedBindingGap(element), center[1]),
|
||||
center,
|
||||
angle,
|
||||
);
|
||||
@@ -1498,12 +1485,12 @@ const snapToMid = (
|
||||
) {
|
||||
// DOWN
|
||||
return pointRotateRads(
|
||||
pointFrom(center[0], y + height + getFixedBindingDistance(element)),
|
||||
pointFrom(center[0], y + height + getFixedBindingGap(element)),
|
||||
center,
|
||||
angle,
|
||||
);
|
||||
} else if (element.type === "diamond") {
|
||||
const distance = getFixedBindingDistance(element);
|
||||
const distance = getFixedBindingGap(element);
|
||||
const topLeft = pointFrom<GlobalPoint>(
|
||||
x + width / 4 - distance,
|
||||
y + height / 4 - distance,
|
||||
|
||||
@@ -26,11 +26,11 @@ import type { AppState } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import {
|
||||
bindPointToSnapToElementOutline,
|
||||
FIXED_BINDING_DISTANCE,
|
||||
BASE_BINDING_GAP,
|
||||
getHeadingForElbowArrowSnap,
|
||||
getGlobalFixedPointForBindableElement,
|
||||
getFixedBindingDistance,
|
||||
maxBindingGap_simple,
|
||||
getFixedBindingGap,
|
||||
maxBindingDistance_simple,
|
||||
} from "./binding";
|
||||
import { distanceToElement } from "./distance";
|
||||
import {
|
||||
@@ -1304,8 +1304,8 @@ const getElbowArrowData = (
|
||||
offsetFromHeading(
|
||||
startHeading,
|
||||
arrow.startArrowhead
|
||||
? getFixedBindingDistance(hoveredStartElement) * 6
|
||||
: getFixedBindingDistance(hoveredStartElement) * 2,
|
||||
? getFixedBindingGap(hoveredStartElement) * 6
|
||||
: getFixedBindingGap(hoveredStartElement) * 2,
|
||||
1,
|
||||
),
|
||||
)
|
||||
@@ -1317,8 +1317,8 @@ const getElbowArrowData = (
|
||||
offsetFromHeading(
|
||||
endHeading,
|
||||
arrow.endArrowhead
|
||||
? getFixedBindingDistance(hoveredEndElement) * 6
|
||||
: getFixedBindingDistance(hoveredEndElement) * 2,
|
||||
? getFixedBindingGap(hoveredEndElement) * 6
|
||||
: getFixedBindingGap(hoveredEndElement) * 2,
|
||||
1,
|
||||
),
|
||||
)
|
||||
@@ -1365,8 +1365,8 @@ const getElbowArrowData = (
|
||||
? 0
|
||||
: BASE_PADDING -
|
||||
(arrow.startArrowhead
|
||||
? FIXED_BINDING_DISTANCE * 6
|
||||
: FIXED_BINDING_DISTANCE * 2),
|
||||
? BASE_BINDING_GAP * 6
|
||||
: BASE_BINDING_GAP * 2),
|
||||
BASE_PADDING,
|
||||
),
|
||||
boundsOverlap
|
||||
@@ -1381,8 +1381,8 @@ const getElbowArrowData = (
|
||||
? 0
|
||||
: BASE_PADDING -
|
||||
(arrow.endArrowhead
|
||||
? FIXED_BINDING_DISTANCE * 6
|
||||
: FIXED_BINDING_DISTANCE * 2),
|
||||
? BASE_BINDING_GAP * 6
|
||||
: BASE_BINDING_GAP * 2),
|
||||
BASE_PADDING,
|
||||
),
|
||||
boundsOverlap,
|
||||
@@ -2274,8 +2274,7 @@ const getHoveredElement = (
|
||||
origPoint,
|
||||
elements,
|
||||
elementsMap,
|
||||
(element) =>
|
||||
maxBindingGap_simple(element, element.width, element.height, zoom),
|
||||
(element) => maxBindingDistance_simple(zoom),
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
debugDrawLine,
|
||||
DEFAULT_ADAPTIVE_RADIUS,
|
||||
DEFAULT_PROPORTIONAL_RADIUS,
|
||||
invariant,
|
||||
@@ -598,5 +599,9 @@ export const projectFixedPointOntoDiagonal = (
|
||||
p = p1 || p2 || null;
|
||||
}
|
||||
|
||||
debugDrawLine(diagonalOne, { color: "purple", permanent: false });
|
||||
debugDrawLine(diagonalTwo, { color: "purple", permanent: false });
|
||||
debugDrawLine(intersector, { color: "orange", permanent: false });
|
||||
|
||||
return p && isPointInElement(p, element, elementsMap) ? p : null;
|
||||
};
|
||||
|
||||
@@ -44,14 +44,3 @@ exports[`Test Linear Elements > Test bound text element > should resize and posi
|
||||
"Online whiteboard
|
||||
collaboration made easy"
|
||||
`;
|
||||
|
||||
exports[`Test Linear Elements > Test bound text element > should wrap the bound text when arrow bound container moves 1`] = `
|
||||
"Online whiteboard
|
||||
collaboration made easy"
|
||||
`;
|
||||
|
||||
exports[`Test Linear Elements > Test bound text element > should wrap the bound text when arrow bound container moves 2`] = `
|
||||
"Online whiteboard
|
||||
collaboration made
|
||||
easy"
|
||||
`;
|
||||
|
||||
@@ -246,7 +246,7 @@ import {
|
||||
getElementBounds,
|
||||
doBoundsIntersect,
|
||||
isPointInElement,
|
||||
maxBindingGap_simple,
|
||||
maxBindingDistance_simple,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math";
|
||||
@@ -6550,7 +6550,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
(el) => maxBindingGap_simple(el, el.width, el.height, this.state.zoom),
|
||||
(el) => maxBindingDistance_simple(this.state.zoom),
|
||||
);
|
||||
if (
|
||||
hit &&
|
||||
|
||||
@@ -268,7 +268,7 @@ const renderBindingHighlightForBindableElement_simple = (
|
||||
context.translate(element.x, element.y);
|
||||
|
||||
context.lineWidth =
|
||||
clamp(2.5, element.strokeWidth * 1.75, 4) /
|
||||
clamp(1.75, element.strokeWidth, 4) /
|
||||
Math.max(0.25, appState.zoom.value);
|
||||
context.strokeStyle =
|
||||
appState.theme === THEME.DARK
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -95,141 +95,3 @@ exports[`move element > rectangle 5`] = `
|
||||
"y": 40,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`move element > rectangles with binding arrow 5`] = `
|
||||
{
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "id6",
|
||||
"type": "arrow",
|
||||
},
|
||||
],
|
||||
"customData": undefined,
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 100,
|
||||
"id": "id0",
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": null,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": 760410951,
|
||||
"width": 100,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`move element > rectangles with binding arrow 6`] = `
|
||||
{
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "id6",
|
||||
"type": "arrow",
|
||||
},
|
||||
],
|
||||
"customData": undefined,
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 300,
|
||||
"id": "id3",
|
||||
"index": "a1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": null,
|
||||
"seed": 1116226695,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 7,
|
||||
"versionNonce": 651223591,
|
||||
"width": 300,
|
||||
"x": 201,
|
||||
"y": 2,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`move element > rectangles with binding arrow 7`] = `
|
||||
{
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": null,
|
||||
"customData": undefined,
|
||||
"elbowed": false,
|
||||
"endArrowhead": "arrow",
|
||||
"endBinding": {
|
||||
"elementId": "id3",
|
||||
"fixedPoint": [
|
||||
"-0.02000",
|
||||
"0.44666",
|
||||
],
|
||||
"mode": "orbit",
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": "89.98900",
|
||||
"id": "id6",
|
||||
"index": "a2",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"moveMidPointsWithElement": false,
|
||||
"opacity": 100,
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
"89.00000",
|
||||
"89.98900",
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 2,
|
||||
},
|
||||
"seed": 23633383,
|
||||
"startArrowhead": null,
|
||||
"startBinding": {
|
||||
"elementId": "id0",
|
||||
"fixedPoint": [
|
||||
"1.06000",
|
||||
"0.46011",
|
||||
],
|
||||
"mode": "orbit",
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 14,
|
||||
"versionNonce": 348321737,
|
||||
"width": "89.00000",
|
||||
"x": 106,
|
||||
"y": "46.01050",
|
||||
}
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user