feat: Focus indicator (#10613)
* feat: Focus indicator Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Snapshot update Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: Move visualdebug to utils and introduce volume bindable volume visualization Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Move visualdebug to elements Due to dep circles Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Possible test timeout Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Incorrect hit test point input Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: Add fallback when dragged outside of allowed area Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Elbow arrows don't need focus point mgmt Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: End bound indirect fix Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Show indicator when arrow endpoint dragging Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Update bound arrow endpoint at mid-point drag Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * chore: Refactor Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Curve endpoint intersection Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Outline focus point is reset on existing arrow drag Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Tests Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * chore: Fix lint Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: Dragging focus point off Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Don't show the focus indicator when arrow endpoint is dragged Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Drag area for focus handles Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Focus point size unified Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Size bump for focus knob Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: Cache hits and scene lookups Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * chore: Remove debug Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Consider hit threshold and inside override too Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Other shape switching Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * perf: Update tolerance params Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Focus know line width Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: knob offset Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Full overlap Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * chore: Remove Map caching Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: incorrect threshold Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: threshold setting Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Hit caching Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: cache override Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Snapshots Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: Redesigned focus point handling Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Inside-inside mode Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * chore: Remove comment Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: Allow focus knob outside the shape Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Arrow endpoint offset Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Focus knob element distance Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Increase iteration on curve intersection calc Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Handle disabled binding Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Alt mode Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Nested shape focus rewrite Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Alt + Ctrl + arrow endpoitn Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Hit ordering for focus points Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Focus point visibility Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * dry out renderFocusPointIndicator * do not higlight point when dragging & make focus point smaller * optimize retrieval of selectedLinearElement * move focus highlighting into renderFocusPointIndicator to DRY out and colocate * remove `disabled` state from focus highlight * make focus point stroke color less prominent * fix: No focus point for multi-point arrows Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Arrow edit mode drag focus point release Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * DRY out arrow point-like drag * move `focus.ts` to `arrows/focus.ts` --------- Signed-off-by: Mark Tolmacs <mark@lazycat.hu> Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,541 @@
|
|||||||
|
import { pointDistance, pointFrom, type GlobalPoint } from "@excalidraw/math";
|
||||||
|
import { invariant } from "@excalidraw/common";
|
||||||
|
|
||||||
|
import type { AppState, NullableGridSize } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
|
import {
|
||||||
|
bindBindingElement,
|
||||||
|
calculateFixedPointForNonElbowArrowBinding,
|
||||||
|
FOCUS_POINT_SIZE,
|
||||||
|
getBindingGap,
|
||||||
|
getGlobalFixedPointForBindableElement,
|
||||||
|
isBindingEnabled,
|
||||||
|
maxBindingDistance_simple,
|
||||||
|
unbindBindingElement,
|
||||||
|
updateBoundPoint,
|
||||||
|
} from "../binding";
|
||||||
|
import {
|
||||||
|
isBindableElement,
|
||||||
|
isBindingElement,
|
||||||
|
isElbowArrow,
|
||||||
|
} from "../typeChecks";
|
||||||
|
import { LinearElementEditor } from "../linearElementEditor";
|
||||||
|
import { getHoveredElementForFocusPoint, hitElementItself } from "../collision";
|
||||||
|
import { moveArrowAboveBindable } from "../zindex";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ElementsMap,
|
||||||
|
ExcalidrawArrowElement,
|
||||||
|
ExcalidrawBindableElement,
|
||||||
|
NonDeletedSceneElementsMap,
|
||||||
|
PointsPositionUpdates,
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
|
import type { Scene } from "../Scene";
|
||||||
|
|
||||||
|
export const isFocusPointVisible = (
|
||||||
|
focusPoint: GlobalPoint,
|
||||||
|
arrow: ExcalidrawArrowElement,
|
||||||
|
bindableElement: ExcalidrawBindableElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
appState: {
|
||||||
|
isBindingEnabled: AppState["isBindingEnabled"];
|
||||||
|
zoom: AppState["zoom"];
|
||||||
|
},
|
||||||
|
ignoreOverlap = false,
|
||||||
|
): boolean => {
|
||||||
|
// No focus point management for elbow arrows, because elbow arrows
|
||||||
|
// always have their focus point at the arrow point itself
|
||||||
|
if (
|
||||||
|
isElbowArrow(arrow) ||
|
||||||
|
!isBindingEnabled(appState) ||
|
||||||
|
arrow.points.length !== 2
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoid showing the focus point indicator if the focus point is essentially
|
||||||
|
// on top of the arrow point it belongs to itself, if not ignoring specifically
|
||||||
|
if (!ignoreOverlap) {
|
||||||
|
const associatedPointIdx =
|
||||||
|
arrow.startBinding?.elementId === bindableElement.id
|
||||||
|
? 0
|
||||||
|
: arrow.points.length - 1;
|
||||||
|
const associatedArrowPoint =
|
||||||
|
LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||||
|
arrow,
|
||||||
|
associatedPointIdx,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
pointDistance(focusPoint, associatedArrowPoint) <
|
||||||
|
(FOCUS_POINT_SIZE * 1.5) / appState.zoom.value
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the focus point is within the element's shape bounds
|
||||||
|
return hitElementItself({
|
||||||
|
element: bindableElement,
|
||||||
|
elementsMap,
|
||||||
|
point: focusPoint,
|
||||||
|
threshold: getBindingGap(bindableElement, arrow),
|
||||||
|
overrideShouldTestInside: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Updates the arrow endpoints in "orbit" configuration
|
||||||
|
const focusPointUpdate = (
|
||||||
|
arrow: ExcalidrawArrowElement,
|
||||||
|
bindableElement: ExcalidrawBindableElement | null,
|
||||||
|
isStartBinding: boolean,
|
||||||
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
|
scene: Scene,
|
||||||
|
appState: AppState,
|
||||||
|
switchToInsideBinding: boolean,
|
||||||
|
) => {
|
||||||
|
const pointUpdates = new Map();
|
||||||
|
|
||||||
|
const bindingField = isStartBinding ? "startBinding" : "endBinding";
|
||||||
|
const adjacentBindingField = isStartBinding ? "endBinding" : "startBinding";
|
||||||
|
let currentBinding = arrow[bindingField];
|
||||||
|
let adjacentBinding = arrow[adjacentBindingField];
|
||||||
|
|
||||||
|
// Update the dragged focus point related end
|
||||||
|
if (currentBinding && bindableElement) {
|
||||||
|
// Update the targeted bindings
|
||||||
|
const boundToSameElement =
|
||||||
|
bindableElement &&
|
||||||
|
adjacentBinding &&
|
||||||
|
currentBinding.elementId === adjacentBinding.elementId;
|
||||||
|
if (switchToInsideBinding || boundToSameElement) {
|
||||||
|
currentBinding = {
|
||||||
|
...currentBinding,
|
||||||
|
mode: "inside",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
currentBinding = {
|
||||||
|
...currentBinding,
|
||||||
|
mode: "orbit",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const pointIndex = isStartBinding ? 0 : arrow.points.length - 1;
|
||||||
|
const newPoint = updateBoundPoint(
|
||||||
|
arrow,
|
||||||
|
bindingField as "startBinding" | "endBinding",
|
||||||
|
currentBinding,
|
||||||
|
bindableElement,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newPoint) {
|
||||||
|
pointUpdates.set(pointIndex, { point: newPoint });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also update the adjacent end if it has a binding
|
||||||
|
if (adjacentBinding) {
|
||||||
|
const adjacentBindableElement = elementsMap.get(
|
||||||
|
adjacentBinding.elementId,
|
||||||
|
) as ExcalidrawBindableElement;
|
||||||
|
|
||||||
|
if (
|
||||||
|
adjacentBindableElement &&
|
||||||
|
isBindableElement(adjacentBindableElement) &&
|
||||||
|
isBindingEnabled(appState)
|
||||||
|
) {
|
||||||
|
// Same shape bound on both ends
|
||||||
|
const boundToSameElementAfterUpdate =
|
||||||
|
bindableElement && adjacentBinding.elementId === bindableElement.id;
|
||||||
|
if (switchToInsideBinding || boundToSameElementAfterUpdate) {
|
||||||
|
adjacentBinding = {
|
||||||
|
...adjacentBinding,
|
||||||
|
mode: "inside",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
adjacentBinding = {
|
||||||
|
...adjacentBinding,
|
||||||
|
mode: "orbit",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const adjacentPointIndex = isStartBinding ? arrow.points.length - 1 : 0;
|
||||||
|
const adjacentNewPoint = updateBoundPoint(
|
||||||
|
arrow,
|
||||||
|
adjacentBindingField,
|
||||||
|
adjacentBinding,
|
||||||
|
adjacentBindableElement,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (adjacentNewPoint) {
|
||||||
|
pointUpdates.set(adjacentPointIndex, {
|
||||||
|
point: adjacentNewPoint,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pointUpdates.size > 0) {
|
||||||
|
LinearElementEditor.movePoints(arrow, scene, pointUpdates, {
|
||||||
|
[bindingField]: currentBinding,
|
||||||
|
[adjacentBindingField]: adjacentBinding,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handleFocusPointDrag = (
|
||||||
|
linearElementEditor: LinearElementEditor,
|
||||||
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
|
pointerCoords: { x: number; y: number },
|
||||||
|
scene: Scene,
|
||||||
|
appState: AppState,
|
||||||
|
gridSize: NullableGridSize,
|
||||||
|
switchToInsideBinding: boolean,
|
||||||
|
) => {
|
||||||
|
const arrow = LinearElementEditor.getElement(
|
||||||
|
linearElementEditor.elementId,
|
||||||
|
elementsMap,
|
||||||
|
) as any;
|
||||||
|
|
||||||
|
// Sanity checks
|
||||||
|
if (
|
||||||
|
!arrow ||
|
||||||
|
!isBindingElement(arrow) ||
|
||||||
|
isElbowArrow(arrow) ||
|
||||||
|
!linearElementEditor.hoveredFocusPointBinding ||
|
||||||
|
!linearElementEditor.draggedFocusPointBinding
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isStartBinding =
|
||||||
|
linearElementEditor.draggedFocusPointBinding === "start";
|
||||||
|
const binding = isStartBinding ? arrow.startBinding : arrow.endBinding;
|
||||||
|
const { x: offsetX, y: offsetY } = linearElementEditor.pointerOffset;
|
||||||
|
const point = pointFrom<GlobalPoint>(
|
||||||
|
pointerCoords.x - offsetX,
|
||||||
|
pointerCoords.y - offsetY,
|
||||||
|
);
|
||||||
|
const bindingField = isStartBinding ? "startBinding" : "endBinding";
|
||||||
|
const hit = getHoveredElementForFocusPoint(
|
||||||
|
point,
|
||||||
|
arrow,
|
||||||
|
scene.getNonDeletedElements(),
|
||||||
|
elementsMap,
|
||||||
|
maxBindingDistance_simple(appState.zoom),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Hovering a bindable element
|
||||||
|
if (hit && isBindingEnabled(appState)) {
|
||||||
|
// Break existing binding if bound to another shape or if binding is disabled
|
||||||
|
if (arrow[bindingField] && hit.id !== binding?.elementId) {
|
||||||
|
unbindBindingElement(
|
||||||
|
arrow,
|
||||||
|
linearElementEditor.draggedFocusPointBinding,
|
||||||
|
scene,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle binding mode switch
|
||||||
|
const newMode =
|
||||||
|
switchToInsideBinding && arrow[bindingField]?.mode === "orbit"
|
||||||
|
? "inside"
|
||||||
|
: !switchToInsideBinding && arrow[bindingField]?.mode === "inside"
|
||||||
|
? "orbit"
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// If no existing binding, create it
|
||||||
|
if (!arrow[bindingField] || newMode) {
|
||||||
|
// Create a new binding if none exists
|
||||||
|
bindBindingElement(
|
||||||
|
arrow,
|
||||||
|
hit,
|
||||||
|
newMode || "orbit",
|
||||||
|
linearElementEditor.draggedFocusPointBinding,
|
||||||
|
scene,
|
||||||
|
point,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the binding's fixed point
|
||||||
|
scene.mutateElement(arrow, {
|
||||||
|
[bindingField]: {
|
||||||
|
...arrow[bindingField],
|
||||||
|
elementId: hit.id,
|
||||||
|
mode: newMode || arrow[bindingField]?.mode || "orbit",
|
||||||
|
...calculateFixedPointForNonElbowArrowBinding(
|
||||||
|
arrow,
|
||||||
|
hit,
|
||||||
|
linearElementEditor.draggedFocusPointBinding,
|
||||||
|
elementsMap,
|
||||||
|
point,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Not hovering any bindable element, move the arrow endpoint
|
||||||
|
const pointUpdates: PointsPositionUpdates = new Map();
|
||||||
|
const pointIndex = isStartBinding ? 0 : arrow.points.length - 1;
|
||||||
|
pointUpdates.set(pointIndex, {
|
||||||
|
point: LinearElementEditor.createPointAt(
|
||||||
|
arrow,
|
||||||
|
elementsMap,
|
||||||
|
point[0],
|
||||||
|
point[1],
|
||||||
|
gridSize,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
LinearElementEditor.movePoints(arrow, scene, pointUpdates);
|
||||||
|
if (arrow[bindingField]) {
|
||||||
|
unbindBindingElement(arrow, isStartBinding ? "start" : "end", scene);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the arrow endpoints
|
||||||
|
focusPointUpdate(
|
||||||
|
arrow,
|
||||||
|
hit,
|
||||||
|
isStartBinding,
|
||||||
|
elementsMap,
|
||||||
|
scene,
|
||||||
|
appState,
|
||||||
|
switchToInsideBinding,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hit && isBindingEnabled(appState)) {
|
||||||
|
moveArrowAboveBindable(
|
||||||
|
point,
|
||||||
|
arrow,
|
||||||
|
scene.getElementsIncludingDeleted(),
|
||||||
|
elementsMap,
|
||||||
|
scene,
|
||||||
|
hit,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handleFocusPointPointerDown = (
|
||||||
|
arrow: ExcalidrawArrowElement,
|
||||||
|
pointerDownState: { origin: { x: number; y: number } },
|
||||||
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
|
appState: AppState,
|
||||||
|
): {
|
||||||
|
hitFocusPoint: "start" | "end" | null;
|
||||||
|
pointerOffset: { x: number; y: number };
|
||||||
|
} => {
|
||||||
|
const pointerPos = pointFrom(
|
||||||
|
pointerDownState.origin.x,
|
||||||
|
pointerDownState.origin.y,
|
||||||
|
);
|
||||||
|
const hitThreshold = (FOCUS_POINT_SIZE * 1.5) / appState.zoom.value;
|
||||||
|
|
||||||
|
// Check start binding focus point
|
||||||
|
if (arrow.startBinding?.elementId) {
|
||||||
|
const bindableElement = elementsMap.get(arrow.startBinding.elementId);
|
||||||
|
if (
|
||||||
|
bindableElement &&
|
||||||
|
isBindableElement(bindableElement) &&
|
||||||
|
!bindableElement.isDeleted
|
||||||
|
) {
|
||||||
|
const focusPoint = getGlobalFixedPointForBindableElement(
|
||||||
|
arrow.startBinding.fixedPoint,
|
||||||
|
bindableElement,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
isFocusPointVisible(
|
||||||
|
focusPoint,
|
||||||
|
arrow,
|
||||||
|
bindableElement,
|
||||||
|
elementsMap,
|
||||||
|
appState,
|
||||||
|
) &&
|
||||||
|
pointDistance(pointerPos, focusPoint) <= hitThreshold
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
hitFocusPoint: "start",
|
||||||
|
pointerOffset: {
|
||||||
|
x: pointerPos[0] - focusPoint[0],
|
||||||
|
y: pointerPos[1] - focusPoint[1],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check end binding focus point (only if start not already hit)
|
||||||
|
if (arrow.endBinding?.elementId) {
|
||||||
|
const bindableElement = elementsMap.get(arrow.endBinding.elementId);
|
||||||
|
if (
|
||||||
|
bindableElement &&
|
||||||
|
isBindableElement(bindableElement) &&
|
||||||
|
!bindableElement.isDeleted
|
||||||
|
) {
|
||||||
|
const focusPoint = getGlobalFixedPointForBindableElement(
|
||||||
|
arrow.endBinding.fixedPoint,
|
||||||
|
bindableElement,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
isFocusPointVisible(
|
||||||
|
focusPoint,
|
||||||
|
arrow,
|
||||||
|
bindableElement,
|
||||||
|
elementsMap,
|
||||||
|
appState,
|
||||||
|
) &&
|
||||||
|
pointDistance(pointerPos, focusPoint) <= hitThreshold
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
hitFocusPoint: "end",
|
||||||
|
pointerOffset: {
|
||||||
|
x: pointerPos[0] - focusPoint[0],
|
||||||
|
y: pointerPos[1] - focusPoint[1],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hitFocusPoint: null,
|
||||||
|
pointerOffset: { x: 0, y: 0 },
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handleFocusPointPointerUp = (
|
||||||
|
linearElementEditor: LinearElementEditor,
|
||||||
|
scene: Scene,
|
||||||
|
) => {
|
||||||
|
invariant(
|
||||||
|
linearElementEditor.draggedFocusPointBinding,
|
||||||
|
"Must have a dragged focus point at pointer release",
|
||||||
|
);
|
||||||
|
|
||||||
|
const arrow = LinearElementEditor.getElement<ExcalidrawArrowElement>(
|
||||||
|
linearElementEditor.elementId,
|
||||||
|
scene.getNonDeletedElementsMap(),
|
||||||
|
);
|
||||||
|
invariant(arrow, "Arrow must be in the scene");
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
const bindingKey =
|
||||||
|
linearElementEditor.draggedFocusPointBinding === "start"
|
||||||
|
? "startBinding"
|
||||||
|
: "endBinding";
|
||||||
|
const otherBindingKey =
|
||||||
|
linearElementEditor.draggedFocusPointBinding === "start"
|
||||||
|
? "endBinding"
|
||||||
|
: "startBinding";
|
||||||
|
const boundElementId = arrow[bindingKey]?.elementId;
|
||||||
|
const otherBoundElementId = arrow[otherBindingKey]?.elementId;
|
||||||
|
const oldBoundElement =
|
||||||
|
boundElementId &&
|
||||||
|
scene
|
||||||
|
.getNonDeletedElements()
|
||||||
|
.find(
|
||||||
|
(element) =>
|
||||||
|
element.id !== boundElementId &&
|
||||||
|
element.id !== otherBoundElementId &&
|
||||||
|
isBindableElement(element) &&
|
||||||
|
element.boundElements?.find(({ id }) => id === arrow.id),
|
||||||
|
);
|
||||||
|
if (oldBoundElement) {
|
||||||
|
scene.mutateElement(oldBoundElement, {
|
||||||
|
boundElements: oldBoundElement.boundElements?.filter(
|
||||||
|
({ id }) => id !== arrow.id,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record the new bound element
|
||||||
|
const boundElement =
|
||||||
|
boundElementId && scene.getNonDeletedElementsMap().get(boundElementId);
|
||||||
|
if (boundElement) {
|
||||||
|
scene.mutateElement(boundElement, {
|
||||||
|
boundElements: [
|
||||||
|
...(boundElement.boundElements || [])?.filter(
|
||||||
|
({ id }) => id !== arrow.id,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
id: arrow.id,
|
||||||
|
type: "arrow",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handleFocusPointHover = (
|
||||||
|
arrow: ExcalidrawArrowElement,
|
||||||
|
scenePointerX: number,
|
||||||
|
scenePointerY: number,
|
||||||
|
scene: Scene,
|
||||||
|
appState: AppState,
|
||||||
|
): "start" | "end" | null => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
|
const pointerPos = pointFrom(scenePointerX, scenePointerY);
|
||||||
|
const hitThreshold = (FOCUS_POINT_SIZE * 1.5) / appState.zoom.value;
|
||||||
|
|
||||||
|
// Check start binding focus point
|
||||||
|
if (arrow.startBinding?.elementId) {
|
||||||
|
const bindableElement = elementsMap.get(arrow.startBinding.elementId);
|
||||||
|
if (
|
||||||
|
bindableElement &&
|
||||||
|
isBindableElement(bindableElement) &&
|
||||||
|
!bindableElement.isDeleted
|
||||||
|
) {
|
||||||
|
const focusPoint = getGlobalFixedPointForBindableElement(
|
||||||
|
arrow.startBinding.fixedPoint,
|
||||||
|
bindableElement,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
isFocusPointVisible(
|
||||||
|
focusPoint,
|
||||||
|
arrow,
|
||||||
|
bindableElement,
|
||||||
|
elementsMap,
|
||||||
|
appState,
|
||||||
|
) &&
|
||||||
|
pointDistance(pointerPos, focusPoint) <= hitThreshold
|
||||||
|
) {
|
||||||
|
return "start";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check end binding focus point (only if start not already hovered)
|
||||||
|
if (arrow.endBinding?.elementId) {
|
||||||
|
const bindableElement = elementsMap.get(arrow.endBinding.elementId);
|
||||||
|
if (
|
||||||
|
bindableElement &&
|
||||||
|
isBindableElement(bindableElement) &&
|
||||||
|
!bindableElement.isDeleted
|
||||||
|
) {
|
||||||
|
const focusPoint = getGlobalFixedPointForBindableElement(
|
||||||
|
arrow.endBinding.fixedPoint,
|
||||||
|
bindableElement,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
isFocusPointVisible(
|
||||||
|
focusPoint,
|
||||||
|
arrow,
|
||||||
|
bindableElement,
|
||||||
|
elementsMap,
|
||||||
|
appState,
|
||||||
|
) &&
|
||||||
|
pointDistance(pointerPos, focusPoint) <= hitThreshold
|
||||||
|
) {
|
||||||
|
return "end";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import type { App } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
|
import { LinearElementEditor } from "../linearElementEditor";
|
||||||
|
|
||||||
|
import { handleFocusPointDrag } from "./focus";
|
||||||
|
|
||||||
|
export const maybeHandleArrowPointlikeDrag = ({
|
||||||
|
app,
|
||||||
|
event,
|
||||||
|
}: {
|
||||||
|
app: App;
|
||||||
|
event: KeyboardEvent | React.KeyboardEvent<Element> | PointerEvent;
|
||||||
|
}): boolean => {
|
||||||
|
const appState = app.state;
|
||||||
|
if (appState.selectedLinearElement && app.lastPointerMoveCoords) {
|
||||||
|
// Update focus point status if the binding mode is changing
|
||||||
|
if (appState.selectedLinearElement.draggedFocusPointBinding) {
|
||||||
|
handleFocusPointDrag(
|
||||||
|
appState.selectedLinearElement,
|
||||||
|
app.scene.getNonDeletedElementsMap(),
|
||||||
|
app.lastPointerMoveCoords,
|
||||||
|
app.scene,
|
||||||
|
appState,
|
||||||
|
app.getEffectiveGridSize(),
|
||||||
|
event.altKey,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} else if (
|
||||||
|
appState.selectedLinearElement.hoverPointIndex !== null &&
|
||||||
|
app.lastPointerMoveEvent &&
|
||||||
|
appState.selectedLinearElement.initialState.lastClickedPoint >= 0 &&
|
||||||
|
appState.selectedLinearElement.isDragging
|
||||||
|
) {
|
||||||
|
LinearElementEditor.handlePointDragging(
|
||||||
|
app.lastPointerMoveEvent,
|
||||||
|
app,
|
||||||
|
app.lastPointerMoveCoords.x,
|
||||||
|
app.lastPointerMoveCoords.y,
|
||||||
|
appState.selectedLinearElement,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
@@ -116,6 +116,7 @@ export type BindingStrategy =
|
|||||||
*/
|
*/
|
||||||
export const BASE_BINDING_GAP = 10;
|
export const BASE_BINDING_GAP = 10;
|
||||||
export const BASE_BINDING_GAP_ELBOW = 5;
|
export const BASE_BINDING_GAP_ELBOW = 5;
|
||||||
|
export const FOCUS_POINT_SIZE = 10 / 1.5;
|
||||||
|
|
||||||
export const getBindingGap = (
|
export const getBindingGap = (
|
||||||
bindTarget: ExcalidrawBindableElement,
|
bindTarget: ExcalidrawBindableElement,
|
||||||
@@ -145,7 +146,9 @@ export const shouldEnableBindingForPointerEvent = (
|
|||||||
return !event[KEYS.CTRL_OR_CMD];
|
return !event[KEYS.CTRL_OR_CMD];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isBindingEnabled = (appState: AppState): boolean => {
|
export const isBindingEnabled = (appState: {
|
||||||
|
isBindingEnabled: AppState["isBindingEnabled"];
|
||||||
|
}): boolean => {
|
||||||
return appState.isBindingEnabled;
|
return appState.isBindingEnabled;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -259,7 +262,7 @@ const bindingStrategyForElbowArrowEndpointDragging = (
|
|||||||
globalPoint,
|
globalPoint,
|
||||||
elements,
|
elements,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
(element) => maxBindingDistance_simple(zoom),
|
maxBindingDistance_simple(zoom),
|
||||||
);
|
);
|
||||||
|
|
||||||
const current = hit
|
const current = hit
|
||||||
@@ -684,7 +687,7 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
|
|||||||
globalPoint,
|
globalPoint,
|
||||||
elements,
|
elements,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
(e) => maxBindingDistance_simple(appState.zoom),
|
maxBindingDistance_simple(appState.zoom),
|
||||||
);
|
);
|
||||||
const pointInElement =
|
const pointInElement =
|
||||||
hit &&
|
hit &&
|
||||||
@@ -711,7 +714,13 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
|
|||||||
const otherFocusPointIsInElement =
|
const otherFocusPointIsInElement =
|
||||||
otherBindableElement &&
|
otherBindableElement &&
|
||||||
otherFocusPoint &&
|
otherFocusPoint &&
|
||||||
isPointInElement(otherFocusPoint, otherBindableElement, elementsMap);
|
hitElementItself({
|
||||||
|
point: otherFocusPoint,
|
||||||
|
element: otherBindableElement,
|
||||||
|
elementsMap,
|
||||||
|
threshold: 0,
|
||||||
|
overrideShouldTestInside: true,
|
||||||
|
});
|
||||||
|
|
||||||
// Handle outside-outside binding to the same element
|
// Handle outside-outside binding to the same element
|
||||||
if (otherBinding && otherBinding.elementId === hit?.id) {
|
if (otherBinding && otherBinding.elementId === hit?.id) {
|
||||||
@@ -1674,7 +1683,9 @@ export const updateBoundPoint = (
|
|||||||
binding: FixedPointBinding | null | undefined,
|
binding: FixedPointBinding | null | undefined,
|
||||||
bindableElement: ExcalidrawBindableElement,
|
bindableElement: ExcalidrawBindableElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
customIntersector?: LineSegment<GlobalPoint>,
|
opts?: {
|
||||||
|
customIntersector?: LineSegment<GlobalPoint>;
|
||||||
|
},
|
||||||
): LocalPoint | null => {
|
): LocalPoint | null => {
|
||||||
if (
|
if (
|
||||||
binding == null ||
|
binding == null ||
|
||||||
@@ -1761,16 +1772,14 @@ export const updateBoundPoint = (
|
|||||||
|
|
||||||
const isNested = (arrowTooShort || isOverlapping) && isLargerThanOther;
|
const isNested = (arrowTooShort || isOverlapping) && isLargerThanOther;
|
||||||
|
|
||||||
let _customIntersector = customIntersector;
|
let _customIntersector = opts?.customIntersector;
|
||||||
if (!elbowed && !_customIntersector) {
|
if (!elbowed && !_customIntersector) {
|
||||||
const [x1, y1, x2, y2] = LinearElementEditor.getElementAbsoluteCoords(
|
const [x1, y1, x2, y2] = LinearElementEditor.getElementAbsoluteCoords(
|
||||||
arrow,
|
arrow,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
const center = pointFrom<GlobalPoint>((x1 + x2) / 2, (y1 + y2) / 2);
|
const center = pointFrom<GlobalPoint>((x1 + x2) / 2, (y1 + y2) / 2);
|
||||||
const edgePoint = isRectanguloidElement(bindableElement)
|
const edgePoint = global;
|
||||||
? avoidRectangularCorner(arrow, bindableElement, elementsMap, global)
|
|
||||||
: global;
|
|
||||||
const adjacentPoint = pointRotateRads(
|
const adjacentPoint = pointRotateRads(
|
||||||
pointFrom<GlobalPoint>(
|
pointFrom<GlobalPoint>(
|
||||||
arrow.x +
|
arrow.x +
|
||||||
@@ -1884,7 +1893,7 @@ export const calculateFixedPointForNonElbowArrowBinding = (
|
|||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
focusPoint?: GlobalPoint,
|
focusPoint?: GlobalPoint,
|
||||||
): { fixedPoint: FixedPoint } => {
|
): { fixedPoint: FixedPoint } => {
|
||||||
const edgePoint = focusPoint
|
const edgePoint: GlobalPoint = focusPoint
|
||||||
? focusPoint
|
? focusPoint
|
||||||
: LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
: LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||||
linearElement,
|
linearElement,
|
||||||
@@ -1892,11 +1901,7 @@ export const calculateFixedPointForNonElbowArrowBinding = (
|
|||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Convert the global point to element-local coordinates
|
const elementCenter = elementCenterPoint(hoveredElement, elementsMap);
|
||||||
const elementCenter = pointFrom(
|
|
||||||
hoveredElement.x + hoveredElement.width / 2,
|
|
||||||
hoveredElement.y + hoveredElement.height / 2,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Rotate the point to account for element rotation
|
// Rotate the point to account for element rotation
|
||||||
const nonRotatedPoint = pointRotateRads(
|
const nonRotatedPoint = pointRotateRads(
|
||||||
|
|||||||
@@ -59,8 +59,11 @@ import { LinearElementEditor } from "./linearElementEditor";
|
|||||||
|
|
||||||
import { distanceToElement } from "./distance";
|
import { distanceToElement } from "./distance";
|
||||||
|
|
||||||
|
import { getBindingGap } from "./binding";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
|
ExcalidrawArrowElement,
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
ExcalidrawDiamondElement,
|
ExcalidrawDiamondElement,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
@@ -290,7 +293,7 @@ export const getAllHoveredElementAtPoint = (
|
|||||||
point: Readonly<GlobalPoint>,
|
point: Readonly<GlobalPoint>,
|
||||||
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
|
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
toleranceFn?: (element: ExcalidrawBindableElement) => number,
|
tolerance?: number,
|
||||||
): NonDeleted<ExcalidrawBindableElement>[] => {
|
): NonDeleted<ExcalidrawBindableElement>[] => {
|
||||||
const candidateElements: NonDeleted<ExcalidrawBindableElement>[] = [];
|
const candidateElements: NonDeleted<ExcalidrawBindableElement>[] = [];
|
||||||
// We need to to hit testing from front (end of the array) to back (beginning of the array)
|
// We need to to hit testing from front (end of the array) to back (beginning of the array)
|
||||||
@@ -306,7 +309,7 @@ export const getAllHoveredElementAtPoint = (
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
isBindableElement(element, false) &&
|
isBindableElement(element, false) &&
|
||||||
bindingBorderTest(element, point, elementsMap, toleranceFn?.(element))
|
bindingBorderTest(element, point, elementsMap, tolerance)
|
||||||
) {
|
) {
|
||||||
candidateElements.push(element);
|
candidateElements.push(element);
|
||||||
|
|
||||||
@@ -323,13 +326,13 @@ export const getHoveredElementForBinding = (
|
|||||||
point: Readonly<GlobalPoint>,
|
point: Readonly<GlobalPoint>,
|
||||||
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
|
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
toleranceFn?: (element: ExcalidrawBindableElement) => number,
|
tolerance?: number,
|
||||||
): NonDeleted<ExcalidrawBindableElement> | null => {
|
): NonDeleted<ExcalidrawBindableElement> | null => {
|
||||||
const candidateElements = getAllHoveredElementAtPoint(
|
const candidateElements = getAllHoveredElementAtPoint(
|
||||||
point,
|
point,
|
||||||
elements,
|
elements,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
toleranceFn,
|
tolerance,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!candidateElements || candidateElements.length === 0) {
|
if (!candidateElements || candidateElements.length === 0) {
|
||||||
@@ -348,6 +351,56 @@ export const getHoveredElementForBinding = (
|
|||||||
.pop() as NonDeleted<ExcalidrawBindableElement>;
|
.pop() as NonDeleted<ExcalidrawBindableElement>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getHoveredElementForFocusPoint = (
|
||||||
|
point: GlobalPoint,
|
||||||
|
arrow: ExcalidrawArrowElement,
|
||||||
|
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
|
||||||
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
|
tolerance?: number,
|
||||||
|
): ExcalidrawBindableElement | null => {
|
||||||
|
const candidateElements: NonDeleted<ExcalidrawBindableElement>[] = [];
|
||||||
|
// We need to to hit testing from front (end of the array) to back (beginning of the array)
|
||||||
|
// because array is ordered from lower z-index to highest and we want element z-index
|
||||||
|
// with higher z-index
|
||||||
|
for (let index = elements.length - 1; index >= 0; --index) {
|
||||||
|
const element = elements[index];
|
||||||
|
|
||||||
|
invariant(
|
||||||
|
!element.isDeleted,
|
||||||
|
"Elements in the function parameter for getAllElementsAtPositionForBinding() should not contain deleted elements",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
isBindableElement(element, false) &&
|
||||||
|
bindingBorderTest(element, point, elementsMap, tolerance)
|
||||||
|
) {
|
||||||
|
candidateElements.push(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!candidateElements || candidateElements.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidateElements.length === 1) {
|
||||||
|
return candidateElements[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const distanceFilteredCandidateElements = candidateElements
|
||||||
|
// Resolve by distance
|
||||||
|
.filter(
|
||||||
|
(el) =>
|
||||||
|
distanceToElement(el, elementsMap, point) <= getBindingGap(el, arrow) ||
|
||||||
|
isPointInElement(point, el, elementsMap),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (distanceFilteredCandidateElements.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return distanceFilteredCandidateElements[0] as NonDeleted<ExcalidrawBindableElement>;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Intersect a line with an element for binding test
|
* Intersect a line with an element for binding test
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -2276,7 +2276,7 @@ const getHoveredElement = (
|
|||||||
origPoint,
|
origPoint,
|
||||||
elements,
|
elements,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
(element) => maxBindingDistance_simple(zoom),
|
maxBindingDistance_simple(zoom),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ export * from "./elbowArrow";
|
|||||||
export * from "./elementLink";
|
export * from "./elementLink";
|
||||||
export * from "./embeddable";
|
export * from "./embeddable";
|
||||||
export * from "./flowchart";
|
export * from "./flowchart";
|
||||||
|
export * from "./arrows/focus";
|
||||||
export * from "./fractionalIndex";
|
export * from "./fractionalIndex";
|
||||||
export * from "./frame";
|
export * from "./frame";
|
||||||
export * from "./groups";
|
export * from "./groups";
|
||||||
@@ -97,3 +98,4 @@ export * from "./transformHandles";
|
|||||||
export * from "./typeChecks";
|
export * from "./typeChecks";
|
||||||
export * from "./utils";
|
export * from "./utils";
|
||||||
export * from "./zindex";
|
export * from "./zindex";
|
||||||
|
export * from "./arrows/helpers";
|
||||||
|
|||||||
@@ -149,6 +149,8 @@ export class LinearElementEditor {
|
|||||||
public readonly pointerOffset: Readonly<{ x: number; y: number }>;
|
public readonly pointerOffset: Readonly<{ x: number; y: number }>;
|
||||||
public readonly hoverPointIndex: number;
|
public readonly hoverPointIndex: number;
|
||||||
public readonly segmentMidPointHoveredCoords: GlobalPoint | null;
|
public readonly segmentMidPointHoveredCoords: GlobalPoint | null;
|
||||||
|
public readonly hoveredFocusPointBinding: "start" | "end" | null;
|
||||||
|
public readonly draggedFocusPointBinding: "start" | "end" | null;
|
||||||
public readonly elbowed: boolean;
|
public readonly elbowed: boolean;
|
||||||
public readonly customLineAngle: number | null;
|
public readonly customLineAngle: number | null;
|
||||||
public readonly isEditing: boolean;
|
public readonly isEditing: boolean;
|
||||||
@@ -194,6 +196,8 @@ export class LinearElementEditor {
|
|||||||
};
|
};
|
||||||
this.hoverPointIndex = -1;
|
this.hoverPointIndex = -1;
|
||||||
this.segmentMidPointHoveredCoords = null;
|
this.segmentMidPointHoveredCoords = null;
|
||||||
|
this.hoveredFocusPointBinding = null;
|
||||||
|
this.draggedFocusPointBinding = null;
|
||||||
this.elbowed = isElbowArrow(element) && element.elbowed;
|
this.elbowed = isElbowArrow(element) && element.elbowed;
|
||||||
this.customLineAngle = null;
|
this.customLineAngle = null;
|
||||||
this.isEditing = isEditing;
|
this.isEditing = isEditing;
|
||||||
@@ -2135,6 +2139,63 @@ const pointDraggingUpdates = (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle the case where neither endpoint is being dragged
|
||||||
|
// but we need to update bound endpoints
|
||||||
|
if (!startIsDragged && !endIsDragged) {
|
||||||
|
const nextArrow = {
|
||||||
|
...element,
|
||||||
|
points: element.points.map((p, idx) => {
|
||||||
|
return naiveDraggingPoints.get(idx)?.point ?? p;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const positions = new Map(naiveDraggingPoints);
|
||||||
|
|
||||||
|
if (element.startBinding) {
|
||||||
|
const startBindable = elementsMap.get(element.startBinding.elementId) as
|
||||||
|
| ExcalidrawBindableElement
|
||||||
|
| undefined;
|
||||||
|
if (startBindable) {
|
||||||
|
const startPoint =
|
||||||
|
updateBoundPoint(
|
||||||
|
nextArrow,
|
||||||
|
"startBinding",
|
||||||
|
element.startBinding,
|
||||||
|
startBindable,
|
||||||
|
elementsMap,
|
||||||
|
) ?? null;
|
||||||
|
if (startPoint) {
|
||||||
|
positions.set(0, { point: startPoint, isDragging: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.endBinding) {
|
||||||
|
const endBindable = elementsMap.get(element.endBinding.elementId) as
|
||||||
|
| ExcalidrawBindableElement
|
||||||
|
| undefined;
|
||||||
|
if (endBindable) {
|
||||||
|
const endPoint =
|
||||||
|
updateBoundPoint(
|
||||||
|
nextArrow,
|
||||||
|
"endBinding",
|
||||||
|
element.endBinding,
|
||||||
|
endBindable,
|
||||||
|
elementsMap,
|
||||||
|
) ?? null;
|
||||||
|
if (endPoint) {
|
||||||
|
positions.set(element.points.length - 1, {
|
||||||
|
point: endPoint,
|
||||||
|
isDragging: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
positions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (startIsDragged === endIsDragged) {
|
if (startIsDragged === endIsDragged) {
|
||||||
return {
|
return {
|
||||||
positions: naiveDraggingPoints,
|
positions: naiveDraggingPoints,
|
||||||
@@ -2279,7 +2340,9 @@ const pointDraggingUpdates = (
|
|||||||
nextArrow.endBinding,
|
nextArrow.endBinding,
|
||||||
endBindable,
|
endBindable,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
endCustomIntersector,
|
{
|
||||||
|
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];
|
||||||
|
|
||||||
@@ -2302,7 +2365,7 @@ const pointDraggingUpdates = (
|
|||||||
: startIsDraggingOverEndElement &&
|
: startIsDraggingOverEndElement &&
|
||||||
app.state.bindMode !== "inside" &&
|
app.state.bindMode !== "inside" &&
|
||||||
getFeatureFlag("COMPLEX_BINDINGS")
|
getFeatureFlag("COMPLEX_BINDINGS")
|
||||||
? nextArrow.points[nextArrow.points.length - 1]
|
? endLocalPoint
|
||||||
: startBindable
|
: startBindable
|
||||||
? updateBoundPoint(
|
? updateBoundPoint(
|
||||||
element,
|
element,
|
||||||
@@ -2310,15 +2373,18 @@ const pointDraggingUpdates = (
|
|||||||
nextArrow.startBinding,
|
nextArrow.startBinding,
|
||||||
startBindable,
|
startBindable,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
startCustomIntersector,
|
{ customIntersector: startCustomIntersector },
|
||||||
) || nextArrow.points[0]
|
) || nextArrow.points[0]
|
||||||
: nextArrow.points[0];
|
: nextArrow.points[0];
|
||||||
|
|
||||||
const endChanged =
|
const endChanged =
|
||||||
pointDistance(
|
!startIsDraggingOverEndElement &&
|
||||||
endLocalPoint,
|
!(
|
||||||
nextArrow.points[nextArrow.points.length - 1],
|
endIsDraggingOverStartElement &&
|
||||||
) !== 0;
|
app.state.bindMode !== "inside" &&
|
||||||
|
getFeatureFlag("COMPLEX_BINDINGS")
|
||||||
|
) &&
|
||||||
|
!!endBindable;
|
||||||
const startChanged =
|
const startChanged =
|
||||||
pointDistance(startLocalPoint, nextArrow.points[0]) !== 0;
|
pointDistance(startLocalPoint, nextArrow.points[0]) !== 0;
|
||||||
|
|
||||||
@@ -2332,13 +2398,7 @@ const pointDraggingUpdates = (
|
|||||||
const indices = Array.from(indicesSet);
|
const indices = Array.from(indicesSet);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
updates:
|
updates,
|
||||||
updates.startBinding || updates.suggestedBinding
|
|
||||||
? {
|
|
||||||
startBinding: updates.startBinding,
|
|
||||||
suggestedBinding: updates.suggestedBinding,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
positions: new Map(
|
positions: new Map(
|
||||||
indices.map((idx) => {
|
indices.map((idx) => {
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -156,12 +156,11 @@ export const moveArrowAboveBindable = (
|
|||||||
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
|
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
|
hit?: NonDeletedExcalidrawElement,
|
||||||
): readonly OrderedExcalidrawElement[] => {
|
): readonly OrderedExcalidrawElement[] => {
|
||||||
const hoveredElement = getHoveredElementForBinding(
|
const hoveredElement = hit
|
||||||
point,
|
? hit
|
||||||
elements,
|
: getHoveredElementForBinding(point, elements, elementsMap);
|
||||||
elementsMap,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!hoveredElement) {
|
if (!hoveredElement) {
|
||||||
return elements;
|
return elements;
|
||||||
|
|||||||
@@ -378,7 +378,7 @@ describe("Test Linear Elements", () => {
|
|||||||
// drag line from midpoint
|
// drag line from midpoint
|
||||||
drag(midpoint, pointFrom(midpoint[0] + delta, midpoint[1] + delta));
|
drag(midpoint, pointFrom(midpoint[0] + delta, midpoint[1] + delta));
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`11`,
|
`12`,
|
||||||
);
|
);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||||
|
|
||||||
@@ -419,7 +419,7 @@ describe("Test Linear Elements", () => {
|
|||||||
fireEvent.click(screen.getByTitle("Round"));
|
fireEvent.click(screen.getByTitle("Round"));
|
||||||
|
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`9`,
|
`10`,
|
||||||
);
|
);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
|
||||||
|
|
||||||
@@ -480,7 +480,7 @@ describe("Test Linear Elements", () => {
|
|||||||
drag(startPoint, endPoint);
|
drag(startPoint, endPoint);
|
||||||
|
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`11`,
|
`12`,
|
||||||
);
|
);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||||
|
|
||||||
@@ -548,7 +548,7 @@ describe("Test Linear Elements", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`14`,
|
`15`,
|
||||||
);
|
);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`9`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`9`);
|
||||||
|
|
||||||
@@ -599,7 +599,7 @@ describe("Test Linear Elements", () => {
|
|||||||
drag(hitCoords, pointFrom(hitCoords[0] - delta, hitCoords[1] - delta));
|
drag(hitCoords, pointFrom(hitCoords[0] - delta, hitCoords[1] - delta));
|
||||||
|
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`11`,
|
`12`,
|
||||||
);
|
);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||||
|
|
||||||
@@ -640,7 +640,7 @@ describe("Test Linear Elements", () => {
|
|||||||
drag(hitCoords, pointFrom(hitCoords[0] + delta, hitCoords[1] + delta));
|
drag(hitCoords, pointFrom(hitCoords[0] + delta, hitCoords[1] + delta));
|
||||||
|
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`11`,
|
`12`,
|
||||||
);
|
);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||||
|
|
||||||
@@ -688,7 +688,7 @@ describe("Test Linear Elements", () => {
|
|||||||
deletePoint(points[2]);
|
deletePoint(points[2]);
|
||||||
expect(line.points.length).toEqual(3);
|
expect(line.points.length).toEqual(3);
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`17`,
|
`18`,
|
||||||
);
|
);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`10`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`10`);
|
||||||
|
|
||||||
@@ -746,7 +746,7 @@ describe("Test Linear Elements", () => {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`14`,
|
`15`,
|
||||||
);
|
);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`9`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`9`);
|
||||||
expect(line.points.length).toEqual(5);
|
expect(line.points.length).toEqual(5);
|
||||||
@@ -844,7 +844,7 @@ describe("Test Linear Elements", () => {
|
|||||||
drag(hitCoords, pointFrom(hitCoords[0] + delta, hitCoords[1] + delta));
|
drag(hitCoords, pointFrom(hitCoords[0] + delta, hitCoords[1] + delta));
|
||||||
|
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`11`,
|
`12`,
|
||||||
);
|
);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||||
|
|
||||||
|
|||||||
@@ -250,6 +250,11 @@ import {
|
|||||||
maxBindingDistance_simple,
|
maxBindingDistance_simple,
|
||||||
convertToExcalidrawElements,
|
convertToExcalidrawElements,
|
||||||
type ExcalidrawElementSkeleton,
|
type ExcalidrawElementSkeleton,
|
||||||
|
handleFocusPointDrag,
|
||||||
|
handleFocusPointHover,
|
||||||
|
handleFocusPointPointerDown,
|
||||||
|
handleFocusPointPointerUp,
|
||||||
|
maybeHandleArrowPointlikeDrag,
|
||||||
} from "@excalidraw/element";
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math";
|
import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math";
|
||||||
@@ -4734,8 +4739,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle Alt key for bind mode
|
// Handle Alt key for bind mode
|
||||||
if (event.key === KEYS.ALT && getFeatureFlag("COMPLEX_BINDINGS")) {
|
if (event.key === KEYS.ALT) {
|
||||||
this.handleSkipBindMode();
|
if (getFeatureFlag("COMPLEX_BINDINGS")) {
|
||||||
|
this.handleSkipBindMode();
|
||||||
|
} else {
|
||||||
|
maybeHandleArrowPointlikeDrag({ app: this, event });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.actionManager.handleKeyDown(event)) {
|
if (this.actionManager.handleKeyDown(event)) {
|
||||||
@@ -4751,7 +4760,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.resetDelayedBindMode();
|
this.resetDelayedBindMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ isBindingEnabled: false });
|
flushSync(() => {
|
||||||
|
this.setState({ isBindingEnabled: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
maybeHandleArrowPointlikeDrag({ app: this, event });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isArrowKey(event.key)) {
|
if (isArrowKey(event.key)) {
|
||||||
@@ -5024,6 +5037,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
isHoldingSpace = false;
|
isHoldingSpace = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.key === KEYS.ALT) {
|
||||||
|
maybeHandleArrowPointlikeDrag({ app: this, event });
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(event.key === KEYS.ALT && this.state.bindMode === "skip") ||
|
(event.key === KEYS.ALT && this.state.bindMode === "skip") ||
|
||||||
(!event[KEYS.CTRL_OR_CMD] && !isBindingEnabled(this.state))
|
(!event[KEYS.CTRL_OR_CMD] && !isBindingEnabled(this.state))
|
||||||
@@ -5034,7 +5052,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Restart the timer if we're creating/editing a linear element and hovering over an element
|
// Restart the timer if we're creating/editing a linear element and hovering over an element
|
||||||
if (this.lastPointerMoveEvent) {
|
if (this.lastPointerMoveEvent && getFeatureFlag("COMPLEX_BINDINGS")) {
|
||||||
const scenePointer = viewportCoordsToSceneCoords(
|
const scenePointer = viewportCoordsToSceneCoords(
|
||||||
{
|
{
|
||||||
clientX: this.lastPointerMoveEvent.clientX,
|
clientX: this.lastPointerMoveEvent.clientX,
|
||||||
@@ -5055,14 +5073,18 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isBindingElement(element) && getFeatureFlag("COMPLEX_BINDINGS")) {
|
if (isBindingElement(element)) {
|
||||||
this.handleDelayedBindModeChange(element, hoveredElement);
|
this.handleDelayedBindModeChange(element, hoveredElement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!event[KEYS.CTRL_OR_CMD] && !this.state.isBindingEnabled) {
|
if (!event[KEYS.CTRL_OR_CMD] && !this.state.isBindingEnabled) {
|
||||||
this.setState({ isBindingEnabled: true });
|
flushSync(() => {
|
||||||
|
this.setState({ isBindingEnabled: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
maybeHandleArrowPointlikeDrag({ app: this, event });
|
||||||
}
|
}
|
||||||
if (isArrowKey(event.key)) {
|
if (isArrowKey(event.key)) {
|
||||||
bindOrUnbindBindingElements(
|
bindOrUnbindBindingElements(
|
||||||
@@ -6379,7 +6401,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
|
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
|
||||||
this.scene.getNonDeletedElements(),
|
this.scene.getNonDeletedElements(),
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
(el) => maxBindingDistance_simple(this.state.zoom),
|
maxBindingDistance_simple(this.state.zoom),
|
||||||
);
|
);
|
||||||
if (hoveredElement) {
|
if (hoveredElement) {
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -6410,7 +6432,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
|
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
|
||||||
this.scene.getNonDeletedElements(),
|
this.scene.getNonDeletedElements(),
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
(el) => maxBindingDistance_simple(this.state.zoom),
|
maxBindingDistance_simple(this.state.zoom),
|
||||||
);
|
);
|
||||||
if (hoveredElement) {
|
if (hoveredElement) {
|
||||||
this.actionManager.executeAction(actionFinalize, "ui", {
|
this.actionManager.executeAction(actionFinalize, "ui", {
|
||||||
@@ -6564,7 +6586,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
|
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
|
||||||
this.scene.getNonDeletedElements(),
|
this.scene.getNonDeletedElements(),
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
(el) => maxBindingDistance_simple(this.state.zoom),
|
maxBindingDistance_simple(this.state.zoom),
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
hit &&
|
hit &&
|
||||||
@@ -6923,6 +6945,37 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for focus point hover
|
||||||
|
let hoveredFocusPointBinding: "start" | "end" | null = null;
|
||||||
|
const arrow = element as any;
|
||||||
|
if (arrow.startBinding || arrow.endBinding) {
|
||||||
|
hoveredFocusPointBinding = handleFocusPointHover(
|
||||||
|
element as ExcalidrawArrowElement,
|
||||||
|
scenePointerX,
|
||||||
|
scenePointerY,
|
||||||
|
this.scene,
|
||||||
|
this.state,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.state.selectedLinearElement.hoveredFocusPointBinding !==
|
||||||
|
hoveredFocusPointBinding
|
||||||
|
) {
|
||||||
|
this.setState({
|
||||||
|
selectedLinearElement: {
|
||||||
|
...this.state.selectedLinearElement,
|
||||||
|
isDragging: false,
|
||||||
|
hoveredFocusPointBinding,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set cursor to pointer when hovering over a focus point
|
||||||
|
if (hoveredFocusPointBinding) {
|
||||||
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO);
|
||||||
}
|
}
|
||||||
@@ -7844,6 +7897,37 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (ret.didAddPoint) {
|
if (ret.didAddPoint) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also check at current pointer position if focus point is being hovered
|
||||||
|
// (in case we're clicking directly without a prior move event)
|
||||||
|
const elementsMap = this.scene.getNonDeletedElementsMap();
|
||||||
|
const arrow = LinearElementEditor.getElement(
|
||||||
|
linearElementEditor.elementId,
|
||||||
|
elementsMap,
|
||||||
|
) as any;
|
||||||
|
|
||||||
|
if (arrow && isBindingElement(arrow)) {
|
||||||
|
const { hitFocusPoint, pointerOffset } =
|
||||||
|
handleFocusPointPointerDown(
|
||||||
|
arrow,
|
||||||
|
pointerDownState,
|
||||||
|
elementsMap,
|
||||||
|
this.state,
|
||||||
|
);
|
||||||
|
|
||||||
|
// If focus point is hit, update state and prevent element selection
|
||||||
|
if (hitFocusPoint) {
|
||||||
|
this.setState({
|
||||||
|
selectedLinearElement: {
|
||||||
|
...linearElementEditor,
|
||||||
|
hoveredFocusPointBinding: hitFocusPoint,
|
||||||
|
draggedFocusPointBinding: hitFocusPoint,
|
||||||
|
pointerOffset,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const allHitElements = this.getElementsAtPosition(
|
const allHitElements = this.getElementsAtPosition(
|
||||||
@@ -8991,6 +9075,31 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (this.state.selectedLinearElement) {
|
if (this.state.selectedLinearElement) {
|
||||||
const linearElementEditor = this.state.selectedLinearElement;
|
const linearElementEditor = this.state.selectedLinearElement;
|
||||||
|
|
||||||
|
// Handle focus point dragging if needed
|
||||||
|
if (linearElementEditor.draggedFocusPointBinding) {
|
||||||
|
handleFocusPointDrag(
|
||||||
|
linearElementEditor,
|
||||||
|
elementsMap,
|
||||||
|
pointerCoords,
|
||||||
|
this.scene,
|
||||||
|
this.state,
|
||||||
|
this.getEffectiveGridSize(),
|
||||||
|
event.altKey,
|
||||||
|
);
|
||||||
|
this.setState({
|
||||||
|
selectedLinearElement: {
|
||||||
|
...linearElementEditor,
|
||||||
|
isDragging: false,
|
||||||
|
selectedPointsIndices: [],
|
||||||
|
initialState: {
|
||||||
|
...linearElementEditor.initialState,
|
||||||
|
lastClickedPoint: -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
LinearElementEditor.shouldAddMidpoint(
|
LinearElementEditor.shouldAddMidpoint(
|
||||||
this.state.selectedLinearElement,
|
this.state.selectedLinearElement,
|
||||||
@@ -9859,12 +9968,14 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
// and sets binding element
|
// and sets binding element
|
||||||
if (
|
if (
|
||||||
this.state.selectedLinearElement?.isEditing &&
|
this.state.selectedLinearElement?.isEditing &&
|
||||||
!this.state.newElement
|
!this.state.newElement &&
|
||||||
|
this.state.selectedLinearElement.draggedFocusPointBinding === null
|
||||||
) {
|
) {
|
||||||
if (
|
if (
|
||||||
!pointerDownState.boxSelection.hasOccurred &&
|
!pointerDownState.boxSelection.hasOccurred &&
|
||||||
pointerDownState.hit?.element?.id !==
|
pointerDownState.hit?.element?.id !==
|
||||||
this.state.selectedLinearElement.elementId
|
this.state.selectedLinearElement.elementId &&
|
||||||
|
this.state.selectedLinearElement.draggedFocusPointBinding === null
|
||||||
) {
|
) {
|
||||||
this.actionManager.executeAction(actionFinalize);
|
this.actionManager.executeAction(actionFinalize);
|
||||||
} else {
|
} else {
|
||||||
@@ -9900,7 +10011,18 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (this.state.selectedLinearElement.draggedFocusPointBinding) {
|
||||||
|
handleFocusPointPointerUp(
|
||||||
|
this.state.selectedLinearElement,
|
||||||
|
this.scene,
|
||||||
|
);
|
||||||
|
this.setState({
|
||||||
|
selectedLinearElement: {
|
||||||
|
...this.state.selectedLinearElement,
|
||||||
|
draggedFocusPointBinding: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (
|
||||||
pointerDownState.hit?.element?.id !==
|
pointerDownState.hit?.element?.id !==
|
||||||
this.state.selectedLinearElement.elementId
|
this.state.selectedLinearElement.elementId
|
||||||
) {
|
) {
|
||||||
@@ -9910,6 +10032,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.setState({ selectedLinearElement: null });
|
this.setState({ selectedLinearElement: null });
|
||||||
}
|
}
|
||||||
} else if (this.state.selectedLinearElement.isDragging) {
|
} else if (this.state.selectedLinearElement.isDragging) {
|
||||||
|
this.setState({
|
||||||
|
selectedLinearElement: {
|
||||||
|
...this.state.selectedLinearElement,
|
||||||
|
isDragging: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
this.actionManager.executeAction(actionFinalize, "ui", {
|
this.actionManager.executeAction(actionFinalize, "ui", {
|
||||||
event: childEvent,
|
event: childEvent,
|
||||||
sceneCoords,
|
sceneCoords,
|
||||||
|
|||||||
@@ -135,7 +135,8 @@ describe("binding with linear elements", () => {
|
|||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
expect(linear.startBinding).not.toBe(null);
|
expect(linear.startBinding).not.toBe(null);
|
||||||
expect(inputX).not.toBeNull();
|
expect(inputX).not.toBeNull();
|
||||||
UI.updateInput(inputX, String("186"));
|
|
||||||
|
UI.updateInput(inputX, String("184"));
|
||||||
expect(linear.startBinding).not.toBe(null);
|
expect(linear.startBinding).not.toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -21,10 +21,13 @@ import {
|
|||||||
deconstructDiamondElement,
|
deconstructDiamondElement,
|
||||||
deconstructRectanguloidElement,
|
deconstructRectanguloidElement,
|
||||||
elementCenterPoint,
|
elementCenterPoint,
|
||||||
|
FOCUS_POINT_SIZE,
|
||||||
getOmitSidesForEditorInterface,
|
getOmitSidesForEditorInterface,
|
||||||
getTransformHandles,
|
getTransformHandles,
|
||||||
getTransformHandlesFromCoords,
|
getTransformHandlesFromCoords,
|
||||||
hasBoundingBox,
|
hasBoundingBox,
|
||||||
|
isArrowElement,
|
||||||
|
isBindableElement,
|
||||||
isElbowArrow,
|
isElbowArrow,
|
||||||
isFrameLikeElement,
|
isFrameLikeElement,
|
||||||
isImageElement,
|
isImageElement,
|
||||||
@@ -44,6 +47,10 @@ import {
|
|||||||
} from "@excalidraw/element";
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
import { getCommonBounds, getElementAbsoluteCoords } from "@excalidraw/element";
|
import { getCommonBounds, getElementAbsoluteCoords } from "@excalidraw/element";
|
||||||
|
import {
|
||||||
|
getGlobalFixedPointForBindableElement,
|
||||||
|
isFocusPointVisible,
|
||||||
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
TransformHandles,
|
TransformHandles,
|
||||||
@@ -52,6 +59,7 @@ import type {
|
|||||||
|
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
|
ExcalidrawArrowElement,
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawFrameLikeElement,
|
ExcalidrawFrameLikeElement,
|
||||||
@@ -71,11 +79,6 @@ import {
|
|||||||
SCROLLBAR_WIDTH,
|
SCROLLBAR_WIDTH,
|
||||||
} from "../scene/scrollbars";
|
} from "../scene/scrollbars";
|
||||||
|
|
||||||
import {
|
|
||||||
type AppClassProperties,
|
|
||||||
type InteractiveCanvasAppState,
|
|
||||||
} from "../types";
|
|
||||||
|
|
||||||
import { getClientColor, renderRemoteCursors } from "../clients";
|
import { getClientColor, renderRemoteCursors } from "../clients";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -85,6 +88,8 @@ import {
|
|||||||
strokeRectWithRotation_simple,
|
strokeRectWithRotation_simple,
|
||||||
} from "./helpers";
|
} from "./helpers";
|
||||||
|
|
||||||
|
import type { AppClassProperties, InteractiveCanvasAppState } from "../types";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
InteractiveCanvasRenderConfig,
|
InteractiveCanvasRenderConfig,
|
||||||
InteractiveSceneRenderConfig,
|
InteractiveSceneRenderConfig,
|
||||||
@@ -123,6 +128,9 @@ const renderLinearElementPointHighlight = (
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (appState.selectedLinearElement?.isDragging) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
||||||
|
|
||||||
if (!element) {
|
if (!element) {
|
||||||
@@ -156,6 +164,19 @@ const highlightPoint = <Point extends LocalPoint | GlobalPoint>(
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderFocusPointHighlight = (
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
appState: InteractiveCanvasAppState,
|
||||||
|
focusPoint: GlobalPoint,
|
||||||
|
) => {
|
||||||
|
context.save();
|
||||||
|
context.translate(appState.scrollX, appState.scrollY);
|
||||||
|
|
||||||
|
highlightPoint(focusPoint, context, appState);
|
||||||
|
|
||||||
|
context.restore();
|
||||||
|
};
|
||||||
|
|
||||||
const renderSingleLinearPoint = <Point extends GlobalPoint | LocalPoint>(
|
const renderSingleLinearPoint = <Point extends GlobalPoint | LocalPoint>(
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
appState: InteractiveCanvasAppState,
|
appState: InteractiveCanvasAppState,
|
||||||
@@ -920,6 +941,145 @@ const renderLinearPointHandles = (
|
|||||||
context.restore();
|
context.restore();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderFocusPointConnectionLine = (
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
appState: InteractiveCanvasAppState,
|
||||||
|
fromPoint: GlobalPoint,
|
||||||
|
toPoint: GlobalPoint,
|
||||||
|
) => {
|
||||||
|
context.save();
|
||||||
|
context.translate(appState.scrollX, appState.scrollY);
|
||||||
|
|
||||||
|
context.strokeStyle = "rgba(134, 131, 226, 0.6)";
|
||||||
|
context.lineWidth = 1 / appState.zoom.value;
|
||||||
|
context.setLineDash([4 / appState.zoom.value, 4 / appState.zoom.value]);
|
||||||
|
|
||||||
|
context.beginPath();
|
||||||
|
context.moveTo(fromPoint[0], fromPoint[1]);
|
||||||
|
context.lineTo(toPoint[0], toPoint[1]);
|
||||||
|
context.stroke();
|
||||||
|
|
||||||
|
context.restore();
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderFocusPointCicle = (
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
appState: InteractiveCanvasAppState,
|
||||||
|
point: GlobalPoint,
|
||||||
|
radius: number,
|
||||||
|
isHovered: boolean,
|
||||||
|
) => {
|
||||||
|
context.save();
|
||||||
|
context.translate(appState.scrollX, appState.scrollY);
|
||||||
|
context.strokeStyle = "rgba(134, 131, 226, 0.6)";
|
||||||
|
context.lineWidth = 1 / appState.zoom.value;
|
||||||
|
context.setLineDash([]);
|
||||||
|
context.fillStyle = isHovered
|
||||||
|
? "rgba(134, 131, 226, 0.9)"
|
||||||
|
: "rgba(255, 255, 255, 0.9)";
|
||||||
|
|
||||||
|
fillCircle(
|
||||||
|
context,
|
||||||
|
point[0],
|
||||||
|
point[1],
|
||||||
|
radius / appState.zoom.value,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
context.restore();
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderFocusPointIndicator = ({
|
||||||
|
arrow,
|
||||||
|
appState,
|
||||||
|
type,
|
||||||
|
context,
|
||||||
|
elementsMap,
|
||||||
|
}: {
|
||||||
|
arrow: NonDeleted<ExcalidrawArrowElement>;
|
||||||
|
appState: InteractiveCanvasAppState;
|
||||||
|
context: CanvasRenderingContext2D;
|
||||||
|
elementsMap: NonDeletedSceneElementsMap;
|
||||||
|
type: "start" | "end";
|
||||||
|
}) => {
|
||||||
|
const binding = type === "start" ? arrow.startBinding : arrow.endBinding;
|
||||||
|
const bindableElement =
|
||||||
|
binding?.elementId && elementsMap.get(binding.elementId);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!bindableElement ||
|
||||||
|
!isBindableElement(bindableElement) ||
|
||||||
|
bindableElement.isDeleted
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const focusPoint = getGlobalFixedPointForBindableElement(
|
||||||
|
binding.fixedPoint,
|
||||||
|
bindableElement,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Only render if focus point is within the bindable element
|
||||||
|
if (
|
||||||
|
!isFocusPointVisible(
|
||||||
|
focusPoint,
|
||||||
|
arrow,
|
||||||
|
bindableElement,
|
||||||
|
elementsMap,
|
||||||
|
appState,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const linearState = appState.selectedLinearElement;
|
||||||
|
const isDragging = !!linearState?.isDragging;
|
||||||
|
const pointIndex = type === "start" ? 0 : arrow.points.length - 1;
|
||||||
|
const pointSelected =
|
||||||
|
!!linearState?.selectedPointsIndices?.includes(pointIndex);
|
||||||
|
|
||||||
|
// render focus point highlight
|
||||||
|
// ----------------------------
|
||||||
|
|
||||||
|
if (
|
||||||
|
linearState?.hoveredFocusPointBinding === type &&
|
||||||
|
!linearState.draggedFocusPointBinding
|
||||||
|
) {
|
||||||
|
renderFocusPointHighlight(context, appState, focusPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// render focus point
|
||||||
|
// ----------------------------
|
||||||
|
|
||||||
|
if (!(pointSelected && isDragging)) {
|
||||||
|
const focusPoint = getGlobalFixedPointForBindableElement(
|
||||||
|
binding.fixedPoint,
|
||||||
|
bindableElement,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
const isHovered = linearState?.hoveredFocusPointBinding === type;
|
||||||
|
|
||||||
|
// Render dashed line from arrow start point to focus point
|
||||||
|
const arrowPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||||
|
arrow,
|
||||||
|
pointIndex,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
renderFocusPointConnectionLine(context, appState, arrowPoint, focusPoint);
|
||||||
|
|
||||||
|
renderFocusPointCicle(
|
||||||
|
context,
|
||||||
|
appState,
|
||||||
|
focusPoint,
|
||||||
|
FOCUS_POINT_SIZE / 1.5,
|
||||||
|
isHovered,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const renderTransformHandles = (
|
const renderTransformHandles = (
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
renderConfig: InteractiveCanvasRenderConfig,
|
renderConfig: InteractiveCanvasRenderConfig,
|
||||||
@@ -1251,26 +1411,44 @@ const _renderInteractiveScene = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const linearState = appState.selectedLinearElement;
|
||||||
|
const selectedLinearElement =
|
||||||
|
linearState &&
|
||||||
|
LinearElementEditor.getElement(linearState.elementId, allElementsMap);
|
||||||
// Arrows have a different highlight behavior when
|
// Arrows have a different highlight behavior when
|
||||||
// they are the only selected element
|
// they are the only selected element
|
||||||
if (appState.selectedLinearElement) {
|
if (selectedLinearElement) {
|
||||||
const editor = appState.selectedLinearElement;
|
|
||||||
const firstSelectedLinear = selectedElements.find(
|
|
||||||
(el) => el.id === editor.elementId, // Don't forget bound text elements!
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!appState.selectedLinearElement.isDragging) {
|
if (!appState.selectedLinearElement.isDragging) {
|
||||||
if (editor.segmentMidPointHoveredCoords) {
|
if (linearState.segmentMidPointHoveredCoords) {
|
||||||
renderElbowArrowMidPointHighlight(context, appState);
|
renderElbowArrowMidPointHighlight(context, appState);
|
||||||
} else if (
|
} else if (
|
||||||
isElbowArrow(firstSelectedLinear)
|
isElbowArrow(selectedLinearElement)
|
||||||
? editor.hoverPointIndex === 0 ||
|
? linearState.hoverPointIndex === 0 ||
|
||||||
editor.hoverPointIndex === firstSelectedLinear.points.length - 1
|
linearState.hoverPointIndex ===
|
||||||
: editor.hoverPointIndex >= 0
|
selectedLinearElement.points.length - 1
|
||||||
|
: linearState.hoverPointIndex >= 0
|
||||||
) {
|
) {
|
||||||
renderLinearElementPointHighlight(context, appState, elementsMap);
|
renderLinearElementPointHighlight(context, appState, elementsMap);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isArrowElement(selectedLinearElement)) {
|
||||||
|
renderFocusPointIndicator({
|
||||||
|
arrow: selectedLinearElement,
|
||||||
|
elementsMap: allElementsMap,
|
||||||
|
appState,
|
||||||
|
context,
|
||||||
|
type: "start",
|
||||||
|
});
|
||||||
|
|
||||||
|
renderFocusPointIndicator({
|
||||||
|
arrow: selectedLinearElement,
|
||||||
|
elementsMap: allElementsMap,
|
||||||
|
appState,
|
||||||
|
context,
|
||||||
|
type: "end",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Paint selected elements
|
// Paint selected elements
|
||||||
|
|||||||
@@ -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": "353.93938",
|
"height": "390.11932",
|
||||||
"id": "id4",
|
"id": "id4",
|
||||||
"index": "a2",
|
"index": "a2",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@@ -2412,8 +2412,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"478.03877",
|
"478.03878",
|
||||||
"-353.93938",
|
"-390.11932",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@@ -2424,7 +2424,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id0",
|
"elementId": "id0",
|
||||||
"fixedPoint": [
|
"fixedPoint": [
|
||||||
"0.50010",
|
1,
|
||||||
"0.50010",
|
"0.50010",
|
||||||
],
|
],
|
||||||
"mode": "orbit",
|
"mode": "orbit",
|
||||||
@@ -2435,9 +2435,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 12,
|
"version": 12,
|
||||||
"width": "478.03877",
|
"width": "478.03878",
|
||||||
"x": 11,
|
"x": 11,
|
||||||
"y": "-45.14705",
|
"y": "-8.96692",
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -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": "353.93938",
|
"height": "390.11932",
|
||||||
"index": "a2",
|
"index": "a2",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"link": null,
|
"link": null,
|
||||||
@@ -2578,8 +2578,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"478.03877",
|
"478.03878",
|
||||||
"-353.93938",
|
"-390.11932",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@@ -2590,7 +2590,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id0",
|
"elementId": "id0",
|
||||||
"fixedPoint": [
|
"fixedPoint": [
|
||||||
"0.50010",
|
1,
|
||||||
"0.50010",
|
"0.50010",
|
||||||
],
|
],
|
||||||
"mode": "orbit",
|
"mode": "orbit",
|
||||||
@@ -2600,9 +2600,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"version": 12,
|
"version": 12,
|
||||||
"width": "478.03877",
|
"width": "478.03878",
|
||||||
"x": 11,
|
"x": 11,
|
||||||
"y": "-45.14705",
|
"y": "-8.96692",
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
|||||||
@@ -8743,9 +8743,11 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
|
|||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
"selectedLinearElement": LinearElementEditor {
|
"selectedLinearElement": LinearElementEditor {
|
||||||
"customLineAngle": null,
|
"customLineAngle": null,
|
||||||
|
"draggedFocusPointBinding": null,
|
||||||
"elbowed": false,
|
"elbowed": false,
|
||||||
"elementId": "id0",
|
"elementId": "id0",
|
||||||
"hoverPointIndex": -1,
|
"hoverPointIndex": -1,
|
||||||
|
"hoveredFocusPointBinding": null,
|
||||||
"initialState": {
|
"initialState": {
|
||||||
"altFocusPoint": null,
|
"altFocusPoint": null,
|
||||||
"arrowStartIsInside": false,
|
"arrowStartIsInside": false,
|
||||||
@@ -8976,9 +8978,11 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
|
|||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
"selectedLinearElement": LinearElementEditor {
|
"selectedLinearElement": LinearElementEditor {
|
||||||
"customLineAngle": null,
|
"customLineAngle": null,
|
||||||
|
"draggedFocusPointBinding": null,
|
||||||
"elbowed": false,
|
"elbowed": false,
|
||||||
"elementId": "id0",
|
"elementId": "id0",
|
||||||
"hoverPointIndex": -1,
|
"hoverPointIndex": -1,
|
||||||
|
"hoveredFocusPointBinding": null,
|
||||||
"initialState": {
|
"initialState": {
|
||||||
"altFocusPoint": null,
|
"altFocusPoint": null,
|
||||||
"arrowStartIsInside": false,
|
"arrowStartIsInside": false,
|
||||||
@@ -9402,9 +9406,11 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
|
|||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
"selectedLinearElement": LinearElementEditor {
|
"selectedLinearElement": LinearElementEditor {
|
||||||
"customLineAngle": null,
|
"customLineAngle": null,
|
||||||
|
"draggedFocusPointBinding": null,
|
||||||
"elbowed": false,
|
"elbowed": false,
|
||||||
"elementId": "id0",
|
"elementId": "id0",
|
||||||
"hoverPointIndex": -1,
|
"hoverPointIndex": -1,
|
||||||
|
"hoveredFocusPointBinding": null,
|
||||||
"initialState": {
|
"initialState": {
|
||||||
"altFocusPoint": null,
|
"altFocusPoint": null,
|
||||||
"arrowStartIsInside": false,
|
"arrowStartIsInside": false,
|
||||||
@@ -9818,9 +9824,11 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
|
|||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
"selectedLinearElement": LinearElementEditor {
|
"selectedLinearElement": LinearElementEditor {
|
||||||
"customLineAngle": null,
|
"customLineAngle": null,
|
||||||
|
"draggedFocusPointBinding": null,
|
||||||
"elbowed": false,
|
"elbowed": false,
|
||||||
"elementId": "id0",
|
"elementId": "id0",
|
||||||
"hoverPointIndex": -1,
|
"hoverPointIndex": -1,
|
||||||
|
"hoveredFocusPointBinding": null,
|
||||||
"initialState": {
|
"initialState": {
|
||||||
"altFocusPoint": null,
|
"altFocusPoint": null,
|
||||||
"arrowStartIsInside": false,
|
"arrowStartIsInside": false,
|
||||||
|
|||||||
@@ -5051,7 +5051,7 @@ describe("history", () => {
|
|||||||
id: arrowId,
|
id: arrowId,
|
||||||
startBinding: expect.objectContaining({
|
startBinding: expect.objectContaining({
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
fixedPoint: expect.arrayContaining([0.5001, 0.5001]),
|
fixedPoint: expect.arrayContaining([1, 0.5001]),
|
||||||
}),
|
}),
|
||||||
endBinding: expect.objectContaining({
|
endBinding: expect.objectContaining({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ import type { isOverScrollBars } from "./scene/scrollbars";
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import type { JSX } from "react";
|
import type { JSX } from "react";
|
||||||
|
|
||||||
|
export type { App };
|
||||||
|
|
||||||
export type SocketId = string & { _brand: "SocketId" };
|
export type SocketId = string & { _brand: "SocketId" };
|
||||||
|
|
||||||
export type Collaborator = Readonly<{
|
export type Collaborator = Readonly<{
|
||||||
|
|||||||
Reference in New Issue
Block a user