Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7a829e22b |
@@ -43,6 +43,7 @@ export const alignElements = (
|
|||||||
// update bound elements
|
// update bound elements
|
||||||
updateBoundElements(element, scene, {
|
updateBoundElements(element, scene, {
|
||||||
simultaneouslyUpdated: group,
|
simultaneouslyUpdated: group,
|
||||||
|
indirectArrowUpdate: true,
|
||||||
});
|
});
|
||||||
return updatedEle;
|
return updatedEle;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ export const isFocusPointVisible = (
|
|||||||
isBindingEnabled: AppState["isBindingEnabled"];
|
isBindingEnabled: AppState["isBindingEnabled"];
|
||||||
zoom: AppState["zoom"];
|
zoom: AppState["zoom"];
|
||||||
},
|
},
|
||||||
startOrEnd: "start" | "end",
|
|
||||||
ignoreOverlap = false,
|
ignoreOverlap = false,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
// No focus point management for elbow arrows, because elbow arrows
|
// No focus point management for elbow arrows, because elbow arrows
|
||||||
@@ -77,25 +76,14 @@ export const isFocusPointVisible = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const arrowPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
|
||||||
arrow,
|
|
||||||
startOrEnd === "end" ? arrow.points.length - 1 : 0,
|
|
||||||
elementsMap,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check if the focus point is within the element's shape bounds
|
// Check if the focus point is within the element's shape bounds
|
||||||
// Endpoint dragging takes precedence
|
return hitElementItself({
|
||||||
return (
|
element: bindableElement,
|
||||||
pointDistance(focusPoint, arrowPoint) >=
|
elementsMap,
|
||||||
(FOCUS_POINT_SIZE * 1.5) / appState.zoom.value &&
|
point: focusPoint,
|
||||||
hitElementItself({
|
threshold: getBindingGap(bindableElement, arrow),
|
||||||
element: bindableElement,
|
overrideShouldTestInside: true,
|
||||||
elementsMap,
|
});
|
||||||
point: focusPoint,
|
|
||||||
threshold: getBindingGap(bindableElement, arrow),
|
|
||||||
overrideShouldTestInside: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Updates the arrow endpoints in "orbit" configuration
|
// Updates the arrow endpoints in "orbit" configuration
|
||||||
@@ -141,7 +129,6 @@ const focusPointUpdate = (
|
|||||||
currentBinding,
|
currentBinding,
|
||||||
bindableElement,
|
bindableElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
true,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (newPoint) {
|
if (newPoint) {
|
||||||
@@ -366,7 +353,6 @@ export const handleFocusPointPointerDown = (
|
|||||||
bindableElement,
|
bindableElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
appState,
|
appState,
|
||||||
"start",
|
|
||||||
) &&
|
) &&
|
||||||
pointDistance(pointerPos, focusPoint) <= hitThreshold
|
pointDistance(pointerPos, focusPoint) <= hitThreshold
|
||||||
) {
|
) {
|
||||||
@@ -401,7 +387,6 @@ export const handleFocusPointPointerDown = (
|
|||||||
bindableElement,
|
bindableElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
appState,
|
appState,
|
||||||
"end",
|
|
||||||
) &&
|
) &&
|
||||||
pointDistance(pointerPos, focusPoint) <= hitThreshold
|
pointDistance(pointerPos, focusPoint) <= hitThreshold
|
||||||
) {
|
) {
|
||||||
@@ -516,7 +501,6 @@ export const handleFocusPointHover = (
|
|||||||
bindableElement,
|
bindableElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
appState,
|
appState,
|
||||||
"start",
|
|
||||||
) &&
|
) &&
|
||||||
pointDistance(pointerPos, focusPoint) <= hitThreshold
|
pointDistance(pointerPos, focusPoint) <= hitThreshold
|
||||||
) {
|
) {
|
||||||
@@ -545,7 +529,6 @@ export const handleFocusPointHover = (
|
|||||||
bindableElement,
|
bindableElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
appState,
|
appState,
|
||||||
"end",
|
|
||||||
) &&
|
) &&
|
||||||
pointDistance(pointerPos, focusPoint) <= hitThreshold
|
pointDistance(pointerPos, focusPoint) <= hitThreshold
|
||||||
) {
|
) {
|
||||||
|
|||||||
+164
-163
@@ -27,7 +27,11 @@ import type { AppState } from "@excalidraw/excalidraw/types";
|
|||||||
import type { MapEntry, Mutable } from "@excalidraw/common/utility-types";
|
import type { MapEntry, Mutable } from "@excalidraw/common/utility-types";
|
||||||
import type { Bounds } from "@excalidraw/common";
|
import type { Bounds } from "@excalidraw/common";
|
||||||
|
|
||||||
import { getCenterForBounds } from "./bounds";
|
import {
|
||||||
|
doBoundsIntersect,
|
||||||
|
getCenterForBounds,
|
||||||
|
getElementBounds,
|
||||||
|
} from "./bounds";
|
||||||
import {
|
import {
|
||||||
getAllHoveredElementAtPoint,
|
getAllHoveredElementAtPoint,
|
||||||
getHoveredElementForBinding,
|
getHoveredElementForBinding,
|
||||||
@@ -63,6 +67,7 @@ import {
|
|||||||
deconstructRectanguloidElement,
|
deconstructRectanguloidElement,
|
||||||
projectFixedPointOntoDiagonal,
|
projectFixedPointOntoDiagonal,
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
|
import { moveArrowAboveBindable } from "./zindex";
|
||||||
|
|
||||||
import type { Scene } from "./Scene";
|
import type { Scene } from "./Scene";
|
||||||
|
|
||||||
@@ -112,7 +117,6 @@ export type BindingStrategy =
|
|||||||
*/
|
*/
|
||||||
export const BASE_BINDING_GAP = 5;
|
export const BASE_BINDING_GAP = 5;
|
||||||
export const BASE_BINDING_GAP_ELBOW = 5;
|
export const BASE_BINDING_GAP_ELBOW = 5;
|
||||||
export const BASE_ARROW_MIN_LENGTH = 10;
|
|
||||||
export const FOCUS_POINT_SIZE = 10 / 1.5;
|
export const FOCUS_POINT_SIZE = 10 / 1.5;
|
||||||
|
|
||||||
export const getBindingGap = (
|
export const getBindingGap = (
|
||||||
@@ -807,23 +811,13 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
|
|||||||
startDragged ? -1 : 0,
|
startDragged ? -1 : 0,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
const pointIsCloseToOtherElement =
|
|
||||||
otherFocusPoint &&
|
|
||||||
otherBindableElement &&
|
|
||||||
hitElementItself({
|
|
||||||
point: globalPoint,
|
|
||||||
element: otherBindableElement,
|
|
||||||
elementsMap,
|
|
||||||
threshold: maxBindingDistance_simple(appState.zoom),
|
|
||||||
overrideShouldTestInside: true,
|
|
||||||
});
|
|
||||||
const otherNeverOverride = opts?.newArrow
|
const otherNeverOverride = opts?.newArrow
|
||||||
? appState.selectedLinearElement?.initialState.arrowStartIsInside
|
? appState.selectedLinearElement?.initialState.arrowStartIsInside
|
||||||
: otherBinding?.mode === "inside";
|
: otherBinding?.mode === "inside";
|
||||||
const other: BindingStrategy = !otherNeverOverride
|
const other: BindingStrategy = !otherNeverOverride
|
||||||
? otherBindableElement &&
|
? otherBindableElement &&
|
||||||
!otherFocusPointIsInElement &&
|
!otherFocusPointIsInElement &&
|
||||||
!pointIsCloseToOtherElement &&
|
|
||||||
appState.selectedLinearElement?.initialState.altFocusPoint
|
appState.selectedLinearElement?.initialState.altFocusPoint
|
||||||
? {
|
? {
|
||||||
mode: "orbit",
|
mode: "orbit",
|
||||||
@@ -1090,6 +1084,8 @@ export const updateBoundElements = (
|
|||||||
options?: {
|
options?: {
|
||||||
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
||||||
changedElements?: Map<string, ExcalidrawElement>;
|
changedElements?: Map<string, ExcalidrawElement>;
|
||||||
|
indirectArrowUpdate?: boolean;
|
||||||
|
skipArrowZindexUpdate?: boolean;
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
if (!isBindableElement(changedElement)) {
|
if (!isBindableElement(changedElement)) {
|
||||||
@@ -1158,6 +1154,21 @@ export const updateBoundElements = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (point) {
|
if (point) {
|
||||||
|
if (!options?.skipArrowZindexUpdate) {
|
||||||
|
moveArrowAboveBindable(
|
||||||
|
LinearElementEditor.getPointGlobalCoordinates(
|
||||||
|
element,
|
||||||
|
point,
|
||||||
|
elementsMap,
|
||||||
|
),
|
||||||
|
element,
|
||||||
|
scene.getNonDeletedElements(),
|
||||||
|
scene.getNonDeletedElementsMap(),
|
||||||
|
scene,
|
||||||
|
changedElement,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
bindingProp === "startBinding" ? 0 : element.points.length - 1,
|
bindingProp === "startBinding" ? 0 : element.points.length - 1,
|
||||||
{ point },
|
{ point },
|
||||||
@@ -1184,6 +1195,11 @@ export const updateBoundElements = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
boundElementsVisitor(elementsMap, changedElement, visitor);
|
boundElementsVisitor(elementsMap, changedElement, visitor);
|
||||||
|
|
||||||
|
if (options?.indirectArrowUpdate) {
|
||||||
|
boundElementsVisitor(elementsMap, changedElement, visitor);
|
||||||
|
boundElementsVisitor(elementsMap, changedElement, visitor);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateArrowBindings = (
|
const updateArrowBindings = (
|
||||||
@@ -1693,41 +1709,10 @@ export const snapToMid = (
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const extractBinding = (
|
const compareElementArea = (
|
||||||
arrow: ExcalidrawArrowElement,
|
a: ExcalidrawBindableElement,
|
||||||
startOrEnd: "startBinding" | "endBinding",
|
b: ExcalidrawBindableElement,
|
||||||
elementsMap: ElementsMap,
|
) => b.width ** 2 + b.height ** 2 - (a.width ** 2 + a.height ** 2);
|
||||||
) => {
|
|
||||||
const binding = arrow[startOrEnd];
|
|
||||||
if (!binding) {
|
|
||||||
return {
|
|
||||||
element: null,
|
|
||||||
fixedPoint: null,
|
|
||||||
focusPoint: null,
|
|
||||||
binding,
|
|
||||||
mode: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const element = elementsMap.get(
|
|
||||||
binding.elementId,
|
|
||||||
) as ExcalidrawBindableElement;
|
|
||||||
|
|
||||||
return {
|
|
||||||
element,
|
|
||||||
fixedPoint: binding.fixedPoint,
|
|
||||||
focusPoint: getGlobalFixedPointForBindableElement(
|
|
||||||
normalizeFixedPoint(binding.fixedPoint),
|
|
||||||
element,
|
|
||||||
elementsMap,
|
|
||||||
),
|
|
||||||
binding,
|
|
||||||
mode: binding.mode,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const elementArea = (element: ExcalidrawBindableElement) =>
|
|
||||||
element.width * element.height;
|
|
||||||
|
|
||||||
export const updateBoundPoint = (
|
export const updateBoundPoint = (
|
||||||
arrow: NonDeleted<ExcalidrawArrowElement>,
|
arrow: NonDeleted<ExcalidrawArrowElement>,
|
||||||
@@ -1735,7 +1720,9 @@ export const updateBoundPoint = (
|
|||||||
binding: FixedPointBinding | null | undefined,
|
binding: FixedPointBinding | null | undefined,
|
||||||
bindableElement: ExcalidrawBindableElement,
|
bindableElement: ExcalidrawBindableElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
dragging?: boolean,
|
opts?: {
|
||||||
|
customIntersector?: LineSegment<GlobalPoint>;
|
||||||
|
},
|
||||||
): LocalPoint | null => {
|
): LocalPoint | null => {
|
||||||
if (
|
if (
|
||||||
binding == null ||
|
binding == null ||
|
||||||
@@ -1750,136 +1737,150 @@ export const updateBoundPoint = (
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const focusPoint = getGlobalFixedPointForBindableElement(
|
const global = getGlobalFixedPointForBindableElement(
|
||||||
normalizeFixedPoint(binding.fixedPoint),
|
normalizeFixedPoint(binding.fixedPoint),
|
||||||
bindableElement,
|
bindableElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
|
const pointIndex =
|
||||||
|
startOrEnd === "startBinding" ? 0 : arrow.points.length - 1;
|
||||||
|
const elbowed = isElbowArrow(arrow);
|
||||||
|
const otherBinding =
|
||||||
|
startOrEnd === "startBinding" ? arrow.endBinding : arrow.startBinding;
|
||||||
|
const otherBindableElement =
|
||||||
|
otherBinding &&
|
||||||
|
(elementsMap.get(otherBinding.elementId)! as ExcalidrawBindableElement);
|
||||||
|
const bounds = getElementBounds(bindableElement, elementsMap);
|
||||||
|
const otherBounds =
|
||||||
|
otherBindableElement && getElementBounds(otherBindableElement, elementsMap);
|
||||||
|
const isLargerThanOther =
|
||||||
|
otherBindableElement &&
|
||||||
|
compareElementArea(bindableElement, otherBindableElement) <
|
||||||
|
// if both shapes the same size, pretend the other is larger
|
||||||
|
(startOrEnd === "endBinding" ? 1 : 0);
|
||||||
|
const isOverlapping = otherBounds && doBoundsIntersect(bounds, otherBounds);
|
||||||
|
|
||||||
// 0. Short-circuit for inside binding as it doesn't require any
|
// GOAL: If the arrow becomes too short, we want to jump the arrow endpoints
|
||||||
// calculations and is not affected by other bindings
|
// to the exact focus points on the elements.
|
||||||
if (binding.mode === "inside") {
|
// INTUITION: We're not interested in the exacts length of the arrow (which
|
||||||
return LinearElementEditor.createPointAt(
|
// will change if we change where we route it), we want to know the length of
|
||||||
arrow,
|
// the part which lies outside of both shapes and consider that as a trigger
|
||||||
elementsMap,
|
// to change where we point the arrow. Avoids jumping the arrow in and out
|
||||||
focusPoint[0],
|
// at every frame.
|
||||||
focusPoint[1],
|
let arrowTooShort = false;
|
||||||
null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { element: otherBindable, focusPoint: otherFocusPoint } =
|
|
||||||
extractBinding(
|
|
||||||
arrow,
|
|
||||||
startOrEnd === "startBinding" ? "endBinding" : "startBinding",
|
|
||||||
elementsMap,
|
|
||||||
);
|
|
||||||
const otherArrowPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
|
||||||
arrow,
|
|
||||||
startOrEnd === "startBinding" ? -1 : 0,
|
|
||||||
elementsMap,
|
|
||||||
);
|
|
||||||
const otherFocusPointOrArrowPoint = otherFocusPoint || otherArrowPoint;
|
|
||||||
const intersector =
|
|
||||||
otherFocusPointOrArrowPoint &&
|
|
||||||
lineSegment(focusPoint, otherFocusPointOrArrowPoint);
|
|
||||||
const otherOutlinePoint =
|
|
||||||
otherBindable &&
|
|
||||||
intersector &&
|
|
||||||
intersectElementWithLineSegment(
|
|
||||||
otherBindable,
|
|
||||||
elementsMap,
|
|
||||||
intersector,
|
|
||||||
getBindingGap(otherBindable, arrow),
|
|
||||||
).sort(
|
|
||||||
(a, b) => pointDistanceSq(a, focusPoint) - pointDistanceSq(b, focusPoint),
|
|
||||||
)[0];
|
|
||||||
const outlinePoint =
|
|
||||||
intersector &&
|
|
||||||
intersectElementWithLineSegment(
|
|
||||||
bindableElement,
|
|
||||||
elementsMap,
|
|
||||||
intersector,
|
|
||||||
getBindingGap(bindableElement, arrow),
|
|
||||||
).sort(
|
|
||||||
(a, b) =>
|
|
||||||
pointDistanceSq(a, otherFocusPointOrArrowPoint) -
|
|
||||||
pointDistanceSq(b, otherFocusPointOrArrowPoint),
|
|
||||||
)[0];
|
|
||||||
const startHasArrowhead = arrow.startArrowhead !== null;
|
|
||||||
const endHasArrowhead = arrow.endArrowhead !== null;
|
|
||||||
const resolvedTarget =
|
|
||||||
(!startHasArrowhead && !endHasArrowhead) ||
|
|
||||||
(startOrEnd === "startBinding" && startHasArrowhead) ||
|
|
||||||
(startOrEnd === "endBinding" && endHasArrowhead)
|
|
||||||
? focusPoint
|
|
||||||
: outlinePoint || focusPoint;
|
|
||||||
|
|
||||||
// 1. Handle case when the outline point (or focus point) is inside
|
|
||||||
// the other shape by short-circuiting to the focus point, otherwise
|
|
||||||
// the arrow would invert
|
|
||||||
if (
|
if (
|
||||||
otherBindable &&
|
!isOverlapping &&
|
||||||
outlinePoint &&
|
!elbowed &&
|
||||||
!dragging &&
|
arrow.startBinding &&
|
||||||
// Arbitrary threshold to handle wireframing use cases
|
arrow.endBinding &&
|
||||||
elementArea(otherBindable) < elementArea(bindableElement) * 2 &&
|
otherBindableElement &&
|
||||||
hitElementItself({
|
arrow.points.length === 2
|
||||||
element: otherBindable,
|
|
||||||
point: outlinePoint,
|
|
||||||
elementsMap,
|
|
||||||
threshold: getBindingGap(otherBindable, arrow),
|
|
||||||
overrideShouldTestInside: true,
|
|
||||||
})
|
|
||||||
) {
|
) {
|
||||||
return LinearElementEditor.createPointAt(
|
const startFocusPoint = getGlobalFixedPointForBindableElement(
|
||||||
|
arrow.startBinding.fixedPoint,
|
||||||
|
startOrEnd === "startBinding" ? bindableElement : otherBindableElement,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
const endFocusPoint = getGlobalFixedPointForBindableElement(
|
||||||
|
arrow.endBinding.fixedPoint,
|
||||||
|
startOrEnd === "endBinding" ? bindableElement : otherBindableElement,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
const segment = lineSegment(startFocusPoint, endFocusPoint);
|
||||||
|
const startIntersection = intersectElementWithLineSegment(
|
||||||
|
startOrEnd === "endBinding" ? bindableElement : otherBindableElement,
|
||||||
|
elementsMap,
|
||||||
|
segment,
|
||||||
|
0,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
const endIntersection = intersectElementWithLineSegment(
|
||||||
|
startOrEnd === "startBinding" ? bindableElement : otherBindableElement,
|
||||||
|
elementsMap,
|
||||||
|
segment,
|
||||||
|
0,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
if (startIntersection.length > 0 && endIntersection.length > 0) {
|
||||||
|
const len = pointDistance(startIntersection[0], endIntersection[0]);
|
||||||
|
arrowTooShort = len < 40;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNested = (arrowTooShort || isOverlapping) && isLargerThanOther;
|
||||||
|
|
||||||
|
let _customIntersector = opts?.customIntersector;
|
||||||
|
if (!elbowed && !_customIntersector) {
|
||||||
|
const [x1, y1, x2, y2] = LinearElementEditor.getElementAbsoluteCoords(
|
||||||
arrow,
|
arrow,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
resolvedTarget[0],
|
);
|
||||||
resolvedTarget[1],
|
const center = pointFrom<GlobalPoint>((x1 + x2) / 2, (y1 + y2) / 2);
|
||||||
null,
|
const edgePoint = global;
|
||||||
|
const adjacentPoint = pointRotateRads(
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
arrow.x +
|
||||||
|
arrow.points[pointIndex === 0 ? 1 : arrow.points.length - 2][0],
|
||||||
|
arrow.y +
|
||||||
|
arrow.points[pointIndex === 0 ? 1 : arrow.points.length - 2][1],
|
||||||
|
),
|
||||||
|
center,
|
||||||
|
arrow.angle as Radians,
|
||||||
|
);
|
||||||
|
const bindingGap = getBindingGap(bindableElement, arrow);
|
||||||
|
const halfVector = vectorScale(
|
||||||
|
vectorNormalize(vectorFromPoint(edgePoint, adjacentPoint)),
|
||||||
|
pointDistance(edgePoint, adjacentPoint) +
|
||||||
|
Math.max(bindableElement.width, bindableElement.height) +
|
||||||
|
bindingGap * 2,
|
||||||
|
);
|
||||||
|
_customIntersector = lineSegment(
|
||||||
|
pointFromVector(halfVector, adjacentPoint),
|
||||||
|
pointFromVector(vectorScale(halfVector, -1), adjacentPoint),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const otherTargetPoint = otherBindable
|
const maybeOutlineGlobal =
|
||||||
? otherOutlinePoint || otherFocusPoint || otherArrowPoint
|
binding.mode === "orbit" && bindableElement
|
||||||
: otherArrowPoint;
|
? isNested
|
||||||
const arrowTooShort =
|
? global
|
||||||
pointDistance(otherTargetPoint, outlinePoint || focusPoint) <=
|
: bindPointToSnapToElementOutline(
|
||||||
BASE_ARROW_MIN_LENGTH;
|
{
|
||||||
|
...arrow,
|
||||||
|
points: [
|
||||||
|
pointIndex === 0
|
||||||
|
? LinearElementEditor.createPointAt(
|
||||||
|
arrow,
|
||||||
|
elementsMap,
|
||||||
|
global[0],
|
||||||
|
global[1],
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
: arrow.points[0],
|
||||||
|
...arrow.points.slice(1, -1),
|
||||||
|
pointIndex === arrow.points.length - 1
|
||||||
|
? LinearElementEditor.createPointAt(
|
||||||
|
arrow,
|
||||||
|
elementsMap,
|
||||||
|
global[0],
|
||||||
|
global[1],
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
: arrow.points[arrow.points.length - 1],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
bindableElement,
|
||||||
|
pointIndex === 0 ? "start" : "end",
|
||||||
|
elementsMap,
|
||||||
|
_customIntersector,
|
||||||
|
)
|
||||||
|
: global;
|
||||||
|
|
||||||
// 2. If the arrow is unconnected at the other end, just check arrow size
|
|
||||||
// and short-circuit to the focus point if the arrow is too short to
|
|
||||||
// avoid inversion
|
|
||||||
if (!otherBindable) {
|
|
||||||
return LinearElementEditor.createPointAt(
|
|
||||||
arrow,
|
|
||||||
elementsMap,
|
|
||||||
arrowTooShort ? focusPoint[0] : outlinePoint?.[0] ?? focusPoint[0],
|
|
||||||
arrowTooShort ? focusPoint[1] : outlinePoint?.[1] ?? focusPoint[1],
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. If the arrow is too short while connected on both ends and
|
|
||||||
// the other arrow endpoint will not be inside the bindable, just
|
|
||||||
// check the arrow size and make a decision based on that
|
|
||||||
if (arrowTooShort) {
|
|
||||||
return LinearElementEditor.createPointAt(
|
|
||||||
arrow,
|
|
||||||
elementsMap,
|
|
||||||
resolvedTarget?.[0] || focusPoint[0],
|
|
||||||
resolvedTarget?.[1] || focusPoint[1],
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. In the general case, snap to the outline if possible
|
|
||||||
return LinearElementEditor.createPointAt(
|
return LinearElementEditor.createPointAt(
|
||||||
arrow,
|
arrow,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
outlinePoint?.[0] || focusPoint[0],
|
maybeOutlineGlobal[0],
|
||||||
outlinePoint?.[1] || focusPoint[1],
|
maybeOutlineGlobal[1],
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1970,6 +1970,7 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
// TODO: with precise bindings this is quite expensive, so consider optimisation so it's only triggered when the arrow does not intersect (imprecise) element bounds
|
// TODO: with precise bindings this is quite expensive, so consider optimisation so it's only triggered when the arrow does not intersect (imprecise) element bounds
|
||||||
updateBoundElements(element, scene, {
|
updateBoundElements(element, scene, {
|
||||||
changedElements: changed,
|
changedElements: changed,
|
||||||
|
skipArrowZindexUpdate: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ const RE_REDDIT =
|
|||||||
const RE_REDDIT_EMBED =
|
const RE_REDDIT_EMBED =
|
||||||
/^<blockquote[\s\S]*?\shref=["'](https?:\/\/(?:www\.)?reddit\.com\/[^"']*)/i;
|
/^<blockquote[\s\S]*?\shref=["'](https?:\/\/(?:www\.)?reddit\.com\/[^"']*)/i;
|
||||||
|
|
||||||
const parseYouTubeLikeTimestamp = (url: string): number => {
|
const parseYouTubeTimestamp = (url: string): number => {
|
||||||
let timeParam: string | null | undefined;
|
let timeParam: string | null | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -85,57 +85,11 @@ const parseYouTubeLikeTimestamp = (url: string): number => {
|
|||||||
return parseInt(hours) * 3600 + parseInt(minutes) * 60 + parseInt(seconds);
|
return parseInt(hours) * 3600 + parseInt(minutes) * 60 + parseInt(seconds);
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseGoogleDriveVideoLink = (
|
|
||||||
url: string,
|
|
||||||
): { fileId: string; resourceKey?: string; timestamp?: number } | null => {
|
|
||||||
try {
|
|
||||||
const urlObj = new URL(url.startsWith("http") ? url : `https://${url}`);
|
|
||||||
const hostname = urlObj.hostname.replace(/^www\./, "");
|
|
||||||
if (hostname !== "drive.google.com") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let fileId: string | null = null;
|
|
||||||
const pathMatch = urlObj.pathname.match(/^\/file\/d\/([^/]+)(?:\/|$)/);
|
|
||||||
if (pathMatch?.[1]) {
|
|
||||||
fileId = pathMatch[1];
|
|
||||||
} else if (urlObj.pathname === "/open" || urlObj.pathname === "/uc") {
|
|
||||||
// Shared Drive links can be emitted as:
|
|
||||||
// - /open?id=<fileId> (common "open in Drive" format)
|
|
||||||
// - /uc?...&id=<fileId> (download/export endpoint often seen in copied links)
|
|
||||||
fileId = urlObj.searchParams.get("id");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fileId || !/^[a-zA-Z0-9_-]+$/.test(fileId)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Some Drive share links include `resourcekey` for access to link-shared
|
|
||||||
// files; preserve it in the preview URL so embeds keep working.
|
|
||||||
const resourceKey = urlObj.searchParams.get("resourcekey");
|
|
||||||
const timestamp = parseYouTubeLikeTimestamp(urlObj.toString());
|
|
||||||
|
|
||||||
return {
|
|
||||||
fileId,
|
|
||||||
resourceKey:
|
|
||||||
resourceKey && /^[a-zA-Z0-9_-]+$/.test(resourceKey)
|
|
||||||
? resourceKey
|
|
||||||
: undefined,
|
|
||||||
// Drive accepts YouTube-like `t` formats (e.g. `t=90`, `t=1m30s`);
|
|
||||||
// normalize to seconds for a stable preview URL.
|
|
||||||
timestamp: timestamp > 0 ? timestamp : undefined,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const ALLOWED_DOMAINS = new Set([
|
const ALLOWED_DOMAINS = new Set([
|
||||||
"youtube.com",
|
"youtube.com",
|
||||||
"youtu.be",
|
"youtu.be",
|
||||||
"vimeo.com",
|
"vimeo.com",
|
||||||
"player.vimeo.com",
|
"player.vimeo.com",
|
||||||
"drive.google.com",
|
|
||||||
"figma.com",
|
"figma.com",
|
||||||
"link.excalidraw.com",
|
"link.excalidraw.com",
|
||||||
"gist.github.com",
|
"gist.github.com",
|
||||||
@@ -154,7 +108,6 @@ const ALLOW_SAME_ORIGIN = new Set([
|
|||||||
"youtu.be",
|
"youtu.be",
|
||||||
"vimeo.com",
|
"vimeo.com",
|
||||||
"player.vimeo.com",
|
"player.vimeo.com",
|
||||||
"drive.google.com",
|
|
||||||
"figma.com",
|
"figma.com",
|
||||||
"twitter.com",
|
"twitter.com",
|
||||||
"x.com",
|
"x.com",
|
||||||
@@ -189,7 +142,7 @@ export const getEmbedLink = (
|
|||||||
let aspectRatio = { w: 560, h: 840 };
|
let aspectRatio = { w: 560, h: 840 };
|
||||||
const ytLink = link.match(RE_YOUTUBE);
|
const ytLink = link.match(RE_YOUTUBE);
|
||||||
if (ytLink?.[2]) {
|
if (ytLink?.[2]) {
|
||||||
const startTime = parseYouTubeLikeTimestamp(originalLink);
|
const startTime = parseYouTubeTimestamp(originalLink);
|
||||||
const time = startTime > 0 ? `&start=${startTime}` : ``;
|
const time = startTime > 0 ? `&start=${startTime}` : ``;
|
||||||
const isPortrait = link.includes("shorts");
|
const isPortrait = link.includes("shorts");
|
||||||
type = "video";
|
type = "video";
|
||||||
@@ -248,36 +201,6 @@ export const getEmbedLink = (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const googleDriveVideo = parseGoogleDriveVideoLink(link);
|
|
||||||
if (googleDriveVideo) {
|
|
||||||
type = "video";
|
|
||||||
const searchParams = new URLSearchParams();
|
|
||||||
if (googleDriveVideo.resourceKey) {
|
|
||||||
searchParams.set("resourcekey", googleDriveVideo.resourceKey);
|
|
||||||
}
|
|
||||||
if (googleDriveVideo.timestamp) {
|
|
||||||
searchParams.set("t", `${googleDriveVideo.timestamp}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const search = searchParams.toString();
|
|
||||||
link = `https://drive.google.com/file/d/${googleDriveVideo.fileId}/preview${
|
|
||||||
search ? `?${search}` : ""
|
|
||||||
}`;
|
|
||||||
aspectRatio = { w: 560, h: 315 };
|
|
||||||
embeddedLinkCache.set(originalLink, {
|
|
||||||
link,
|
|
||||||
intrinsicSize: aspectRatio,
|
|
||||||
type,
|
|
||||||
sandbox: { allowSameOrigin },
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
link,
|
|
||||||
intrinsicSize: aspectRatio,
|
|
||||||
type,
|
|
||||||
sandbox: { allowSameOrigin },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const figmaLink = link.match(RE_FIGMA);
|
const figmaLink = link.match(RE_FIGMA);
|
||||||
if (figmaLink) {
|
if (figmaLink) {
|
||||||
type = "generic";
|
type = "generic";
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
vectorFromPoint,
|
vectorFromPoint,
|
||||||
curveLength,
|
curveLength,
|
||||||
curvePointAtLength,
|
curvePointAtLength,
|
||||||
|
lineSegment,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import { getCurvePathOps } from "@excalidraw/utils/shape";
|
import { getCurvePathOps } from "@excalidraw/utils/shape";
|
||||||
@@ -2338,6 +2339,19 @@ const pointDraggingUpdates = (
|
|||||||
: updates.endBinding,
|
: updates.endBinding,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// We need to use a custom intersector to ensure that if there is a big "jump"
|
||||||
|
// in the arrow's position, we can position it with outline avoidance
|
||||||
|
// pixel-perfectly and avoid "dancing" arrows.
|
||||||
|
// NOTE: Direction matters here, so we create two intersectors
|
||||||
|
const startCustomIntersector =
|
||||||
|
start.focusPoint && end.focusPoint
|
||||||
|
? lineSegment(start.focusPoint, end.focusPoint)
|
||||||
|
: undefined;
|
||||||
|
const endCustomIntersector =
|
||||||
|
start.focusPoint && end.focusPoint
|
||||||
|
? lineSegment(end.focusPoint, start.focusPoint)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
// Needed to handle a special case where an existing arrow is dragged over
|
// Needed to handle a special case where an existing arrow is dragged over
|
||||||
// the same element it is bound to on the other side
|
// the same element it is bound to on the other side
|
||||||
const startIsDraggingOverEndElement =
|
const startIsDraggingOverEndElement =
|
||||||
@@ -2373,7 +2387,9 @@ const pointDraggingUpdates = (
|
|||||||
nextArrow.endBinding,
|
nextArrow.endBinding,
|
||||||
endBindable,
|
endBindable,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
endIsDragged,
|
{
|
||||||
|
customIntersector: endCustomIntersector,
|
||||||
|
},
|
||||||
) || nextArrow.points[nextArrow.points.length - 1]
|
) || nextArrow.points[nextArrow.points.length - 1]
|
||||||
: nextArrow.points[nextArrow.points.length - 1];
|
: nextArrow.points[nextArrow.points.length - 1];
|
||||||
|
|
||||||
@@ -2404,7 +2420,7 @@ const pointDraggingUpdates = (
|
|||||||
nextArrow.startBinding,
|
nextArrow.startBinding,
|
||||||
startBindable,
|
startBindable,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
startIsDragged,
|
{ customIntersector: startCustomIntersector },
|
||||||
) || nextArrow.points[0]
|
) || nextArrow.points[0]
|
||||||
: nextArrow.points[0];
|
: nextArrow.points[0];
|
||||||
|
|
||||||
|
|||||||
@@ -43,11 +43,6 @@ import { LinearElementEditor } from "./linearElementEditor";
|
|||||||
import { isRectangularElement } from "./typeChecks";
|
import { isRectangularElement } from "./typeChecks";
|
||||||
import { maxBindingDistance_simple } from "./binding";
|
import { maxBindingDistance_simple } from "./binding";
|
||||||
|
|
||||||
import {
|
|
||||||
getGlobalFixedPointForBindableElement,
|
|
||||||
normalizeFixedPoint,
|
|
||||||
} from "./binding";
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
@@ -682,35 +677,11 @@ export const projectFixedPointOntoDiagonal = (
|
|||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
// To avoid working with stale arrow state, we use the opposite focus point
|
const a = LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||||
// of the current endpoint, which will always be unchanged during moving of
|
|
||||||
// the endpoint. This is only needed when the arrow has only two points.
|
|
||||||
let a = LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
|
||||||
arrow,
|
arrow,
|
||||||
startOrEnd === "start" ? 1 : arrow.points.length - 2,
|
startOrEnd === "start" ? 1 : arrow.points.length - 2,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
if (arrow.points.length === 2) {
|
|
||||||
const otherBinding =
|
|
||||||
startOrEnd === "start" ? arrow.endBinding : arrow.startBinding;
|
|
||||||
const otherBindable =
|
|
||||||
otherBinding &&
|
|
||||||
(elementsMap.get(otherBinding.elementId) as
|
|
||||||
| ExcalidrawBindableElement
|
|
||||||
| undefined);
|
|
||||||
const otherFocusPoint =
|
|
||||||
otherBinding &&
|
|
||||||
otherBindable &&
|
|
||||||
getGlobalFixedPointForBindableElement(
|
|
||||||
normalizeFixedPoint(otherBinding.fixedPoint),
|
|
||||||
otherBindable,
|
|
||||||
elementsMap,
|
|
||||||
);
|
|
||||||
if (otherFocusPoint) {
|
|
||||||
a = otherFocusPoint;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const b = pointFromVector<GlobalPoint>(
|
const b = pointFromVector<GlobalPoint>(
|
||||||
vectorScale(
|
vectorScale(
|
||||||
vectorFromPoint(point, a),
|
vectorFromPoint(point, a),
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import type {
|
|||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
NonDeletedSceneElementsMap,
|
NonDeletedSceneElementsMap,
|
||||||
Ordered,
|
Ordered,
|
||||||
OrderedExcalidrawElement,
|
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
const isOfTargetFrame = (element: ExcalidrawElement, frameId: string) => {
|
const isOfTargetFrame = (element: ExcalidrawElement, frameId: string) => {
|
||||||
@@ -157,7 +156,7 @@ export const moveArrowAboveBindable = (
|
|||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
hit?: NonDeletedExcalidrawElement,
|
hit?: NonDeletedExcalidrawElement,
|
||||||
): readonly OrderedExcalidrawElement[] => {
|
) => {
|
||||||
const hoveredElement = hit
|
const hoveredElement = hit
|
||||||
? hit
|
? hit
|
||||||
: getHoveredElementForBinding(point, elements, elementsMap);
|
: getHoveredElementForBinding(point, elements, elementsMap);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { embeddableURLValidator, getEmbedLink } from "../src/embeddable";
|
import { getEmbedLink } from "../src/embeddable";
|
||||||
|
|
||||||
describe("YouTube timestamp parsing", () => {
|
describe("YouTube timestamp parsing", () => {
|
||||||
it("should parse YouTube URLs with timestamp in seconds", () => {
|
it("should parse YouTube URLs with timestamp in seconds", () => {
|
||||||
@@ -151,83 +151,3 @@ describe("YouTube timestamp parsing", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Google Drive video embedding", () => {
|
|
||||||
it.each([
|
|
||||||
{
|
|
||||||
url: "https://drive.google.com/file/d/1AbCdEfGhIjKlMnOpQrStUvWxYz123456/view?usp=sharing",
|
|
||||||
expectedLink:
|
|
||||||
"https://drive.google.com/file/d/1AbCdEfGhIjKlMnOpQrStUvWxYz123456/preview",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
url: "https://drive.google.com/open?id=1AbCdEfGhIjKlMnOpQrStUvWxYz123456",
|
|
||||||
expectedLink:
|
|
||||||
"https://drive.google.com/file/d/1AbCdEfGhIjKlMnOpQrStUvWxYz123456/preview",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
url: "https://drive.google.com/uc?export=download&id=1AbCdEfGhIjKlMnOpQrStUvWxYz123456",
|
|
||||||
expectedLink:
|
|
||||||
"https://drive.google.com/file/d/1AbCdEfGhIjKlMnOpQrStUvWxYz123456/preview",
|
|
||||||
},
|
|
||||||
])("should normalize Google Drive link: $url", ({ url, expectedLink }) => {
|
|
||||||
const result = getEmbedLink(url);
|
|
||||||
|
|
||||||
expect(result).toBeTruthy();
|
|
||||||
expect(result?.type).toBe("video");
|
|
||||||
if (result?.type === "video" || result?.type === "generic") {
|
|
||||||
expect(result.link).toBe(expectedLink);
|
|
||||||
}
|
|
||||||
expect(result?.intrinsicSize).toEqual({ w: 560, h: 315 });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should preserve resourcekey when available", () => {
|
|
||||||
const url =
|
|
||||||
"https://drive.google.com/file/d/1AbCdEfGhIjKlMnOpQrStUvWxYz123456/view?resourcekey=0-abcdef123456";
|
|
||||||
const result = getEmbedLink(url);
|
|
||||||
|
|
||||||
expect(result).toBeTruthy();
|
|
||||||
expect(result?.type).toBe("video");
|
|
||||||
if (result?.type === "video" || result?.type === "generic") {
|
|
||||||
expect(result.link).toBe(
|
|
||||||
"https://drive.google.com/file/d/1AbCdEfGhIjKlMnOpQrStUvWxYz123456/preview?resourcekey=0-abcdef123456",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should preserve timestamp when available", () => {
|
|
||||||
const url =
|
|
||||||
"https://drive.google.com/file/d/1AbCdEfGhIjKlMnOpQrStUvWxYz123456/view?t=9";
|
|
||||||
const result = getEmbedLink(url);
|
|
||||||
|
|
||||||
expect(result).toBeTruthy();
|
|
||||||
expect(result?.type).toBe("video");
|
|
||||||
if (result?.type === "video" || result?.type === "generic") {
|
|
||||||
expect(result.link).toBe(
|
|
||||||
"https://drive.google.com/file/d/1AbCdEfGhIjKlMnOpQrStUvWxYz123456/preview?t=9",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should preserve resourcekey and timestamp together", () => {
|
|
||||||
const url =
|
|
||||||
"https://drive.google.com/file/d/1AbCdEfGhIjKlMnOpQrStUvWxYz123456/view?resourcekey=0-abcdef123456&t=9";
|
|
||||||
const result = getEmbedLink(url);
|
|
||||||
|
|
||||||
expect(result).toBeTruthy();
|
|
||||||
expect(result?.type).toBe("video");
|
|
||||||
if (result?.type === "video" || result?.type === "generic") {
|
|
||||||
expect(result.link).toBe(
|
|
||||||
"https://drive.google.com/file/d/1AbCdEfGhIjKlMnOpQrStUvWxYz123456/preview?resourcekey=0-abcdef123456&t=9",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should validate Google Drive domain by default", () => {
|
|
||||||
expect(
|
|
||||||
embeddableURLValidator(
|
|
||||||
"https://drive.google.com/file/d/1AbCdEfGhIjKlMnOpQrStUvWxYz123456/view",
|
|
||||||
undefined,
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1555,7 +1555,7 @@ const getArrowheadOptions = (flip: boolean) => {
|
|||||||
value: null,
|
value: null,
|
||||||
text: t("labels.arrowhead_none"),
|
text: t("labels.arrowhead_none"),
|
||||||
keyBinding: "q",
|
keyBinding: "q",
|
||||||
icon: <ArrowheadNoneIcon flip={flip} />,
|
icon: ArrowheadNoneIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "arrow",
|
value: "arrow",
|
||||||
|
|||||||
@@ -442,7 +442,10 @@ import { searchItemInFocusAtom } from "./SearchMenu";
|
|||||||
import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
|
import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
|
||||||
import { StaticCanvas, InteractiveCanvas } from "./canvases";
|
import { StaticCanvas, InteractiveCanvas } from "./canvases";
|
||||||
import NewElementCanvas from "./canvases/NewElementCanvas";
|
import NewElementCanvas from "./canvases/NewElementCanvas";
|
||||||
import { isPointHittingLink } from "./hyperlink/helpers";
|
import {
|
||||||
|
isPointHittingLink,
|
||||||
|
isPointHittingLinkIcon,
|
||||||
|
} from "./hyperlink/helpers";
|
||||||
import { MagicIcon, copyIcon, fullscreenIcon } from "./icons";
|
import { MagicIcon, copyIcon, fullscreenIcon } from "./icons";
|
||||||
import { Toast } from "./Toast";
|
import { Toast } from "./Toast";
|
||||||
|
|
||||||
@@ -1207,99 +1210,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
return this.iFrameRefs.get(element.id);
|
return this.iFrameRefs.get(element.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleIframeLikeElementHover = ({
|
private handleEmbeddableCenterClick(element: ExcalidrawIframeLikeElement) {
|
||||||
hitElement,
|
|
||||||
scenePointer,
|
|
||||||
moveEvent,
|
|
||||||
}: {
|
|
||||||
hitElement: NonDeleted<ExcalidrawElement> | null;
|
|
||||||
scenePointer: { x: number; y: number };
|
|
||||||
moveEvent: React.PointerEvent<HTMLCanvasElement>;
|
|
||||||
}): boolean => {
|
|
||||||
if (
|
if (
|
||||||
hitElement &&
|
this.state.activeEmbeddable?.element === element &&
|
||||||
isIframeLikeElement(hitElement) &&
|
|
||||||
this.isIframeLikeElementCenter(
|
|
||||||
hitElement,
|
|
||||||
moveEvent,
|
|
||||||
scenePointer.x,
|
|
||||||
scenePointer.y,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
|
||||||
this.setState({
|
|
||||||
activeEmbeddable: { element: hitElement, state: "hover" },
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
} else if (this.state.activeEmbeddable?.state === "hover") {
|
|
||||||
this.setState({ activeEmbeddable: null });
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @returns true if iframe-like element click handled */
|
|
||||||
private handleIframeLikeCenterClick(): boolean {
|
|
||||||
if (!this.lastPointerDownEvent || !this.lastPointerUpEvent) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const scenePointerStart = viewportCoordsToSceneCoords(
|
|
||||||
{
|
|
||||||
clientX: this.lastPointerDownEvent.clientX,
|
|
||||||
clientY: this.lastPointerDownEvent.clientY,
|
|
||||||
},
|
|
||||||
this.state,
|
|
||||||
);
|
|
||||||
const scenePointerEnd = viewportCoordsToSceneCoords(
|
|
||||||
{
|
|
||||||
clientX: this.lastPointerUpEvent.clientX,
|
|
||||||
clientY: this.lastPointerUpEvent.clientY,
|
|
||||||
},
|
|
||||||
this.state,
|
|
||||||
);
|
|
||||||
|
|
||||||
const hitElementStart = this.getElementAtPosition(
|
|
||||||
scenePointerStart.x,
|
|
||||||
scenePointerStart.y,
|
|
||||||
);
|
|
||||||
|
|
||||||
const hitElementEnd = this.getElementAtPosition(
|
|
||||||
scenePointerEnd.x,
|
|
||||||
scenePointerEnd.y,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
!hitElementStart ||
|
|
||||||
!hitElementEnd ||
|
|
||||||
hitElementStart !== hitElementEnd ||
|
|
||||||
this.lastPointerUpEvent.timeStamp - this.lastPointerDownEvent.timeStamp >
|
|
||||||
300 ||
|
|
||||||
gesture.pointers.size > 1 ||
|
|
||||||
!isIframeLikeElement(hitElementStart) ||
|
|
||||||
!isIframeLikeElement(hitElementEnd) ||
|
|
||||||
!this.isIframeLikeElementCenter(
|
|
||||||
hitElementStart,
|
|
||||||
this.lastPointerUpEvent,
|
|
||||||
scenePointerStart.x,
|
|
||||||
scenePointerStart.y,
|
|
||||||
) ||
|
|
||||||
!this.isIframeLikeElementCenter(
|
|
||||||
hitElementEnd,
|
|
||||||
this.lastPointerUpEvent,
|
|
||||||
scenePointerEnd.x,
|
|
||||||
scenePointerEnd.y,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const iframeLikeElement = hitElementEnd;
|
|
||||||
|
|
||||||
if (
|
|
||||||
this.state.activeEmbeddable?.element === iframeLikeElement &&
|
|
||||||
this.state.activeEmbeddable?.state === "active"
|
this.state.activeEmbeddable?.state === "active"
|
||||||
) {
|
) {
|
||||||
return true;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The delay serves two purposes
|
// The delay serves two purposes
|
||||||
@@ -1310,34 +1226,31 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
// in fullscreen mode
|
// in fullscreen mode
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.setState({
|
this.setState({
|
||||||
activeEmbeddable: { element: iframeLikeElement, state: "active" },
|
activeEmbeddable: { element, state: "active" },
|
||||||
selectedElementIds: { [iframeLikeElement.id]: true },
|
selectedElementIds: { [element.id]: true },
|
||||||
newElement: null,
|
newElement: null,
|
||||||
selectionElement: null,
|
selectionElement: null,
|
||||||
});
|
});
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
if (isIframeElement(iframeLikeElement)) {
|
if (isIframeElement(element)) {
|
||||||
return true;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const iframe = this.getHTMLIFrameElement(iframeLikeElement);
|
const iframe = this.getHTMLIFrameElement(element);
|
||||||
|
|
||||||
if (!iframe?.contentWindow) {
|
if (!iframe?.contentWindow) {
|
||||||
return true;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (iframe.src.includes("youtube")) {
|
if (iframe.src.includes("youtube")) {
|
||||||
const state = YOUTUBE_VIDEO_STATES.get(iframeLikeElement.id);
|
const state = YOUTUBE_VIDEO_STATES.get(element.id);
|
||||||
if (!state) {
|
if (!state) {
|
||||||
YOUTUBE_VIDEO_STATES.set(
|
YOUTUBE_VIDEO_STATES.set(element.id, YOUTUBE_STATES.UNSTARTED);
|
||||||
iframeLikeElement.id,
|
|
||||||
YOUTUBE_STATES.UNSTARTED,
|
|
||||||
);
|
|
||||||
iframe.contentWindow.postMessage(
|
iframe.contentWindow.postMessage(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
event: "listening",
|
event: "listening",
|
||||||
id: iframeLikeElement.id,
|
id: element.id,
|
||||||
}),
|
}),
|
||||||
"*",
|
"*",
|
||||||
);
|
);
|
||||||
@@ -1374,8 +1287,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
"*",
|
"*",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private isIframeLikeElementCenter(
|
private isIframeLikeElementCenter(
|
||||||
@@ -6711,14 +6622,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isPressingAnyButton = Boolean(event.buttons);
|
const hasDeselectedButton = Boolean(event.buttons);
|
||||||
const isLaserTool = this.state.activeTool.type === "laser";
|
|
||||||
if (
|
if (
|
||||||
isPressingAnyButton ||
|
hasDeselectedButton ||
|
||||||
// checking against laser so that if you mouseover with a laser tool
|
(this.state.activeTool.type !== "selection" &&
|
||||||
// over a link/embeddable, we change the cursor
|
|
||||||
(!isLaserTool &&
|
|
||||||
this.state.activeTool.type !== "selection" &&
|
|
||||||
this.state.activeTool.type !== "lasso" &&
|
this.state.activeTool.type !== "lasso" &&
|
||||||
this.state.activeTool.type !== "text" &&
|
this.state.activeTool.type !== "text" &&
|
||||||
this.state.activeTool.type !== "eraser")
|
this.state.activeTool.type !== "eraser")
|
||||||
@@ -6838,14 +6745,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
hideHyperlinkToolip();
|
hideHyperlinkToolip();
|
||||||
if (isLaserTool) {
|
|
||||||
this.handleIframeLikeElementHover({
|
|
||||||
hitElement,
|
|
||||||
scenePointer,
|
|
||||||
moveEvent: event,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (
|
if (
|
||||||
hitElement &&
|
hitElement &&
|
||||||
(hitElement.link || isEmbeddableElement(hitElement)) &&
|
(hitElement.link || isEmbeddableElement(hitElement)) &&
|
||||||
@@ -6878,15 +6777,24 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
!hitElement?.locked
|
!hitElement?.locked
|
||||||
) {
|
) {
|
||||||
if (
|
if (
|
||||||
!this.handleIframeLikeElementHover({
|
hitElement &&
|
||||||
|
isIframeLikeElement(hitElement) &&
|
||||||
|
this.isIframeLikeElementCenter(
|
||||||
hitElement,
|
hitElement,
|
||||||
scenePointer,
|
event,
|
||||||
moveEvent: event,
|
scenePointerX,
|
||||||
}) &&
|
scenePointerY,
|
||||||
(!hitElement ||
|
)
|
||||||
// Elbow arrows can only be moved when unconnected
|
) {
|
||||||
!isElbowArrow(hitElement) ||
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
||||||
!(hitElement.startBinding || hitElement.endBinding))
|
this.setState({
|
||||||
|
activeEmbeddable: { element: hitElement, state: "hover" },
|
||||||
|
});
|
||||||
|
} else if (
|
||||||
|
!hitElement ||
|
||||||
|
// Elbow arrows can only be moved when unconnected
|
||||||
|
!isElbowArrow(hitElement) ||
|
||||||
|
!(hitElement.startBinding || hitElement.endBinding)
|
||||||
) {
|
) {
|
||||||
if (
|
if (
|
||||||
this.state.activeTool.type !== "lasso" ||
|
this.state.activeTool.type !== "lasso" ||
|
||||||
@@ -6894,6 +6802,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
) {
|
) {
|
||||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
||||||
}
|
}
|
||||||
|
if (this.state.activeEmbeddable?.state === "hover") {
|
||||||
|
this.setState({ activeEmbeddable: null });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -7545,9 +7456,26 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
x: scenePointerX,
|
x: scenePointerX,
|
||||||
y: scenePointerY,
|
y: scenePointerY,
|
||||||
};
|
};
|
||||||
|
const clicklength =
|
||||||
|
event.timeStamp - (this.lastPointerDownEvent?.timeStamp ?? 0);
|
||||||
|
|
||||||
if (this.handleIframeLikeCenterClick()) {
|
if (this.editorInterface.formFactor === "phone" && clicklength < 300) {
|
||||||
return;
|
const hitElement = this.getElementAtPosition(
|
||||||
|
scenePointer.x,
|
||||||
|
scenePointer.y,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
isIframeLikeElement(hitElement) &&
|
||||||
|
this.isIframeLikeElementCenter(
|
||||||
|
hitElement,
|
||||||
|
event,
|
||||||
|
scenePointer.x,
|
||||||
|
scenePointer.y,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this.handleEmbeddableCenterClick(hitElement);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.editorInterface.isTouchScreen) {
|
if (this.editorInterface.isTouchScreen) {
|
||||||
@@ -7568,7 +7496,20 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.hitLinkElement &&
|
this.hitLinkElement &&
|
||||||
!this.state.selectedElementIds[this.hitLinkElement.id]
|
!this.state.selectedElementIds[this.hitLinkElement.id]
|
||||||
) {
|
) {
|
||||||
this.redirectToLink(event, this.editorInterface.isTouchScreen);
|
if (
|
||||||
|
clicklength < 300 &&
|
||||||
|
isIframeLikeElement(this.hitLinkElement) &&
|
||||||
|
!isPointHittingLinkIcon(
|
||||||
|
this.hitLinkElement,
|
||||||
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
this.state,
|
||||||
|
pointFrom(scenePointer.x, scenePointer.y),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this.handleEmbeddableCenterClick(this.hitLinkElement);
|
||||||
|
} else {
|
||||||
|
this.redirectToLink(event, this.editorInterface.isTouchScreen);
|
||||||
|
}
|
||||||
} else if (this.state.viewModeEnabled) {
|
} else if (this.state.viewModeEnabled) {
|
||||||
this.setState({
|
this.setState({
|
||||||
activeEmbeddable: null,
|
activeEmbeddable: null,
|
||||||
@@ -9682,7 +9623,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
isBindableElement(element) &&
|
isBindableElement(element) &&
|
||||||
element.boundElements?.some((other) => other.type === "arrow")
|
element.boundElements?.some((other) => other.type === "arrow")
|
||||||
) {
|
) {
|
||||||
updateBoundElements(element, this.scene);
|
updateBoundElements(element, this.scene, {
|
||||||
|
indirectArrowUpdate: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -10911,6 +10854,25 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
suggestedBinding: null,
|
suggestedBinding: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
hitElement &&
|
||||||
|
this.lastPointerUpEvent &&
|
||||||
|
this.lastPointerDownEvent &&
|
||||||
|
this.lastPointerUpEvent.timeStamp -
|
||||||
|
this.lastPointerDownEvent.timeStamp <
|
||||||
|
300 &&
|
||||||
|
gesture.pointers.size <= 1 &&
|
||||||
|
isIframeLikeElement(hitElement) &&
|
||||||
|
this.isIframeLikeElementCenter(
|
||||||
|
hitElement,
|
||||||
|
this.lastPointerUpEvent,
|
||||||
|
pointerDownState.origin.x,
|
||||||
|
pointerDownState.origin.y,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this.handleEmbeddableCenterClick(hitElement);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1287,21 +1287,13 @@ export const EdgeRoundIcon = createIcon(
|
|||||||
tablerIconProps,
|
tablerIconProps,
|
||||||
);
|
);
|
||||||
|
|
||||||
export const ArrowheadNoneIcon = React.memo(
|
export const ArrowheadNoneIcon = createIcon(
|
||||||
({ flip = false }: { flip?: boolean }) =>
|
<g stroke="currentColor" opacity={0.3} strokeWidth={2}>
|
||||||
createIcon(
|
<path d="M12 12l9 0" />
|
||||||
<g
|
<path d="M3 9l6 6" />
|
||||||
transform={flip ? "translate(24, 0) scale(-1, 1)" : ""}
|
<path d="M3 15l6 -6" />
|
||||||
stroke="currentColor"
|
</g>,
|
||||||
opacity={0.3}
|
tablerIconProps,
|
||||||
strokeWidth={2}
|
|
||||||
>
|
|
||||||
<path d="M12 12l-9 0" />
|
|
||||||
<path d="M21 9l-6 6" />
|
|
||||||
<path d="M21 15l-6 -6" />
|
|
||||||
</g>,
|
|
||||||
tablerIconProps,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export const ArrowheadArrowIcon = React.memo(
|
export const ArrowheadArrowIcon = React.memo(
|
||||||
|
|||||||
@@ -1269,7 +1269,6 @@ const renderFocusPointIndicator = ({
|
|||||||
bindableElement,
|
bindableElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
appState,
|
appState,
|
||||||
type,
|
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 29,
|
"version": 33,
|
||||||
"width": "94.00000",
|
"width": "94.00000",
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@@ -334,15 +334,15 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
],
|
],
|
||||||
"mode": "orbit",
|
"mode": "orbit",
|
||||||
},
|
},
|
||||||
"height": "105.58874",
|
"height": "105.58873",
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"88.00000",
|
88,
|
||||||
"105.58874",
|
"105.58873",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
@@ -353,8 +353,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
],
|
],
|
||||||
"mode": "orbit",
|
"mode": "orbit",
|
||||||
},
|
},
|
||||||
"version": 28,
|
"version": 32,
|
||||||
"width": "88.00000",
|
"width": 88,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
@@ -365,7 +365,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
],
|
],
|
||||||
"mode": "orbit",
|
"mode": "orbit",
|
||||||
},
|
},
|
||||||
"height": 0,
|
"height": "0.00000",
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
@@ -373,7 +373,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
"88.00000",
|
"88.00000",
|
||||||
0,
|
"0.00000",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
@@ -384,7 +384,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
],
|
],
|
||||||
"mode": "orbit",
|
"mode": "orbit",
|
||||||
},
|
},
|
||||||
"version": 25,
|
"version": 29,
|
||||||
"width": "88.00000",
|
"width": "88.00000",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -440,21 +440,21 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
"startBinding": null,
|
"startBinding": null,
|
||||||
"version": 29,
|
"version": 33,
|
||||||
"width": "94.00000",
|
"width": "94.00000",
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"height": "105.58874",
|
"height": "105.58873",
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"88.00000",
|
88,
|
||||||
"105.58874",
|
"105.58873",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
@@ -465,10 +465,10 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
],
|
],
|
||||||
"mode": "orbit",
|
"mode": "orbit",
|
||||||
},
|
},
|
||||||
"version": 28,
|
"version": 32,
|
||||||
"width": "88.00000",
|
"width": 88,
|
||||||
"x": 6,
|
"x": 6,
|
||||||
"y": "7.20923",
|
"y": "7.09000",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -854,7 +854,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 25,
|
"version": 30,
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"x": 150,
|
"x": 150,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@@ -904,15 +904,15 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"id4": {
|
"id4": {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"endBinding": null,
|
"endBinding": null,
|
||||||
"height": "0.01000",
|
"height": "0.00293",
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
-44,
|
"-44.00000",
|
||||||
"-0.01000",
|
"-0.00293",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
@@ -923,9 +923,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
],
|
],
|
||||||
"mode": "orbit",
|
"mode": "orbit",
|
||||||
},
|
},
|
||||||
"version": 24,
|
"version": 29,
|
||||||
"width": 44,
|
"width": "44.00000",
|
||||||
"y": "0.01000",
|
"y": "0.00293",
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
@@ -943,7 +943,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
-100,
|
"6.00000",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -955,8 +955,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
],
|
],
|
||||||
"mode": "orbit",
|
"mode": "orbit",
|
||||||
},
|
},
|
||||||
"version": 23,
|
"version": 27,
|
||||||
"width": 100,
|
"width": "6.00000",
|
||||||
"y": "0.01000",
|
"y": "0.01000",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1004,21 +1004,21 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
"startBinding": null,
|
"startBinding": null,
|
||||||
"version": 25,
|
"version": 30,
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"x": 150,
|
"x": 150,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"height": "0.01000",
|
"height": "0.00293",
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
-44,
|
"-44.00000",
|
||||||
"-0.01000",
|
"-0.00293",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
@@ -1029,10 +1029,10 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
],
|
],
|
||||||
"mode": "orbit",
|
"mode": "orbit",
|
||||||
},
|
},
|
||||||
"version": 24,
|
"version": 29,
|
||||||
"width": 44,
|
"width": "44.00000",
|
||||||
"x": 250,
|
"x": 144,
|
||||||
"y": "0.01000",
|
"y": "0.00293",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1335,7 +1335,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": "29.32551",
|
"height": "29.36414",
|
||||||
"id": "id4",
|
"id": "id4",
|
||||||
"index": "Zz",
|
"index": "Zz",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@@ -1350,7 +1350,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
88,
|
88,
|
||||||
"29.32551",
|
"29.36414",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@@ -1369,10 +1369,10 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 8,
|
"version": 10,
|
||||||
"width": 88,
|
"width": 88,
|
||||||
"x": 6,
|
"x": 6,
|
||||||
"y": "2.00947",
|
"y": "2.00946",
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -1546,7 +1546,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
],
|
],
|
||||||
"mode": "orbit",
|
"mode": "orbit",
|
||||||
},
|
},
|
||||||
"version": 8,
|
"version": 10,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"endBinding": null,
|
"endBinding": null,
|
||||||
@@ -1698,7 +1698,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": "17.59718",
|
"height": "14.91372",
|
||||||
"id": "id5",
|
"id": "id5",
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@@ -1712,8 +1712,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
88,
|
"88.00000",
|
||||||
"-17.59718",
|
"-14.91372",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@@ -1732,10 +1732,10 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 8,
|
"version": 11,
|
||||||
"width": 88,
|
"width": "88.00000",
|
||||||
"x": 6,
|
"x": 6,
|
||||||
"y": "38.80379",
|
"y": "37.05219",
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -1846,7 +1846,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": "17.59718",
|
"height": "14.91372",
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"link": null,
|
"link": null,
|
||||||
@@ -1858,8 +1858,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
88,
|
"88.00000",
|
||||||
"-17.59718",
|
"-14.91372",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@@ -1877,14 +1877,14 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"version": 8,
|
"version": 11,
|
||||||
"width": 88,
|
"width": "88.00000",
|
||||||
"x": 6,
|
"x": 6,
|
||||||
"y": "38.80379",
|
"y": "37.05219",
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
"version": 7,
|
"version": 8,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -2398,7 +2398,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": "439.20000",
|
"height": "439.13521",
|
||||||
"id": "id4",
|
"id": "id4",
|
||||||
"index": "a2",
|
"index": "a2",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@@ -2413,7 +2413,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
488,
|
488,
|
||||||
"-439.20000",
|
"-439.13521",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@@ -2434,10 +2434,10 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 12,
|
"version": 13,
|
||||||
"width": 488,
|
"width": 488,
|
||||||
"x": 6,
|
"x": 6,
|
||||||
"y": "-5.39000",
|
"y": "-5.38920",
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -2566,7 +2566,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": "439.20000",
|
"height": "439.13521",
|
||||||
"index": "a2",
|
"index": "a2",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"link": null,
|
"link": null,
|
||||||
@@ -2579,7 +2579,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
488,
|
488,
|
||||||
"-439.20000",
|
"-439.13521",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@@ -2599,14 +2599,14 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"version": 12,
|
"version": 13,
|
||||||
"width": 488,
|
"width": 488,
|
||||||
"x": 6,
|
"x": 6,
|
||||||
"y": "-5.39000",
|
"y": "-5.38920",
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
"version": 9,
|
"version": 10,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -16383,7 +16383,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 0,
|
"height": "0.00004",
|
||||||
"id": "id13",
|
"id": "id13",
|
||||||
"index": "a3",
|
"index": "a3",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@@ -16398,7 +16398,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
"88.00000",
|
"88.00000",
|
||||||
0,
|
"0.00004",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@@ -16419,10 +16419,10 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 10,
|
"version": 12,
|
||||||
"width": "88.00000",
|
"width": "88.00000",
|
||||||
"x": 6,
|
"x": 6,
|
||||||
"y": "0.01000",
|
"y": "0.00996",
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -16472,7 +16472,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
],
|
],
|
||||||
"mode": "orbit",
|
"mode": "orbit",
|
||||||
},
|
},
|
||||||
"version": 10,
|
"version": 12,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
@@ -16492,7 +16492,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
],
|
],
|
||||||
"mode": "orbit",
|
"mode": "orbit",
|
||||||
},
|
},
|
||||||
"version": 8,
|
"version": 9,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -16801,7 +16801,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": "0.00120",
|
"height": "0.00936",
|
||||||
"index": "a3",
|
"index": "a3",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"link": null,
|
"link": null,
|
||||||
@@ -16813,8 +16813,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"88.00000",
|
88,
|
||||||
"0.00120",
|
"0.00936",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@@ -16834,14 +16834,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"version": 7,
|
"version": 8,
|
||||||
"width": "88.00000",
|
"width": 88,
|
||||||
"x": 6,
|
"x": 6,
|
||||||
"y": "0.00880",
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
"version": 6,
|
"version": 7,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -17134,7 +17134,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 0,
|
"height": "0.00004",
|
||||||
"id": "id13",
|
"id": "id13",
|
||||||
"index": "a3",
|
"index": "a3",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@@ -17149,7 +17149,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
"88.00000",
|
"88.00000",
|
||||||
0,
|
"0.00004",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@@ -17170,10 +17170,10 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 10,
|
"version": 12,
|
||||||
"width": "88.00000",
|
"width": "88.00000",
|
||||||
"x": 6,
|
"x": 6,
|
||||||
"y": "0.01000",
|
"y": "0.00996",
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -17442,7 +17442,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 0,
|
"height": "0.00004",
|
||||||
"index": "a3",
|
"index": "a3",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"link": null,
|
"link": null,
|
||||||
@@ -17455,7 +17455,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
"88.00000",
|
"88.00000",
|
||||||
0,
|
"0.00004",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@@ -17475,14 +17475,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"version": 10,
|
"version": 12,
|
||||||
"width": "88.00000",
|
"width": "88.00000",
|
||||||
"x": 6,
|
"x": 6,
|
||||||
"y": "0.01000",
|
"y": "0.00996",
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
"version": 8,
|
"version": 9,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -17783,7 +17783,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 0,
|
"height": "0.00004",
|
||||||
"id": "id13",
|
"id": "id13",
|
||||||
"index": "a3",
|
"index": "a3",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@@ -17798,7 +17798,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
"88.00000",
|
"88.00000",
|
||||||
0,
|
"0.00004",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@@ -17819,10 +17819,10 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 10,
|
"version": 12,
|
||||||
"width": "88.00000",
|
"width": "88.00000",
|
||||||
"x": 6,
|
"x": 6,
|
||||||
"y": "0.01000",
|
"y": "0.00996",
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -18091,7 +18091,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 0,
|
"height": "0.00004",
|
||||||
"index": "a3",
|
"index": "a3",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"link": null,
|
"link": null,
|
||||||
@@ -18104,7 +18104,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
"88.00000",
|
"88.00000",
|
||||||
0,
|
"0.00004",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@@ -18124,14 +18124,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"version": 10,
|
"version": 12,
|
||||||
"width": "88.00000",
|
"width": "88.00000",
|
||||||
"x": 6,
|
"x": 6,
|
||||||
"y": "0.01000",
|
"y": "0.00996",
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
"version": 8,
|
"version": 9,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -18430,7 +18430,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 0,
|
"height": "0.00004",
|
||||||
"id": "id13",
|
"id": "id13",
|
||||||
"index": "a3",
|
"index": "a3",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@@ -18445,7 +18445,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
"88.00000",
|
"88.00000",
|
||||||
0,
|
"0.00004",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@@ -18466,10 +18466,10 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 10,
|
"version": 12,
|
||||||
"width": "88.00000",
|
"width": "88.00000",
|
||||||
"x": 6,
|
"x": 6,
|
||||||
"y": "0.01000",
|
"y": "0.00996",
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -18535,7 +18535,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
],
|
],
|
||||||
"mode": "orbit",
|
"mode": "orbit",
|
||||||
},
|
},
|
||||||
"version": 10,
|
"version": 12,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
@@ -18547,7 +18547,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"mode": "orbit",
|
"mode": "orbit",
|
||||||
},
|
},
|
||||||
"startBinding": null,
|
"startBinding": null,
|
||||||
"version": 8,
|
"version": 9,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"id2": {
|
"id2": {
|
||||||
@@ -18824,7 +18824,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": "0.00120",
|
"height": "0.00936",
|
||||||
"index": "a3",
|
"index": "a3",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"link": null,
|
"link": null,
|
||||||
@@ -18836,8 +18836,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"88.00000",
|
88,
|
||||||
"0.00120",
|
"0.00936",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@@ -18857,14 +18857,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"version": 7,
|
"version": 8,
|
||||||
"width": "88.00000",
|
"width": 88,
|
||||||
"x": 6,
|
"x": 6,
|
||||||
"y": "0.00880",
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
"version": 6,
|
"version": 7,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -19185,7 +19185,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 0,
|
"height": "0.00004",
|
||||||
"id": "id13",
|
"id": "id13",
|
||||||
"index": "a3",
|
"index": "a3",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@@ -19200,7 +19200,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
"88.00000",
|
"88.00000",
|
||||||
0,
|
"0.00004",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@@ -19221,10 +19221,10 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 11,
|
"version": 13,
|
||||||
"width": "88.00000",
|
"width": "88.00000",
|
||||||
"x": 6,
|
"x": 6,
|
||||||
"y": "0.01000",
|
"y": "0.00996",
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -19301,12 +19301,12 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
],
|
],
|
||||||
"mode": "orbit",
|
"mode": "orbit",
|
||||||
},
|
},
|
||||||
"version": 11,
|
"version": 13,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"endBinding": null,
|
"endBinding": null,
|
||||||
"startBinding": null,
|
"startBinding": null,
|
||||||
"version": 9,
|
"version": 10,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -19575,7 +19575,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": "0.00120",
|
"height": "0.00936",
|
||||||
"index": "a3",
|
"index": "a3",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"link": null,
|
"link": null,
|
||||||
@@ -19587,8 +19587,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"88.00000",
|
88,
|
||||||
"0.00120",
|
"0.00936",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@@ -19608,14 +19608,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"version": 7,
|
"version": 8,
|
||||||
"width": "88.00000",
|
"width": 88,
|
||||||
"x": 6,
|
"x": 6,
|
||||||
"y": "0.00880",
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
"version": 6,
|
"version": 7,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -182,14 +182,14 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||||||
"elementId": "id3",
|
"elementId": "id3",
|
||||||
"fixedPoint": [
|
"fixedPoint": [
|
||||||
"-0.02000",
|
"-0.02000",
|
||||||
"0.48010",
|
"0.47904",
|
||||||
],
|
],
|
||||||
"mode": "orbit",
|
"mode": "orbit",
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": "90.01760",
|
"height": "90.02554",
|
||||||
"id": "id6",
|
"id": "id6",
|
||||||
"index": "a2",
|
"index": "a2",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@@ -204,7 +204,7 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
89,
|
89,
|
||||||
"90.01760",
|
"90.02554",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@@ -217,7 +217,7 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||||||
"elementId": "id0",
|
"elementId": "id0",
|
||||||
"fixedPoint": [
|
"fixedPoint": [
|
||||||
"1.06000",
|
"1.06000",
|
||||||
"0.56011",
|
"0.55687",
|
||||||
],
|
],
|
||||||
"mode": "orbit",
|
"mode": "orbit",
|
||||||
},
|
},
|
||||||
@@ -230,6 +230,6 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||||||
"versionNonce": 271613161,
|
"versionNonce": 271613161,
|
||||||
"width": 89,
|
"width": 89,
|
||||||
"x": 106,
|
"x": 106,
|
||||||
"y": "56.01120",
|
"y": "55.68677",
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
import { vi } from "vitest";
|
|
||||||
|
|
||||||
import { CURSOR_TYPE } from "@excalidraw/common";
|
|
||||||
import { getElementAbsoluteCoords } from "@excalidraw/element";
|
|
||||||
|
|
||||||
import { Excalidraw } from "../index";
|
|
||||||
import { getLinkHandleFromCoords } from "../components/hyperlink/helpers";
|
|
||||||
|
|
||||||
import { API } from "./helpers/api";
|
|
||||||
import { Pointer } from "./helpers/ui";
|
|
||||||
import { act, GlobalTestState, render, waitFor } from "./test-utils";
|
|
||||||
|
|
||||||
import type { ExcalidrawProps } from "../types";
|
|
||||||
|
|
||||||
describe("laser tool interactions", () => {
|
|
||||||
const h = window.h;
|
|
||||||
const mouse = new Pointer("mouse");
|
|
||||||
|
|
||||||
it("opens links while using the laser tool", async () => {
|
|
||||||
const onLinkOpenSpy = vi.fn();
|
|
||||||
const onLinkOpen: NonNullable<ExcalidrawProps["onLinkOpen"]> = (
|
|
||||||
...args
|
|
||||||
) => {
|
|
||||||
onLinkOpenSpy(...args);
|
|
||||||
args[1].preventDefault();
|
|
||||||
};
|
|
||||||
await render(<Excalidraw onLinkOpen={onLinkOpen} />);
|
|
||||||
|
|
||||||
const linkedRect = API.createElement({
|
|
||||||
type: "rectangle",
|
|
||||||
x: 20,
|
|
||||||
y: 20,
|
|
||||||
width: 120,
|
|
||||||
height: 90,
|
|
||||||
});
|
|
||||||
API.setElements([linkedRect]);
|
|
||||||
API.updateElement(linkedRect, {
|
|
||||||
link: "https://example.com",
|
|
||||||
});
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
h.app.setActiveTool({ type: "laser" });
|
|
||||||
});
|
|
||||||
|
|
||||||
const elementsMap = h.app.scene.getNonDeletedElementsMap();
|
|
||||||
const currentRect = API.getElement(linkedRect);
|
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(currentRect, elementsMap);
|
|
||||||
const [linkX, linkY, linkWidth, linkHeight] = getLinkHandleFromCoords(
|
|
||||||
[x1, y1, x2, y2],
|
|
||||||
currentRect.angle,
|
|
||||||
h.state,
|
|
||||||
);
|
|
||||||
const iconCenterX = linkX + linkWidth / 2;
|
|
||||||
const iconCenterY = linkY + linkHeight / 2;
|
|
||||||
|
|
||||||
mouse.moveTo(iconCenterX, iconCenterY);
|
|
||||||
expect(GlobalTestState.interactiveCanvas.style.cursor).toBe(
|
|
||||||
CURSOR_TYPE.POINTER,
|
|
||||||
);
|
|
||||||
|
|
||||||
mouse.clickAt(iconCenterX, iconCenterY);
|
|
||||||
expect(onLinkOpenSpy).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("activates embeddables on center click while using the laser tool", async () => {
|
|
||||||
await render(<Excalidraw />);
|
|
||||||
|
|
||||||
const embeddable = API.createElement({
|
|
||||||
type: "embeddable",
|
|
||||||
x: 40,
|
|
||||||
y: 40,
|
|
||||||
width: 300,
|
|
||||||
height: 180,
|
|
||||||
});
|
|
||||||
API.setElements([embeddable]);
|
|
||||||
API.updateElement(embeddable, {
|
|
||||||
link: "https://www.youtube.com/watch?v=gkGMXY0wekg",
|
|
||||||
});
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
h.app.setActiveTool({ type: "laser" });
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleIframeLikeCenterClickSpy = vi.spyOn(
|
|
||||||
h.app as unknown as {
|
|
||||||
handleIframeLikeCenterClick: () => void;
|
|
||||||
},
|
|
||||||
"handleIframeLikeCenterClick",
|
|
||||||
);
|
|
||||||
|
|
||||||
const centerX = embeddable.x + embeddable.width / 2;
|
|
||||||
const centerY = embeddable.y + embeddable.height / 2;
|
|
||||||
|
|
||||||
mouse.moveTo(centerX, centerY);
|
|
||||||
expect(GlobalTestState.interactiveCanvas.style.cursor).toBe(
|
|
||||||
CURSOR_TYPE.POINTER,
|
|
||||||
);
|
|
||||||
mouse.clickAt(centerX, centerY);
|
|
||||||
|
|
||||||
expect(handleIframeLikeCenterClickSpy).toHaveBeenCalled();
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(h.state.activeEmbeddable?.element.id).toBe(embeddable.id);
|
|
||||||
expect(h.state.activeEmbeddable?.state).toBe("active");
|
|
||||||
});
|
|
||||||
|
|
||||||
handleIframeLikeCenterClickSpy.mockRestore();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user