mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
[skip ci] First iteration of bringing over previous changes
This commit is contained in:
parent
4ee99de2fb
commit
fbde68c849
19 changed files with 599 additions and 596 deletions
|
@ -81,11 +81,10 @@ import type {
|
|||
NonDeletedSceneElementsMap,
|
||||
ExcalidrawTextElement,
|
||||
ExcalidrawArrowElement,
|
||||
OrderedExcalidrawElement,
|
||||
ExcalidrawElbowArrowElement,
|
||||
FixedPoint,
|
||||
SceneElementsMap,
|
||||
FixedPointBinding,
|
||||
ExcalidrawElbowArrowElement,
|
||||
} from "./types";
|
||||
|
||||
export type SuggestedBinding =
|
||||
|
@ -108,6 +107,7 @@ export const isBindingEnabled = (appState: AppState): boolean => {
|
|||
return appState.isBindingEnabled;
|
||||
};
|
||||
|
||||
export const INSIDE_BINDING_BAND_PERCENT = 0.1;
|
||||
export const FIXED_BINDING_DISTANCE = 5;
|
||||
export const BINDING_HIGHLIGHT_THICKNESS = 10;
|
||||
export const BINDING_HIGHLIGHT_OFFSET = 4;
|
||||
|
@ -463,26 +463,6 @@ export const maybeBindLinearElement = (
|
|||
}
|
||||
};
|
||||
|
||||
const normalizePointBinding = (
|
||||
binding: { focus: number; gap: number },
|
||||
hoveredElement: ExcalidrawBindableElement,
|
||||
) => {
|
||||
let gap = binding.gap;
|
||||
const maxGap = maxBindingGap(
|
||||
hoveredElement,
|
||||
hoveredElement.width,
|
||||
hoveredElement.height,
|
||||
);
|
||||
|
||||
if (gap > maxGap) {
|
||||
gap = BINDING_HIGHLIGHT_THICKNESS + BINDING_HIGHLIGHT_OFFSET;
|
||||
}
|
||||
return {
|
||||
...binding,
|
||||
gap,
|
||||
};
|
||||
};
|
||||
|
||||
export const bindLinearElement = (
|
||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||
hoveredElement: ExcalidrawBindableElement,
|
||||
|
@ -493,17 +473,25 @@ export const bindLinearElement = (
|
|||
return;
|
||||
}
|
||||
|
||||
const direction = startOrEnd === "start" ? -1 : 1;
|
||||
const edgePointIndex = direction === -1 ? 0 : linearElement.points.length - 1;
|
||||
const adjacentPointIndex = edgePointIndex - direction;
|
||||
|
||||
const edgePoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||
linearElement,
|
||||
edgePointIndex,
|
||||
elementsMap,
|
||||
);
|
||||
const adjacentPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||
linearElement,
|
||||
adjacentPointIndex,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
let binding: PointBinding | FixedPointBinding = {
|
||||
elementId: hoveredElement.id,
|
||||
...normalizePointBinding(
|
||||
calculateFocusAndGap(
|
||||
linearElement,
|
||||
hoveredElement,
|
||||
startOrEnd,
|
||||
elementsMap,
|
||||
),
|
||||
hoveredElement,
|
||||
),
|
||||
focus: determineFocusDistance(hoveredElement, adjacentPoint, edgePoint),
|
||||
gap: FIXED_BINDING_DISTANCE,
|
||||
};
|
||||
|
||||
if (isElbowArrow(linearElement)) {
|
||||
|
@ -706,33 +694,6 @@ const getAllElementsAtPositionForBinding = (
|
|||
return elementsAtPosition;
|
||||
};
|
||||
|
||||
const calculateFocusAndGap = (
|
||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||
hoveredElement: ExcalidrawBindableElement,
|
||||
startOrEnd: "start" | "end",
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
): { focus: number; gap: number } => {
|
||||
const direction = startOrEnd === "start" ? -1 : 1;
|
||||
const edgePointIndex = direction === -1 ? 0 : linearElement.points.length - 1;
|
||||
const adjacentPointIndex = edgePointIndex - direction;
|
||||
|
||||
const edgePoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||
linearElement,
|
||||
edgePointIndex,
|
||||
elementsMap,
|
||||
);
|
||||
const adjacentPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||
linearElement,
|
||||
adjacentPointIndex,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
return {
|
||||
focus: determineFocusDistance(hoveredElement, adjacentPoint, edgePoint),
|
||||
gap: Math.max(1, distanceToBindableElement(hoveredElement, edgePoint)),
|
||||
};
|
||||
};
|
||||
|
||||
// Supports translating, rotating and scaling `changedElement` with bound
|
||||
// linear elements.
|
||||
// Because scaling involves moving the focus points as well, it is
|
||||
|
@ -743,11 +704,9 @@ export const updateBoundElements = (
|
|||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||
options?: {
|
||||
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
||||
newSize?: { width: number; height: number };
|
||||
changedElements?: Map<string, OrderedExcalidrawElement>;
|
||||
},
|
||||
) => {
|
||||
const { newSize, simultaneouslyUpdated } = options ?? {};
|
||||
const { simultaneouslyUpdated } = options ?? {};
|
||||
const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds(
|
||||
simultaneouslyUpdated,
|
||||
);
|
||||
|
@ -781,22 +740,13 @@ export const updateBoundElements = (
|
|||
endBounds = getElementBounds(endBindingElement, elementsMap);
|
||||
}
|
||||
|
||||
const bindings = {
|
||||
startBinding: maybeCalculateNewGapWhenScaling(
|
||||
changedElement,
|
||||
element.startBinding,
|
||||
newSize,
|
||||
),
|
||||
endBinding: maybeCalculateNewGapWhenScaling(
|
||||
changedElement,
|
||||
element.endBinding,
|
||||
newSize,
|
||||
),
|
||||
};
|
||||
|
||||
// `linearElement` is being moved/scaled already, just update the binding
|
||||
if (simultaneouslyUpdatedElementIds.has(element.id)) {
|
||||
mutateElement(element, bindings, true);
|
||||
mutateElement(
|
||||
element,
|
||||
{ startBinding: element.startBinding, endBinding: element.endBinding },
|
||||
true,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -818,7 +768,9 @@ export const updateBoundElements = (
|
|||
const point = updateBoundPoint(
|
||||
element,
|
||||
bindingProp,
|
||||
bindings[bindingProp],
|
||||
bindingProp === "startBinding"
|
||||
? element.startBinding
|
||||
: element.endBinding,
|
||||
bindableElement,
|
||||
elementsMap,
|
||||
);
|
||||
|
@ -848,10 +800,10 @@ export const updateBoundElements = (
|
|||
updates,
|
||||
{
|
||||
...(changedElement.id === element.startBinding?.elementId
|
||||
? { startBinding: bindings.startBinding }
|
||||
? { startBinding: element.startBinding }
|
||||
: {}),
|
||||
...(changedElement.id === element.endBinding?.elementId
|
||||
? { endBinding: bindings.endBinding }
|
||||
? { endBinding: element.endBinding }
|
||||
: {}),
|
||||
},
|
||||
elementsMap as NonDeletedSceneElementsMap,
|
||||
|
@ -885,7 +837,6 @@ export const getHeadingForElbowArrowSnap = (
|
|||
otherPoint: Readonly<GlobalPoint>,
|
||||
bindableElement: ExcalidrawBindableElement | undefined | null,
|
||||
aabb: Bounds | undefined | null,
|
||||
elementsMap: ElementsMap,
|
||||
origPoint: GlobalPoint,
|
||||
zoom?: AppState["zoom"],
|
||||
): Heading => {
|
||||
|
@ -895,12 +846,7 @@ export const getHeadingForElbowArrowSnap = (
|
|||
return otherPointHeading;
|
||||
}
|
||||
|
||||
const distance = getDistanceForBinding(
|
||||
origPoint,
|
||||
bindableElement,
|
||||
elementsMap,
|
||||
zoom,
|
||||
);
|
||||
const distance = getDistanceForBinding(origPoint, bindableElement, zoom);
|
||||
|
||||
if (!distance) {
|
||||
return vectorToHeading(
|
||||
|
@ -920,7 +866,6 @@ export const getHeadingForElbowArrowSnap = (
|
|||
const getDistanceForBinding = (
|
||||
point: Readonly<GlobalPoint>,
|
||||
bindableElement: ExcalidrawBindableElement,
|
||||
elementsMap: ElementsMap,
|
||||
zoom?: AppState["zoom"],
|
||||
) => {
|
||||
const distance = distanceToBindableElement(bindableElement, point);
|
||||
|
@ -935,40 +880,47 @@ const getDistanceForBinding = (
|
|||
};
|
||||
|
||||
export const bindPointToSnapToElementOutline = (
|
||||
arrow: ExcalidrawElbowArrowElement,
|
||||
linearElement: ExcalidrawLinearElement,
|
||||
bindableElement: ExcalidrawBindableElement,
|
||||
startOrEnd: "start" | "end",
|
||||
elementsMap: ElementsMap,
|
||||
): GlobalPoint => {
|
||||
if (isDevEnv() || isTestEnv()) {
|
||||
invariant(arrow.points.length > 1, "Arrow should have at least 2 points");
|
||||
invariant(
|
||||
linearElement.points.length > 0,
|
||||
"Arrow should have at least 1 point",
|
||||
);
|
||||
}
|
||||
|
||||
const elbowed = isElbowArrow(linearElement);
|
||||
const aabb = aabbForElement(bindableElement);
|
||||
const localP =
|
||||
arrow.points[startOrEnd === "start" ? 0 : arrow.points.length - 1];
|
||||
const globalP = pointFrom<GlobalPoint>(
|
||||
arrow.x + localP[0],
|
||||
arrow.y + localP[1],
|
||||
);
|
||||
const edgePoint = isRectanguloidElement(bindableElement)
|
||||
? avoidRectangularCorner(bindableElement, globalP)
|
||||
: globalP;
|
||||
const elbowed = isElbowArrow(arrow);
|
||||
const center = getCenterForBounds(aabb);
|
||||
const adjacentPointIdx = startOrEnd === "start" ? 1 : arrow.points.length - 2;
|
||||
const adjacentPoint = pointRotateRads(
|
||||
pointFrom<GlobalPoint>(
|
||||
arrow.x + arrow.points[adjacentPointIdx][0],
|
||||
arrow.y + arrow.points[adjacentPointIdx][1],
|
||||
),
|
||||
center,
|
||||
arrow.angle ?? 0,
|
||||
|
||||
const pointIdx = startOrEnd === "start" ? 0 : linearElement.points.length - 1;
|
||||
const p = pointFrom<GlobalPoint>(
|
||||
linearElement.x + linearElement.points[pointIdx][0],
|
||||
linearElement.y + linearElement.points[pointIdx][1],
|
||||
);
|
||||
const edgePoint = avoidRectangularCorner(bindableElement, p);
|
||||
|
||||
const adjacentPointIdx =
|
||||
startOrEnd === "start" ? 1 : linearElement.points.length - 2;
|
||||
const adjacentPoint =
|
||||
linearElement.points.length === 1
|
||||
? center
|
||||
: pointRotateRads(
|
||||
pointFrom<GlobalPoint>(
|
||||
linearElement.x + linearElement.points[adjacentPointIdx][0],
|
||||
linearElement.y + linearElement.points[adjacentPointIdx][1],
|
||||
),
|
||||
center,
|
||||
linearElement.angle ?? 0,
|
||||
);
|
||||
|
||||
let intersection: GlobalPoint | null = null;
|
||||
if (elbowed) {
|
||||
const isHorizontal = headingIsHorizontal(
|
||||
headingForPointFromElement(bindableElement, aabb, globalP),
|
||||
headingForPointFromElement(bindableElement, aabb, p),
|
||||
);
|
||||
const otherPoint = pointFrom<GlobalPoint>(
|
||||
isHorizontal ? center[0] : edgePoint[0],
|
||||
|
@ -1033,6 +985,28 @@ export const bindPointToSnapToElementOutline = (
|
|||
);
|
||||
}
|
||||
|
||||
const isInside = isPointInShape(
|
||||
edgePoint,
|
||||
getElementShape(
|
||||
{
|
||||
...bindableElement,
|
||||
x:
|
||||
bindableElement.x +
|
||||
bindableElement.width * INSIDE_BINDING_BAND_PERCENT,
|
||||
y:
|
||||
bindableElement.y +
|
||||
bindableElement.height * INSIDE_BINDING_BAND_PERCENT,
|
||||
width: bindableElement.width * (1 - INSIDE_BINDING_BAND_PERCENT * 2),
|
||||
height: bindableElement.height * (1 - INSIDE_BINDING_BAND_PERCENT * 2),
|
||||
} as ExcalidrawBindableElement,
|
||||
elementsMap,
|
||||
),
|
||||
);
|
||||
|
||||
if (!isInside) {
|
||||
return intersection;
|
||||
}
|
||||
|
||||
return edgePoint;
|
||||
};
|
||||
|
||||
|
@ -1040,6 +1014,10 @@ export const avoidRectangularCorner = (
|
|||
element: ExcalidrawBindableElement,
|
||||
p: GlobalPoint,
|
||||
): GlobalPoint => {
|
||||
if (!isRectanguloidElement(element)) {
|
||||
return p;
|
||||
}
|
||||
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
|
@ -1200,6 +1178,45 @@ export const snapToMid = (
|
|||
return p;
|
||||
};
|
||||
|
||||
export const getOutlineAvoidingPoint = (
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
coords: GlobalPoint,
|
||||
pointIndex: number,
|
||||
scene: Scene,
|
||||
zoom: AppState["zoom"],
|
||||
fallback?: GlobalPoint,
|
||||
): GlobalPoint => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const hoveredElement = getHoveredElementForBinding(
|
||||
{ x: coords[0], y: coords[1] },
|
||||
scene.getNonDeletedElements(),
|
||||
elementsMap,
|
||||
zoom,
|
||||
true,
|
||||
isElbowArrow(element),
|
||||
);
|
||||
|
||||
if (hoveredElement) {
|
||||
const newPoints = Array.from(element.points);
|
||||
newPoints[pointIndex] = pointFrom<LocalPoint>(
|
||||
coords[0] - element.x,
|
||||
coords[1] - element.y,
|
||||
);
|
||||
|
||||
return bindPointToSnapToElementOutline(
|
||||
{
|
||||
...element,
|
||||
points: newPoints,
|
||||
},
|
||||
hoveredElement,
|
||||
pointIndex === 0 ? "start" : "end",
|
||||
elementsMap,
|
||||
);
|
||||
}
|
||||
|
||||
return fallback ?? coords;
|
||||
};
|
||||
|
||||
const updateBoundPoint = (
|
||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||
startOrEnd: "startBinding" | "endBinding",
|
||||
|
@ -1263,66 +1280,65 @@ const updateBoundPoint = (
|
|||
|
||||
let newEdgePoint: GlobalPoint;
|
||||
|
||||
// The linear element was not originally pointing inside the bound shape,
|
||||
// we can point directly at the focus point
|
||||
if (binding.gap === 0) {
|
||||
// // The linear element was not originally pointing inside the bound shape,
|
||||
// // we can point directly at the focus point
|
||||
// if (binding.gap === 0) {
|
||||
// newEdgePoint = focusPointAbsolute;
|
||||
// } else {
|
||||
// ...
|
||||
// }
|
||||
const edgePointAbsolute =
|
||||
LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||
linearElement,
|
||||
edgePointIndex,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
bindableElement.x + bindableElement.width / 2,
|
||||
bindableElement.y + bindableElement.height / 2,
|
||||
);
|
||||
const interceptorLength =
|
||||
pointDistance(adjacentPoint, edgePointAbsolute) +
|
||||
pointDistance(adjacentPoint, center) +
|
||||
Math.max(bindableElement.width, bindableElement.height) * 2;
|
||||
const intersections = [
|
||||
...intersectElementWithLineSegment(
|
||||
bindableElement,
|
||||
lineSegment<GlobalPoint>(
|
||||
adjacentPoint,
|
||||
pointFromVector(
|
||||
vectorScale(
|
||||
vectorNormalize(vectorFromPoint(focusPointAbsolute, adjacentPoint)),
|
||||
interceptorLength,
|
||||
),
|
||||
adjacentPoint,
|
||||
),
|
||||
),
|
||||
FIXED_BINDING_DISTANCE,
|
||||
).sort(
|
||||
(g, h) =>
|
||||
pointDistanceSq(g, adjacentPoint) - pointDistanceSq(h, adjacentPoint),
|
||||
),
|
||||
// Fallback when arrow doesn't point to the shape
|
||||
pointFromVector(
|
||||
vectorScale(
|
||||
vectorNormalize(vectorFromPoint(focusPointAbsolute, adjacentPoint)),
|
||||
pointDistance(adjacentPoint, edgePointAbsolute),
|
||||
),
|
||||
adjacentPoint,
|
||||
),
|
||||
];
|
||||
|
||||
if (intersections.length > 1) {
|
||||
// The adjacent point is outside the shape (+ gap)
|
||||
newEdgePoint = intersections[0];
|
||||
} else if (intersections.length === 1) {
|
||||
// The adjacent point is inside the shape (+ gap)
|
||||
newEdgePoint = focusPointAbsolute;
|
||||
} else {
|
||||
const edgePointAbsolute =
|
||||
LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||
linearElement,
|
||||
edgePointIndex,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
bindableElement.x + bindableElement.width / 2,
|
||||
bindableElement.y + bindableElement.height / 2,
|
||||
);
|
||||
const interceptorLength =
|
||||
pointDistance(adjacentPoint, edgePointAbsolute) +
|
||||
pointDistance(adjacentPoint, center) +
|
||||
Math.max(bindableElement.width, bindableElement.height) * 2;
|
||||
const intersections = [
|
||||
...intersectElementWithLineSegment(
|
||||
bindableElement,
|
||||
lineSegment<GlobalPoint>(
|
||||
adjacentPoint,
|
||||
pointFromVector(
|
||||
vectorScale(
|
||||
vectorNormalize(
|
||||
vectorFromPoint(focusPointAbsolute, adjacentPoint),
|
||||
),
|
||||
interceptorLength,
|
||||
),
|
||||
adjacentPoint,
|
||||
),
|
||||
),
|
||||
binding.gap,
|
||||
).sort(
|
||||
(g, h) =>
|
||||
pointDistanceSq(g, adjacentPoint) - pointDistanceSq(h, adjacentPoint),
|
||||
),
|
||||
// Fallback when arrow doesn't point to the shape
|
||||
pointFromVector(
|
||||
vectorScale(
|
||||
vectorNormalize(vectorFromPoint(focusPointAbsolute, adjacentPoint)),
|
||||
pointDistance(adjacentPoint, edgePointAbsolute),
|
||||
),
|
||||
adjacentPoint,
|
||||
),
|
||||
];
|
||||
|
||||
if (intersections.length > 1) {
|
||||
// The adjacent point is outside the shape (+ gap)
|
||||
newEdgePoint = intersections[0];
|
||||
} else if (intersections.length === 1) {
|
||||
// The adjacent point is inside the shape (+ gap)
|
||||
newEdgePoint = focusPointAbsolute;
|
||||
} else {
|
||||
// Shouldn't happend, but just in case
|
||||
newEdgePoint = edgePointAbsolute;
|
||||
}
|
||||
// Shouldn't happend, but just in case
|
||||
newEdgePoint = edgePointAbsolute;
|
||||
}
|
||||
|
||||
return LinearElementEditor.pointFromAbsoluteCoords(
|
||||
|
@ -1333,7 +1349,7 @@ const updateBoundPoint = (
|
|||
};
|
||||
|
||||
export const calculateFixedPointForElbowArrowBinding = (
|
||||
linearElement: NonDeleted<ExcalidrawElbowArrowElement>,
|
||||
linearElement: NonDeleted<ExcalidrawArrowElement>,
|
||||
hoveredElement: ExcalidrawBindableElement,
|
||||
startOrEnd: "start" | "end",
|
||||
elementsMap: ElementsMap,
|
||||
|
@ -1348,6 +1364,7 @@ export const calculateFixedPointForElbowArrowBinding = (
|
|||
linearElement,
|
||||
hoveredElement,
|
||||
startOrEnd,
|
||||
elementsMap,
|
||||
);
|
||||
const globalMidPoint = pointFrom(
|
||||
bounds[0] + (bounds[2] - bounds[0]) / 2,
|
||||
|
@ -1369,28 +1386,6 @@ export const calculateFixedPointForElbowArrowBinding = (
|
|||
};
|
||||
};
|
||||
|
||||
const maybeCalculateNewGapWhenScaling = (
|
||||
changedElement: ExcalidrawBindableElement,
|
||||
currentBinding: PointBinding | null | undefined,
|
||||
newSize: { width: number; height: number } | undefined,
|
||||
): PointBinding | null | undefined => {
|
||||
if (currentBinding == null || newSize == null) {
|
||||
return currentBinding;
|
||||
}
|
||||
const { width: newWidth, height: newHeight } = newSize;
|
||||
const { width, height } = changedElement;
|
||||
const newGap = Math.max(
|
||||
1,
|
||||
Math.min(
|
||||
maxBindingGap(changedElement, newWidth, newHeight),
|
||||
currentBinding.gap *
|
||||
(newWidth < newHeight ? newWidth / width : newHeight / height),
|
||||
),
|
||||
);
|
||||
|
||||
return { ...currentBinding, gap: newGap };
|
||||
};
|
||||
|
||||
const getElligibleElementForBindingElement = (
|
||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||
startOrEnd: "start" | "end",
|
||||
|
|
|
@ -1254,6 +1254,7 @@ const getElbowArrowData = (
|
|||
"start",
|
||||
arrow.startBinding?.fixedPoint,
|
||||
origStartGlobalPoint,
|
||||
elementsMap,
|
||||
hoveredStartElement,
|
||||
options?.isDragging,
|
||||
);
|
||||
|
@ -1267,6 +1268,7 @@ const getElbowArrowData = (
|
|||
"end",
|
||||
arrow.endBinding?.fixedPoint,
|
||||
origEndGlobalPoint,
|
||||
elementsMap,
|
||||
hoveredEndElement,
|
||||
options?.isDragging,
|
||||
);
|
||||
|
@ -2212,6 +2214,7 @@ const getGlobalPoint = (
|
|||
startOrEnd: "start" | "end",
|
||||
fixedPointRatio: [number, number] | undefined | null,
|
||||
initialPoint: GlobalPoint,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
element?: ExcalidrawBindableElement | null,
|
||||
isDragging?: boolean,
|
||||
): GlobalPoint => {
|
||||
|
@ -2221,6 +2224,7 @@ const getGlobalPoint = (
|
|||
arrow,
|
||||
element,
|
||||
startOrEnd,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
return snapToMid(element, snapPoint);
|
||||
|
@ -2240,7 +2244,7 @@ const getGlobalPoint = (
|
|||
distanceToBindableElement(element, fixedGlobalPoint) -
|
||||
FIXED_BINDING_DISTANCE,
|
||||
) > 0.01
|
||||
? bindPointToSnapToElementOutline(arrow, element, startOrEnd)
|
||||
? bindPointToSnapToElementOutline(arrow, element, startOrEnd, elementsMap)
|
||||
: fixedGlobalPoint;
|
||||
}
|
||||
|
||||
|
@ -2268,7 +2272,6 @@ const getBindPointHeading = (
|
|||
number,
|
||||
],
|
||||
),
|
||||
elementsMap,
|
||||
origPoint,
|
||||
);
|
||||
|
||||
|
|
|
@ -42,6 +42,7 @@ import type { Mutable } from "@excalidraw/common/utility-types";
|
|||
import {
|
||||
bindOrUnbindLinearElement,
|
||||
getHoveredElementForBinding,
|
||||
getOutlineAvoidingPoint,
|
||||
isBindingEnabled,
|
||||
} from "./binding";
|
||||
import {
|
||||
|
@ -252,27 +253,28 @@ export class LinearElementEditor {
|
|||
pointSceneCoords: { x: number; y: number }[],
|
||||
) => void,
|
||||
linearElementEditor: LinearElementEditor,
|
||||
scene: Scene,
|
||||
): LinearElementEditor | null {
|
||||
if (!linearElementEditor) {
|
||||
return null;
|
||||
}
|
||||
const { elementId } = linearElementEditor;
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const elementsMap = app.scene.getNonDeletedElementsMap();
|
||||
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
||||
if (!element) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const elbowed = isElbowArrow(element);
|
||||
|
||||
if (
|
||||
isElbowArrow(element) &&
|
||||
elbowed &&
|
||||
!linearElementEditor.pointerDownState.lastClickedIsEndPoint &&
|
||||
linearElementEditor.pointerDownState.lastClickedPoint !== 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectedPointsIndices = isElbowArrow(element)
|
||||
const selectedPointsIndices = elbowed
|
||||
? [
|
||||
!!linearElementEditor.selectedPointsIndices?.includes(0)
|
||||
? 0
|
||||
|
@ -282,7 +284,7 @@ export class LinearElementEditor {
|
|||
: undefined,
|
||||
].filter((idx): idx is number => idx !== undefined)
|
||||
: linearElementEditor.selectedPointsIndices;
|
||||
const lastClickedPoint = isElbowArrow(element)
|
||||
const lastClickedPoint = elbowed
|
||||
? linearElementEditor.pointerDownState.lastClickedPoint > 0
|
||||
? element.points.length - 1
|
||||
: 0
|
||||
|
@ -334,19 +336,43 @@ export class LinearElementEditor {
|
|||
LinearElementEditor.movePoints(
|
||||
element,
|
||||
selectedPointsIndices.map((pointIndex) => {
|
||||
const newPointPosition: LocalPoint =
|
||||
pointIndex === lastClickedPoint
|
||||
? LinearElementEditor.createPointAt(
|
||||
element,
|
||||
elementsMap,
|
||||
scenePointerX - linearElementEditor.pointerOffset.x,
|
||||
scenePointerY - linearElementEditor.pointerOffset.y,
|
||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||
)
|
||||
: pointFrom(
|
||||
element.points[pointIndex][0] + deltaX,
|
||||
element.points[pointIndex][1] + deltaY,
|
||||
);
|
||||
let newPointPosition = pointFrom<LocalPoint>(
|
||||
element.points[pointIndex][0] + deltaX,
|
||||
element.points[pointIndex][1] + deltaY,
|
||||
);
|
||||
|
||||
// Check if point dragging is happening
|
||||
if (pointIndex === lastClickedPoint) {
|
||||
let globalNewPointPosition = pointFrom<GlobalPoint>(
|
||||
scenePointerX - linearElementEditor.pointerOffset.x,
|
||||
scenePointerY - linearElementEditor.pointerOffset.y,
|
||||
);
|
||||
|
||||
if (
|
||||
pointIndex === 0 ||
|
||||
pointIndex === element.points.length - 1
|
||||
) {
|
||||
globalNewPointPosition = getOutlineAvoidingPoint(
|
||||
element,
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + element.points[pointIndex][0] + deltaX,
|
||||
element.y + element.points[pointIndex][1] + deltaY,
|
||||
),
|
||||
pointIndex,
|
||||
app.scene,
|
||||
app.state.zoom,
|
||||
);
|
||||
}
|
||||
|
||||
newPointPosition = LinearElementEditor.createPointAt(
|
||||
element,
|
||||
elementsMap,
|
||||
globalNewPointPosition[0],
|
||||
globalNewPointPosition[1],
|
||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
index: pointIndex,
|
||||
point: newPointPosition,
|
||||
|
|
|
@ -969,10 +969,7 @@ export const resizeSingleElement = (
|
|||
|
||||
mutateElement(latestElement, updates, shouldInformMutation);
|
||||
|
||||
updateBoundElements(latestElement, elementsMap as SceneElementsMap, {
|
||||
// TODO: confirm with MARK if this actually makes sense
|
||||
newSize: { width: nextWidth, height: nextHeight },
|
||||
});
|
||||
updateBoundElements(latestElement, elementsMap as SceneElementsMap);
|
||||
|
||||
if (boundTextElement && boundTextFont != null) {
|
||||
mutateElement(boundTextElement, {
|
||||
|
@ -1525,7 +1522,7 @@ export const resizeMultipleElements = (
|
|||
element,
|
||||
update: { boundTextFontSize, ...update },
|
||||
} of elementsAndUpdates) {
|
||||
const { width, height, angle } = update;
|
||||
const { angle } = update;
|
||||
|
||||
mutateElement(element, update, false, {
|
||||
// needed for the fixed binding point udpate to take effect
|
||||
|
@ -1534,7 +1531,6 @@ export const resizeMultipleElements = (
|
|||
|
||||
updateBoundElements(element, elementsMap as SceneElementsMap, {
|
||||
simultaneouslyUpdated: elementsToUpdate,
|
||||
newSize: { width, height },
|
||||
});
|
||||
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
|
|
|
@ -190,7 +190,18 @@ describe("element binding", () => {
|
|||
|
||||
// Sever connection
|
||||
expect(API.getSelectedElement().type).toBe("arrow");
|
||||
Keyboard.keyPress(KEYS.ARROW_LEFT);
|
||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||
// We have to move a significant distance to get out of the binding zone
|
||||
Keyboard.keyPress(KEYS.ARROW_LEFT);
|
||||
Keyboard.keyPress(KEYS.ARROW_LEFT);
|
||||
Keyboard.keyPress(KEYS.ARROW_LEFT);
|
||||
Keyboard.keyPress(KEYS.ARROW_LEFT);
|
||||
Keyboard.keyPress(KEYS.ARROW_LEFT);
|
||||
Keyboard.keyPress(KEYS.ARROW_LEFT);
|
||||
Keyboard.keyPress(KEYS.ARROW_LEFT);
|
||||
Keyboard.keyPress(KEYS.ARROW_LEFT);
|
||||
Keyboard.keyPress(KEYS.ARROW_LEFT);
|
||||
});
|
||||
expect(arrow.endBinding).toBe(null);
|
||||
Keyboard.keyPress(KEYS.ARROW_RIGHT);
|
||||
expect(arrow.endBinding).toBe(null);
|
||||
|
|
|
@ -195,7 +195,7 @@ describe("generic element", () => {
|
|||
UI.resize(rectangle, "w", [50, 0]);
|
||||
|
||||
expect(arrow.endBinding?.elementId).toEqual(rectangle.id);
|
||||
expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(80, 0);
|
||||
expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(81, 0);
|
||||
});
|
||||
|
||||
it("resizes with a label", async () => {
|
||||
|
@ -826,8 +826,9 @@ describe("image element", () => {
|
|||
UI.resize(image, "nw", [50, 20]);
|
||||
|
||||
expect(arrow.endBinding?.elementId).toEqual(image.id);
|
||||
expect(Math.floor(arrow.width + arrow.endBinding!.gap)).toBeCloseTo(
|
||||
30 + imageWidth * scale,
|
||||
|
||||
expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(
|
||||
30 + imageWidth * scale + 1,
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
@ -1033,11 +1034,11 @@ describe("multiple selection", () => {
|
|||
|
||||
expect(leftBoundArrow.x).toBeCloseTo(-110);
|
||||
expect(leftBoundArrow.y).toBeCloseTo(50);
|
||||
expect(leftBoundArrow.width).toBeCloseTo(143, 0);
|
||||
expect(leftBoundArrow.width).toBeCloseTo(146, 0);
|
||||
expect(leftBoundArrow.height).toBeCloseTo(7, 0);
|
||||
expect(leftBoundArrow.angle).toEqual(0);
|
||||
expect(leftBoundArrow.startBinding).toBeNull();
|
||||
expect(leftBoundArrow.endBinding?.gap).toBeCloseTo(10);
|
||||
expect(leftBoundArrow.endBinding?.gap).toBeCloseTo(5);
|
||||
expect(leftBoundArrow.endBinding?.elementId).toBe(
|
||||
leftArrowBinding.elementId,
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue