feat: Overlap & distance based binding candidate selection
Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
This commit is contained in:
@@ -291,54 +291,6 @@ const borderDistance = (
|
|||||||
return distance > tolerance ? -Infinity : -distance;
|
return distance > tolerance ? -Infinity : -distance;
|
||||||
};
|
};
|
||||||
|
|
||||||
// const bindingBorderTest = (
|
|
||||||
// element: NonDeleted<ExcalidrawBindableElement>,
|
|
||||||
// [x, y]: Readonly<GlobalPoint>,
|
|
||||||
// elementsMap: NonDeletedSceneElementsMap,
|
|
||||||
// tolerance: number = 0,
|
|
||||||
// ): boolean => {
|
|
||||||
// const p = pointFrom<GlobalPoint>(x, y);
|
|
||||||
// const shouldTestInside =
|
|
||||||
// // disable fullshape snapping for frame elements so we
|
|
||||||
// // can bind to frame children
|
|
||||||
// !isFrameLikeElement(element);
|
|
||||||
|
|
||||||
// // PERF: Run a cheap test to see if the binding element
|
|
||||||
// // is even close to the element
|
|
||||||
// const t = Math.max(1, tolerance);
|
|
||||||
// const bounds = [x - t, y - t, x + t, y + t] as Bounds;
|
|
||||||
// const elementBounds = getElementBounds(element, elementsMap);
|
|
||||||
// if (!doBoundsIntersect(bounds, elementBounds)) {
|
|
||||||
// return false;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // If the element is inside a frame, we should clip the element
|
|
||||||
// if (element.frameId) {
|
|
||||||
// const enclosingFrame = elementsMap.get(element.frameId);
|
|
||||||
// if (enclosingFrame && isFrameLikeElement(enclosingFrame)) {
|
|
||||||
// const enclosingFrameBounds = getElementBounds(
|
|
||||||
// enclosingFrame,
|
|
||||||
// elementsMap,
|
|
||||||
// );
|
|
||||||
// if (!pointInsideBounds(p, enclosingFrameBounds)) {
|
|
||||||
// return false;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Do the intersection test against the element since it's close enough
|
|
||||||
// const intersections = intersectElementWithLineSegment(
|
|
||||||
// element,
|
|
||||||
// elementsMap,
|
|
||||||
// lineSegment(elementCenterPoint(element, elementsMap), p),
|
|
||||||
// );
|
|
||||||
// const distance = distanceToElement(element, elementsMap, p);
|
|
||||||
|
|
||||||
// return shouldTestInside
|
|
||||||
// ? intersections.length === 0 || distance <= tolerance
|
|
||||||
// : intersections.length > 0 && distance <= t;
|
|
||||||
// };
|
|
||||||
|
|
||||||
export const getAllHoveredElementAtPoint = (
|
export const getAllHoveredElementAtPoint = (
|
||||||
arrow: { elbowed: boolean },
|
arrow: { elbowed: boolean },
|
||||||
point: Readonly<GlobalPoint>,
|
point: Readonly<GlobalPoint>,
|
||||||
@@ -347,7 +299,7 @@ export const getAllHoveredElementAtPoint = (
|
|||||||
tolerance?: number,
|
tolerance?: number,
|
||||||
): NonDeleted<ExcalidrawBindableElement>[] => {
|
): NonDeleted<ExcalidrawBindableElement>[] => {
|
||||||
const candidateElements: NonDeleted<ExcalidrawBindableElement>[] = [];
|
const candidateElements: NonDeleted<ExcalidrawBindableElement>[] = [];
|
||||||
// We need to to hit testing from front (end of the array) to back (beginning of the array)
|
// We need to do hit testing from front (end of the array) to back (beginning of the array)
|
||||||
// because array is ordered from lower z-index to highest and we want element z-index
|
// because array is ordered from lower z-index to highest and we want element z-index
|
||||||
// with higher z-index
|
// with higher z-index
|
||||||
for (let index = elements.length - 1; index >= 0; --index) {
|
for (let index = elements.length - 1; index >= 0; --index) {
|
||||||
@@ -400,7 +352,12 @@ export const getHoveredElementForBinding = (
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const distance = borderDistance(element, point, elementsMap, tolerance);
|
const distance = borderDistance(
|
||||||
|
element,
|
||||||
|
point,
|
||||||
|
elementsMap,
|
||||||
|
tolerance ?? 0,
|
||||||
|
);
|
||||||
const bindingGap = getBindingGap(element, arrow);
|
const bindingGap = getBindingGap(element, arrow);
|
||||||
|
|
||||||
if (distance > -(tolerance ?? bindingGap)) {
|
if (distance > -(tolerance ?? bindingGap)) {
|
||||||
@@ -416,9 +373,44 @@ export const getHoveredElementForBinding = (
|
|||||||
return candidateElements[0].element;
|
return candidateElements[0].element;
|
||||||
}
|
}
|
||||||
|
|
||||||
return candidateElements
|
const furthestDistance = candidateElements.sort(
|
||||||
.sort((a, b) => a.distance - b.distance)
|
(a, b) => a.distance - b.distance,
|
||||||
.map((c) => c.element)[0];
|
);
|
||||||
|
|
||||||
|
const candidate = furthestDistance[furthestDistance.length - 1].element;
|
||||||
|
const [cx1, cy1, cx2, cy2] = getElementBounds(candidate, elementsMap);
|
||||||
|
const candidateArea = Math.max(
|
||||||
|
0.00001,
|
||||||
|
Math.abs(cx2 - cx1) * Math.abs(cy2 - cy1),
|
||||||
|
);
|
||||||
|
const overlaps = furthestDistance
|
||||||
|
.map((c) => {
|
||||||
|
if (c.element === candidate) {
|
||||||
|
return { ...c, overlapPercent: 0, relativeArea: 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const [x1, y1, x2, y2] = getElementBounds(c.element, elementsMap);
|
||||||
|
const overlapX1 = x1 > cx1 && x1 < cx2 ? x1 : cx1;
|
||||||
|
const overlapY1 = y1 > cy1 && y1 < cy2 ? y1 : cy1;
|
||||||
|
const overlapX2 = x2 < cx2 && x2 > cx1 ? x2 : cx2;
|
||||||
|
const overlapY2 = y2 < cy2 && y2 > cy1 ? y2 : cy2;
|
||||||
|
const overlapWdith =
|
||||||
|
overlapX1 !== cx1 || overlapX2 !== cx2 ? overlapX2 - overlapX1 : 0;
|
||||||
|
const overlapHeight =
|
||||||
|
overlapY1 !== cy1 || overlapY2 !== cy2 ? overlapY2 - overlapY1 : 0;
|
||||||
|
const area = Math.max(0.00001, Math.abs(x2 - x1) * Math.abs(y2 - y1));
|
||||||
|
const overlapPercent = Math.abs(overlapHeight * overlapWdith) / area;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...c,
|
||||||
|
overlapPercent,
|
||||||
|
relativeArea:
|
||||||
|
overlapPercent === 0 ? 1 : Math.min(area / candidateArea, 1),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((c) => c.overlapPercent > 0.25 && c.relativeArea < 0.75);
|
||||||
|
|
||||||
|
return overlaps.length > 0 ? overlaps[0].element : candidate;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -344,7 +344,7 @@ export class LinearElementEditor {
|
|||||||
|
|
||||||
// Apply the point movement if needed
|
// Apply the point movement if needed
|
||||||
let suggestedBinding: AppState["suggestedBinding"] = null;
|
let suggestedBinding: AppState["suggestedBinding"] = null;
|
||||||
const { positions, updates } = pointDraggingUpdates(
|
const { positions, updates, hit } = pointDraggingUpdates(
|
||||||
[idx],
|
[idx],
|
||||||
deltaX,
|
deltaX,
|
||||||
deltaY,
|
deltaY,
|
||||||
@@ -382,17 +382,20 @@ export class LinearElementEditor {
|
|||||||
|
|
||||||
// Move the arrow over the bindable object in terms of z-index
|
// Move the arrow over the bindable object in terms of z-index
|
||||||
if (isBindingElement(element)) {
|
if (isBindingElement(element)) {
|
||||||
moveArrowAboveBindable(
|
if (hit) {
|
||||||
LinearElementEditor.getPointGlobalCoordinates(
|
moveArrowAboveBindable(
|
||||||
|
LinearElementEditor.getPointGlobalCoordinates(
|
||||||
|
element,
|
||||||
|
element.points[element.points.length - 1],
|
||||||
|
elementsMap,
|
||||||
|
),
|
||||||
element,
|
element,
|
||||||
element.points[element.points.length - 1],
|
elements,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
),
|
app.scene,
|
||||||
element,
|
hit,
|
||||||
elements,
|
);
|
||||||
elementsMap,
|
}
|
||||||
app.scene,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PERF: Avoid state updates if not absolutely necessary
|
// PERF: Avoid state updates if not absolutely necessary
|
||||||
@@ -539,7 +542,7 @@ export class LinearElementEditor {
|
|||||||
|
|
||||||
// Apply the point movement if needed
|
// Apply the point movement if needed
|
||||||
let suggestedBinding: AppState["suggestedBinding"] = null;
|
let suggestedBinding: AppState["suggestedBinding"] = null;
|
||||||
const { positions, updates } = pointDraggingUpdates(
|
const { positions, updates, hit } = pointDraggingUpdates(
|
||||||
selectedPointsIndices,
|
selectedPointsIndices,
|
||||||
deltaX,
|
deltaX,
|
||||||
deltaY,
|
deltaY,
|
||||||
@@ -578,19 +581,22 @@ export class LinearElementEditor {
|
|||||||
|
|
||||||
// Move the arrow over the bindable object in terms of z-index
|
// Move the arrow over the bindable object in terms of z-index
|
||||||
if (isBindingElement(element) && startIsSelected !== endIsSelected) {
|
if (isBindingElement(element) && startIsSelected !== endIsSelected) {
|
||||||
moveArrowAboveBindable(
|
if (hit) {
|
||||||
LinearElementEditor.getPointGlobalCoordinates(
|
moveArrowAboveBindable(
|
||||||
|
LinearElementEditor.getPointGlobalCoordinates(
|
||||||
|
element,
|
||||||
|
startIsSelected
|
||||||
|
? element.points[0]
|
||||||
|
: element.points[element.points.length - 1],
|
||||||
|
elementsMap,
|
||||||
|
),
|
||||||
element,
|
element,
|
||||||
startIsSelected
|
elements,
|
||||||
? element.points[0]
|
|
||||||
: element.points[element.points.length - 1],
|
|
||||||
elementsMap,
|
elementsMap,
|
||||||
),
|
app.scene,
|
||||||
element,
|
hit,
|
||||||
elements,
|
);
|
||||||
elementsMap,
|
}
|
||||||
app.scene,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attached text might need to update if arrow dimensions change
|
// Attached text might need to update if arrow dimensions change
|
||||||
@@ -2136,6 +2142,7 @@ const pointDraggingUpdates = (
|
|||||||
): {
|
): {
|
||||||
positions: PointsPositionUpdates;
|
positions: PointsPositionUpdates;
|
||||||
updates?: PointMoveOtherUpdates;
|
updates?: PointMoveOtherUpdates;
|
||||||
|
hit?: ExcalidrawBindableElement | null;
|
||||||
} => {
|
} => {
|
||||||
const naiveDraggingPoints = new Map(
|
const naiveDraggingPoints = new Map(
|
||||||
selectedPointsIndices.map((pointIndex) => {
|
selectedPointsIndices.map((pointIndex) => {
|
||||||
@@ -2497,6 +2504,7 @@ const pointDraggingUpdates = (
|
|||||||
];
|
];
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
hit: startIsDragged ? start.element : end.element,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { getElementsInGroup } from "./groups";
|
|||||||
import { syncMovedIndices } from "./fractionalIndex";
|
import { syncMovedIndices } from "./fractionalIndex";
|
||||||
import { getSelectedElements } from "./selection";
|
import { getSelectedElements } from "./selection";
|
||||||
import { getBoundTextElement, getContainerElement } from "./textElement";
|
import { getBoundTextElement, getContainerElement } from "./textElement";
|
||||||
import { getHoveredElementForBinding } from "./collision";
|
|
||||||
|
|
||||||
import type { Scene } from "./Scene";
|
import type { Scene } from "./Scene";
|
||||||
import type {
|
import type {
|
||||||
@@ -156,12 +155,8 @@ export const moveArrowAboveBindable = (
|
|||||||
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
|
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
hit?: NonDeletedExcalidrawElement,
|
hoveredElement: NonDeletedExcalidrawElement,
|
||||||
): readonly OrderedExcalidrawElement[] => {
|
): readonly OrderedExcalidrawElement[] => {
|
||||||
const hoveredElement = hit
|
|
||||||
? hit
|
|
||||||
: getHoveredElementForBinding(arrow, point, elements, elementsMap);
|
|
||||||
|
|
||||||
if (!hoveredElement) {
|
if (!hoveredElement) {
|
||||||
return elements;
|
return elements;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9887,17 +9887,16 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isBindingElement(element)) {
|
if (isBindingElement(element) && getFeatureFlag("COMPLEX_BINDINGS")) {
|
||||||
const hoveredElement = getHoveredElementForBinding(
|
const hoveredElement = getHoveredElementForBinding(
|
||||||
element,
|
element,
|
||||||
pointFrom<GlobalPoint>(pointerCoords.x, pointerCoords.y),
|
pointFrom<GlobalPoint>(pointerCoords.x, pointerCoords.y),
|
||||||
this.scene.getNonDeletedElements(),
|
this.scene.getNonDeletedElements(),
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
maxBindingDistance_simple(this.state.zoom),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (getFeatureFlag("COMPLEX_BINDINGS")) {
|
this.handleDelayedBindModeChange(element, hoveredElement);
|
||||||
this.handleDelayedBindModeChange(element, hoveredElement);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
Reference in New Issue
Block a user