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
parent 216afc3625
commit b552c60714
16 changed files with 1112 additions and 90 deletions
+20 -15
View File
@@ -116,6 +116,7 @@ export type BindingStrategy =
*/
export const BASE_BINDING_GAP = 10;
export const BASE_BINDING_GAP_ELBOW = 5;
export const FOCUS_POINT_SIZE = 10 / 1.5;
export const getBindingGap = (
bindTarget: ExcalidrawBindableElement,
@@ -145,7 +146,9 @@ export const shouldEnableBindingForPointerEvent = (
return !event[KEYS.CTRL_OR_CMD];
};
export const isBindingEnabled = (appState: AppState): boolean => {
export const isBindingEnabled = (appState: {
isBindingEnabled: AppState["isBindingEnabled"];
}): boolean => {
return appState.isBindingEnabled;
};
@@ -259,7 +262,7 @@ const bindingStrategyForElbowArrowEndpointDragging = (
globalPoint,
elements,
elementsMap,
(element) => maxBindingDistance_simple(zoom),
maxBindingDistance_simple(zoom),
);
const current = hit
@@ -684,7 +687,7 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
globalPoint,
elements,
elementsMap,
(e) => maxBindingDistance_simple(appState.zoom),
maxBindingDistance_simple(appState.zoom),
);
const pointInElement =
hit &&
@@ -711,7 +714,13 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
const otherFocusPointIsInElement =
otherBindableElement &&
otherFocusPoint &&
isPointInElement(otherFocusPoint, otherBindableElement, elementsMap);
hitElementItself({
point: otherFocusPoint,
element: otherBindableElement,
elementsMap,
threshold: 0,
overrideShouldTestInside: true,
});
// Handle outside-outside binding to the same element
if (otherBinding && otherBinding.elementId === hit?.id) {
@@ -1674,7 +1683,9 @@ export const updateBoundPoint = (
binding: FixedPointBinding | null | undefined,
bindableElement: ExcalidrawBindableElement,
elementsMap: ElementsMap,
customIntersector?: LineSegment<GlobalPoint>,
opts?: {
customIntersector?: LineSegment<GlobalPoint>;
},
): LocalPoint | null => {
if (
binding == null ||
@@ -1761,16 +1772,14 @@ export const updateBoundPoint = (
const isNested = (arrowTooShort || isOverlapping) && isLargerThanOther;
let _customIntersector = customIntersector;
let _customIntersector = opts?.customIntersector;
if (!elbowed && !_customIntersector) {
const [x1, y1, x2, y2] = LinearElementEditor.getElementAbsoluteCoords(
arrow,
elementsMap,
);
const center = pointFrom<GlobalPoint>((x1 + x2) / 2, (y1 + y2) / 2);
const edgePoint = isRectanguloidElement(bindableElement)
? avoidRectangularCorner(arrow, bindableElement, elementsMap, global)
: global;
const edgePoint = global;
const adjacentPoint = pointRotateRads(
pointFrom<GlobalPoint>(
arrow.x +
@@ -1884,7 +1893,7 @@ export const calculateFixedPointForNonElbowArrowBinding = (
elementsMap: ElementsMap,
focusPoint?: GlobalPoint,
): { fixedPoint: FixedPoint } => {
const edgePoint = focusPoint
const edgePoint: GlobalPoint = focusPoint
? focusPoint
: LinearElementEditor.getPointAtIndexGlobalCoordinates(
linearElement,
@@ -1892,11 +1901,7 @@ export const calculateFixedPointForNonElbowArrowBinding = (
elementsMap,
);
// Convert the global point to element-local coordinates
const elementCenter = pointFrom(
hoveredElement.x + hoveredElement.width / 2,
hoveredElement.y + hoveredElement.height / 2,
);
const elementCenter = elementCenterPoint(hoveredElement, elementsMap);
// Rotate the point to account for element rotation
const nonRotatedPoint = pointRotateRads(