feat(editor): visualize binding midpoints + support for simple arrows (#10611)

* feat: Force exact center focus point

When the projected point is close to center snap it to the exact center.

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Tests

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Snap to center around side mid point.

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* Trigger CI

* fix: Midpoint outline focus point

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Tests

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Dragging existing arrow reset focus point on outline

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Tests

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* feat: Midpoint indicator

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Rotated mid points

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: No hole

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: Increase outline midpoint sticky distance

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Don't show midpoint indicator when no snapping is possible

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* feat: Indicate lock-in

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: Simple arrow mid point selection inconsistency

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: cache override

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Precise know dragging with midpoint refactor

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fear: Frame support

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Crossing arrow won't trigger mid point

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Arrow creation point highlight

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Restore types & tests

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* chore: Restore restore.ts

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: restore.ts

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Elbow arrows reliably highlight center point

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Highlight point ordering

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* feat: Bind with focus point across shape

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Lint

* fix: Midpoint and binding alignment

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* chore: Indicator color

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* chore: More knob tuning

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Radius

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Tests

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* simplify point indicators

---------

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-02-11 11:06:27 +01:00
committed by GitHub
co-authored by dwelle
parent 1c8e8bb0f3
commit a70417f23f
15 changed files with 699 additions and 268 deletions
+22 -21
View File
@@ -1590,7 +1590,7 @@ describe("history", () => {
expect(API.getUndoStack().length).toBe(5);
expect(arrow.startBinding).toEqual({
elementId: rect1.id,
fixedPoint: expect.arrayContaining([0.5001, 0.5001]),
fixedPoint: expect.arrayContaining([1, 0.5001]),
mode: "orbit",
});
expect(arrow.endBinding).toEqual({
@@ -1613,7 +1613,7 @@ describe("history", () => {
expect(API.getRedoStack().length).toBe(1);
expect(arrow.startBinding).toEqual({
elementId: rect1.id,
fixedPoint: expect.arrayContaining([0.5001, 0.5001]),
fixedPoint: expect.arrayContaining([1, 0.5001]),
mode: "orbit",
});
expect(arrow.endBinding).toEqual({
@@ -1636,7 +1636,7 @@ describe("history", () => {
expect(API.getRedoStack().length).toBe(0);
expect(arrow.startBinding).toEqual({
elementId: rect1.id,
fixedPoint: expect.arrayContaining([0.5001, 0.5001]),
fixedPoint: expect.arrayContaining([1, 0.5001]),
mode: "orbit",
});
expect(arrow.endBinding).toEqual({
@@ -1667,7 +1667,7 @@ describe("history", () => {
expect(API.getRedoStack().length).toBe(0);
expect(arrow.startBinding).toEqual({
elementId: rect1.id,
fixedPoint: expect.arrayContaining([0.5001, 0.5001]),
fixedPoint: expect.arrayContaining([1, 0.5001]),
mode: "orbit",
});
expect(arrow.endBinding).toEqual({
@@ -1690,7 +1690,7 @@ describe("history", () => {
expect(API.getRedoStack().length).toBe(1);
expect(arrow.startBinding).toEqual({
elementId: rect1.id,
fixedPoint: expect.arrayContaining([0.5001, 0.5001]),
fixedPoint: expect.arrayContaining([1, 0.5001]),
mode: "orbit",
});
expect(arrow.endBinding).toEqual({
@@ -4510,20 +4510,20 @@ describe("history", () => {
// create start binding
mouse.downAt(0, 0);
mouse.moveTo(0, 10);
mouse.moveTo(0, 10);
mouse.moveTo(0, 3);
mouse.moveTo(0, 3);
mouse.up();
// create end binding
mouse.downAt(100, 0);
mouse.moveTo(100, 10);
mouse.moveTo(100, 10);
mouse.moveTo(100, 3);
mouse.moveTo(100, 3);
mouse.up();
expect(
(h.elements[2] as ExcalidrawElbowArrowElement).startBinding
?.fixedPoint,
).not.toEqual([1, 0.5001]);
).toEqual([1, 0.5001]);
expect(
(h.elements[2] as ExcalidrawElbowArrowElement).startBinding?.mode,
).toBe("orbit");
@@ -4623,12 +4623,12 @@ describe("history", () => {
id: arrowId,
startBinding: expect.objectContaining({
elementId: rect1.id,
fixedPoint: [0.6363636363636364, 0.6363636363636364],
fixedPoint: [1, 0.5001],
mode: "orbit",
}),
endBinding: expect.objectContaining({
elementId: rect2.id,
fixedPoint: [0.4106696643494564, 0.5893303356505437],
fixedPoint: [0, 0.5001],
mode: "orbit",
}),
}),
@@ -4666,13 +4666,13 @@ describe("history", () => {
// create start binding
mouse.downAt(0, 0);
mouse.moveTo(0, 10);
mouse.upAt(0, 10);
mouse.moveTo(0, 3);
mouse.upAt(0, 3);
// create end binding
mouse.downAt(100, 0);
mouse.moveTo(100, 10);
mouse.upAt(100, 10);
mouse.moveTo(100, 3);
mouse.upAt(100, 3);
expect(h.elements).toEqual(
expect.arrayContaining([
@@ -4766,13 +4766,13 @@ describe("history", () => {
id: arrowId,
startBinding: expect.objectContaining({
elementId: rect1.id,
fixedPoint: [0.6363636363636364, 0.6363636363636364],
fixedPoint: [1, 0.5001],
mode: "orbit",
}),
// rebound with previous rectangle
endBinding: expect.objectContaining({
elementId: rect2.id,
fixedPoint: [0.39746300211416496, 0.6025369978858351],
fixedPoint: [0, 0.5001],
mode: "orbit",
}),
}),
@@ -5031,8 +5031,9 @@ describe("history", () => {
// bind arrow to rect1 and rect2
UI.clickTool("arrow");
mouse.down(0, 0);
mouse.moveTo(50, 0);
mouse.up(50, 0);
mouse.moveTo(25, 0);
mouse.moveTo(47, 0);
mouse.up(47, 0);
const arrowId = h.elements[2].id;
@@ -5055,7 +5056,7 @@ describe("history", () => {
}),
endBinding: expect.objectContaining({
elementId: rect2.id,
fixedPoint: expect.arrayContaining([0.5001, 0.5001]),
fixedPoint: expect.arrayContaining([0, 0.5001]),
}),
isDeleted: true,
}),