Compare commits
26 Commits
master
...
mtolmacs/f
Author | SHA1 | Date | |
---|---|---|---|
|
bca6b3fbe1 | ||
|
3ccb7e3239 | ||
|
fbff9a3463 | ||
|
133cc2b354 | ||
|
5cf79b078e | ||
|
4980631cb1 | ||
|
8c2de8c3c4 | ||
|
06dc04ea5d | ||
|
c4a52a982a | ||
|
d13f6477b9 | ||
|
3ae89aba47 | ||
|
b8fac37115 | ||
|
71cfd1d82d | ||
|
087353e06a | ||
|
fdc6ea6d2e | ||
|
1e99d28a6e | ||
|
b04814cca7 | ||
|
b696b7bf98 | ||
|
331790e9af | ||
|
0dd76db5f0 | ||
|
77d691e397 | ||
|
6efe4c6f38 | ||
|
898499777f | ||
|
5d16a81327 | ||
|
c3d40c3781 | ||
|
bc70f06edd |
@ -514,3 +514,5 @@ export enum UserIdleState {
|
|||||||
* the start and end points)
|
* the start and end points)
|
||||||
*/
|
*/
|
||||||
export const LINE_POLYGON_POINT_MERGE_DISTANCE = 20;
|
export const LINE_POLYGON_POINT_MERGE_DISTANCE = 20;
|
||||||
|
|
||||||
|
export const BIND_MODE_TIMEOUT = 1000; // ms
|
||||||
|
@ -3,9 +3,6 @@ import {
|
|||||||
arrayToMap,
|
arrayToMap,
|
||||||
isBindingFallthroughEnabled,
|
isBindingFallthroughEnabled,
|
||||||
tupleToCoors,
|
tupleToCoors,
|
||||||
invariant,
|
|
||||||
isDevEnv,
|
|
||||||
isTestEnv,
|
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -37,7 +34,7 @@ import {
|
|||||||
getCenterForBounds,
|
getCenterForBounds,
|
||||||
getElementBounds,
|
getElementBounds,
|
||||||
} from "./bounds";
|
} from "./bounds";
|
||||||
import { intersectElementWithLineSegment } from "./collision";
|
import { hitElementItself, intersectElementWithLineSegment } from "./collision";
|
||||||
import { distanceToElement } from "./distance";
|
import { distanceToElement } from "./distance";
|
||||||
import {
|
import {
|
||||||
headingForPointFromElement,
|
headingForPointFromElement,
|
||||||
@ -127,6 +124,9 @@ export const bindOrUnbindLinearElement = (
|
|||||||
endBindingElement: ExcalidrawBindableElement | null | "keep",
|
endBindingElement: ExcalidrawBindableElement | null | "keep",
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
): void => {
|
): void => {
|
||||||
|
const bothEndBoundToTheSameElement =
|
||||||
|
linearElement.startBinding?.elementId ===
|
||||||
|
linearElement.endBinding?.elementId && !!linearElement.startBinding;
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const boundToElementIds: Set<ExcalidrawBindableElement["id"]> = new Set();
|
const boundToElementIds: Set<ExcalidrawBindableElement["id"]> = new Set();
|
||||||
const unboundFromElementIds: Set<ExcalidrawBindableElement["id"]> = new Set();
|
const unboundFromElementIds: Set<ExcalidrawBindableElement["id"]> = new Set();
|
||||||
@ -151,6 +151,7 @@ export const bindOrUnbindLinearElement = (
|
|||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!bothEndBoundToTheSameElement) {
|
||||||
const onlyUnbound = Array.from(unboundFromElementIds).filter(
|
const onlyUnbound = Array.from(unboundFromElementIds).filter(
|
||||||
(id) => !boundToElementIds.has(id),
|
(id) => !boundToElementIds.has(id),
|
||||||
);
|
);
|
||||||
@ -163,6 +164,7 @@ export const bindOrUnbindLinearElement = (
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const bindOrUnbindLinearElementEdge = (
|
const bindOrUnbindLinearElementEdge = (
|
||||||
@ -203,6 +205,7 @@ const bindOrUnbindLinearElementEdge = (
|
|||||||
linearElement,
|
linearElement,
|
||||||
bindableElement,
|
bindableElement,
|
||||||
startOrEnd,
|
startOrEnd,
|
||||||
|
elementsMap,
|
||||||
)
|
)
|
||||||
: startOrEnd === "start" ||
|
: startOrEnd === "start" ||
|
||||||
otherEdgeBindableElement.id !== bindableElement.id)
|
otherEdgeBindableElement.id !== bindableElement.id)
|
||||||
@ -393,6 +396,7 @@ export const maybeSuggestBindingsForLinearElementAtCoords = (
|
|||||||
}[],
|
}[],
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
zoom: AppState["zoom"],
|
zoom: AppState["zoom"],
|
||||||
|
elementsMap: ElementsMap,
|
||||||
// During line creation the start binding hasn't been written yet
|
// During line creation the start binding hasn't been written yet
|
||||||
// into `linearElement`
|
// into `linearElement`
|
||||||
oppositeBindingBoundElement?: ExcalidrawBindableElement | null,
|
oppositeBindingBoundElement?: ExcalidrawBindableElement | null,
|
||||||
@ -411,11 +415,12 @@ export const maybeSuggestBindingsForLinearElementAtCoords = (
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
hoveredBindableElement != null &&
|
hoveredBindableElement != null &&
|
||||||
|
(oppositeBindingBoundElement?.id === hoveredBindableElement.id ||
|
||||||
!isLinearElementSimpleAndAlreadyBound(
|
!isLinearElementSimpleAndAlreadyBound(
|
||||||
linearElement,
|
linearElement,
|
||||||
oppositeBindingBoundElement?.id,
|
oppositeBindingBoundElement?.id,
|
||||||
hoveredBindableElement,
|
hoveredBindableElement,
|
||||||
)
|
))
|
||||||
) {
|
) {
|
||||||
acc.add(hoveredBindableElement);
|
acc.add(hoveredBindableElement);
|
||||||
}
|
}
|
||||||
@ -459,6 +464,7 @@ export const maybeBindLinearElement = (
|
|||||||
linearElement,
|
linearElement,
|
||||||
hoveredElement,
|
hoveredElement,
|
||||||
"end",
|
"end",
|
||||||
|
elementsMap,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
bindLinearElement(linearElement, hoveredElement, "end", scene);
|
bindLinearElement(linearElement, hoveredElement, "end", scene);
|
||||||
@ -487,29 +493,120 @@ export const bindLinearElement = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let binding: PointBinding | FixedPointBinding = {
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
|
const edgePoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||||
|
linearElement,
|
||||||
|
startOrEnd === "start" ? 0 : -1,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
let binding: PointBinding | FixedPointBinding;
|
||||||
|
|
||||||
|
if (isElbowArrow(linearElement)) {
|
||||||
|
binding = {
|
||||||
elementId: hoveredElement.id,
|
elementId: hoveredElement.id,
|
||||||
...normalizePointBinding(
|
...normalizePointBinding(
|
||||||
calculateFocusAndGap(
|
calculateFocusAndGap(
|
||||||
linearElement,
|
linearElement,
|
||||||
hoveredElement,
|
hoveredElement,
|
||||||
startOrEnd,
|
startOrEnd,
|
||||||
scene.getNonDeletedElementsMap(),
|
elementsMap,
|
||||||
),
|
),
|
||||||
hoveredElement,
|
hoveredElement,
|
||||||
),
|
),
|
||||||
};
|
|
||||||
|
|
||||||
if (isElbowArrow(linearElement)) {
|
|
||||||
binding = {
|
|
||||||
...binding,
|
|
||||||
...calculateFixedPointForElbowArrowBinding(
|
...calculateFixedPointForElbowArrowBinding(
|
||||||
linearElement,
|
linearElement,
|
||||||
hoveredElement,
|
hoveredElement,
|
||||||
startOrEnd,
|
startOrEnd,
|
||||||
scene.getNonDeletedElementsMap(),
|
elementsMap,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
} else if (
|
||||||
|
hitElementItself({
|
||||||
|
point: edgePoint,
|
||||||
|
element: hoveredElement,
|
||||||
|
elementsMap,
|
||||||
|
threshold: 0, // TODO: Not ideal, should be calculated from the same source
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
// Use FixedPoint binding when the arrow endpoint is inside the shape
|
||||||
|
binding = {
|
||||||
|
elementId: hoveredElement.id,
|
||||||
|
focus: 0,
|
||||||
|
gap: 0,
|
||||||
|
...calculateFixedPointForNonElbowArrowBinding(
|
||||||
|
linearElement,
|
||||||
|
hoveredElement,
|
||||||
|
startOrEnd,
|
||||||
|
elementsMap,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// For non-elbow arrows, extend the last segment and check intersection
|
||||||
|
const adjacentPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||||
|
linearElement,
|
||||||
|
startOrEnd === "start" ? 1 : -2,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
const extendedDirection = vectorScale(
|
||||||
|
vectorNormalize(
|
||||||
|
vectorFromPoint(
|
||||||
|
pointFrom(
|
||||||
|
edgePoint[0] - adjacentPoint[0],
|
||||||
|
edgePoint[1] - adjacentPoint[1],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Math.max(hoveredElement.width, hoveredElement.height) * 2,
|
||||||
|
);
|
||||||
|
const intersector = lineSegment(
|
||||||
|
edgePoint,
|
||||||
|
pointFromVector(
|
||||||
|
vectorFromPoint(
|
||||||
|
pointFrom(
|
||||||
|
edgePoint[0] + extendedDirection[0],
|
||||||
|
edgePoint[1] + extendedDirection[1],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if this extended segment intersects the bindable element
|
||||||
|
const intersections = intersectElementWithLineSegment(
|
||||||
|
hoveredElement,
|
||||||
|
elementsMap,
|
||||||
|
intersector,
|
||||||
|
);
|
||||||
|
|
||||||
|
const intersectsElement = intersections.length > 0;
|
||||||
|
|
||||||
|
if (intersectsElement) {
|
||||||
|
// Use traditional focus/gap binding when the extended segment intersects
|
||||||
|
binding = {
|
||||||
|
elementId: hoveredElement.id,
|
||||||
|
...normalizePointBinding(
|
||||||
|
calculateFocusAndGap(
|
||||||
|
linearElement,
|
||||||
|
hoveredElement,
|
||||||
|
startOrEnd,
|
||||||
|
elementsMap,
|
||||||
|
),
|
||||||
|
hoveredElement,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Use FixedPoint binding when the extended segment doesn't intersect
|
||||||
|
binding = {
|
||||||
|
elementId: hoveredElement.id,
|
||||||
|
focus: 0,
|
||||||
|
gap: 0,
|
||||||
|
...calculateFixedPointForNonElbowArrowBinding(
|
||||||
|
linearElement,
|
||||||
|
hoveredElement,
|
||||||
|
startOrEnd,
|
||||||
|
elementsMap,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scene.mutateElement(linearElement, {
|
scene.mutateElement(linearElement, {
|
||||||
@ -532,14 +629,43 @@ const isLinearElementSimpleAndAlreadyBoundOnOppositeEdge = (
|
|||||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
bindableElement: ExcalidrawBindableElement,
|
bindableElement: ExcalidrawBindableElement,
|
||||||
startOrEnd: "start" | "end",
|
startOrEnd: "start" | "end",
|
||||||
|
elementsMap: ElementsMap,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
const otherBinding =
|
const otherBinding =
|
||||||
linearElement[startOrEnd === "start" ? "endBinding" : "startBinding"];
|
linearElement[startOrEnd === "start" ? "endBinding" : "startBinding"];
|
||||||
return isLinearElementSimpleAndAlreadyBound(
|
|
||||||
|
// Only prevent binding if opposite end is bound to the same element
|
||||||
|
if (
|
||||||
|
otherBinding?.elementId !== bindableElement.id ||
|
||||||
|
!isLinearElementSimple(linearElement)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For non-elbow arrows, allow FixedPoint binding even when both ends bind to the same element
|
||||||
|
if (!isElbowArrow(linearElement)) {
|
||||||
|
const currentEndPoint =
|
||||||
|
LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||||
linearElement,
|
linearElement,
|
||||||
otherBinding?.elementId,
|
startOrEnd === "start" ? 0 : -1,
|
||||||
bindableElement,
|
elementsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// If current end would use FixedPoint binding, allow it
|
||||||
|
if (
|
||||||
|
hitElementItself({
|
||||||
|
point: currentEndPoint,
|
||||||
|
element: bindableElement,
|
||||||
|
elementsMap,
|
||||||
|
threshold: 0, // TODO: Not ideal, should be calculated from the same source
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent traditional focus/gap binding when both ends would bind to the same element
|
||||||
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isLinearElementSimpleAndAlreadyBound = (
|
export const isLinearElementSimpleAndAlreadyBound = (
|
||||||
@ -776,7 +902,10 @@ export const updateBoundElements = (
|
|||||||
? elementsMap.get(element.startBinding.elementId)
|
? elementsMap.get(element.startBinding.elementId)
|
||||||
: null;
|
: null;
|
||||||
const endBindingElement = element.endBinding
|
const endBindingElement = element.endBinding
|
||||||
? elementsMap.get(element.endBinding.elementId)
|
? // PERF: If the arrow is bound to the same element on both ends.
|
||||||
|
startBindingElement?.id === element.endBinding.elementId
|
||||||
|
? startBindingElement
|
||||||
|
: elementsMap.get(element.endBinding.elementId)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
let startBounds: Bounds | null = null;
|
let startBounds: Bounds | null = null;
|
||||||
@ -849,6 +978,9 @@ export const updateBoundElements = (
|
|||||||
...(changedElement.id === element.endBinding?.elementId
|
...(changedElement.id === element.endBinding?.elementId
|
||||||
? { endBinding: bindings.endBinding }
|
? { endBinding: bindings.endBinding }
|
||||||
: {}),
|
: {}),
|
||||||
|
moveMidPointsWithElement:
|
||||||
|
!!startBindingElement &&
|
||||||
|
startBindingElement?.id === endBindingElement?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const boundText = getBoundTextElement(element, elementsMap);
|
const boundText = getBoundTextElement(element, elementsMap);
|
||||||
@ -942,35 +1074,40 @@ const getDistanceForBinding = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const bindPointToSnapToElementOutline = (
|
export const bindPointToSnapToElementOutline = (
|
||||||
arrow: ExcalidrawElbowArrowElement,
|
linearElement: ExcalidrawLinearElement,
|
||||||
bindableElement: ExcalidrawBindableElement,
|
bindableElement: ExcalidrawBindableElement,
|
||||||
startOrEnd: "start" | "end",
|
startOrEnd: "start" | "end",
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
): GlobalPoint => {
|
): GlobalPoint => {
|
||||||
if (isDevEnv() || isTestEnv()) {
|
|
||||||
invariant(arrow.points.length > 1, "Arrow should have at least 2 points");
|
|
||||||
}
|
|
||||||
|
|
||||||
const aabb = aabbForElement(bindableElement, elementsMap);
|
const aabb = aabbForElement(bindableElement, elementsMap);
|
||||||
const localP =
|
const localP =
|
||||||
arrow.points[startOrEnd === "start" ? 0 : arrow.points.length - 1];
|
linearElement.points[
|
||||||
|
startOrEnd === "start" ? 0 : linearElement.points.length - 1
|
||||||
|
];
|
||||||
const globalP = pointFrom<GlobalPoint>(
|
const globalP = pointFrom<GlobalPoint>(
|
||||||
arrow.x + localP[0],
|
linearElement.x + localP[0],
|
||||||
arrow.y + localP[1],
|
linearElement.y + localP[1],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (linearElement.points.length < 2) {
|
||||||
|
// New arrow creation, so no snapping
|
||||||
|
return globalP;
|
||||||
|
}
|
||||||
|
|
||||||
const edgePoint = isRectanguloidElement(bindableElement)
|
const edgePoint = isRectanguloidElement(bindableElement)
|
||||||
? avoidRectangularCorner(bindableElement, elementsMap, globalP)
|
? avoidRectangularCorner(bindableElement, elementsMap, globalP)
|
||||||
: globalP;
|
: globalP;
|
||||||
const elbowed = isElbowArrow(arrow);
|
const elbowed = isElbowArrow(linearElement);
|
||||||
const center = getCenterForBounds(aabb);
|
const center = getCenterForBounds(aabb);
|
||||||
const adjacentPointIdx = startOrEnd === "start" ? 1 : arrow.points.length - 2;
|
const adjacentPointIdx =
|
||||||
|
startOrEnd === "start" ? 1 : linearElement.points.length - 2;
|
||||||
const adjacentPoint = pointRotateRads(
|
const adjacentPoint = pointRotateRads(
|
||||||
pointFrom<GlobalPoint>(
|
pointFrom<GlobalPoint>(
|
||||||
arrow.x + arrow.points[adjacentPointIdx][0],
|
linearElement.x + linearElement.points[adjacentPointIdx][0],
|
||||||
arrow.y + arrow.points[adjacentPointIdx][1],
|
linearElement.y + linearElement.points[adjacentPointIdx][1],
|
||||||
),
|
),
|
||||||
center,
|
center,
|
||||||
arrow.angle ?? 0,
|
linearElement.angle ?? 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
let intersection: GlobalPoint | null = null;
|
let intersection: GlobalPoint | null = null;
|
||||||
@ -1029,7 +1166,35 @@ export const bindPointToSnapToElementOutline = (
|
|||||||
return edgePoint;
|
return edgePoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
return elbowed ? intersection : edgePoint;
|
return intersection;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getOutlineAvoidingPoint = (
|
||||||
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
hoveredElement: ExcalidrawBindableElement | null,
|
||||||
|
coords: GlobalPoint,
|
||||||
|
pointIndex: number,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
): GlobalPoint => {
|
||||||
|
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 coords;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const avoidRectangularCorner = (
|
export const avoidRectangularCorner = (
|
||||||
@ -1254,15 +1419,22 @@ const updateBoundPoint = (
|
|||||||
const direction = startOrEnd === "startBinding" ? -1 : 1;
|
const direction = startOrEnd === "startBinding" ? -1 : 1;
|
||||||
const edgePointIndex = direction === -1 ? 0 : linearElement.points.length - 1;
|
const edgePointIndex = direction === -1 ? 0 : linearElement.points.length - 1;
|
||||||
|
|
||||||
if (isElbowArrow(linearElement) && isFixedPointBinding(binding)) {
|
if (isFixedPointBinding(binding)) {
|
||||||
const fixedPoint =
|
const fixedPoint =
|
||||||
normalizeFixedPoint(binding.fixedPoint) ??
|
normalizeFixedPoint(binding.fixedPoint) ??
|
||||||
calculateFixedPointForElbowArrowBinding(
|
(isElbowArrow(linearElement)
|
||||||
|
? calculateFixedPointForElbowArrowBinding(
|
||||||
linearElement,
|
linearElement,
|
||||||
bindableElement,
|
bindableElement,
|
||||||
startOrEnd === "startBinding" ? "start" : "end",
|
startOrEnd === "startBinding" ? "start" : "end",
|
||||||
elementsMap,
|
elementsMap,
|
||||||
).fixedPoint;
|
).fixedPoint
|
||||||
|
: calculateFixedPointForNonElbowArrowBinding(
|
||||||
|
linearElement,
|
||||||
|
bindableElement,
|
||||||
|
startOrEnd === "startBinding" ? "start" : "end",
|
||||||
|
elementsMap,
|
||||||
|
).fixedPoint);
|
||||||
const globalMidPoint = elementCenterPoint(bindableElement, elementsMap);
|
const globalMidPoint = elementCenterPoint(bindableElement, elementsMap);
|
||||||
const global = pointFrom<GlobalPoint>(
|
const global = pointFrom<GlobalPoint>(
|
||||||
bindableElement.x + fixedPoint[0] * bindableElement.width,
|
bindableElement.x + fixedPoint[0] * bindableElement.width,
|
||||||
@ -1401,6 +1573,42 @@ export const calculateFixedPointForElbowArrowBinding = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const calculateFixedPointForNonElbowArrowBinding = (
|
||||||
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
hoveredElement: ExcalidrawBindableElement,
|
||||||
|
startOrEnd: "start" | "end",
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
): { fixedPoint: FixedPoint } => {
|
||||||
|
const edgePoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||||
|
linearElement,
|
||||||
|
startOrEnd === "start" ? 0 : -1,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Convert the global point to element-local coordinates
|
||||||
|
const elementCenter = pointFrom(
|
||||||
|
hoveredElement.x + hoveredElement.width / 2,
|
||||||
|
hoveredElement.y + hoveredElement.height / 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Rotate the point to account for element rotation
|
||||||
|
const nonRotatedPoint = pointRotateRads(
|
||||||
|
edgePoint,
|
||||||
|
elementCenter,
|
||||||
|
-hoveredElement.angle as Radians,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate the ratio relative to the element's bounds
|
||||||
|
const fixedPointX =
|
||||||
|
(nonRotatedPoint[0] - hoveredElement.x) / hoveredElement.width;
|
||||||
|
const fixedPointY =
|
||||||
|
(nonRotatedPoint[1] - hoveredElement.y) / hoveredElement.height;
|
||||||
|
|
||||||
|
return {
|
||||||
|
fixedPoint: normalizeFixedPoint([fixedPointX, fixedPointY]),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const maybeCalculateNewGapWhenScaling = (
|
const maybeCalculateNewGapWhenScaling = (
|
||||||
changedElement: ExcalidrawBindableElement,
|
changedElement: ExcalidrawBindableElement,
|
||||||
currentBinding: PointBinding | null | undefined,
|
currentBinding: PointBinding | null | undefined,
|
||||||
@ -2212,16 +2420,18 @@ export const getGlobalFixedPointForBindableElement = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getGlobalFixedPoints = (
|
export const getGlobalFixedPoints = (
|
||||||
arrow: ExcalidrawElbowArrowElement,
|
arrow: ExcalidrawArrowElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
): [GlobalPoint, GlobalPoint] => {
|
): [GlobalPoint, GlobalPoint] => {
|
||||||
const startElement =
|
const startElement =
|
||||||
arrow.startBinding &&
|
arrow.startBinding &&
|
||||||
|
isFixedPointBinding(arrow.startBinding) &&
|
||||||
(elementsMap.get(arrow.startBinding.elementId) as
|
(elementsMap.get(arrow.startBinding.elementId) as
|
||||||
| ExcalidrawBindableElement
|
| ExcalidrawBindableElement
|
||||||
| undefined);
|
| undefined);
|
||||||
const endElement =
|
const endElement =
|
||||||
arrow.endBinding &&
|
arrow.endBinding &&
|
||||||
|
isFixedPointBinding(arrow.endBinding) &&
|
||||||
(elementsMap.get(arrow.endBinding.elementId) as
|
(elementsMap.get(arrow.endBinding.elementId) as
|
||||||
| ExcalidrawBindableElement
|
| ExcalidrawBindableElement
|
||||||
| undefined);
|
| undefined);
|
||||||
|
@ -13,7 +13,7 @@ import type {
|
|||||||
|
|
||||||
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
|
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { updateBoundElements } from "./binding";
|
import { bindOrUnbindLinearElement, updateBoundElements } from "./binding";
|
||||||
import { getCommonBounds } from "./bounds";
|
import { getCommonBounds } from "./bounds";
|
||||||
import { getPerfectElementSize } from "./sizeHelpers";
|
import { getPerfectElementSize } from "./sizeHelpers";
|
||||||
import { getBoundTextElement } from "./textElement";
|
import { getBoundTextElement } from "./textElement";
|
||||||
@ -102,9 +102,26 @@ export const dragSelectedElements = (
|
|||||||
gridSize,
|
gridSize,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const elementsToUpdateIds = new Set(
|
||||||
|
Array.from(elementsToUpdate, (el) => el.id),
|
||||||
|
);
|
||||||
|
|
||||||
elementsToUpdate.forEach((element) => {
|
elementsToUpdate.forEach((element) => {
|
||||||
updateElementCoords(pointerDownState, element, scene, adjustedOffset);
|
const isArrow = !isArrowElement(element);
|
||||||
|
const isStartBoundElementSelected =
|
||||||
|
isArrow ||
|
||||||
|
(element.startBinding
|
||||||
|
? elementsToUpdateIds.has(element.startBinding.elementId)
|
||||||
|
: false);
|
||||||
|
const isEndBoundElementSelected =
|
||||||
|
isArrow ||
|
||||||
|
(element.endBinding
|
||||||
|
? elementsToUpdateIds.has(element.endBinding.elementId)
|
||||||
|
: false);
|
||||||
|
|
||||||
if (!isArrowElement(element)) {
|
if (!isArrowElement(element)) {
|
||||||
|
updateElementCoords(pointerDownState, element, scene, adjustedOffset);
|
||||||
|
|
||||||
// skip arrow labels since we calculate its position during render
|
// skip arrow labels since we calculate its position during render
|
||||||
const textElement = getBoundTextElement(
|
const textElement = getBoundTextElement(
|
||||||
element,
|
element,
|
||||||
@ -118,9 +135,33 @@ export const dragSelectedElements = (
|
|||||||
adjustedOffset,
|
adjustedOffset,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateBoundElements(element, scene, {
|
updateBoundElements(element, scene, {
|
||||||
simultaneouslyUpdated: Array.from(elementsToUpdate),
|
simultaneouslyUpdated: Array.from(elementsToUpdate),
|
||||||
});
|
});
|
||||||
|
} else if (
|
||||||
|
// NOTE: Add a little initial drag to the arrow dragging to avoid
|
||||||
|
// accidentally unbinding the arrow when the user just wants to select it.
|
||||||
|
Math.max(Math.abs(adjustedOffset.x), Math.abs(adjustedOffset.y)) > 1
|
||||||
|
) {
|
||||||
|
updateElementCoords(pointerDownState, element, scene, adjustedOffset);
|
||||||
|
|
||||||
|
const shouldUnbindStart =
|
||||||
|
element.startBinding && !isStartBoundElementSelected;
|
||||||
|
const shouldUnbindEnd = element.endBinding && !isEndBoundElementSelected;
|
||||||
|
|
||||||
|
if (shouldUnbindStart || shouldUnbindEnd) {
|
||||||
|
// NOTE: Moving the bound arrow should unbind it, otherwise we would
|
||||||
|
// have weird situations, like 0 lenght arrow when the user moves
|
||||||
|
// the arrow outside a filled shape suddenly forcing the arrow start
|
||||||
|
// and end point to jump "outside" the shape.
|
||||||
|
bindOrUnbindLinearElement(
|
||||||
|
element,
|
||||||
|
shouldUnbindStart ? null : "keep",
|
||||||
|
shouldUnbindEnd ? null : "keep",
|
||||||
|
scene,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -25,7 +25,9 @@ import {
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
deconstructLinearOrFreeDrawElement,
|
deconstructLinearOrFreeDrawElement,
|
||||||
|
hitElementItself,
|
||||||
isPathALoop,
|
isPathALoop,
|
||||||
|
shouldTestInside,
|
||||||
type Store,
|
type Store,
|
||||||
} from "@excalidraw/element";
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
@ -43,8 +45,10 @@ import type {
|
|||||||
import type { Mutable } from "@excalidraw/common/utility-types";
|
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
bindLinearElement,
|
||||||
bindOrUnbindLinearElement,
|
bindOrUnbindLinearElement,
|
||||||
getHoveredElementForBinding,
|
getHoveredElementForBinding,
|
||||||
|
getOutlineAvoidingPoint,
|
||||||
isBindingEnabled,
|
isBindingEnabled,
|
||||||
maybeSuggestBindingsForLinearElementAtCoords,
|
maybeSuggestBindingsForLinearElementAtCoords,
|
||||||
} from "./binding";
|
} from "./binding";
|
||||||
@ -58,6 +62,8 @@ import { headingIsHorizontal, vectorToHeading } from "./heading";
|
|||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
||||||
import {
|
import {
|
||||||
|
isArrowElement,
|
||||||
|
isBindableElement,
|
||||||
isBindingElement,
|
isBindingElement,
|
||||||
isElbowArrow,
|
isElbowArrow,
|
||||||
isFixedPointBinding,
|
isFixedPointBinding,
|
||||||
@ -85,6 +91,8 @@ import type {
|
|||||||
FixedSegment,
|
FixedSegment,
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
PointsPositionUpdates,
|
PointsPositionUpdates,
|
||||||
|
NonDeletedExcalidrawElement,
|
||||||
|
Ordered,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -134,6 +142,7 @@ export class LinearElementEditor {
|
|||||||
index: number | null;
|
index: number | null;
|
||||||
added: boolean;
|
added: boolean;
|
||||||
};
|
};
|
||||||
|
arrowOtherPoint?: GlobalPoint;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
/** whether you're dragging a point */
|
/** whether you're dragging a point */
|
||||||
@ -278,6 +287,7 @@ export class LinearElementEditor {
|
|||||||
scenePointerX: number,
|
scenePointerX: number,
|
||||||
scenePointerY: number,
|
scenePointerY: number,
|
||||||
linearElementEditor: LinearElementEditor,
|
linearElementEditor: LinearElementEditor,
|
||||||
|
thresholdCallback: (element: ExcalidrawElement) => number,
|
||||||
): Pick<AppState, keyof AppState> | null {
|
): Pick<AppState, keyof AppState> | null {
|
||||||
if (!linearElementEditor) {
|
if (!linearElementEditor) {
|
||||||
return null;
|
return null;
|
||||||
@ -286,19 +296,23 @@ export class LinearElementEditor {
|
|||||||
const elementsMap = app.scene.getNonDeletedElementsMap();
|
const elementsMap = app.scene.getNonDeletedElementsMap();
|
||||||
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
||||||
let customLineAngle = linearElementEditor.customLineAngle;
|
let customLineAngle = linearElementEditor.customLineAngle;
|
||||||
|
let arrowOtherPoint: GlobalPoint | undefined =
|
||||||
|
linearElementEditor.pointerDownState.arrowOtherPoint;
|
||||||
if (!element) {
|
if (!element) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const elbowed = isElbowArrow(element);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isElbowArrow(element) &&
|
elbowed &&
|
||||||
!linearElementEditor.pointerDownState.lastClickedIsEndPoint &&
|
!linearElementEditor.pointerDownState.lastClickedIsEndPoint &&
|
||||||
linearElementEditor.pointerDownState.lastClickedPoint !== 0
|
linearElementEditor.pointerDownState.lastClickedPoint !== 0
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedPointsIndices = isElbowArrow(element)
|
const selectedPointsIndices = elbowed
|
||||||
? [
|
? [
|
||||||
!!linearElementEditor.selectedPointsIndices?.includes(0)
|
!!linearElementEditor.selectedPointsIndices?.includes(0)
|
||||||
? 0
|
? 0
|
||||||
@ -308,7 +322,7 @@ export class LinearElementEditor {
|
|||||||
: undefined,
|
: undefined,
|
||||||
].filter((idx): idx is number => idx !== undefined)
|
].filter((idx): idx is number => idx !== undefined)
|
||||||
: linearElementEditor.selectedPointsIndices;
|
: linearElementEditor.selectedPointsIndices;
|
||||||
const lastClickedPoint = isElbowArrow(element)
|
const lastClickedPoint = elbowed
|
||||||
? linearElementEditor.pointerDownState.lastClickedPoint > 0
|
? linearElementEditor.pointerDownState.lastClickedPoint > 0
|
||||||
? element.points.length - 1
|
? element.points.length - 1
|
||||||
: 0
|
: 0
|
||||||
@ -366,38 +380,38 @@ export class LinearElementEditor {
|
|||||||
scenePointerY - linearElementEditor.pointerOffset.y,
|
scenePointerY - linearElementEditor.pointerOffset.y,
|
||||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const deltaX = newDraggingPointPosition[0] - draggingPoint[0];
|
const deltaX = newDraggingPointPosition[0] - draggingPoint[0];
|
||||||
const deltaY = newDraggingPointPosition[1] - draggingPoint[1];
|
const deltaY = newDraggingPointPosition[1] - draggingPoint[1];
|
||||||
|
const elements = app.scene.getNonDeletedElements();
|
||||||
|
|
||||||
|
arrowOtherPoint = pointDraggingOtherEndpoint(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
selectedPointsIndices,
|
||||||
|
scenePointerX,
|
||||||
|
scenePointerY,
|
||||||
|
linearElementEditor,
|
||||||
|
app.scene,
|
||||||
|
thresholdCallback,
|
||||||
|
);
|
||||||
|
|
||||||
LinearElementEditor.movePoints(
|
LinearElementEditor.movePoints(
|
||||||
element,
|
element,
|
||||||
app.scene,
|
app.scene,
|
||||||
new Map(
|
pointDraggingUpdates(
|
||||||
selectedPointsIndices.map((pointIndex) => {
|
selectedPointsIndices,
|
||||||
const newPointPosition: LocalPoint =
|
deltaX,
|
||||||
pointIndex === lastClickedPoint
|
deltaY,
|
||||||
? LinearElementEditor.createPointAt(
|
|
||||||
element,
|
|
||||||
elementsMap,
|
elementsMap,
|
||||||
scenePointerX - linearElementEditor.pointerOffset.x,
|
lastClickedPoint,
|
||||||
scenePointerY - linearElementEditor.pointerOffset.y,
|
element,
|
||||||
event[KEYS.CTRL_OR_CMD]
|
scenePointerX,
|
||||||
? null
|
scenePointerY,
|
||||||
: app.getEffectiveGridSize(),
|
linearElementEditor,
|
||||||
)
|
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||||
: pointFrom(
|
elements,
|
||||||
element.points[pointIndex][0] + deltaX,
|
app.state.zoom,
|
||||||
element.points[pointIndex][1] + deltaY,
|
app.state.bindMode,
|
||||||
);
|
|
||||||
return [
|
|
||||||
pointIndex,
|
|
||||||
{
|
|
||||||
point: newPointPosition,
|
|
||||||
isDragging: pointIndex === lastClickedPoint,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -410,16 +424,14 @@ export class LinearElementEditor {
|
|||||||
// suggest bindings for first and last point if selected
|
// suggest bindings for first and last point if selected
|
||||||
let suggestedBindings: ExcalidrawBindableElement[] = [];
|
let suggestedBindings: ExcalidrawBindableElement[] = [];
|
||||||
if (isBindingElement(element, false)) {
|
if (isBindingElement(element, false)) {
|
||||||
const firstSelectedIndex = selectedPointsIndices[0] === 0;
|
const firstIndexIsSelected = selectedPointsIndices[0] === 0;
|
||||||
const lastSelectedIndex =
|
const lastIndexIsSelected =
|
||||||
selectedPointsIndices[selectedPointsIndices.length - 1] ===
|
selectedPointsIndices[selectedPointsIndices.length - 1] ===
|
||||||
element.points.length - 1;
|
element.points.length - 1;
|
||||||
const coords: { x: number; y: number }[] = [];
|
const coords: { x: number; y: number }[] = [];
|
||||||
|
|
||||||
if (!firstSelectedIndex !== !lastSelectedIndex) {
|
if (firstIndexIsSelected !== lastIndexIsSelected) {
|
||||||
coords.push({ x: scenePointerX, y: scenePointerY });
|
if (firstIndexIsSelected) {
|
||||||
} else {
|
|
||||||
if (firstSelectedIndex) {
|
|
||||||
coords.push(
|
coords.push(
|
||||||
tupleToCoors(
|
tupleToCoors(
|
||||||
LinearElementEditor.getPointGlobalCoordinates(
|
LinearElementEditor.getPointGlobalCoordinates(
|
||||||
@ -431,7 +443,7 @@ export class LinearElementEditor {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastSelectedIndex) {
|
if (lastIndexIsSelected) {
|
||||||
coords.push(
|
coords.push(
|
||||||
tupleToCoors(
|
tupleToCoors(
|
||||||
LinearElementEditor.getPointGlobalCoordinates(
|
LinearElementEditor.getPointGlobalCoordinates(
|
||||||
@ -452,6 +464,7 @@ export class LinearElementEditor {
|
|||||||
coords,
|
coords,
|
||||||
app.scene,
|
app.scene,
|
||||||
app.state.zoom,
|
app.state.zoom,
|
||||||
|
elementsMap,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -475,6 +488,10 @@ export class LinearElementEditor {
|
|||||||
: -1,
|
: -1,
|
||||||
isDragging: true,
|
isDragging: true,
|
||||||
customLineAngle,
|
customLineAngle,
|
||||||
|
pointerDownState: {
|
||||||
|
...linearElementEditor.pointerDownState,
|
||||||
|
arrowOtherPoint,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -605,6 +622,10 @@ export class LinearElementEditor {
|
|||||||
isDragging: false,
|
isDragging: false,
|
||||||
pointerOffset: { x: 0, y: 0 },
|
pointerOffset: { x: 0, y: 0 },
|
||||||
customLineAngle: null,
|
customLineAngle: null,
|
||||||
|
pointerDownState: {
|
||||||
|
...editingLinearElement.pointerDownState,
|
||||||
|
arrowOtherPoint: undefined,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -943,8 +964,15 @@ export class LinearElementEditor {
|
|||||||
// from the end points of the `linearElement` - this is to allow disabling
|
// from the end points of the `linearElement` - this is to allow disabling
|
||||||
// binding (which needs to happen at the point the user finishes moving
|
// binding (which needs to happen at the point the user finishes moving
|
||||||
// the point).
|
// the point).
|
||||||
|
const allPointSelected =
|
||||||
|
linearElementEditor.pointerDownState.prevSelectedPointsIndices
|
||||||
|
?.length === element.points.length;
|
||||||
const { startBindingElement, endBindingElement } = linearElementEditor;
|
const { startBindingElement, endBindingElement } = linearElementEditor;
|
||||||
if (isBindingEnabled(appState) && isBindingElement(element)) {
|
if (
|
||||||
|
!allPointSelected &&
|
||||||
|
isBindingEnabled(appState) &&
|
||||||
|
isBindingElement(element)
|
||||||
|
) {
|
||||||
bindOrUnbindLinearElement(
|
bindOrUnbindLinearElement(
|
||||||
element,
|
element,
|
||||||
startBindingElement,
|
startBindingElement,
|
||||||
@ -1404,6 +1432,7 @@ export class LinearElementEditor {
|
|||||||
otherUpdates?: {
|
otherUpdates?: {
|
||||||
startBinding?: PointBinding | null;
|
startBinding?: PointBinding | null;
|
||||||
endBinding?: PointBinding | null;
|
endBinding?: PointBinding | null;
|
||||||
|
moveMidPointsWithElement?: boolean | null;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const { points } = element;
|
const { points } = element;
|
||||||
@ -1449,6 +1478,15 @@ export class LinearElementEditor {
|
|||||||
: points.map((p, idx) => {
|
: points.map((p, idx) => {
|
||||||
const current = pointUpdates.get(idx)?.point ?? p;
|
const current = pointUpdates.get(idx)?.point ?? p;
|
||||||
|
|
||||||
|
if (
|
||||||
|
otherUpdates?.moveMidPointsWithElement &&
|
||||||
|
idx !== 0 &&
|
||||||
|
idx !== points.length - 1 &&
|
||||||
|
!pointUpdates.has(idx)
|
||||||
|
) {
|
||||||
|
return pointFrom<LocalPoint>(current[0], current[1]);
|
||||||
|
}
|
||||||
|
|
||||||
return pointFrom<LocalPoint>(
|
return pointFrom<LocalPoint>(
|
||||||
current[0] - offsetX,
|
current[0] - offsetX,
|
||||||
current[1] - offsetY,
|
current[1] - offsetY,
|
||||||
@ -1977,3 +2015,242 @@ const normalizeSelectedPoints = (
|
|||||||
nextPoints = nextPoints.sort((a, b) => a - b);
|
nextPoints = nextPoints.sort((a, b) => a - b);
|
||||||
return nextPoints.length ? nextPoints : null;
|
return nextPoints.length ? nextPoints : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const pointDraggingUpdates = (
|
||||||
|
selectedPointsIndices: readonly number[],
|
||||||
|
deltaX: number,
|
||||||
|
deltaY: number,
|
||||||
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
|
lastClickedPoint: number,
|
||||||
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
scenePointerX: number,
|
||||||
|
scenePointerY: number,
|
||||||
|
linearElementEditor: LinearElementEditor,
|
||||||
|
gridSize: NullableGridSize,
|
||||||
|
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
|
||||||
|
zoom: AppState["zoom"],
|
||||||
|
bindMode: AppState["bindMode"],
|
||||||
|
): PointsPositionUpdates => {
|
||||||
|
const hasMidPoints =
|
||||||
|
selectedPointsIndices.filter(
|
||||||
|
(_, idx) => idx > 0 && idx < element.points.length - 1,
|
||||||
|
).length > 0;
|
||||||
|
|
||||||
|
return new Map(
|
||||||
|
selectedPointsIndices.map((pointIndex) => {
|
||||||
|
let newPointPosition: LocalPoint =
|
||||||
|
pointIndex === lastClickedPoint
|
||||||
|
? LinearElementEditor.createPointAt(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
scenePointerX - linearElementEditor.pointerOffset.x,
|
||||||
|
scenePointerY - linearElementEditor.pointerOffset.y,
|
||||||
|
gridSize,
|
||||||
|
)
|
||||||
|
: pointFrom(
|
||||||
|
element.points[pointIndex][0] + deltaX,
|
||||||
|
element.points[pointIndex][1] + deltaY,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!hasMidPoints &&
|
||||||
|
(pointIndex === 0 || pointIndex === element.points.length - 1)
|
||||||
|
) {
|
||||||
|
const [, , , , cx, cy] = getElementAbsoluteCoords(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
let newGlobalPointPosition = pointRotateRads(
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
element.x + newPointPosition[0],
|
||||||
|
element.y + newPointPosition[1],
|
||||||
|
),
|
||||||
|
pointFrom<GlobalPoint>(cx, cy),
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
|
const hoveredElement = getHoveredElementForBinding(
|
||||||
|
{
|
||||||
|
x: newGlobalPointPosition[0],
|
||||||
|
y: newGlobalPointPosition[1],
|
||||||
|
},
|
||||||
|
elements,
|
||||||
|
elementsMap,
|
||||||
|
zoom,
|
||||||
|
shouldTestInside(element),
|
||||||
|
isElbowArrow(element),
|
||||||
|
);
|
||||||
|
|
||||||
|
const otherGlobalPoint =
|
||||||
|
LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||||
|
element,
|
||||||
|
pointIndex === 0 ? element.points.length - 1 : 0,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
const otherHoveredElement = getHoveredElementForBinding(
|
||||||
|
{
|
||||||
|
x: otherGlobalPoint[0],
|
||||||
|
y: otherGlobalPoint[1],
|
||||||
|
},
|
||||||
|
elements,
|
||||||
|
elementsMap,
|
||||||
|
zoom,
|
||||||
|
shouldTestInside(element),
|
||||||
|
isElbowArrow(element),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Allow binding inside the element if both ends are inside
|
||||||
|
if (
|
||||||
|
isArrowElement(element) &&
|
||||||
|
!(
|
||||||
|
hoveredElement?.id === otherHoveredElement?.id &&
|
||||||
|
hoveredElement != null
|
||||||
|
) &&
|
||||||
|
bindMode === "focus"
|
||||||
|
) {
|
||||||
|
newGlobalPointPosition = getOutlineAvoidingPoint(
|
||||||
|
element,
|
||||||
|
hoveredElement,
|
||||||
|
newGlobalPointPosition,
|
||||||
|
pointIndex,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
newPointPosition = LinearElementEditor.createPointAt(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
newGlobalPointPosition[0] - linearElementEditor.pointerOffset.x,
|
||||||
|
newGlobalPointPosition[1] - linearElementEditor.pointerOffset.y,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
pointIndex,
|
||||||
|
{
|
||||||
|
point: newPointPosition,
|
||||||
|
isDragging: pointIndex === lastClickedPoint,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const pointDraggingOtherEndpoint = (
|
||||||
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
|
selectedPointsIndices: readonly number[],
|
||||||
|
scenePointerX: number,
|
||||||
|
scenePointerY: number,
|
||||||
|
linearElementEditor: LinearElementEditor,
|
||||||
|
scene: Scene,
|
||||||
|
thresholdCallback: (element: ExcalidrawElement) => number,
|
||||||
|
) => {
|
||||||
|
let arrowOtherPoint = linearElementEditor.pointerDownState.arrowOtherPoint;
|
||||||
|
|
||||||
|
if (isArrowElement(element) && !isElbowArrow(element)) {
|
||||||
|
const startPointIsIncluded = selectedPointsIndices.includes(0);
|
||||||
|
const endPointIsIncluded = selectedPointsIndices.includes(
|
||||||
|
element.points.length - 1,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
// Make sure that not both of the endpoints are selected
|
||||||
|
(startPointIsIncluded || endPointIsIncluded) &&
|
||||||
|
startPointIsIncluded !== endPointIsIncluded
|
||||||
|
) {
|
||||||
|
const otherBinding =
|
||||||
|
element[startPointIsIncluded ? "endBinding" : "startBinding"];
|
||||||
|
if (
|
||||||
|
// The other end is bound
|
||||||
|
otherBinding
|
||||||
|
) {
|
||||||
|
const otherElement = elementsMap.get(otherBinding.elementId);
|
||||||
|
|
||||||
|
invariant(
|
||||||
|
isBindableElement(otherElement),
|
||||||
|
"Other element should exist in elementsMap at all times and be a bindable element",
|
||||||
|
);
|
||||||
|
|
||||||
|
let newOtherPointPosition;
|
||||||
|
|
||||||
|
// Only avoid shape if the start and end point is not inside
|
||||||
|
// the same element
|
||||||
|
if (
|
||||||
|
!hitElementItself({
|
||||||
|
point: pointFrom(scenePointerX, scenePointerY),
|
||||||
|
element: otherElement,
|
||||||
|
elementsMap,
|
||||||
|
threshold: thresholdCallback(otherElement),
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
// If we don't have a restore point, that means we need to jump out
|
||||||
|
// of the element but first, create the restore point
|
||||||
|
if (!arrowOtherPoint) {
|
||||||
|
arrowOtherPoint = LinearElementEditor.getPointGlobalCoordinates(
|
||||||
|
element,
|
||||||
|
element.points[
|
||||||
|
startPointIsIncluded ? element.points.length - 1 : 0
|
||||||
|
],
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find a snap point outside the element
|
||||||
|
const newOtherGlobalPoint = getOutlineAvoidingPoint(
|
||||||
|
element,
|
||||||
|
otherElement,
|
||||||
|
arrowOtherPoint,
|
||||||
|
startPointIsIncluded ? element.points.length - 1 : 0,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
newOtherPointPosition = LinearElementEditor.createPointAt(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
newOtherGlobalPoint[0] - linearElementEditor.pointerOffset.x,
|
||||||
|
newOtherGlobalPoint[1] - linearElementEditor.pointerOffset.y,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Restore the saved point if we are back inside the element
|
||||||
|
else if (arrowOtherPoint) {
|
||||||
|
newOtherPointPosition = LinearElementEditor.createPointAt(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
arrowOtherPoint[0] - linearElementEditor.pointerOffset.x,
|
||||||
|
arrowOtherPoint[1] - linearElementEditor.pointerOffset.y,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
arrowOtherPoint = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, move the other endpoint if needed
|
||||||
|
if (newOtherPointPosition) {
|
||||||
|
LinearElementEditor.movePoints(
|
||||||
|
element,
|
||||||
|
scene,
|
||||||
|
new Map([
|
||||||
|
[
|
||||||
|
startPointIsIncluded ? element.points.length - 1 : 0,
|
||||||
|
{
|
||||||
|
point: newOtherPointPosition,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
bindLinearElement(
|
||||||
|
element,
|
||||||
|
otherElement,
|
||||||
|
startPointIsIncluded ? "end" : "start",
|
||||||
|
scene,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return arrowOtherPoint;
|
||||||
|
};
|
||||||
|
@ -12,7 +12,7 @@ import { ShapeCache } from "./shape";
|
|||||||
|
|
||||||
import { updateElbowArrowPoints } from "./elbowArrow";
|
import { updateElbowArrowPoints } from "./elbowArrow";
|
||||||
|
|
||||||
import { isElbowArrow } from "./typeChecks";
|
import { isElbowArrow, isFixedPointBinding } from "./typeChecks";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
@ -54,8 +54,8 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
|||||||
(Object.keys(updates).length === 0 || // normalization case
|
(Object.keys(updates).length === 0 || // normalization case
|
||||||
typeof points !== "undefined" || // repositioning
|
typeof points !== "undefined" || // repositioning
|
||||||
typeof fixedSegments !== "undefined" || // segment fixing
|
typeof fixedSegments !== "undefined" || // segment fixing
|
||||||
typeof startBinding !== "undefined" ||
|
isFixedPointBinding(startBinding) ||
|
||||||
typeof endBinding !== "undefined") // manual binding to element
|
isFixedPointBinding(endBinding)) // manual binding to element
|
||||||
) {
|
) {
|
||||||
updates = {
|
updates = {
|
||||||
...updates,
|
...updates,
|
||||||
|
@ -362,6 +362,7 @@ export const isFixedPointBinding = (
|
|||||||
binding: PointBinding | FixedPointBinding,
|
binding: PointBinding | FixedPointBinding,
|
||||||
): binding is FixedPointBinding => {
|
): binding is FixedPointBinding => {
|
||||||
return (
|
return (
|
||||||
|
binding != null &&
|
||||||
Object.hasOwn(binding, "fixedPoint") &&
|
Object.hasOwn(binding, "fixedPoint") &&
|
||||||
(binding as FixedPointBinding).fixedPoint != null
|
(binding as FixedPointBinding).fixedPoint != null
|
||||||
);
|
);
|
||||||
|
@ -323,8 +323,8 @@ export type ExcalidrawLinearElement = _ExcalidrawElementBase &
|
|||||||
type: "line" | "arrow";
|
type: "line" | "arrow";
|
||||||
points: readonly LocalPoint[];
|
points: readonly LocalPoint[];
|
||||||
lastCommittedPoint: LocalPoint | null;
|
lastCommittedPoint: LocalPoint | null;
|
||||||
startBinding: PointBinding | null;
|
startBinding: FixedPointBinding | PointBinding | null;
|
||||||
endBinding: PointBinding | null;
|
endBinding: FixedPointBinding | PointBinding | null;
|
||||||
startArrowhead: Arrowhead | null;
|
startArrowhead: Arrowhead | null;
|
||||||
endArrowhead: Arrowhead | null;
|
endArrowhead: Arrowhead | null;
|
||||||
}>;
|
}>;
|
||||||
@ -351,9 +351,9 @@ export type ExcalidrawElbowArrowElement = Merge<
|
|||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
{
|
{
|
||||||
elbowed: true;
|
elbowed: true;
|
||||||
|
fixedSegments: readonly FixedSegment[] | null;
|
||||||
startBinding: FixedPointBinding | null;
|
startBinding: FixedPointBinding | null;
|
||||||
endBinding: FixedPointBinding | null;
|
endBinding: FixedPointBinding | null;
|
||||||
fixedSegments: readonly FixedSegment[] | null;
|
|
||||||
/**
|
/**
|
||||||
* Marks that the 3rd point should be used as the 2nd point of the arrow in
|
* Marks that the 3rd point should be used as the 2nd point of the arrow in
|
||||||
* order to temporarily hide the first segment of the arrow without losing
|
* order to temporarily hide the first segment of the arrow without losing
|
||||||
|
@ -8,7 +8,13 @@ import { Excalidraw, isLinearElement } from "@excalidraw/excalidraw";
|
|||||||
|
|
||||||
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||||
import { UI, Pointer, Keyboard } from "@excalidraw/excalidraw/tests/helpers/ui";
|
import { UI, Pointer, Keyboard } from "@excalidraw/excalidraw/tests/helpers/ui";
|
||||||
import { fireEvent, render } from "@excalidraw/excalidraw/tests/test-utils";
|
import {
|
||||||
|
act,
|
||||||
|
fireEvent,
|
||||||
|
render,
|
||||||
|
} from "@excalidraw/excalidraw/tests/test-utils";
|
||||||
|
|
||||||
|
import { defaultLang, setLanguage } from "@excalidraw/excalidraw/i18n";
|
||||||
|
|
||||||
import { getTransformHandles } from "../src/transformHandles";
|
import { getTransformHandles } from "../src/transformHandles";
|
||||||
import {
|
import {
|
||||||
@ -16,6 +22,8 @@ import {
|
|||||||
TEXT_EDITOR_SELECTOR,
|
TEXT_EDITOR_SELECTOR,
|
||||||
} from "../../excalidraw/tests/queries/dom";
|
} from "../../excalidraw/tests/queries/dom";
|
||||||
|
|
||||||
|
import type { ExcalidrawLinearElement, FixedPointBinding } from "../src/types";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
const mouse = new Pointer("mouse");
|
const mouse = new Pointer("mouse");
|
||||||
@ -71,8 +79,9 @@ describe("element binding", () => {
|
|||||||
|
|
||||||
expect(arrow.startBinding).toEqual({
|
expect(arrow.startBinding).toEqual({
|
||||||
elementId: rect.id,
|
elementId: rect.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
focus: 0,
|
||||||
gap: expect.toBeNonNaNNumber(),
|
gap: 0,
|
||||||
|
fixedPoint: expect.arrayContaining([1.1, 0]),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Move the end point to the overlapping binding position
|
// Move the end point to the overlapping binding position
|
||||||
@ -83,13 +92,15 @@ describe("element binding", () => {
|
|||||||
// Both the start and the end points should be bound
|
// Both the start and the end points should be bound
|
||||||
expect(arrow.startBinding).toEqual({
|
expect(arrow.startBinding).toEqual({
|
||||||
elementId: rect.id,
|
elementId: rect.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
focus: 0,
|
||||||
gap: expect.toBeNonNaNNumber(),
|
gap: 0,
|
||||||
|
fixedPoint: expect.arrayContaining([1.1, 0]),
|
||||||
});
|
});
|
||||||
expect(arrow.endBinding).toEqual({
|
expect(arrow.endBinding).toEqual({
|
||||||
elementId: rect.id,
|
elementId: rect.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
focus: 0,
|
||||||
gap: expect.toBeNonNaNNumber(),
|
gap: 0,
|
||||||
|
fixedPoint: expect.arrayContaining([1.1, 0]),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -188,9 +199,9 @@ describe("element binding", () => {
|
|||||||
// Test sticky connection
|
// Test sticky connection
|
||||||
expect(API.getSelectedElement().type).toBe("arrow");
|
expect(API.getSelectedElement().type).toBe("arrow");
|
||||||
Keyboard.keyPress(KEYS.ARROW_RIGHT);
|
Keyboard.keyPress(KEYS.ARROW_RIGHT);
|
||||||
expect(arrow.endBinding?.elementId).toBe(rectangle.id);
|
expect(arrow.endBinding?.elementId).not.toBe(rectangle.id);
|
||||||
Keyboard.keyPress(KEYS.ARROW_LEFT);
|
Keyboard.keyPress(KEYS.ARROW_LEFT);
|
||||||
expect(arrow.endBinding?.elementId).toBe(rectangle.id);
|
expect(arrow.endBinding?.elementId).not.toBe(rectangle.id);
|
||||||
|
|
||||||
// Sever connection
|
// Sever connection
|
||||||
expect(API.getSelectedElement().type).toBe("arrow");
|
expect(API.getSelectedElement().type).toBe("arrow");
|
||||||
@ -476,3 +487,354 @@ describe("element binding", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Fixed-point arrow binding", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create fixed-point binding when both arrow endpoint is inside rectangle", () => {
|
||||||
|
// Create a filled solid rectangle
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
mouse.downAt(100, 100);
|
||||||
|
mouse.moveTo(200, 200);
|
||||||
|
mouse.up();
|
||||||
|
|
||||||
|
const rect = API.getSelectedElement();
|
||||||
|
API.updateElement(rect, { fillStyle: "solid", backgroundColor: "#a5d8ff" });
|
||||||
|
|
||||||
|
// Draw arrow with endpoint inside the filled rectangle, since only
|
||||||
|
// filled bindables bind inside the shape
|
||||||
|
UI.clickTool("arrow");
|
||||||
|
mouse.downAt(110, 110);
|
||||||
|
mouse.moveTo(160, 160);
|
||||||
|
mouse.up();
|
||||||
|
|
||||||
|
const arrow = API.getSelectedElement() as ExcalidrawLinearElement;
|
||||||
|
expect(arrow.x).toBe(110);
|
||||||
|
expect(arrow.y).toBe(110);
|
||||||
|
|
||||||
|
// Should bind to the rectangle since endpoint is inside
|
||||||
|
expect(arrow.startBinding?.elementId).toBe(rect.id);
|
||||||
|
expect(arrow.endBinding?.elementId).toBe(rect.id);
|
||||||
|
|
||||||
|
const startBinding = arrow.startBinding as FixedPointBinding;
|
||||||
|
expect(startBinding.fixedPoint[0]).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(startBinding.fixedPoint[0]).toBeLessThanOrEqual(1);
|
||||||
|
expect(startBinding.fixedPoint[1]).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(startBinding.fixedPoint[1]).toBeLessThanOrEqual(1);
|
||||||
|
|
||||||
|
const endBinding = arrow.endBinding as FixedPointBinding;
|
||||||
|
expect(endBinding.fixedPoint[0]).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(endBinding.fixedPoint[0]).toBeLessThanOrEqual(1);
|
||||||
|
expect(endBinding.fixedPoint[1]).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(endBinding.fixedPoint[1]).toBeLessThanOrEqual(1);
|
||||||
|
|
||||||
|
mouse.reset();
|
||||||
|
|
||||||
|
// Move the bindable
|
||||||
|
mouse.downAt(130, 110);
|
||||||
|
mouse.moveTo(280, 110);
|
||||||
|
mouse.up();
|
||||||
|
|
||||||
|
// Check if the arrow moved
|
||||||
|
expect(arrow.x).toBe(260);
|
||||||
|
expect(arrow.y).toBe(110);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create fixed-point binding when one of the arrow endpoint is inside rectangle", () => {
|
||||||
|
// Create a filled solid rectangle
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
mouse.downAt(100, 100);
|
||||||
|
mouse.moveTo(200, 200);
|
||||||
|
mouse.up();
|
||||||
|
|
||||||
|
const rect = API.getSelectedElement();
|
||||||
|
API.updateElement(rect, { fillStyle: "solid", backgroundColor: "#a5d8ff" });
|
||||||
|
|
||||||
|
// Draw arrow with endpoint inside the filled rectangle, since only
|
||||||
|
// filled bindables bind inside the shape
|
||||||
|
UI.clickTool("arrow");
|
||||||
|
mouse.downAt(10, 10);
|
||||||
|
mouse.moveTo(160, 160);
|
||||||
|
mouse.up();
|
||||||
|
|
||||||
|
const arrow = API.getSelectedElement() as ExcalidrawLinearElement;
|
||||||
|
expect(arrow.x).toBe(10);
|
||||||
|
expect(arrow.y).toBe(10);
|
||||||
|
expect(arrow.width).toBe(150);
|
||||||
|
expect(arrow.height).toBe(150);
|
||||||
|
|
||||||
|
// Should bind to the rectangle since endpoint is inside
|
||||||
|
expect(arrow.startBinding).toBe(null);
|
||||||
|
expect(arrow.endBinding?.elementId).toBe(rect.id);
|
||||||
|
|
||||||
|
const endBinding = arrow.endBinding as FixedPointBinding;
|
||||||
|
expect(endBinding.fixedPoint[0]).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(endBinding.fixedPoint[0]).toBeLessThanOrEqual(1);
|
||||||
|
expect(endBinding.fixedPoint[1]).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(endBinding.fixedPoint[1]).toBeLessThanOrEqual(1);
|
||||||
|
|
||||||
|
mouse.reset();
|
||||||
|
|
||||||
|
// Move the bindable
|
||||||
|
mouse.downAt(130, 110);
|
||||||
|
mouse.moveTo(280, 110);
|
||||||
|
mouse.up();
|
||||||
|
|
||||||
|
// Check if the arrow moved
|
||||||
|
expect(arrow.x).toBe(10);
|
||||||
|
expect(arrow.y).toBe(10);
|
||||||
|
expect(arrow.width).toBe(300);
|
||||||
|
expect(arrow.height).toBe(150);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should maintain relative position when arrow start point is dragged outside and rectangle is moved", () => {
|
||||||
|
// Create a filled solid rectangle
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
mouse.downAt(100, 100);
|
||||||
|
mouse.moveTo(200, 200);
|
||||||
|
mouse.up();
|
||||||
|
|
||||||
|
const rect = API.getSelectedElement();
|
||||||
|
API.updateElement(rect, { fillStyle: "solid", backgroundColor: "#a5d8ff" });
|
||||||
|
|
||||||
|
// Draw arrow with both endpoints inside the filled rectangle, creating same-element binding
|
||||||
|
UI.clickTool("arrow");
|
||||||
|
mouse.downAt(120, 120);
|
||||||
|
mouse.moveTo(180, 180);
|
||||||
|
mouse.up();
|
||||||
|
|
||||||
|
const arrow = API.getSelectedElement() as ExcalidrawLinearElement;
|
||||||
|
|
||||||
|
// Both ends should be bound to the same rectangle
|
||||||
|
expect(arrow.startBinding?.elementId).toBe(rect.id);
|
||||||
|
expect(arrow.endBinding?.elementId).toBe(rect.id);
|
||||||
|
|
||||||
|
mouse.reset();
|
||||||
|
|
||||||
|
// Select the arrow and drag the start point outside the rectangle
|
||||||
|
mouse.downAt(120, 120);
|
||||||
|
mouse.moveTo(50, 50); // Move start point outside rectangle
|
||||||
|
mouse.up();
|
||||||
|
|
||||||
|
mouse.reset();
|
||||||
|
|
||||||
|
// Move the rectangle by dragging it
|
||||||
|
mouse.downAt(150, 110);
|
||||||
|
mouse.moveTo(300, 300);
|
||||||
|
mouse.up();
|
||||||
|
|
||||||
|
// The end point should be a normal point binding
|
||||||
|
const endBinding = arrow.endBinding as FixedPointBinding;
|
||||||
|
expect(endBinding.focus).toBeCloseTo(0);
|
||||||
|
expect(endBinding.gap).toBeCloseTo(0);
|
||||||
|
|
||||||
|
expect(arrow.x).toBe(50);
|
||||||
|
expect(arrow.y).toBe(50);
|
||||||
|
expect(arrow.width).toBeCloseTo(304, 0);
|
||||||
|
expect(arrow.height).toBeCloseTo(344, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should move inner points when arrow is bound to same element on both ends", () => {
|
||||||
|
// Create one rectangle as binding target
|
||||||
|
const rect = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 50,
|
||||||
|
y: 50,
|
||||||
|
width: 200,
|
||||||
|
height: 100,
|
||||||
|
fillStyle: "solid",
|
||||||
|
backgroundColor: "#a5d8ff",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a non-elbowed arrow with inner points bound to the same element on both ends
|
||||||
|
const arrow = API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
x: 100,
|
||||||
|
y: 75,
|
||||||
|
width: 100,
|
||||||
|
height: 50,
|
||||||
|
points: [
|
||||||
|
pointFrom(0, 0), // start point
|
||||||
|
pointFrom(25, -25), // first inner point
|
||||||
|
pointFrom(75, 25), // second inner point
|
||||||
|
pointFrom(100, 0), // end point
|
||||||
|
],
|
||||||
|
startBinding: {
|
||||||
|
elementId: rect.id,
|
||||||
|
focus: 0,
|
||||||
|
gap: 0,
|
||||||
|
fixedPoint: [0.25, 0.5],
|
||||||
|
},
|
||||||
|
endBinding: {
|
||||||
|
elementId: rect.id,
|
||||||
|
focus: 0,
|
||||||
|
gap: 0,
|
||||||
|
fixedPoint: [0.75, 0.5],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([rect, arrow]);
|
||||||
|
|
||||||
|
// Store original inner point positions (local coordinates)
|
||||||
|
const originalInnerPoint1 = [...arrow.points[1]];
|
||||||
|
const originalInnerPoint2 = [...arrow.points[2]];
|
||||||
|
|
||||||
|
// Move the rectangle
|
||||||
|
mouse.reset();
|
||||||
|
mouse.downAt(150, 100); // Click on the rectangle
|
||||||
|
mouse.moveTo(300, 200); // Move it down and to the right
|
||||||
|
mouse.up();
|
||||||
|
|
||||||
|
// Verify that inner points moved with the arrow (same local coordinates)
|
||||||
|
// When both ends are bound to the same element, inner points should maintain
|
||||||
|
// their local coordinates relative to the arrow's origin
|
||||||
|
expect(arrow.points[1][0]).toBe(originalInnerPoint1[0]);
|
||||||
|
expect(arrow.points[1][1]).toBe(originalInnerPoint1[1]);
|
||||||
|
expect(arrow.points[2][0]).toBe(originalInnerPoint2[0]);
|
||||||
|
expect(arrow.points[2][1]).toBe(originalInnerPoint2[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should NOT move inner points when arrow is bound to different elements", () => {
|
||||||
|
// Create two rectangles as binding targets
|
||||||
|
const rectLeft = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const rectRight = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 300,
|
||||||
|
y: 0,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a non-elbowed arrow with inner points bound to different elements
|
||||||
|
const arrow = API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
x: 100,
|
||||||
|
y: 50,
|
||||||
|
width: 200,
|
||||||
|
height: 0,
|
||||||
|
points: [
|
||||||
|
pointFrom(0, 0), // start point
|
||||||
|
pointFrom(50, -20), // first inner point
|
||||||
|
pointFrom(150, 20), // second inner point
|
||||||
|
pointFrom(200, 0), // end point
|
||||||
|
],
|
||||||
|
startBinding: {
|
||||||
|
elementId: rectLeft.id,
|
||||||
|
focus: 0.5,
|
||||||
|
gap: 5,
|
||||||
|
},
|
||||||
|
endBinding: {
|
||||||
|
elementId: rectRight.id,
|
||||||
|
focus: 0.5,
|
||||||
|
gap: 5,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([rectLeft, rectRight, arrow]);
|
||||||
|
|
||||||
|
// Store original inner point positions
|
||||||
|
const originalInnerPoint1 = [...arrow.points[1]];
|
||||||
|
const originalInnerPoint2 = [...arrow.points[2]];
|
||||||
|
|
||||||
|
// Move the right rectangle down by 50 pixels
|
||||||
|
mouse.reset();
|
||||||
|
mouse.downAt(350, 50); // Click on the right rectangle
|
||||||
|
mouse.moveTo(350, 100); // Move it down
|
||||||
|
mouse.up();
|
||||||
|
|
||||||
|
// Verify that inner points did NOT move when bound to different elements
|
||||||
|
// The arrow should NOT translate inner points proportionally when only one end moves
|
||||||
|
expect(arrow.points[1][0]).toBe(originalInnerPoint1[0]);
|
||||||
|
expect(arrow.points[1][1]).toBe(originalInnerPoint1[1]);
|
||||||
|
expect(arrow.points[2][0]).toBe(originalInnerPoint2[0]);
|
||||||
|
expect(arrow.points[2][1]).toBe(originalInnerPoint2[1]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("line segment extension binding", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
mouse.reset();
|
||||||
|
|
||||||
|
await act(() => {
|
||||||
|
return setLanguage(defaultLang);
|
||||||
|
});
|
||||||
|
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should use point binding when extended segment intersects element", () => {
|
||||||
|
// Create a rectangle that will be intersected by the extended arrow segment
|
||||||
|
const rect = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([rect]);
|
||||||
|
|
||||||
|
// Draw an arrow that points at the rectangle (extended segment will intersect)
|
||||||
|
UI.clickTool("arrow");
|
||||||
|
mouse.downAt(0, 0); // Start point
|
||||||
|
mouse.moveTo(120, 95); // End point - arrow direction points toward rectangle
|
||||||
|
mouse.up();
|
||||||
|
|
||||||
|
const arrow = API.getSelectedElement() as ExcalidrawLinearElement;
|
||||||
|
|
||||||
|
// Should create a normal point binding since the extended line segment
|
||||||
|
// from the last arrow segment intersects the rectangle
|
||||||
|
expect(arrow.endBinding?.elementId).toBe(rect.id);
|
||||||
|
expect(arrow.endBinding).toHaveProperty("focus");
|
||||||
|
expect(arrow.endBinding).toHaveProperty("gap");
|
||||||
|
expect(arrow.endBinding).not.toHaveProperty("fixedPoint");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should use fixed point binding when extended segment misses element", () => {
|
||||||
|
// Create a rectangle positioned so the extended arrow segment will miss it
|
||||||
|
const rect = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([rect]);
|
||||||
|
|
||||||
|
// Draw an arrow that doesn't point at the rectangle (extended segment will miss)
|
||||||
|
UI.clickTool("arrow");
|
||||||
|
mouse.reset();
|
||||||
|
mouse.downAt(125, 93); // Start point
|
||||||
|
mouse.moveTo(175, 93); // End point - arrow direction is horizontal, misses rectangle
|
||||||
|
mouse.up();
|
||||||
|
|
||||||
|
const arrow = API.getSelectedElement() as ExcalidrawLinearElement;
|
||||||
|
|
||||||
|
// Should create a fixed point binding since the extended line segment
|
||||||
|
// from the last arrow segment misses the rectangle
|
||||||
|
expect(arrow.startBinding?.elementId).toBe(rect.id);
|
||||||
|
expect(arrow.startBinding).toHaveProperty("fixedPoint");
|
||||||
|
expect(
|
||||||
|
(arrow.startBinding as FixedPointBinding).fixedPoint[0],
|
||||||
|
).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(
|
||||||
|
(arrow.startBinding as FixedPointBinding).fixedPoint[0],
|
||||||
|
).toBeLessThanOrEqual(1);
|
||||||
|
expect(
|
||||||
|
(arrow.startBinding as FixedPointBinding).fixedPoint[1],
|
||||||
|
).toBeLessThanOrEqual(0);
|
||||||
|
expect(
|
||||||
|
(arrow.startBinding as FixedPointBinding).fixedPoint[1],
|
||||||
|
).toBeLessThanOrEqual(1);
|
||||||
|
expect(arrow.endBinding).toBe(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -27,6 +27,7 @@ import type {
|
|||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
ExcalidrawFreeDrawElement,
|
ExcalidrawFreeDrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
|
PointBinding,
|
||||||
} from "../src/types";
|
} from "../src/types";
|
||||||
|
|
||||||
unmountComponent();
|
unmountComponent();
|
||||||
@ -1023,8 +1024,20 @@ describe("multiple selection", () => {
|
|||||||
1 - move[0] / selectionWidth,
|
1 - move[0] / selectionWidth,
|
||||||
1 - move[1] / selectionHeight,
|
1 - move[1] / selectionHeight,
|
||||||
);
|
);
|
||||||
const leftArrowBinding = { ...leftBoundArrow.endBinding };
|
const leftArrowBinding: {
|
||||||
const rightArrowBinding = { ...rightBoundArrow.endBinding };
|
elementId: string;
|
||||||
|
gap?: number;
|
||||||
|
focus?: number;
|
||||||
|
} = {
|
||||||
|
...leftBoundArrow.endBinding,
|
||||||
|
} as PointBinding;
|
||||||
|
const rightArrowBinding: {
|
||||||
|
elementId: string;
|
||||||
|
gap?: number;
|
||||||
|
focus?: number;
|
||||||
|
} = {
|
||||||
|
...rightBoundArrow.endBinding,
|
||||||
|
} as PointBinding;
|
||||||
delete rightArrowBinding.gap;
|
delete rightArrowBinding.gap;
|
||||||
|
|
||||||
UI.resize([rectangle, rightBoundArrow], "nw", move, {
|
UI.resize([rectangle, rightBoundArrow], "nw", move, {
|
||||||
|
@ -4,8 +4,14 @@ import {
|
|||||||
maybeBindLinearElement,
|
maybeBindLinearElement,
|
||||||
bindOrUnbindLinearElement,
|
bindOrUnbindLinearElement,
|
||||||
isBindingEnabled,
|
isBindingEnabled,
|
||||||
|
getHoveredElementForBinding,
|
||||||
} from "@excalidraw/element/binding";
|
} from "@excalidraw/element/binding";
|
||||||
import { isValidPolygon, LinearElementEditor } from "@excalidraw/element";
|
import {
|
||||||
|
isElbowArrow,
|
||||||
|
isValidPolygon,
|
||||||
|
LinearElementEditor,
|
||||||
|
shouldTestInside,
|
||||||
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
isBindingElement,
|
isBindingElement,
|
||||||
@ -26,7 +32,7 @@ import { isInvisiblySmallElement } from "@excalidraw/element";
|
|||||||
|
|
||||||
import { CaptureUpdateAction } from "@excalidraw/element";
|
import { CaptureUpdateAction } from "@excalidraw/element";
|
||||||
|
|
||||||
import type { LocalPoint } from "@excalidraw/math";
|
import type { GlobalPoint, LocalPoint } from "@excalidraw/math";
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
@ -94,13 +100,22 @@ export const actionFinalize = register({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appState.editingLinearElement) {
|
if (appState.editingLinearElement && !appState.newElement) {
|
||||||
const { elementId, startBindingElement, endBindingElement } =
|
const { elementId, startBindingElement, endBindingElement } =
|
||||||
appState.editingLinearElement;
|
appState.editingLinearElement;
|
||||||
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
||||||
|
|
||||||
if (element) {
|
if (element) {
|
||||||
if (isBindingElement(element)) {
|
// NOTE: Dragging the entire arrow doesn't allow binding.
|
||||||
|
const allPointsSelected =
|
||||||
|
appState.editingLinearElement?.pointerDownState
|
||||||
|
.prevSelectedPointsIndices?.length === element.points.length;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!allPointsSelected &&
|
||||||
|
isBindingEnabled(appState) &&
|
||||||
|
isBindingElement(element)
|
||||||
|
) {
|
||||||
bindOrUnbindLinearElement(
|
bindOrUnbindLinearElement(
|
||||||
element,
|
element,
|
||||||
startBindingElement,
|
startBindingElement,
|
||||||
@ -108,6 +123,7 @@ export const actionFinalize = register({
|
|||||||
scene,
|
scene,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLineElement(element) && !isValidPolygon(element.points)) {
|
if (isLineElement(element) && !isValidPolygon(element.points)) {
|
||||||
scene.mutateElement(element, {
|
scene.mutateElement(element, {
|
||||||
polygon: false,
|
polygon: false,
|
||||||
@ -159,10 +175,26 @@ export const actionFinalize = register({
|
|||||||
element.type !== "freedraw" &&
|
element.type !== "freedraw" &&
|
||||||
appState.lastPointerDownWith !== "touch"
|
appState.lastPointerDownWith !== "touch"
|
||||||
) {
|
) {
|
||||||
const { points, lastCommittedPoint } = element;
|
const { x: rx, y: ry, points, lastCommittedPoint } = element;
|
||||||
|
const lastGlobalPoint = pointFrom<GlobalPoint>(
|
||||||
|
rx + points[points.length - 1][0],
|
||||||
|
ry + points[points.length - 1][1],
|
||||||
|
);
|
||||||
|
const hoveredElementForBinding = getHoveredElementForBinding(
|
||||||
|
{
|
||||||
|
x: lastGlobalPoint[0],
|
||||||
|
y: lastGlobalPoint[1],
|
||||||
|
},
|
||||||
|
elements,
|
||||||
|
elementsMap,
|
||||||
|
app.state.zoom,
|
||||||
|
shouldTestInside(element),
|
||||||
|
isElbowArrow(element),
|
||||||
|
);
|
||||||
if (
|
if (
|
||||||
!lastCommittedPoint ||
|
!hoveredElementForBinding &&
|
||||||
points[points.length - 1] !== lastCommittedPoint
|
(!lastCommittedPoint ||
|
||||||
|
points[points.length - 1] !== lastCommittedPoint)
|
||||||
) {
|
) {
|
||||||
scene.mutateElement(element, {
|
scene.mutateElement(element, {
|
||||||
points: element.points.slice(0, -1),
|
points: element.points.slice(0, -1),
|
||||||
@ -282,6 +314,17 @@ export const actionFinalize = register({
|
|||||||
element && isLinearElement(element)
|
element && isLinearElement(element)
|
||||||
? new LinearElementEditor(element, arrayToMap(newElements))
|
? new LinearElementEditor(element, arrayToMap(newElements))
|
||||||
: appState.selectedLinearElement,
|
: appState.selectedLinearElement,
|
||||||
|
editingLinearElement: appState.newElement
|
||||||
|
? null
|
||||||
|
: appState.editingLinearElement
|
||||||
|
? {
|
||||||
|
...appState.editingLinearElement,
|
||||||
|
pointerDownState: {
|
||||||
|
...appState.editingLinearElement.pointerDownState,
|
||||||
|
arrowOtherPoint: undefined,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: null,
|
||||||
},
|
},
|
||||||
// TODO: #7348 we should not capture everything, but if we don't, it leads to incosistencies -> revisit
|
// TODO: #7348 we should not capture everything, but if we don't, it leads to incosistencies -> revisit
|
||||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||||
|
@ -124,6 +124,7 @@ export const getDefaultAppState = (): Omit<
|
|||||||
searchMatches: null,
|
searchMatches: null,
|
||||||
lockedMultiSelections: {},
|
lockedMultiSelections: {},
|
||||||
activeLockedId: null,
|
activeLockedId: null,
|
||||||
|
bindMode: "focus",
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -249,6 +250,7 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
searchMatches: { browser: false, export: false, server: false },
|
searchMatches: { browser: false, export: false, server: false },
|
||||||
lockedMultiSelections: { browser: true, export: true, server: true },
|
lockedMultiSelections: { browser: true, export: true, server: true },
|
||||||
activeLockedId: { browser: false, export: false, server: false },
|
activeLockedId: { browser: false, export: false, server: false },
|
||||||
|
bindMode: { browser: true, export: false, server: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
const _clearAppStateForStorage = <
|
const _clearAppStateForStorage = <
|
||||||
|
@ -100,6 +100,7 @@ import {
|
|||||||
randomInteger,
|
randomInteger,
|
||||||
CLASSES,
|
CLASSES,
|
||||||
Emitter,
|
Emitter,
|
||||||
|
BIND_MODE_TIMEOUT,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -232,9 +233,11 @@ import {
|
|||||||
hitElementBoundingBox,
|
hitElementBoundingBox,
|
||||||
isLineElement,
|
isLineElement,
|
||||||
isSimpleArrow,
|
isSimpleArrow,
|
||||||
|
getOutlineAvoidingPoint,
|
||||||
|
bindOrUnbindLinearElement,
|
||||||
} from "@excalidraw/element";
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
import type { LocalPoint, Radians } from "@excalidraw/math";
|
import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
@ -568,7 +571,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
public renderer: Renderer;
|
public renderer: Renderer;
|
||||||
public visibleElements: readonly NonDeletedExcalidrawElement[];
|
public visibleElements: readonly NonDeletedExcalidrawElement[];
|
||||||
private resizeObserver: ResizeObserver | undefined;
|
private resizeObserver: ResizeObserver | undefined;
|
||||||
private nearestScrollableContainer: HTMLElement | Document | undefined;
|
|
||||||
public library: AppClassProperties["library"];
|
public library: AppClassProperties["library"];
|
||||||
public libraryItemsFromStorage: LibraryItems | undefined;
|
public libraryItemsFromStorage: LibraryItems | undefined;
|
||||||
public id: string;
|
public id: string;
|
||||||
@ -598,6 +600,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
public flowChartCreator: FlowChartCreator = new FlowChartCreator();
|
public flowChartCreator: FlowChartCreator = new FlowChartCreator();
|
||||||
private flowChartNavigator: FlowChartNavigator = new FlowChartNavigator();
|
private flowChartNavigator: FlowChartNavigator = new FlowChartNavigator();
|
||||||
|
|
||||||
|
private bindModeHandler: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
hitLinkElement?: NonDeletedExcalidrawElement;
|
hitLinkElement?: NonDeletedExcalidrawElement;
|
||||||
lastPointerDownEvent: React.PointerEvent<HTMLElement> | null = null;
|
lastPointerDownEvent: React.PointerEvent<HTMLElement> | null = null;
|
||||||
lastPointerUpEvent: React.PointerEvent<HTMLElement> | PointerEvent | null =
|
lastPointerUpEvent: React.PointerEvent<HTMLElement> | PointerEvent | null =
|
||||||
@ -4387,6 +4391,14 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
{ informMutation: false, isDragging: false },
|
{ informMutation: false, isDragging: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isSimpleArrow(element)) {
|
||||||
|
// NOTE: Moving the bound arrow should unbind it, otherwise we would
|
||||||
|
// have weird situations, like 0 lenght arrow when the user moves
|
||||||
|
// the arrow outside a filled shape suddenly forcing the arrow start
|
||||||
|
// and end point to jump "outside" the shape.
|
||||||
|
bindOrUnbindLinearElement(element, null, null, this.scene);
|
||||||
|
}
|
||||||
|
|
||||||
updateBoundElements(element, this.scene, {
|
updateBoundElements(element, this.scene, {
|
||||||
simultaneouslyUpdated: selectedElements,
|
simultaneouslyUpdated: selectedElements,
|
||||||
});
|
});
|
||||||
@ -5864,10 +5876,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (editingLinearElement?.lastUncommittedPoint != null) {
|
if (
|
||||||
|
editingLinearElement?.lastUncommittedPoint != null ||
|
||||||
|
this.state.newElement
|
||||||
|
) {
|
||||||
this.maybeSuggestBindingAtCursor(
|
this.maybeSuggestBindingAtCursor(
|
||||||
scenePointer,
|
scenePointer,
|
||||||
editingLinearElement.elbowed,
|
editingLinearElement?.elbowed || false,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// causes stack overflow if not sync
|
// causes stack overflow if not sync
|
||||||
@ -5888,7 +5903,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
[scenePointer],
|
[scenePointer],
|
||||||
this.scene,
|
this.scene,
|
||||||
this.state.zoom,
|
this.state.zoom,
|
||||||
this.state.startBoundElement,
|
this.scene.getNonDeletedElementsMap(),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -5898,9 +5913,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
if (this.state.multiElement) {
|
if (this.state.multiElement) {
|
||||||
const { multiElement } = this.state;
|
const { multiElement } = this.state;
|
||||||
const { x: rx, y: ry } = multiElement;
|
const { x: rx, y: ry, points, lastCommittedPoint } = multiElement;
|
||||||
|
|
||||||
const { points, lastCommittedPoint } = multiElement;
|
|
||||||
const lastPoint = points[points.length - 1];
|
const lastPoint = points[points.length - 1];
|
||||||
|
|
||||||
setCursorForShape(this.interactiveCanvas, this.state);
|
setCursorForShape(this.interactiveCanvas, this.state);
|
||||||
@ -5946,19 +5959,42 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
{ informMutation: false, isDragging: false },
|
{ informMutation: false, isDragging: false },
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
const hoveredElement = getHoveredElementForBinding(
|
||||||
|
{
|
||||||
|
x: scenePointerX,
|
||||||
|
y: scenePointerY,
|
||||||
|
},
|
||||||
|
this.scene.getNonDeletedElements(),
|
||||||
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
this.state.zoom,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
);
|
||||||
const [gridX, gridY] = getGridPoint(
|
const [gridX, gridY] = getGridPoint(
|
||||||
scenePointerX,
|
scenePointerX,
|
||||||
scenePointerY,
|
scenePointerY,
|
||||||
event[KEYS.CTRL_OR_CMD] || isElbowArrow(multiElement)
|
event[KEYS.CTRL_OR_CMD] || hoveredElement
|
||||||
? null
|
? null
|
||||||
: this.getEffectiveGridSize(),
|
: this.getEffectiveGridSize(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const avoidancePoint =
|
||||||
|
hoveredElement &&
|
||||||
|
getOutlineAvoidingPoint(
|
||||||
|
multiElement,
|
||||||
|
hoveredElement,
|
||||||
|
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
|
||||||
|
multiElement.points.length - 1,
|
||||||
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
);
|
||||||
|
|
||||||
const [lastCommittedX, lastCommittedY] =
|
const [lastCommittedX, lastCommittedY] =
|
||||||
multiElement?.lastCommittedPoint ?? [0, 0];
|
multiElement?.lastCommittedPoint ?? [0, 0];
|
||||||
|
|
||||||
let dxFromLastCommitted = gridX - rx - lastCommittedX;
|
let dxFromLastCommitted =
|
||||||
let dyFromLastCommitted = gridY - ry - lastCommittedY;
|
(avoidancePoint ? avoidancePoint[0] : gridX) - rx - lastCommittedX;
|
||||||
|
let dyFromLastCommitted =
|
||||||
|
(avoidancePoint ? avoidancePoint[1] : gridY) - ry - lastCommittedY;
|
||||||
|
|
||||||
if (shouldRotateWithDiscreteAngle(event)) {
|
if (shouldRotateWithDiscreteAngle(event)) {
|
||||||
({ width: dxFromLastCommitted, height: dyFromLastCommitted } =
|
({ width: dxFromLastCommitted, height: dyFromLastCommitted } =
|
||||||
@ -7704,10 +7740,26 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { x: rx, y: ry, lastCommittedPoint } = multiElement;
|
const { x: rx, y: ry, lastCommittedPoint } = multiElement;
|
||||||
|
const lastGlobalPoint = pointFrom<GlobalPoint>(
|
||||||
|
rx + multiElement.points[multiElement.points.length - 1][0],
|
||||||
|
ry + multiElement.points[multiElement.points.length - 1][1],
|
||||||
|
);
|
||||||
|
const hoveredElementForBinding = getHoveredElementForBinding(
|
||||||
|
{
|
||||||
|
x: lastGlobalPoint[0],
|
||||||
|
y: lastGlobalPoint[1],
|
||||||
|
},
|
||||||
|
this.scene.getNonDeletedElements(),
|
||||||
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
this.state.zoom,
|
||||||
|
true,
|
||||||
|
isElbowArrow(multiElement),
|
||||||
|
);
|
||||||
|
|
||||||
// clicking inside commit zone → finalize arrow
|
// clicking inside commit zone → finalize arrow
|
||||||
if (
|
if (
|
||||||
multiElement.points.length > 1 &&
|
hoveredElementForBinding ||
|
||||||
|
(multiElement.points.length > 1 &&
|
||||||
lastCommittedPoint &&
|
lastCommittedPoint &&
|
||||||
pointDistance(
|
pointDistance(
|
||||||
pointFrom(
|
pointFrom(
|
||||||
@ -7715,7 +7767,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
pointerDownState.origin.y - ry,
|
pointerDownState.origin.y - ry,
|
||||||
),
|
),
|
||||||
lastCommittedPoint,
|
lastCommittedPoint,
|
||||||
) < LINE_CONFIRM_THRESHOLD
|
) < LINE_CONFIRM_THRESHOLD)
|
||||||
) {
|
) {
|
||||||
this.actionManager.executeAction(actionFinalize);
|
this.actionManager.executeAction(actionFinalize);
|
||||||
return;
|
return;
|
||||||
@ -7758,7 +7810,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
elementType === "arrow"
|
elementType === "arrow"
|
||||||
? [currentItemStartArrowhead, currentItemEndArrowhead]
|
? [currentItemStartArrowhead, currentItemEndArrowhead]
|
||||||
: [null, null];
|
: [null, null];
|
||||||
|
|
||||||
const element =
|
const element =
|
||||||
elementType === "arrow"
|
elementType === "arrow"
|
||||||
? newArrowElement({
|
? newArrowElement({
|
||||||
@ -7806,21 +7857,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
locked: false,
|
locked: false,
|
||||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||||
});
|
});
|
||||||
this.setState((prevState) => {
|
|
||||||
const nextSelectedElementIds = {
|
|
||||||
...prevState.selectedElementIds,
|
|
||||||
};
|
|
||||||
delete nextSelectedElementIds[element.id];
|
|
||||||
return {
|
|
||||||
selectedElementIds: makeNextSelectedElementIds(
|
|
||||||
nextSelectedElementIds,
|
|
||||||
prevState,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
this.scene.mutateElement(element, {
|
|
||||||
points: [...element.points, pointFrom<LocalPoint>(0, 0)],
|
|
||||||
});
|
|
||||||
const boundElement = getHoveredElementForBinding(
|
const boundElement = getHoveredElementForBinding(
|
||||||
pointerDownState.origin,
|
pointerDownState.origin,
|
||||||
this.scene.getNonDeletedElements(),
|
this.scene.getNonDeletedElements(),
|
||||||
@ -7830,11 +7867,72 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
isElbowArrow(element),
|
isElbowArrow(element),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isSimpleArrow(element)) {
|
||||||
|
this.setState((prevState) => {
|
||||||
|
const linearElement = new LinearElementEditor(
|
||||||
|
element,
|
||||||
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
);
|
||||||
|
const linearElementEditor = {
|
||||||
|
...linearElement,
|
||||||
|
startBindingElement: boundElement,
|
||||||
|
pointerDownState: {
|
||||||
|
...linearElement.pointerDownState,
|
||||||
|
arrowOtherPoint: pointFrom<GlobalPoint>(
|
||||||
|
pointerDownState.origin.x,
|
||||||
|
pointerDownState.origin.y,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const nextSelectedElementIds = makeNextSelectedElementIds(
|
||||||
|
{ [element.id]: true },
|
||||||
|
prevState,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectedElementIds: nextSelectedElementIds,
|
||||||
|
editingLinearElement: linearElementEditor,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scene.mutateElement(element, {
|
||||||
|
points: [...element.points, pointFrom<LocalPoint>(0, 0)],
|
||||||
|
});
|
||||||
this.scene.insertElement(element);
|
this.scene.insertElement(element);
|
||||||
this.setState({
|
this.setState((prevState) => {
|
||||||
|
let linearElementEditor = null;
|
||||||
|
let nextSelectedElementIds = prevState.selectedElementIds;
|
||||||
|
if (isSimpleArrow(element)) {
|
||||||
|
const linearElement = new LinearElementEditor(
|
||||||
|
element,
|
||||||
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
);
|
||||||
|
linearElementEditor = {
|
||||||
|
...linearElement,
|
||||||
|
startBindingElement: boundElement,
|
||||||
|
pointerDownState: {
|
||||||
|
...linearElement.pointerDownState,
|
||||||
|
arrowOtherPoint: pointFrom<GlobalPoint>(
|
||||||
|
pointerDownState.origin.x,
|
||||||
|
pointerDownState.origin.y,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
nextSelectedElementIds = makeNextSelectedElementIds(
|
||||||
|
{ [element.id]: true },
|
||||||
|
prevState,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prevState,
|
||||||
newElement: element,
|
newElement: element,
|
||||||
startBoundElement: boundElement,
|
startBoundElement: boundElement,
|
||||||
suggestedBindings: [],
|
suggestedBindings: [],
|
||||||
|
selectedElementIds: nextSelectedElementIds,
|
||||||
|
editingLinearElement: linearElementEditor,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -8220,12 +8318,39 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Timed bind mode handler for arrow elements
|
||||||
|
if (this.state.bindMode === "focus") {
|
||||||
|
const pointerMovementDistance = Math.hypot(
|
||||||
|
(this.lastPointerMoveCoords?.x ?? Infinity) - pointerCoords.x,
|
||||||
|
);
|
||||||
|
if (this.bindModeHandler && pointerMovementDistance < 1) {
|
||||||
|
clearTimeout(this.bindModeHandler);
|
||||||
|
}
|
||||||
|
this.bindModeHandler = setTimeout(() => {
|
||||||
|
const hoveredElement = getHoveredElementForBinding(
|
||||||
|
pointerCoords,
|
||||||
|
this.scene.getNonDeletedElements(),
|
||||||
|
elementsMap,
|
||||||
|
this.state.zoom,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hoveredElement) {
|
||||||
|
this.setState({
|
||||||
|
bindMode: "fixed",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.bindModeHandler = null;
|
||||||
|
}
|
||||||
|
}, BIND_MODE_TIMEOUT);
|
||||||
|
}
|
||||||
|
|
||||||
const newState = LinearElementEditor.handlePointDragging(
|
const newState = LinearElementEditor.handlePointDragging(
|
||||||
event,
|
event,
|
||||||
this,
|
this,
|
||||||
pointerCoords.x,
|
pointerCoords.x,
|
||||||
pointerCoords.y,
|
pointerCoords.y,
|
||||||
linearElementEditor,
|
linearElementEditor,
|
||||||
|
(element) => this.getElementHitThreshold(element),
|
||||||
);
|
);
|
||||||
if (newState) {
|
if (newState) {
|
||||||
pointerDownState.lastCoords.x = pointerCoords.x;
|
pointerDownState.lastCoords.x = pointerCoords.x;
|
||||||
@ -8672,9 +8797,76 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
} else if (isLinearElement(newElement)) {
|
} else if (isLinearElement(newElement)) {
|
||||||
pointerDownState.drag.hasOccurred = true;
|
pointerDownState.drag.hasOccurred = true;
|
||||||
const points = newElement.points;
|
const points = newElement.points;
|
||||||
|
const startBindingElement =
|
||||||
|
this.state.editingLinearElement?.startBindingElement;
|
||||||
|
let [firstPointX, firstPointY] =
|
||||||
|
LinearElementEditor.getPointGlobalCoordinates(
|
||||||
|
newElement,
|
||||||
|
newElement.points[0],
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
let dx = gridX - newElement.x;
|
let dx = gridX - newElement.x;
|
||||||
let dy = gridY - newElement.y;
|
let dy = gridY - newElement.y;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isElbowArrow(newElement) &&
|
||||||
|
this.state.editingLinearElement &&
|
||||||
|
isBindingElement(newElement, false)
|
||||||
|
) {
|
||||||
|
// Handles the case where we need to "jump out" the simple arrow
|
||||||
|
// start point as we drag-create it.
|
||||||
|
const hoveredElement = getHoveredElementForBinding(
|
||||||
|
{ x: gridX, y: gridY },
|
||||||
|
this.scene.getNonDeletedElements(),
|
||||||
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
this.state.zoom,
|
||||||
|
isElbowArrow(newElement),
|
||||||
|
isElbowArrow(newElement),
|
||||||
|
);
|
||||||
|
const arrowIsInsideTheSameElement =
|
||||||
|
startBindingElement &&
|
||||||
|
startBindingElement !== "keep" &&
|
||||||
|
hoveredElement?.id === startBindingElement.id;
|
||||||
|
|
||||||
|
if (!arrowIsInsideTheSameElement) {
|
||||||
|
const [outlinePointX, outlinePointY] = getOutlineAvoidingPoint(
|
||||||
|
newElement,
|
||||||
|
hoveredElement,
|
||||||
|
hoveredElement
|
||||||
|
? pointFrom(pointerCoords.x, pointerCoords.y)
|
||||||
|
: pointFrom(gridX, gridY),
|
||||||
|
newElement.points.length - 1,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
const otherHoveredElement = getHoveredElementForBinding(
|
||||||
|
{ x: firstPointX, y: firstPointY },
|
||||||
|
this.scene.getNonDeletedElements(),
|
||||||
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
this.state.zoom,
|
||||||
|
isElbowArrow(newElement),
|
||||||
|
isElbowArrow(newElement),
|
||||||
|
);
|
||||||
|
[firstPointX, firstPointY] = getOutlineAvoidingPoint(
|
||||||
|
newElement,
|
||||||
|
otherHoveredElement,
|
||||||
|
pointFrom(firstPointX, firstPointY),
|
||||||
|
0,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
dx = outlinePointX - firstPointX;
|
||||||
|
dy = outlinePointY - firstPointY;
|
||||||
|
} else {
|
||||||
|
firstPointX =
|
||||||
|
this.state.editingLinearElement?.pointerDownState
|
||||||
|
.arrowOtherPoint?.[0] ?? firstPointX;
|
||||||
|
firstPointY =
|
||||||
|
this.state.editingLinearElement?.pointerDownState
|
||||||
|
.arrowOtherPoint?.[1] ?? firstPointY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (shouldRotateWithDiscreteAngle(event) && points.length === 2) {
|
if (shouldRotateWithDiscreteAngle(event) && points.length === 2) {
|
||||||
({ width: dx, height: dy } = getLockedLinearCursorAlignSize(
|
({ width: dx, height: dy } = getLockedLinearCursorAlignSize(
|
||||||
newElement.x,
|
newElement.x,
|
||||||
@ -8688,6 +8880,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.scene.mutateElement(
|
this.scene.mutateElement(
|
||||||
newElement,
|
newElement,
|
||||||
{
|
{
|
||||||
|
x: firstPointX,
|
||||||
|
y: firstPointY,
|
||||||
points: [...points, pointFrom<LocalPoint>(dx, dy)],
|
points: [...points, pointFrom<LocalPoint>(dx, dy)],
|
||||||
},
|
},
|
||||||
{ informMutation: false, isDragging: false },
|
{ informMutation: false, isDragging: false },
|
||||||
@ -8699,6 +8893,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.scene.mutateElement(
|
this.scene.mutateElement(
|
||||||
newElement,
|
newElement,
|
||||||
{
|
{
|
||||||
|
x: firstPointX,
|
||||||
|
y: firstPointY,
|
||||||
points: [...points.slice(0, -1), pointFrom<LocalPoint>(dx, dy)],
|
points: [...points.slice(0, -1), pointFrom<LocalPoint>(dx, dy)],
|
||||||
},
|
},
|
||||||
{ isDragging: true, informMutation: false },
|
{ isDragging: true, informMutation: false },
|
||||||
@ -8717,6 +8913,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
[pointerCoords],
|
[pointerCoords],
|
||||||
this.scene,
|
this.scene,
|
||||||
this.state.zoom,
|
this.state.zoom,
|
||||||
|
elementsMap,
|
||||||
this.state.startBoundElement,
|
this.state.startBoundElement,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@ -8953,8 +9150,14 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.bindModeHandler) {
|
||||||
|
clearTimeout(this.bindModeHandler);
|
||||||
|
this.bindModeHandler = null;
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedElementsAreBeingDragged: false,
|
selectedElementsAreBeingDragged: false,
|
||||||
|
bindMode: "focus",
|
||||||
});
|
});
|
||||||
const elementsMap = this.scene.getNonDeletedElementsMap();
|
const elementsMap = this.scene.getNonDeletedElementsMap();
|
||||||
|
|
||||||
@ -8977,7 +9180,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
// Handle end of dragging a point of a linear element, might close a loop
|
// Handle end of dragging a point of a linear element, might close a loop
|
||||||
// and sets binding element
|
// and sets binding element
|
||||||
if (this.state.editingLinearElement) {
|
if (this.state.editingLinearElement && !this.state.newElement) {
|
||||||
if (
|
if (
|
||||||
!pointerDownState.boxSelection.hasOccurred &&
|
!pointerDownState.boxSelection.hasOccurred &&
|
||||||
pointerDownState.hit?.element?.id !==
|
pointerDownState.hit?.element?.id !==
|
||||||
@ -9116,10 +9319,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
newElement,
|
newElement,
|
||||||
});
|
});
|
||||||
} else if (pointerDownState.drag.hasOccurred && !multiElement) {
|
} else if (pointerDownState.drag.hasOccurred && !multiElement) {
|
||||||
if (
|
if (isBindingElement(newElement, false)) {
|
||||||
isBindingEnabled(this.state) &&
|
|
||||||
isBindingElement(newElement, false)
|
|
||||||
) {
|
|
||||||
this.actionManager.executeAction(actionFinalize, "ui", {
|
this.actionManager.executeAction(actionFinalize, "ui", {
|
||||||
event: childEvent,
|
event: childEvent,
|
||||||
sceneCoords,
|
sceneCoords,
|
||||||
|
@ -88,8 +88,12 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "ellipse-1",
|
"elementId": "ellipse-1",
|
||||||
"focus": -0.007519379844961235,
|
"fixedPoint": [
|
||||||
"gap": 11.562288374879595,
|
0.04,
|
||||||
|
0.4633333333333333,
|
||||||
|
],
|
||||||
|
"focus": 0,
|
||||||
|
"gap": 0,
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
@ -118,8 +122,12 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id49",
|
"elementId": "id49",
|
||||||
"focus": -0.0813953488372095,
|
"fixedPoint": [
|
||||||
"gap": 1,
|
1,
|
||||||
|
0.5001,
|
||||||
|
],
|
||||||
|
"focus": 0,
|
||||||
|
"gap": 0,
|
||||||
},
|
},
|
||||||
"strokeColor": "#1864ab",
|
"strokeColor": "#1864ab",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -334,8 +342,12 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "text-2",
|
"elementId": "text-2",
|
||||||
|
"fixedPoint": [
|
||||||
|
-2.05,
|
||||||
|
0.5001,
|
||||||
|
],
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 16,
|
"gap": 0,
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
@ -364,8 +376,12 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "text-1",
|
"elementId": "text-1",
|
||||||
|
"fixedPoint": [
|
||||||
|
1,
|
||||||
|
0.5001,
|
||||||
|
],
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 0,
|
||||||
},
|
},
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -436,8 +452,12 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id42",
|
"elementId": "id42",
|
||||||
"focus": -0,
|
"fixedPoint": [
|
||||||
"gap": 1,
|
0,
|
||||||
|
0.5001,
|
||||||
|
],
|
||||||
|
"focus": 0,
|
||||||
|
"gap": 0,
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
@ -466,8 +486,12 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id41",
|
"elementId": "id41",
|
||||||
|
"fixedPoint": [
|
||||||
|
1,
|
||||||
|
0.5001,
|
||||||
|
],
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 0,
|
||||||
},
|
},
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -612,8 +636,12 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id46",
|
"elementId": "id46",
|
||||||
"focus": -0,
|
"fixedPoint": [
|
||||||
"gap": 1,
|
0,
|
||||||
|
0.5001,
|
||||||
|
],
|
||||||
|
"focus": 0,
|
||||||
|
"gap": 0,
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
@ -642,8 +670,12 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id45",
|
"elementId": "id45",
|
||||||
|
"fixedPoint": [
|
||||||
|
1,
|
||||||
|
0.5001,
|
||||||
|
],
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 0,
|
||||||
},
|
},
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -1539,8 +1571,12 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "B",
|
"elementId": "B",
|
||||||
|
"fixedPoint": [
|
||||||
|
0.46387050630528887,
|
||||||
|
0.48466257668711654,
|
||||||
|
],
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 32,
|
"gap": 0,
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
@ -1567,8 +1603,12 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "Bob",
|
"elementId": "Bob",
|
||||||
|
"fixedPoint": [
|
||||||
|
0.39381496335223337,
|
||||||
|
1,
|
||||||
|
],
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 0,
|
||||||
},
|
},
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
|
@ -433,11 +433,11 @@ describe("Test Transform", () => {
|
|||||||
startBinding: {
|
startBinding: {
|
||||||
elementId: rectangle.id,
|
elementId: rectangle.id,
|
||||||
focus: 0,
|
focus: 0,
|
||||||
gap: 1,
|
gap: 0,
|
||||||
},
|
},
|
||||||
endBinding: {
|
endBinding: {
|
||||||
elementId: ellipse.id,
|
elementId: ellipse.id,
|
||||||
focus: -0,
|
focus: 0,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -518,11 +518,11 @@ describe("Test Transform", () => {
|
|||||||
startBinding: {
|
startBinding: {
|
||||||
elementId: text2.id,
|
elementId: text2.id,
|
||||||
focus: 0,
|
focus: 0,
|
||||||
gap: 1,
|
gap: 0,
|
||||||
},
|
},
|
||||||
endBinding: {
|
endBinding: {
|
||||||
elementId: text3.id,
|
elementId: text3.id,
|
||||||
focus: -0,
|
focus: 0,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"items": [
|
"items": [
|
||||||
@ -1083,6 +1084,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -1296,6 +1298,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -1626,6 +1629,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -1956,6 +1960,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -2169,6 +2174,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -2409,6 +2415,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -2706,6 +2713,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -3077,6 +3085,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -3569,6 +3578,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -3891,6 +3901,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -4213,6 +4224,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -4623,6 +4635,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"items": [
|
"items": [
|
||||||
@ -5839,6 +5852,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"items": [
|
"items": [
|
||||||
@ -7106,6 +7120,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"items": [
|
"items": [
|
||||||
@ -7772,6 +7787,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"items": [
|
"items": [
|
||||||
@ -8762,6 +8778,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"items": [
|
"items": [
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -193,6 +193,7 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||||||
"lastCommittedPoint": null,
|
"lastCommittedPoint": null,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
|
"moveMidPointsWithElement": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
|
@ -11,6 +11,7 @@ exports[`given element A and group of elements B and given both are selected whe
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -436,6 +437,7 @@ exports[`given element A and group of elements B and given both are selected whe
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -851,6 +853,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -1416,6 +1419,7 @@ exports[`regression tests > Drags selected element when hitting only bounding bo
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -1622,6 +1626,7 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -2005,6 +2010,7 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -2249,6 +2255,7 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = `
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -2428,6 +2435,7 @@ exports[`regression tests > can drag element that covers another element, while
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -2752,6 +2760,7 @@ exports[`regression tests > change the properties of a shape > [end of test] app
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -3006,6 +3015,7 @@ exports[`regression tests > click on an element and drag it > [dragged] appState
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -3246,6 +3256,7 @@ exports[`regression tests > click on an element and drag it > [end of test] appS
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -3481,6 +3492,7 @@ exports[`regression tests > click to select a shape > [end of test] appState 1`]
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -3738,6 +3750,7 @@ exports[`regression tests > click-drag to select a group > [end of test] appStat
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -4051,6 +4064,7 @@ exports[`regression tests > deleting last but one element in editing group shoul
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -4486,6 +4500,7 @@ exports[`regression tests > deselects group of selected elements on pointer down
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -4768,6 +4783,7 @@ exports[`regression tests > deselects group of selected elements on pointer up w
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -5043,6 +5059,7 @@ exports[`regression tests > deselects selected element on pointer down when poin
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -5250,6 +5267,7 @@ exports[`regression tests > deselects selected element, on pointer up, when clic
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -5449,6 +5467,7 @@ exports[`regression tests > double click to edit a group > [end of test] appStat
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -5841,6 +5860,7 @@ exports[`regression tests > drags selected elements from point inside common bou
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -6137,6 +6157,7 @@ exports[`regression tests > draw every type of shape > [end of test] appState 1`
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "freedraw",
|
"type": "freedraw",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -6558,12 +6579,14 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
|
|||||||
"appState": AppStateDelta {
|
"appState": AppStateDelta {
|
||||||
"delta": Delta {
|
"delta": Delta {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
|
"editingLinearElementId": "id15",
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id15": true,
|
"id15": true,
|
||||||
},
|
},
|
||||||
"selectedLinearElementId": null,
|
"selectedLinearElementId": null,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
|
"editingLinearElementId": null,
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id12": true,
|
"id12": true,
|
||||||
},
|
},
|
||||||
@ -6694,9 +6717,11 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
|
|||||||
"appState": AppStateDelta {
|
"appState": AppStateDelta {
|
||||||
"delta": Delta {
|
"delta": Delta {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
|
"editingLinearElementId": null,
|
||||||
"selectedLinearElementId": "id15",
|
"selectedLinearElementId": "id15",
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
|
"editingLinearElementId": "id15",
|
||||||
"selectedLinearElementId": null,
|
"selectedLinearElementId": null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -6968,6 +6993,7 @@ exports[`regression tests > given a group of selected elements with an element t
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -7301,6 +7327,7 @@ exports[`regression tests > given a selected element A and a not selected elemen
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -7579,6 +7606,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -7813,6 +7841,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -8052,6 +8081,7 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] appStat
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -8231,6 +8261,7 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] appState
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -8410,6 +8441,7 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] appState
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -8589,6 +8621,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -8814,6 +8847,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -9037,6 +9071,7 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "freedraw",
|
"type": "freedraw",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -9232,6 +9267,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -9457,6 +9493,7 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -9636,6 +9673,7 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -9859,6 +9897,7 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -10038,6 +10077,7 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "freedraw",
|
"type": "freedraw",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -10233,6 +10273,7 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -10412,6 +10453,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -10942,6 +10984,7 @@ exports[`regression tests > noop interaction after undo shouldn't create history
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -11221,6 +11264,7 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = `
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -11343,6 +11387,7 @@ exports[`regression tests > shift click on selected element should deselect it o
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -11542,6 +11587,7 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -11860,6 +11906,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -12288,6 +12335,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -12927,6 +12975,7 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -13052,6 +13101,7 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`]
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -13682,6 +13732,7 @@ exports[`regression tests > switches from group of selected elements to another
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -14020,6 +14071,7 @@ exports[`regression tests > switches selected element on pointer down > [end of
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -14283,6 +14335,7 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`]
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -14405,6 +14458,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -14520,9 +14574,11 @@ exports[`regression tests > undo/redo drawing an element > [end of test] redo st
|
|||||||
"appState": AppStateDelta {
|
"appState": AppStateDelta {
|
||||||
"delta": Delta {
|
"delta": Delta {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
|
"editingLinearElementId": "id6",
|
||||||
"selectedLinearElementId": null,
|
"selectedLinearElementId": null,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
|
"editingLinearElementId": null,
|
||||||
"selectedLinearElementId": "id6",
|
"selectedLinearElementId": "id6",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -14597,11 +14653,13 @@ exports[`regression tests > undo/redo drawing an element > [end of test] redo st
|
|||||||
"appState": AppStateDelta {
|
"appState": AppStateDelta {
|
||||||
"delta": Delta {
|
"delta": Delta {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
|
"editingLinearElementId": null,
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id3": true,
|
"id3": true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
|
"editingLinearElementId": "id6",
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id6": true,
|
"id6": true,
|
||||||
},
|
},
|
||||||
@ -14793,6 +14851,7 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "text",
|
"type": "text",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -14915,6 +14974,7 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = `
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
|
@ -157,7 +157,7 @@ describe("Test dragCreate", () => {
|
|||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`5`,
|
`6`,
|
||||||
);
|
);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
|
@ -1148,7 +1148,7 @@ describe("history", () => {
|
|||||||
expect(API.getUndoStack().length).toBe(2);
|
expect(API.getUndoStack().length).toBe(2);
|
||||||
expect(API.getRedoStack().length).toBe(4);
|
expect(API.getRedoStack().length).toBe(4);
|
||||||
expect(assertSelectedElements(h.elements[0]));
|
expect(assertSelectedElements(h.elements[0]));
|
||||||
expect(h.state.editingLinearElement).toBeNull();
|
expect(h.state.editingLinearElement).not.toBeNull();
|
||||||
expect(h.state.selectedLinearElement).toBeNull(); // undo `actionFinalize`
|
expect(h.state.selectedLinearElement).toBeNull(); // undo `actionFinalize`
|
||||||
expect(h.elements).toEqual([
|
expect(h.elements).toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@ -1165,7 +1165,7 @@ describe("history", () => {
|
|||||||
expect(API.getUndoStack().length).toBe(1);
|
expect(API.getUndoStack().length).toBe(1);
|
||||||
expect(API.getRedoStack().length).toBe(5);
|
expect(API.getRedoStack().length).toBe(5);
|
||||||
expect(assertSelectedElements(h.elements[0]));
|
expect(assertSelectedElements(h.elements[0]));
|
||||||
expect(h.state.editingLinearElement).toBeNull();
|
expect(h.state.editingLinearElement).not.toBeNull();
|
||||||
expect(h.state.selectedLinearElement).toBeNull();
|
expect(h.state.selectedLinearElement).toBeNull();
|
||||||
expect(h.elements).toEqual([
|
expect(h.elements).toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@ -1197,7 +1197,7 @@ describe("history", () => {
|
|||||||
expect(API.getUndoStack().length).toBe(1);
|
expect(API.getUndoStack().length).toBe(1);
|
||||||
expect(API.getRedoStack().length).toBe(5);
|
expect(API.getRedoStack().length).toBe(5);
|
||||||
expect(assertSelectedElements(h.elements[0]));
|
expect(assertSelectedElements(h.elements[0]));
|
||||||
expect(h.state.editingLinearElement).toBeNull();
|
expect(h.state.editingLinearElement).not.toBeNull();
|
||||||
expect(h.state.selectedLinearElement).toBeNull();
|
expect(h.state.selectedLinearElement).toBeNull();
|
||||||
expect(h.elements).toEqual([
|
expect(h.elements).toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@ -1213,7 +1213,7 @@ describe("history", () => {
|
|||||||
expect(API.getUndoStack().length).toBe(2);
|
expect(API.getUndoStack().length).toBe(2);
|
||||||
expect(API.getRedoStack().length).toBe(4);
|
expect(API.getRedoStack().length).toBe(4);
|
||||||
expect(assertSelectedElements(h.elements[0]));
|
expect(assertSelectedElements(h.elements[0]));
|
||||||
expect(h.state.editingLinearElement).toBeNull();
|
expect(h.state.editingLinearElement).not.toBeNull();
|
||||||
expect(h.state.selectedLinearElement).toBeNull(); // undo `actionFinalize`
|
expect(h.state.selectedLinearElement).toBeNull(); // undo `actionFinalize`
|
||||||
expect(h.elements).toEqual([
|
expect(h.elements).toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@ -1638,13 +1638,15 @@ describe("history", () => {
|
|||||||
expect(API.getUndoStack().length).toBe(5);
|
expect(API.getUndoStack().length).toBe(5);
|
||||||
expect(arrow.startBinding).toEqual({
|
expect(arrow.startBinding).toEqual({
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: expect.arrayContaining([1, 0.5001]),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
focus: 0,
|
||||||
|
gap: 0,
|
||||||
});
|
});
|
||||||
expect(arrow.endBinding).toEqual({
|
expect(arrow.endBinding).toEqual({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: expect.arrayContaining([0, 0.5001]),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
focus: 0,
|
||||||
|
gap: 0,
|
||||||
});
|
});
|
||||||
expect(rect1.boundElements).toStrictEqual([
|
expect(rect1.boundElements).toStrictEqual([
|
||||||
{ id: text.id, type: "text" },
|
{ id: text.id, type: "text" },
|
||||||
@ -1661,13 +1663,15 @@ describe("history", () => {
|
|||||||
expect(API.getRedoStack().length).toBe(1);
|
expect(API.getRedoStack().length).toBe(1);
|
||||||
expect(arrow.startBinding).toEqual({
|
expect(arrow.startBinding).toEqual({
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: expect.arrayContaining([1, 0.5001]),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
focus: 0,
|
||||||
|
gap: 0,
|
||||||
});
|
});
|
||||||
expect(arrow.endBinding).toEqual({
|
expect(arrow.endBinding).toEqual({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: expect.arrayContaining([0, 0.5001]),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
focus: 0,
|
||||||
|
gap: 0,
|
||||||
});
|
});
|
||||||
expect(h.elements).toEqual([
|
expect(h.elements).toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@ -1684,13 +1688,15 @@ describe("history", () => {
|
|||||||
expect(API.getRedoStack().length).toBe(0);
|
expect(API.getRedoStack().length).toBe(0);
|
||||||
expect(arrow.startBinding).toEqual({
|
expect(arrow.startBinding).toEqual({
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: expect.arrayContaining([1, 0.5001]),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
focus: 0,
|
||||||
|
gap: 0,
|
||||||
});
|
});
|
||||||
expect(arrow.endBinding).toEqual({
|
expect(arrow.endBinding).toEqual({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: expect.arrayContaining([0, 0.5001]),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
focus: 0,
|
||||||
|
gap: 0,
|
||||||
});
|
});
|
||||||
expect(h.elements).toEqual([
|
expect(h.elements).toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@ -1715,13 +1721,15 @@ describe("history", () => {
|
|||||||
expect(API.getRedoStack().length).toBe(0);
|
expect(API.getRedoStack().length).toBe(0);
|
||||||
expect(arrow.startBinding).toEqual({
|
expect(arrow.startBinding).toEqual({
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: expect.arrayContaining([1, 0.5001]),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
focus: 0,
|
||||||
|
gap: 0,
|
||||||
});
|
});
|
||||||
expect(arrow.endBinding).toEqual({
|
expect(arrow.endBinding).toEqual({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: expect.arrayContaining([0, 0.5001]),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
focus: 0,
|
||||||
|
gap: 0,
|
||||||
});
|
});
|
||||||
expect(h.elements).toEqual([
|
expect(h.elements).toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@ -1738,13 +1746,15 @@ describe("history", () => {
|
|||||||
expect(API.getRedoStack().length).toBe(1);
|
expect(API.getRedoStack().length).toBe(1);
|
||||||
expect(arrow.startBinding).toEqual({
|
expect(arrow.startBinding).toEqual({
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: expect.arrayContaining([1, 0.5001]),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
focus: 0,
|
||||||
|
gap: 0,
|
||||||
});
|
});
|
||||||
expect(arrow.endBinding).toEqual({
|
expect(arrow.endBinding).toEqual({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: expect.arrayContaining([0, 0.5001]),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
focus: 0,
|
||||||
|
gap: 0,
|
||||||
});
|
});
|
||||||
expect(h.elements).toEqual([
|
expect(h.elements).toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@ -5079,13 +5089,11 @@ describe("history", () => {
|
|||||||
id: arrowId,
|
id: arrowId,
|
||||||
startBinding: expect.objectContaining({
|
startBinding: expect.objectContaining({
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
focus: 0,
|
fixedPoint: expect.arrayContaining([1, 0.5001]),
|
||||||
gap: 1,
|
|
||||||
}),
|
}),
|
||||||
endBinding: expect.objectContaining({
|
endBinding: expect.objectContaining({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
focus: -0,
|
fixedPoint: expect.arrayContaining([0, 0.5001]),
|
||||||
gap: 1,
|
|
||||||
}),
|
}),
|
||||||
isDeleted: true,
|
isDeleted: true,
|
||||||
}),
|
}),
|
||||||
|
@ -97,7 +97,7 @@ describe("move element", () => {
|
|||||||
new Pointer("mouse").clickOn(rectB);
|
new Pointer("mouse").clickOn(rectB);
|
||||||
|
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`17`,
|
`18`,
|
||||||
);
|
);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`13`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`13`);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
|
@ -118,7 +118,7 @@ describe("multi point mode in linear elements", () => {
|
|||||||
key: KEYS.ENTER,
|
key: KEYS.ENTER,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`9`);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
|
|
||||||
|
@ -425,7 +425,7 @@ describe("select single element on the scene", () => {
|
|||||||
fireEvent.pointerDown(canvas, { clientX: 40, clientY: 40 });
|
fireEvent.pointerDown(canvas, { clientX: 40, clientY: 40 });
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(8);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
@ -487,7 +487,12 @@ describe("tool locking & selection", () => {
|
|||||||
expect(h.state.activeTool.locked).toBe(true);
|
expect(h.state.activeTool.locked).toBe(true);
|
||||||
|
|
||||||
for (const { value } of Object.values(SHAPES)) {
|
for (const { value } of Object.values(SHAPES)) {
|
||||||
if (value !== "image" && value !== "selection" && value !== "eraser") {
|
if (
|
||||||
|
value !== "image" &&
|
||||||
|
value !== "selection" &&
|
||||||
|
value !== "eraser" &&
|
||||||
|
value !== "arrow"
|
||||||
|
) {
|
||||||
const element = UI.createElement(value);
|
const element = UI.createElement(value);
|
||||||
expect(h.state.selectedElementIds[element.id]).not.toBe(true);
|
expect(h.state.selectedElementIds[element.id]).not.toBe(true);
|
||||||
}
|
}
|
||||||
|
@ -444,6 +444,7 @@ export interface AppState {
|
|||||||
// as elements are unlocked, we remove the groupId from the elements
|
// as elements are unlocked, we remove the groupId from the elements
|
||||||
// and also remove groupId from this map
|
// and also remove groupId from this map
|
||||||
lockedMultiSelections: { [groupId: string]: true };
|
lockedMultiSelections: { [groupId: string]: true };
|
||||||
|
bindMode: "focus" | "fixed";
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SearchMatch = {
|
export type SearchMatch = {
|
||||||
|
@ -2,3 +2,4 @@ export * from "./export";
|
|||||||
export * from "./withinBounds";
|
export * from "./withinBounds";
|
||||||
export * from "./bbox";
|
export * from "./bbox";
|
||||||
export { getCommonBounds } from "@excalidraw/element";
|
export { getCommonBounds } from "@excalidraw/element";
|
||||||
|
export * from "./visualdebug";
|
||||||
|
@ -11,6 +11,7 @@ exports[`exportToSvg > with default arguments 1`] = `
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "focus",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user