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:
Márk Tolmács
2026-01-31 15:08:28 +01:00
committed by GitHub
co-authored by dwelle
parent 216afc3625
commit b552c60714
16 changed files with 1112 additions and 90 deletions
+140 -12
View File
@@ -250,6 +250,11 @@ import {
maxBindingDistance_simple,
convertToExcalidrawElements,
type ExcalidrawElementSkeleton,
handleFocusPointDrag,
handleFocusPointHover,
handleFocusPointPointerDown,
handleFocusPointPointerUp,
maybeHandleArrowPointlikeDrag,
} from "@excalidraw/element";
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
if (event.key === KEYS.ALT && getFeatureFlag("COMPLEX_BINDINGS")) {
this.handleSkipBindMode();
if (event.key === KEYS.ALT) {
if (getFeatureFlag("COMPLEX_BINDINGS")) {
this.handleSkipBindMode();
} else {
maybeHandleArrowPointlikeDrag({ app: this, event });
}
}
if (this.actionManager.handleKeyDown(event)) {
@@ -4751,7 +4760,11 @@ class App extends React.Component<AppProps, AppState> {
this.resetDelayedBindMode();
}
this.setState({ isBindingEnabled: false });
flushSync(() => {
this.setState({ isBindingEnabled: false });
});
maybeHandleArrowPointlikeDrag({ app: this, event });
}
if (isArrowKey(event.key)) {
@@ -5024,6 +5037,11 @@ class App extends React.Component<AppProps, AppState> {
}
isHoldingSpace = false;
}
if (event.key === KEYS.ALT) {
maybeHandleArrowPointlikeDrag({ app: this, event });
}
if (
(event.key === KEYS.ALT && this.state.bindMode === "skip") ||
(!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
if (this.lastPointerMoveEvent) {
if (this.lastPointerMoveEvent && getFeatureFlag("COMPLEX_BINDINGS")) {
const scenePointer = viewportCoordsToSceneCoords(
{
clientX: this.lastPointerMoveEvent.clientX,
@@ -5055,14 +5073,18 @@ class App extends React.Component<AppProps, AppState> {
this.scene.getNonDeletedElementsMap(),
);
if (isBindingElement(element) && getFeatureFlag("COMPLEX_BINDINGS")) {
if (isBindingElement(element)) {
this.handleDelayedBindModeChange(element, hoveredElement);
}
}
}
}
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)) {
bindOrUnbindBindingElements(
@@ -6379,7 +6401,7 @@ class App extends React.Component<AppProps, AppState> {
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
this.scene.getNonDeletedElements(),
this.scene.getNonDeletedElementsMap(),
(el) => maxBindingDistance_simple(this.state.zoom),
maxBindingDistance_simple(this.state.zoom),
);
if (hoveredElement) {
this.setState({
@@ -6410,7 +6432,7 @@ class App extends React.Component<AppProps, AppState> {
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
this.scene.getNonDeletedElements(),
this.scene.getNonDeletedElementsMap(),
(el) => maxBindingDistance_simple(this.state.zoom),
maxBindingDistance_simple(this.state.zoom),
);
if (hoveredElement) {
this.actionManager.executeAction(actionFinalize, "ui", {
@@ -6564,7 +6586,7 @@ class App extends React.Component<AppProps, AppState> {
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
this.scene.getNonDeletedElements(),
this.scene.getNonDeletedElementsMap(),
(el) => maxBindingDistance_simple(this.state.zoom),
maxBindingDistance_simple(this.state.zoom),
);
if (
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 {
setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO);
}
@@ -7844,6 +7897,37 @@ class App extends React.Component<AppProps, AppState> {
if (ret.didAddPoint) {
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(
@@ -8991,6 +9075,31 @@ class App extends React.Component<AppProps, AppState> {
if (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 (
LinearElementEditor.shouldAddMidpoint(
this.state.selectedLinearElement,
@@ -9859,12 +9968,14 @@ class App extends React.Component<AppProps, AppState> {
// and sets binding element
if (
this.state.selectedLinearElement?.isEditing &&
!this.state.newElement
!this.state.newElement &&
this.state.selectedLinearElement.draggedFocusPointBinding === null
) {
if (
!pointerDownState.boxSelection.hasOccurred &&
pointerDownState.hit?.element?.id !==
this.state.selectedLinearElement.elementId
this.state.selectedLinearElement.elementId &&
this.state.selectedLinearElement.draggedFocusPointBinding === null
) {
this.actionManager.executeAction(actionFinalize);
} 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 !==
this.state.selectedLinearElement.elementId
) {
@@ -9910,6 +10032,12 @@ class App extends React.Component<AppProps, AppState> {
this.setState({ selectedLinearElement: null });
}
} else if (this.state.selectedLinearElement.isDragging) {
this.setState({
selectedLinearElement: {
...this.state.selectedLinearElement,
isDragging: false,
},
});
this.actionManager.executeAction(actionFinalize, "ui", {
event: childEvent,
sceneCoords,
@@ -135,7 +135,8 @@ describe("binding with linear elements", () => {
) as HTMLInputElement;
expect(linear.startBinding).not.toBe(null);
expect(inputX).not.toBeNull();
UI.updateInput(inputX, String("186"));
UI.updateInput(inputX, String("184"));
expect(linear.startBinding).not.toBe(null);
});
+194 -16
View File
@@ -21,10 +21,13 @@ import {
deconstructDiamondElement,
deconstructRectanguloidElement,
elementCenterPoint,
FOCUS_POINT_SIZE,
getOmitSidesForEditorInterface,
getTransformHandles,
getTransformHandlesFromCoords,
hasBoundingBox,
isArrowElement,
isBindableElement,
isElbowArrow,
isFrameLikeElement,
isImageElement,
@@ -44,6 +47,10 @@ import {
} from "@excalidraw/element";
import { getCommonBounds, getElementAbsoluteCoords } from "@excalidraw/element";
import {
getGlobalFixedPointForBindableElement,
isFocusPointVisible,
} from "@excalidraw/element";
import type {
TransformHandles,
@@ -52,6 +59,7 @@ import type {
import type {
ElementsMap,
ExcalidrawArrowElement,
ExcalidrawBindableElement,
ExcalidrawElement,
ExcalidrawFrameLikeElement,
@@ -71,11 +79,6 @@ import {
SCROLLBAR_WIDTH,
} from "../scene/scrollbars";
import {
type AppClassProperties,
type InteractiveCanvasAppState,
} from "../types";
import { getClientColor, renderRemoteCursors } from "../clients";
import {
@@ -85,6 +88,8 @@ import {
strokeRectWithRotation_simple,
} from "./helpers";
import type { AppClassProperties, InteractiveCanvasAppState } from "../types";
import type {
InteractiveCanvasRenderConfig,
InteractiveSceneRenderConfig,
@@ -123,6 +128,9 @@ const renderLinearElementPointHighlight = (
) {
return;
}
if (appState.selectedLinearElement?.isDragging) {
return;
}
const element = LinearElementEditor.getElement(elementId, elementsMap);
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>(
context: CanvasRenderingContext2D,
appState: InteractiveCanvasAppState,
@@ -920,6 +941,145 @@ const renderLinearPointHandles = (
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 = (
context: CanvasRenderingContext2D,
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
// they are the only selected element
if (appState.selectedLinearElement) {
const editor = appState.selectedLinearElement;
const firstSelectedLinear = selectedElements.find(
(el) => el.id === editor.elementId, // Don't forget bound text elements!
);
if (selectedLinearElement) {
if (!appState.selectedLinearElement.isDragging) {
if (editor.segmentMidPointHoveredCoords) {
if (linearState.segmentMidPointHoveredCoords) {
renderElbowArrowMidPointHighlight(context, appState);
} else if (
isElbowArrow(firstSelectedLinear)
? editor.hoverPointIndex === 0 ||
editor.hoverPointIndex === firstSelectedLinear.points.length - 1
: editor.hoverPointIndex >= 0
isElbowArrow(selectedLinearElement)
? linearState.hoverPointIndex === 0 ||
linearState.hoverPointIndex ===
selectedLinearElement.points.length - 1
: linearState.hoverPointIndex >= 0
) {
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
@@ -2398,7 +2398,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": "353.93938",
"height": "390.11932",
"id": "id4",
"index": "a2",
"isDeleted": false,
@@ -2412,8 +2412,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
0,
],
[
"478.03877",
"-353.93938",
"478.03878",
"-390.11932",
],
],
"roughness": 1,
@@ -2424,7 +2424,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"startBinding": {
"elementId": "id0",
"fixedPoint": [
"0.50010",
1,
"0.50010",
],
"mode": "orbit",
@@ -2435,9 +2435,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"type": "arrow",
"updated": 1,
"version": 12,
"width": "478.03877",
"width": "478.03878",
"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",
"frameId": null,
"groupIds": [],
"height": "353.93938",
"height": "390.11932",
"index": "a2",
"isDeleted": false,
"link": null,
@@ -2578,8 +2578,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
0,
],
[
"478.03877",
"-353.93938",
"478.03878",
"-390.11932",
],
],
"roughness": 1,
@@ -2590,7 +2590,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"startBinding": {
"elementId": "id0",
"fixedPoint": [
"0.50010",
1,
"0.50010",
],
"mode": "orbit",
@@ -2600,9 +2600,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"strokeWidth": 2,
"type": "arrow",
"version": 12,
"width": "478.03877",
"width": "478.03878",
"x": 11,
"y": "-45.14705",
"y": "-8.96692",
},
"inserted": {
"isDeleted": true,
@@ -8743,9 +8743,11 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
"selectedGroupIds": {},
"selectedLinearElement": LinearElementEditor {
"customLineAngle": null,
"draggedFocusPointBinding": null,
"elbowed": false,
"elementId": "id0",
"hoverPointIndex": -1,
"hoveredFocusPointBinding": null,
"initialState": {
"altFocusPoint": null,
"arrowStartIsInside": false,
@@ -8976,9 +8978,11 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
"selectedGroupIds": {},
"selectedLinearElement": LinearElementEditor {
"customLineAngle": null,
"draggedFocusPointBinding": null,
"elbowed": false,
"elementId": "id0",
"hoverPointIndex": -1,
"hoveredFocusPointBinding": null,
"initialState": {
"altFocusPoint": null,
"arrowStartIsInside": false,
@@ -9402,9 +9406,11 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
"selectedGroupIds": {},
"selectedLinearElement": LinearElementEditor {
"customLineAngle": null,
"draggedFocusPointBinding": null,
"elbowed": false,
"elementId": "id0",
"hoverPointIndex": -1,
"hoveredFocusPointBinding": null,
"initialState": {
"altFocusPoint": null,
"arrowStartIsInside": false,
@@ -9818,9 +9824,11 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
"selectedGroupIds": {},
"selectedLinearElement": LinearElementEditor {
"customLineAngle": null,
"draggedFocusPointBinding": null,
"elbowed": false,
"elementId": "id0",
"hoverPointIndex": -1,
"hoveredFocusPointBinding": null,
"initialState": {
"altFocusPoint": null,
"arrowStartIsInside": false,
+1 -1
View File
@@ -5051,7 +5051,7 @@ describe("history", () => {
id: arrowId,
startBinding: expect.objectContaining({
elementId: rect1.id,
fixedPoint: expect.arrayContaining([0.5001, 0.5001]),
fixedPoint: expect.arrayContaining([1, 0.5001]),
}),
endBinding: expect.objectContaining({
elementId: rect2.id,
+2
View File
@@ -63,6 +63,8 @@ import type { isOverScrollBars } from "./scene/scrollbars";
import type React from "react";
import type { JSX } from "react";
export type { App };
export type SocketId = string & { _brand: "SocketId" };
export type Collaborator = Readonly<{