From 7f23f37a0191bdf16339f47b8033684ed1aaa935 Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Tue, 22 Apr 2025 18:45:59 +0200 Subject: [PATCH 01/28] More precise binding distance for elbow arrows --- packages/element/src/binding.ts | 55 ++++++++++++++------------------- 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index ee5d037a8..560398df4 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -27,7 +27,7 @@ import { PRECISION, } from "@excalidraw/math"; -import { isPointOnShape } from "@excalidraw/utils/collision"; +import { isPointInShape, isPointOnShape } from "@excalidraw/utils/collision"; import type { LocalPoint, Radians } from "@excalidraw/math"; @@ -63,7 +63,7 @@ import { isTextElement, } from "./typeChecks"; -import { aabbForElement, getElementShape, pointInsideBounds } from "./shapes"; +import { aabbForElement, getElementShape } from "./shapes"; import { updateElbowArrowPoints } from "./elbowArrow"; import type Scene from "./Scene"; @@ -230,7 +230,13 @@ const getOriginalBindingIfStillCloseOfLinearElementEdge = ( const element = elementsMap.get(elementId); if ( isBindableElement(element) && - bindingBorderTest(element, coors, elementsMap, zoom) + bindingBorderTest( + element, + coors, + elementsMap, + zoom, + isElbowArrow(element), + ) ) { return element; } @@ -567,19 +573,7 @@ export const getHoveredElementForBinding = ( elements, (element) => isBindableElement(element, false) && - bindingBorderTest( - element, - pointerCoords, - elementsMap, - zoom, - (fullShape || - !isBindingFallthroughEnabled( - element as ExcalidrawBindableElement, - )) && - // disable fullshape snapping for frame elements so we - // can bind to frame children - !isFrameLikeElement(element), - ), + bindingBorderTest(element, pointerCoords, elementsMap, zoom, fullShape), ).filter((element) => { if (cullRest) { return false; @@ -621,16 +615,7 @@ export const getHoveredElementForBinding = ( elements, (element) => isBindableElement(element, false) && - bindingBorderTest( - element, - pointerCoords, - elementsMap, - zoom, - // disable fullshape snapping for frame elements so we - // can bind to frame children - (fullShape || !isBindingFallthroughEnabled(element)) && - !isFrameLikeElement(element), - ), + bindingBorderTest(element, pointerCoords, elementsMap, zoom, fullShape), ); return hoveredElement as NonDeleted | null; @@ -1512,13 +1497,19 @@ export const bindingBorderTest = ( fullShape?: boolean, ): boolean => { const threshold = maxBindingGap(element, element.width, element.height, zoom); - const shape = getElementShape(element, elementsMap); - return ( - isPointOnShape(pointFrom(x, y), shape, threshold) || - (fullShape === true && - pointInsideBounds(pointFrom(x, y), aabbForElement(element))) - ); + const shouldTestInside = + // disable fullshape snapping for frame elements so we + // can bind to frame children + (fullShape || !isBindingFallthroughEnabled(element)) && + !isFrameLikeElement(element); + + return shouldTestInside + ? // Since `inShape` tests STRICTLY againt the insides of a shape + // we would need `onShape` as well to include the "borders" + isPointInShape(pointFrom(x, y), shape) || + isPointOnShape(pointFrom(x, y), shape, threshold) + : isPointOnShape(pointFrom(x, y), shape, threshold); }; export const maxBindingGap = ( From 8f6a81c58e4174a4e0abb4271cd5eb270a7dc215 Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Tue, 22 Apr 2025 19:02:26 +0200 Subject: [PATCH 02/28] Scale binding highlight to cover the entire binding area --- packages/element/src/binding.ts | 25 ++++++------------- .../excalidraw/renderer/interactiveScene.ts | 6 ++--- 2 files changed, 11 insertions(+), 20 deletions(-) diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index 560398df4..d63b1c043 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -107,7 +107,7 @@ export const isBindingEnabled = (appState: AppState): boolean => { }; export const FIXED_BINDING_DISTANCE = 5; -export const BINDING_HIGHLIGHT_THICKNESS = 10; +const BINDING_HIGHLIGHT_THICKNESS = 10; export const BINDING_HIGHLIGHT_OFFSET = 4; const getNonDeletedElements = ( @@ -446,22 +446,13 @@ export const maybeBindLinearElement = ( const normalizePointBinding = ( binding: { focus: number; gap: number }, hoveredElement: ExcalidrawBindableElement, -) => { - let gap = binding.gap; - const maxGap = maxBindingGap( - hoveredElement, - hoveredElement.width, - hoveredElement.height, - ); - - if (gap > maxGap) { - gap = BINDING_HIGHLIGHT_THICKNESS + BINDING_HIGHLIGHT_OFFSET; - } - return { - ...binding, - gap, - }; -}; +) => ({ + ...binding, + gap: Math.min( + binding.gap, + maxBindingGap(hoveredElement, hoveredElement.width, hoveredElement.height), + ), +}); export const bindLinearElement = ( linearElement: NonDeleted, diff --git a/packages/excalidraw/renderer/interactiveScene.ts b/packages/excalidraw/renderer/interactiveScene.ts index 69c6a8196..ca2daa72a 100644 --- a/packages/excalidraw/renderer/interactiveScene.ts +++ b/packages/excalidraw/renderer/interactiveScene.ts @@ -17,7 +17,6 @@ import { import { BINDING_HIGHLIGHT_OFFSET, - BINDING_HIGHLIGHT_THICKNESS, maxBindingGap, } from "@excalidraw/element/binding"; import { LinearElementEditor } from "@excalidraw/element/linearElementEditor"; @@ -262,8 +261,9 @@ const renderBindingHighlightForBindableElement = ( context.strokeStyle = "rgba(0,0,0,.05)"; // When zooming out, make line width greater for visibility - const zoomValue = zoom.value < 1 ? zoom.value : 1; - context.lineWidth = BINDING_HIGHLIGHT_THICKNESS / zoomValue; + context.lineWidth = + maxBindingGap(element, element.width, element.height, zoom) - + BINDING_HIGHLIGHT_OFFSET; // To ensure the binding highlight doesn't overlap the element itself const padding = context.lineWidth / 2 + BINDING_HIGHLIGHT_OFFSET; From cfaabf546ec4f76ad9fe4a0d77a850cb69837811 Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Tue, 22 Apr 2025 19:20:20 +0200 Subject: [PATCH 03/28] Fix snapping at diamond edges --- packages/element/src/binding.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index d63b1c043..88100cc8b 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -44,7 +44,6 @@ import { intersectElementWithLineSegment } from "./collision"; import { distanceToBindableElement } from "./distance"; import { headingForPointFromElement, - headingIsHorizontal, vectorToHeading, type Heading, } from "./heading"; @@ -923,23 +922,16 @@ export const bindPointToSnapToElementOutline = ( let intersection: GlobalPoint | null = null; if (elbowed) { - const isHorizontal = headingIsHorizontal( - headingForPointFromElement(bindableElement, aabb, globalP), - ); - const otherPoint = pointFrom( - isHorizontal ? center[0] : edgePoint[0], - !isHorizontal ? center[1] : edgePoint[1], - ); intersection = intersectElementWithLineSegment( bindableElement, lineSegment( - otherPoint, + center, pointFromVector( vectorScale( - vectorNormalize(vectorFromPoint(edgePoint, otherPoint)), + vectorNormalize(vectorFromPoint(edgePoint, center)), Math.max(bindableElement.width, bindableElement.height) * 2, ), - otherPoint, + center, ), ), )[0]; From 3428148e4d28f34271ca95941b691432d41dc652 Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Tue, 22 Apr 2025 21:40:25 +0200 Subject: [PATCH 04/28] Restore elbow point snapping --- packages/element/src/binding.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index 88100cc8b..d63b1c043 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -44,6 +44,7 @@ import { intersectElementWithLineSegment } from "./collision"; import { distanceToBindableElement } from "./distance"; import { headingForPointFromElement, + headingIsHorizontal, vectorToHeading, type Heading, } from "./heading"; @@ -922,16 +923,23 @@ export const bindPointToSnapToElementOutline = ( let intersection: GlobalPoint | null = null; if (elbowed) { + const isHorizontal = headingIsHorizontal( + headingForPointFromElement(bindableElement, aabb, globalP), + ); + const otherPoint = pointFrom( + isHorizontal ? center[0] : edgePoint[0], + !isHorizontal ? center[1] : edgePoint[1], + ); intersection = intersectElementWithLineSegment( bindableElement, lineSegment( - center, + otherPoint, pointFromVector( vectorScale( - vectorNormalize(vectorFromPoint(edgePoint, center)), + vectorNormalize(vectorFromPoint(edgePoint, otherPoint)), Math.max(bindableElement.width, bindableElement.height) * 2, ), - center, + otherPoint, ), ), )[0]; From 6e22710bc82b84cd91461e09c8cc90db88e7061e Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Tue, 22 Apr 2025 21:40:40 +0200 Subject: [PATCH 05/28] Rounded diamond binding highlight --- packages/element/src/bounds.ts | 17 +- .../excalidraw/renderer/interactiveScene.ts | 161 +++++++++++++++--- 2 files changed, 151 insertions(+), 27 deletions(-) diff --git a/packages/element/src/bounds.ts b/packages/element/src/bounds.ts index d0c071f2c..d1b8daca8 100644 --- a/packages/element/src/bounds.ts +++ b/packages/element/src/bounds.ts @@ -476,7 +476,11 @@ export const getRectangleBoxAbsoluteCoords = (boxSceneCoords: RectangleBox) => { ]; }; -export const getDiamondPoints = (element: ExcalidrawElement) => { +export const getDiamondPoints = ( + element: ExcalidrawElement, + wPadding: number = 0, + hPadding: number = 0, +) => { // Here we add +1 to avoid these numbers to be 0 // otherwise rough.js will throw an error complaining about it const topX = Math.floor(element.width / 2) + 1; @@ -488,7 +492,16 @@ export const getDiamondPoints = (element: ExcalidrawElement) => { const leftX = 0; const leftY = rightY; - return [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY]; + return [ + topX, + topY - hPadding, + rightX + wPadding, + rightY, + bottomX, + bottomY + hPadding, + leftX - wPadding, + leftY, + ]; }; // reference: https://eliot-jones.com/2019/12/cubic-bezier-curve-bounding-boxes diff --git a/packages/excalidraw/renderer/interactiveScene.ts b/packages/excalidraw/renderer/interactiveScene.ts index ca2daa72a..f5796bf9b 100644 --- a/packages/excalidraw/renderer/interactiveScene.ts +++ b/packages/excalidraw/renderer/interactiveScene.ts @@ -1,6 +1,7 @@ import oc from "open-color"; import { pointFrom, + pointRotateRads, type GlobalPoint, type LocalPoint, type Radians, @@ -11,6 +12,7 @@ import { FRAME_STYLE, THEME, arrayToMap, + elementCenterPoint, invariant, throttleRAF, } from "@excalidraw/common"; @@ -47,6 +49,7 @@ import { import { getCommonBounds, + getDiamondPoints, getElementAbsoluteCoords, } from "@excalidraw/element/bounds"; @@ -63,6 +66,7 @@ import type { import type { ElementsMap, ExcalidrawBindableElement, + ExcalidrawDiamondElement, ExcalidrawElement, ExcalidrawFrameLikeElement, ExcalidrawImageElement, @@ -191,22 +195,138 @@ const strokeRectWithRotation = ( const strokeDiamondWithRotation = ( context: CanvasRenderingContext2D, - width: number, - height: number, - cx: number, - cy: number, - angle: number, + padding: number, + element: ExcalidrawDiamondElement, ) => { + const { width, height } = element; + const side = Math.hypot(width, height); + const wPaddingMax = (1.8 * (padding * side)) / height; + const hPaddingMax = (1.8 * (padding * side)) / width; + const [x, y] = pointRotateRads( + pointFrom(element.x, element.y), + elementCenterPoint(element), + element.angle, + ); context.save(); - context.translate(cx, cy); - context.rotate(angle); - context.beginPath(); - context.moveTo(0, height / 2); - context.lineTo(width / 2, 0); - context.lineTo(0, -height / 2); - context.lineTo(-width / 2, 0); - context.closePath(); - context.stroke(); + context.translate(x, y); + context.rotate(element.angle); + + { + context.beginPath(); + + const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] = + getDiamondPoints(element, wPaddingMax, hPaddingMax); + if (element.roundness) { + const verticalRadius = getCornerRadius(Math.abs(topX - leftX), element); + const horizontalRadius = getCornerRadius( + Math.abs(rightY - topY), + element, + ); + + context.moveTo(topX + verticalRadius, topY + horizontalRadius); + context.lineTo(rightX - verticalRadius, rightY - horizontalRadius); + context.bezierCurveTo( + rightX, + rightY, + rightX, + rightY, + rightX - verticalRadius, + rightY + horizontalRadius, + ); + context.lineTo(bottomX + verticalRadius, bottomY - horizontalRadius); + context.bezierCurveTo( + bottomX, + bottomY, + bottomX, + bottomY, + bottomX - verticalRadius, + bottomY - horizontalRadius, + ); + context.lineTo(leftX + verticalRadius, leftY + horizontalRadius); + context.bezierCurveTo( + leftX, + leftY, + leftX, + leftY, + leftX + verticalRadius, + leftY - horizontalRadius, + ); + context.lineTo(topX - verticalRadius, topY + horizontalRadius); + context.bezierCurveTo( + topX, + topY, + topX, + topY, + topX + verticalRadius, + topY + horizontalRadius, + ); + } else { + context.moveTo(topX, topY); + context.lineTo(rightX, rightY); + context.lineTo(bottomX, bottomY); + context.lineTo(leftX, leftY); + } + } + + { + const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] = + getDiamondPoints(element, 5, 5); + if (element.roundness) { + const verticalRadius = getCornerRadius(Math.abs(topX - leftX), element); + const horizontalRadius = getCornerRadius( + Math.abs(rightY - topY), + element, + ); + + context.moveTo(topX - verticalRadius, topY + horizontalRadius); + context.lineTo(leftX + verticalRadius, leftY - horizontalRadius); + context.bezierCurveTo( + leftX, + leftY, + leftX, + leftY, + leftX + verticalRadius, + leftY + horizontalRadius, + ); + context.lineTo(bottomX - verticalRadius, bottomY - horizontalRadius); + context.bezierCurveTo( + bottomX, + bottomY, + bottomX, + bottomY, + bottomX + verticalRadius, + bottomY - horizontalRadius, + ); + context.lineTo(rightX - verticalRadius, rightY + horizontalRadius); + context.bezierCurveTo( + rightX, + rightY, + rightX, + rightY, + rightX - verticalRadius, + rightY - horizontalRadius, + ); + context.lineTo(topX + verticalRadius, topY + horizontalRadius); + context.bezierCurveTo( + topX, + topY, + topX, + topY, + topX - verticalRadius, + topY + horizontalRadius, + ); + } else { + context.moveTo(topX, topY); + context.lineTo(rightX, rightY); + context.lineTo(bottomX, bottomY); + context.lineTo(leftX, leftY); + } + + context.closePath(); + context.fill(); + //context.stroke(); + } + context.restore(); }; @@ -260,6 +380,7 @@ const renderBindingHighlightForBindableElement = ( const height = y2 - y1; context.strokeStyle = "rgba(0,0,0,.05)"; + context.fillStyle = "rgba(0,0,0,.05)"; // When zooming out, make line width greater for visibility context.lineWidth = maxBindingGap(element, element.width, element.height, zoom) - @@ -294,17 +415,7 @@ const renderBindingHighlightForBindableElement = ( ); break; case "diamond": - const side = Math.hypot(width, height); - const wPadding = (padding * side) / height; - const hPadding = (padding * side) / width; - strokeDiamondWithRotation( - context, - width + wPadding * 2, - height + hPadding * 2, - x1 + width / 2, - y1 + height / 2, - element.angle, - ); + strokeDiamondWithRotation(context, padding, element); break; case "ellipse": strokeEllipseWithRotation( From c5f066beeb759c4e71c0981b757cf7d9fc8c0af6 Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Tue, 22 Apr 2025 21:49:06 +0200 Subject: [PATCH 06/28] More precise binding for elbow arrows --- packages/element/src/binding.ts | 2 +- packages/element/src/elbowArrow.ts | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index d63b1c043..3664744d3 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -874,7 +874,7 @@ export const getHeadingForElbowArrowSnap = ( return headingForPointFromElement(bindableElement, aabb, p); }; -const getDistanceForBinding = ( +export const getDistanceForBinding = ( point: Readonly, bindableElement: ExcalidrawBindableElement, zoom?: AppState["zoom"], diff --git a/packages/element/src/elbowArrow.ts b/packages/element/src/elbowArrow.ts index 95a2aa8ef..b122fae00 100644 --- a/packages/element/src/elbowArrow.ts +++ b/packages/element/src/elbowArrow.ts @@ -31,6 +31,7 @@ import { getGlobalFixedPointForBindableElement, snapToMid, getHoveredElementForBinding, + getDistanceForBinding, } from "./binding"; import { distanceToBindableElement } from "./distance"; import { @@ -52,7 +53,7 @@ import { type NonDeletedSceneElementsMap, } from "./types"; -import { aabbForElement, pointInsideBounds } from "./shapes"; +import { aabbForElement, getElementShape, pointInsideBounds } from "./shapes"; import type { Bounds } from "./bounds"; import type { Heading } from "./heading"; @@ -64,6 +65,7 @@ import type { FixedSegment, NonDeletedExcalidrawElement, } from "./types"; +import { isPointInShape } from "@excalidraw/utils/collision"; type GridAddress = [number, number] & { _brand: "gridaddress" }; @@ -1253,8 +1255,10 @@ const getElbowArrowData = ( "start", arrow.startBinding?.fixedPoint, origStartGlobalPoint, + elementsMap, hoveredStartElement, options?.isDragging, + options?.zoom, ); const endGlobalPoint = getGlobalPoint( { @@ -1266,8 +1270,10 @@ const getElbowArrowData = ( "end", arrow.endBinding?.fixedPoint, origEndGlobalPoint, + elementsMap, hoveredEndElement, options?.isDragging, + options?.zoom, ); const startHeading = getBindPointHeading( startGlobalPoint, @@ -2209,11 +2215,17 @@ const getGlobalPoint = ( startOrEnd: "start" | "end", fixedPointRatio: [number, number] | undefined | null, initialPoint: GlobalPoint, + elementsMap: ElementsMap, element?: ExcalidrawBindableElement | null, isDragging?: boolean, + zoom?: AppState["zoom"], ): GlobalPoint => { if (isDragging) { - if (element) { + if ( + element && + (getDistanceForBinding(initialPoint, element, zoom) || + isPointInShape(initialPoint, getElementShape(element, elementsMap))) + ) { const snapPoint = bindPointToSnapToElementOutline( arrow, element, From e16cc9a72b402e9ae13e61df28ba16bf7d7d5f17 Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Tue, 22 Apr 2025 21:50:31 +0200 Subject: [PATCH 07/28] Import order lint fix --- packages/element/src/elbowArrow.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/element/src/elbowArrow.ts b/packages/element/src/elbowArrow.ts index b122fae00..bc389d844 100644 --- a/packages/element/src/elbowArrow.ts +++ b/packages/element/src/elbowArrow.ts @@ -22,6 +22,8 @@ import { isDevEnv, } from "@excalidraw/common"; +import { isPointInShape } from "@excalidraw/utils/collision"; + import type { AppState } from "@excalidraw/excalidraw/types"; import { @@ -48,6 +50,7 @@ import { } from "./heading"; import { type ElementUpdate } from "./mutateElement"; import { isBindableElement } from "./typeChecks"; + import { type ExcalidrawElbowArrowElement, type NonDeletedSceneElementsMap, @@ -65,7 +68,6 @@ import type { FixedSegment, NonDeletedExcalidrawElement, } from "./types"; -import { isPointInShape } from "@excalidraw/utils/collision"; type GridAddress = [number, number] & { _brand: "gridaddress" }; From 41afafcce641fe86bb0588eb97d45d307791febf Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Tue, 22 Apr 2025 21:52:02 +0200 Subject: [PATCH 08/28] Adjust diamond highlight size --- packages/excalidraw/renderer/interactiveScene.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/excalidraw/renderer/interactiveScene.ts b/packages/excalidraw/renderer/interactiveScene.ts index f5796bf9b..49a7af65c 100644 --- a/packages/excalidraw/renderer/interactiveScene.ts +++ b/packages/excalidraw/renderer/interactiveScene.ts @@ -200,8 +200,8 @@ const strokeDiamondWithRotation = ( ) => { const { width, height } = element; const side = Math.hypot(width, height); - const wPaddingMax = (1.8 * (padding * side)) / height; - const hPaddingMax = (1.8 * (padding * side)) / width; + const wPaddingMax = (1.5 * (padding * side)) / height; + const hPaddingMax = (1.5 * (padding * side)) / width; const [x, y] = pointRotateRads( pointFrom(element.x, element.y), elementCenterPoint(element), From 2a71add05c024c0d4a89f9563522f0abe2d5a7a3 Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Tue, 22 Apr 2025 21:52:57 +0200 Subject: [PATCH 09/28] Non-rounded diamond highlight --- packages/excalidraw/renderer/interactiveScene.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/excalidraw/renderer/interactiveScene.ts b/packages/excalidraw/renderer/interactiveScene.ts index 49a7af65c..1be2348a2 100644 --- a/packages/excalidraw/renderer/interactiveScene.ts +++ b/packages/excalidraw/renderer/interactiveScene.ts @@ -317,9 +317,9 @@ const strokeDiamondWithRotation = ( ); } else { context.moveTo(topX, topY); - context.lineTo(rightX, rightY); - context.lineTo(bottomX, bottomY); context.lineTo(leftX, leftY); + context.lineTo(bottomX, bottomY); + context.lineTo(rightX, rightY); } context.closePath(); From f842a89fef0e29d53556ca37a8ef8b8510a7380b Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Tue, 22 Apr 2025 22:02:21 +0200 Subject: [PATCH 10/28] Update tests --- packages/element/tests/elbowArrow.test.tsx | 12 ++++++------ .../data/__snapshots__/transform.test.ts.snap | 4 ++-- packages/excalidraw/data/transform.test.ts | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/element/tests/elbowArrow.test.tsx b/packages/element/tests/elbowArrow.test.tsx index 25f64072e..9e71e47ce 100644 --- a/packages/element/tests/elbowArrow.test.tsx +++ b/packages/element/tests/elbowArrow.test.tsx @@ -78,9 +78,9 @@ describe("elbow arrow segment move", () => { expect(arrow.points).toCloselyEqualPoints([ [0, 0], - [110, 0], - [110, 200], - [190, 200], + [115, 0], + [115, 199.9], + [195, 199.9], ]); mouse.reset(); @@ -89,9 +89,9 @@ describe("elbow arrow segment move", () => { expect(arrow.points).toCloselyEqualPoints([ [0, 0], - [110, 0], - [110, 200], - [190, 200], + [115, 0], + [115, 199.9], + [195, 199.9], ]); }); diff --git a/packages/excalidraw/data/__snapshots__/transform.test.ts.snap b/packages/excalidraw/data/__snapshots__/transform.test.ts.snap index 70f8daa31..b732aca52 100644 --- a/packages/excalidraw/data/__snapshots__/transform.test.ts.snap +++ b/packages/excalidraw/data/__snapshots__/transform.test.ts.snap @@ -335,7 +335,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t "endBinding": { "elementId": "text-2", "focus": 0, - "gap": 14, + "gap": 16, }, "fillStyle": "solid", "frameId": null, @@ -1538,7 +1538,7 @@ exports[`Test Transform > should transform the elements correctly when linear el "endBinding": { "elementId": "B", "focus": 0, - "gap": 14, + "gap": 32, }, "fillStyle": "solid", "frameId": null, diff --git a/packages/excalidraw/data/transform.test.ts b/packages/excalidraw/data/transform.test.ts index 0b0718e8e..0d9fcf316 100644 --- a/packages/excalidraw/data/transform.test.ts +++ b/packages/excalidraw/data/transform.test.ts @@ -781,7 +781,7 @@ describe("Test Transform", () => { expect((arrow as ExcalidrawArrowElement).endBinding).toStrictEqual({ elementId: "rect-1", focus: -0, - gap: 14, + gap: 25, }); expect(rect.boundElements).toStrictEqual([ { From 10c5ec2c7b8a8266fbab2e747bf49daf892a566b Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Wed, 23 Apr 2025 08:40:52 +0200 Subject: [PATCH 11/28] Binding distance fix with elbow arrows Signed-off-by: Mark Tolmacs --- packages/element/src/elbowArrow.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/element/src/elbowArrow.ts b/packages/element/src/elbowArrow.ts index bc389d844..83d48b1f5 100644 --- a/packages/element/src/elbowArrow.ts +++ b/packages/element/src/elbowArrow.ts @@ -33,7 +33,6 @@ import { getGlobalFixedPointForBindableElement, snapToMid, getHoveredElementForBinding, - getDistanceForBinding, } from "./binding"; import { distanceToBindableElement } from "./distance"; import { @@ -2225,8 +2224,7 @@ const getGlobalPoint = ( if (isDragging) { if ( element && - (getDistanceForBinding(initialPoint, element, zoom) || - isPointInShape(initialPoint, getElementShape(element, elementsMap))) + isPointInShape(initialPoint, getElementShape(element, elementsMap)) ) { const snapPoint = bindPointToSnapToElementOutline( arrow, From 95a2b03686f1957bf6d53a2f048094d307f9537a Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Wed, 23 Apr 2025 16:07:50 +0200 Subject: [PATCH 12/28] Corner snapping for diamonds no longer has dead zones Signed-off-by: Mark Tolmacs --- packages/element/src/binding.ts | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index 3664744d3..bb6d1bc86 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -1104,9 +1104,11 @@ export const snapToMid = ( const horizontalThrehsold = clamp(tolerance * width, 5, 80); if ( - nonRotated[0] <= x + width / 2 && - nonRotated[1] > center[1] - verticalThrehsold && - nonRotated[1] < center[1] + verticalThrehsold + element.type === "diamond" + ? nonRotated[0] <= x + width * (element.roundness ? 0.035 : 1) + : nonRotated[0] <= x + width / 2 && + nonRotated[1] > center[1] - verticalThrehsold && + nonRotated[1] < center[1] + verticalThrehsold ) { // LEFT return pointRotateRads( @@ -1115,9 +1117,11 @@ export const snapToMid = ( angle, ); } else if ( - nonRotated[1] <= y + height / 2 && - nonRotated[0] > center[0] - horizontalThrehsold && - nonRotated[0] < center[0] + horizontalThrehsold + element.type === "diamond" + ? nonRotated[1] <= y + height * (element.roundness ? 0.035 : 1) + : nonRotated[1] <= y + height / 2 && + nonRotated[0] > center[0] - horizontalThrehsold && + nonRotated[0] < center[0] + horizontalThrehsold ) { // TOP return pointRotateRads( @@ -1126,9 +1130,11 @@ export const snapToMid = ( angle, ); } else if ( - nonRotated[0] >= x + width / 2 && - nonRotated[1] > center[1] - verticalThrehsold && - nonRotated[1] < center[1] + verticalThrehsold + element.type === "diamond" + ? nonRotated[0] >= x + width * (element.roundness ? 0.035 : 1) + : nonRotated[0] >= x + width / 2 && + nonRotated[1] > center[1] - verticalThrehsold && + nonRotated[1] < center[1] + verticalThrehsold ) { // RIGHT return pointRotateRads( @@ -1137,9 +1143,11 @@ export const snapToMid = ( angle, ); } else if ( - nonRotated[1] >= y + height / 2 && - nonRotated[0] > center[0] - horizontalThrehsold && - nonRotated[0] < center[0] + horizontalThrehsold + element.type === "diamond" + ? nonRotated[1] >= y - height * (element.roundness ? 0.035 : 1) + : nonRotated[1] >= y + height / 2 && + nonRotated[0] > center[0] - horizontalThrehsold && + nonRotated[0] < center[0] + horizontalThrehsold ) { // DOWN return pointRotateRads( From de91f092a720553c83889d32d10a27834f23e450 Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Wed, 23 Apr 2025 16:08:22 +0200 Subject: [PATCH 13/28] Remove elbow arrow snap incorrect optimization Signed-off-by: Mark Tolmacs --- packages/element/src/elbowArrow.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/element/src/elbowArrow.ts b/packages/element/src/elbowArrow.ts index 83d48b1f5..4e15fcae6 100644 --- a/packages/element/src/elbowArrow.ts +++ b/packages/element/src/elbowArrow.ts @@ -22,8 +22,6 @@ import { isDevEnv, } from "@excalidraw/common"; -import { isPointInShape } from "@excalidraw/utils/collision"; - import type { AppState } from "@excalidraw/excalidraw/types"; import { @@ -55,7 +53,7 @@ import { type NonDeletedSceneElementsMap, } from "./types"; -import { aabbForElement, getElementShape, pointInsideBounds } from "./shapes"; +import { aabbForElement, pointInsideBounds } from "./shapes"; import type { Bounds } from "./bounds"; import type { Heading } from "./heading"; @@ -2222,10 +2220,7 @@ const getGlobalPoint = ( zoom?: AppState["zoom"], ): GlobalPoint => { if (isDragging) { - if ( - element && - isPointInShape(initialPoint, getElementShape(element, elementsMap)) - ) { + if (element) { const snapPoint = bindPointToSnapToElementOutline( arrow, element, From d478974e0dba9124acee0907d3cf1c94042ef5b7 Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Wed, 23 Apr 2025 16:14:53 +0200 Subject: [PATCH 14/28] Fix #2 on diamond corners Signed-off-by: Mark Tolmacs --- packages/element/src/binding.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index bb6d1bc86..f480d7fd9 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -1131,7 +1131,7 @@ export const snapToMid = ( ); } else if ( element.type === "diamond" - ? nonRotated[0] >= x + width * (element.roundness ? 0.035 : 1) + ? nonRotated[0] >= x + width * (element.roundness ? 1 - 0.035 : 1) : nonRotated[0] >= x + width / 2 && nonRotated[1] > center[1] - verticalThrehsold && nonRotated[1] < center[1] + verticalThrehsold @@ -1144,7 +1144,7 @@ export const snapToMid = ( ); } else if ( element.type === "diamond" - ? nonRotated[1] >= y - height * (element.roundness ? 0.035 : 1) + ? nonRotated[1] >= y + height * (element.roundness ? 1 - 0.035 : 1) : nonRotated[1] >= y + height / 2 && nonRotated[0] > center[0] - horizontalThrehsold && nonRotated[0] < center[0] + horizontalThrehsold From cd15d852e2eed79c7ef48b69898782b582eb613b Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Wed, 23 Apr 2025 19:41:23 +0200 Subject: [PATCH 15/28] Comments Signed-off-by: Mark Tolmacs --- packages/excalidraw/renderer/interactiveScene.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/excalidraw/renderer/interactiveScene.ts b/packages/excalidraw/renderer/interactiveScene.ts index 1be2348a2..266e52643 100644 --- a/packages/excalidraw/renderer/interactiveScene.ts +++ b/packages/excalidraw/renderer/interactiveScene.ts @@ -200,8 +200,8 @@ const strokeDiamondWithRotation = ( ) => { const { width, height } = element; const side = Math.hypot(width, height); - const wPaddingMax = (1.5 * (padding * side)) / height; - const hPaddingMax = (1.5 * (padding * side)) / width; + const wPaddingMax = (1.8 * (padding * side)) / height; + const hPaddingMax = (1.8 * (padding * side)) / width; const [x, y] = pointRotateRads( pointFrom(element.x, element.y), elementCenterPoint(element), @@ -268,6 +268,9 @@ const strokeDiamondWithRotation = ( } } + // Counter-clockwise for the cutout in the middle. We need to have an "inverse + // mask" on a filled shape for the diamond highlight, because stroking creates + // sharp inset edges on line joins < 90 degrees. { const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] = getDiamondPoints(element, 5, 5); @@ -324,7 +327,6 @@ const strokeDiamondWithRotation = ( context.closePath(); context.fill(); - //context.stroke(); } context.restore(); From e6b808e86f15d879d997f100a9bdf9fe484e309f Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Wed, 23 Apr 2025 19:42:40 +0200 Subject: [PATCH 16/28] Remove unneeded Signed-off-by: Mark Tolmacs --- packages/element/src/binding.ts | 2 +- packages/element/src/elbowArrow.ts | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index f480d7fd9..0033bdd1b 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -874,7 +874,7 @@ export const getHeadingForElbowArrowSnap = ( return headingForPointFromElement(bindableElement, aabb, p); }; -export const getDistanceForBinding = ( +const getDistanceForBinding = ( point: Readonly, bindableElement: ExcalidrawBindableElement, zoom?: AppState["zoom"], diff --git a/packages/element/src/elbowArrow.ts b/packages/element/src/elbowArrow.ts index 4e15fcae6..95a2aa8ef 100644 --- a/packages/element/src/elbowArrow.ts +++ b/packages/element/src/elbowArrow.ts @@ -47,7 +47,6 @@ import { } from "./heading"; import { type ElementUpdate } from "./mutateElement"; import { isBindableElement } from "./typeChecks"; - import { type ExcalidrawElbowArrowElement, type NonDeletedSceneElementsMap, @@ -1254,10 +1253,8 @@ const getElbowArrowData = ( "start", arrow.startBinding?.fixedPoint, origStartGlobalPoint, - elementsMap, hoveredStartElement, options?.isDragging, - options?.zoom, ); const endGlobalPoint = getGlobalPoint( { @@ -1269,10 +1266,8 @@ const getElbowArrowData = ( "end", arrow.endBinding?.fixedPoint, origEndGlobalPoint, - elementsMap, hoveredEndElement, options?.isDragging, - options?.zoom, ); const startHeading = getBindPointHeading( startGlobalPoint, @@ -2214,10 +2209,8 @@ const getGlobalPoint = ( startOrEnd: "start" | "end", fixedPointRatio: [number, number] | undefined | null, initialPoint: GlobalPoint, - elementsMap: ElementsMap, element?: ExcalidrawBindableElement | null, isDragging?: boolean, - zoom?: AppState["zoom"], ): GlobalPoint => { if (isDragging) { if (element) { From 4d6104018450c1a94a399cc752248e6f1dcb5bb7 Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Thu, 24 Apr 2025 19:42:18 +0200 Subject: [PATCH 17/28] Equidistant binding highlight for diamonds Signed-off-by: Mark Tolmacs --- packages/element/src/bounds.ts | 47 ++-- .../excalidraw/renderer/interactiveScene.ts | 220 ++++++++++-------- packages/math/src/curve.ts | 24 +- packages/math/src/vector.ts | 5 + 4 files changed, 189 insertions(+), 107 deletions(-) diff --git a/packages/element/src/bounds.ts b/packages/element/src/bounds.ts index d1b8daca8..4b27843ac 100644 --- a/packages/element/src/bounds.ts +++ b/packages/element/src/bounds.ts @@ -14,6 +14,13 @@ import { pointDistance, pointFromArray, pointRotateRads, + bezierEquation, + curve, + curveTangent, + vectorNormalize, + vectorNormal, + vectorScale, + pointFromVector, } from "@excalidraw/math"; import { getCurvePathOps } from "@excalidraw/utils/shape"; @@ -476,11 +483,7 @@ export const getRectangleBoxAbsoluteCoords = (boxSceneCoords: RectangleBox) => { ]; }; -export const getDiamondPoints = ( - element: ExcalidrawElement, - wPadding: number = 0, - hPadding: number = 0, -) => { +export const getDiamondPoints = (element: ExcalidrawElement) => { // Here we add +1 to avoid these numbers to be 0 // otherwise rough.js will throw an error complaining about it const topX = Math.floor(element.width / 2) + 1; @@ -492,16 +495,7 @@ export const getDiamondPoints = ( const leftX = 0; const leftY = rightY; - return [ - topX, - topY - hPadding, - rightX + wPadding, - rightY, - bottomX, - bottomY + hPadding, - leftX - wPadding, - leftY, - ]; + return [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY]; }; // reference: https://eliot-jones.com/2019/12/cubic-bezier-curve-bounding-boxes @@ -1159,3 +1153,26 @@ export const doBoundsIntersect = ( return minX1 < maxX2 && maxX1 > minX2 && minY1 < maxY2 && maxY1 > minY2; }; + +export function offsetBezier( + p0: GlobalPoint, + p1: GlobalPoint, + p2: GlobalPoint, + p3: GlobalPoint, + offsetDist: number, + steps = 20, +) { + const offsetPoints = []; + + for (let i = 0; i <= steps; i++) { + const t = i / steps; + const c = curve(p0, p1, p2, p3); + const point = bezierEquation(c, t); + const tangent = vectorNormalize(curveTangent(c, t)); + const normal = vectorNormal(tangent); + + offsetPoints.push(pointFromVector(vectorScale(normal, offsetDist), point)); + } + + return offsetPoints; +} diff --git a/packages/excalidraw/renderer/interactiveScene.ts b/packages/excalidraw/renderer/interactiveScene.ts index 266e52643..0c98fda21 100644 --- a/packages/excalidraw/renderer/interactiveScene.ts +++ b/packages/excalidraw/renderer/interactiveScene.ts @@ -51,6 +51,7 @@ import { getCommonBounds, getDiamondPoints, getElementAbsoluteCoords, + offsetBezier, } from "@excalidraw/element/bounds"; import type { @@ -198,10 +199,6 @@ const strokeDiamondWithRotation = ( padding: number, element: ExcalidrawDiamondElement, ) => { - const { width, height } = element; - const side = Math.hypot(width, height); - const wPaddingMax = (1.8 * (padding * side)) / height; - const hPaddingMax = (1.8 * (padding * side)) / width; const [x, y] = pointRotateRads( pointFrom(element.x, element.y), elementCenterPoint(element), @@ -215,56 +212,59 @@ const strokeDiamondWithRotation = ( context.beginPath(); const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] = - getDiamondPoints(element, wPaddingMax, hPaddingMax); + getDiamondPoints(element); if (element.roundness) { const verticalRadius = getCornerRadius(Math.abs(topX - leftX), element); const horizontalRadius = getCornerRadius( Math.abs(rightY - topY), element, ); + const topApprox = offsetBezier( + pointFrom(topX - verticalRadius, topY + horizontalRadius), + pointFrom(topX, topY), + pointFrom(topX, topY), + pointFrom(topX + verticalRadius, topY + horizontalRadius), + padding, + ); + const rightApprox = offsetBezier( + pointFrom(rightX - verticalRadius, rightY - horizontalRadius), + pointFrom(rightX, rightY), + pointFrom(rightX, rightY), + pointFrom(rightX - verticalRadius, rightY + horizontalRadius), + padding, + ); + const bottomApprox = offsetBezier( + pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius), + pointFrom(bottomX, bottomY), + pointFrom(bottomX, bottomY), + pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius), + padding, + ); + const leftApprox = offsetBezier( + pointFrom(leftX + verticalRadius, leftY + horizontalRadius), + pointFrom(leftX, leftY), + pointFrom(leftX, leftY), + pointFrom(leftX + verticalRadius, leftY - horizontalRadius), + padding, + ); - context.moveTo(topX + verticalRadius, topY + horizontalRadius); - context.lineTo(rightX - verticalRadius, rightY - horizontalRadius); - context.bezierCurveTo( - rightX, - rightY, - rightX, - rightY, - rightX - verticalRadius, - rightY + horizontalRadius, - ); - context.lineTo(bottomX + verticalRadius, bottomY - horizontalRadius); - context.bezierCurveTo( - bottomX, - bottomY, - bottomX, - bottomY, - bottomX - verticalRadius, - bottomY - horizontalRadius, - ); - context.lineTo(leftX + verticalRadius, leftY + horizontalRadius); - context.bezierCurveTo( - leftX, - leftY, - leftX, - leftY, - leftX + verticalRadius, - leftY - horizontalRadius, - ); - context.lineTo(topX - verticalRadius, topY + horizontalRadius); - context.bezierCurveTo( - topX, - topY, - topX, - topY, - topX + verticalRadius, - topY + horizontalRadius, + context.moveTo( + topApprox[topApprox.length - 1][0], + topApprox[topApprox.length - 1][1], ); + context.lineTo(rightApprox[0][0], rightApprox[0][1]); + drawCatmullRom(context, rightApprox); + context.lineTo(bottomApprox[0][0], bottomApprox[0][1]); + drawCatmullRom(context, bottomApprox); + context.lineTo(leftApprox[0][0], leftApprox[0][1]); + drawCatmullRom(context, leftApprox); + context.lineTo(topApprox[0][0], topApprox[0][1]); + drawCatmullRom(context, topApprox); } else { - context.moveTo(topX, topY); - context.lineTo(rightX, rightY); - context.lineTo(bottomX, bottomY); - context.lineTo(leftX, leftY); + context.moveTo(topX, topY - padding); + context.lineTo(rightX + padding, rightY); + context.lineTo(bottomX, bottomY + padding); + context.lineTo(leftX - padding, leftY); } } @@ -273,62 +273,63 @@ const strokeDiamondWithRotation = ( // sharp inset edges on line joins < 90 degrees. { const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] = - getDiamondPoints(element, 5, 5); + getDiamondPoints(element); if (element.roundness) { const verticalRadius = getCornerRadius(Math.abs(topX - leftX), element); const horizontalRadius = getCornerRadius( Math.abs(rightY - topY), element, ); + const topApprox = offsetBezier( + pointFrom(topX + verticalRadius, topY + horizontalRadius), + pointFrom(topX, topY), + pointFrom(topX, topY), + pointFrom(topX - verticalRadius, topY + horizontalRadius), + -5, + ); + const rightApprox = offsetBezier( + pointFrom(rightX - verticalRadius, rightY + horizontalRadius), + pointFrom(rightX, rightY), + pointFrom(rightX, rightY), + pointFrom(rightX - verticalRadius, rightY - horizontalRadius), + -5, + ); + const bottomApprox = offsetBezier( + pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius), + pointFrom(bottomX, bottomY), + pointFrom(bottomX, bottomY), + pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius), + -5, + ); + const leftApprox = offsetBezier( + pointFrom(leftX + verticalRadius, leftY - horizontalRadius), + pointFrom(leftX, leftY), + pointFrom(leftX, leftY), + pointFrom(leftX + verticalRadius, leftY + horizontalRadius), + -5, + ); - context.moveTo(topX - verticalRadius, topY + horizontalRadius); - context.lineTo(leftX + verticalRadius, leftY - horizontalRadius); - context.bezierCurveTo( - leftX, - leftY, - leftX, - leftY, - leftX + verticalRadius, - leftY + horizontalRadius, - ); - context.lineTo(bottomX - verticalRadius, bottomY - horizontalRadius); - context.bezierCurveTo( - bottomX, - bottomY, - bottomX, - bottomY, - bottomX + verticalRadius, - bottomY - horizontalRadius, - ); - context.lineTo(rightX - verticalRadius, rightY + horizontalRadius); - context.bezierCurveTo( - rightX, - rightY, - rightX, - rightY, - rightX - verticalRadius, - rightY - horizontalRadius, - ); - context.lineTo(topX + verticalRadius, topY + horizontalRadius); - context.bezierCurveTo( - topX, - topY, - topX, - topY, - topX - verticalRadius, - topY + horizontalRadius, + context.moveTo( + topApprox[topApprox.length - 1][0], + topApprox[topApprox.length - 1][1], ); + context.lineTo(leftApprox[0][0], leftApprox[0][1]); + drawCatmullRom(context, leftApprox); + context.lineTo(bottomApprox[0][0], bottomApprox[0][1]); + drawCatmullRom(context, bottomApprox); + context.lineTo(rightApprox[0][0], rightApprox[0][1]); + drawCatmullRom(context, rightApprox); + context.lineTo(topApprox[0][0], topApprox[0][1]); + drawCatmullRom(context, topApprox); } else { - context.moveTo(topX, topY); - context.lineTo(leftX, leftY); - context.lineTo(bottomX, bottomY); - context.lineTo(rightX, rightY); + context.moveTo(topX, topY - 5); + context.lineTo(leftX + 5, leftY); + context.lineTo(bottomX, bottomY + 5); + context.lineTo(rightX - 5, rightY); } - - context.closePath(); - context.fill(); } - + context.closePath(); + context.fill(); context.restore(); }; @@ -388,7 +389,8 @@ const renderBindingHighlightForBindableElement = ( maxBindingGap(element, element.width, element.height, zoom) - BINDING_HIGHLIGHT_OFFSET; // To ensure the binding highlight doesn't overlap the element itself - const padding = context.lineWidth / 2 + BINDING_HIGHLIGHT_OFFSET; + const padding = maxBindingGap(element, element.width, element.height, zoom); + //context.lineWidth / 2 + BINDING_HIGHLIGHT_OFFSET; const radius = getCornerRadius( Math.min(element.width, element.height), @@ -1354,3 +1356,39 @@ export const renderInteractiveScene = < renderConfig.callback(ret); return ret as T extends true ? void : ReturnType; }; + +function drawCatmullRom( + ctx: CanvasRenderingContext2D, + points: GlobalPoint[], + segments = 20, +) { + ctx.lineTo(points[0][0], points[0][1]); + + for (let i = 0; i < points.length - 1; i++) { + const p0 = points[i - 1 < 0 ? 0 : i - 1]; + const p1 = points[i]; + const p2 = points[i + 1 >= points.length ? points.length - 1 : i + 1]; + const p3 = points[i + 2 >= points.length ? points.length - 1 : i + 2]; + + for (let t = 0; t <= 1; t += 1 / segments) { + const t2 = t * t; + const t3 = t2 * t; + + const x = + 0.5 * + (2 * p1[0] + + (-p0[0] + p2[0]) * t + + (2 * p0[0] - 5 * p1[0] + 4 * p2[0] - p3[0]) * t2 + + (-p0[0] + 3 * p1[0] - 3 * p2[0] + p3[0]) * t3); + + const y = + 0.5 * + (2 * p1[1] + + (-p0[1] + p2[1]) * t + + (2 * p0[1] - 5 * p1[1] + 4 * p2[1] - p3[1]) * t2 + + (-p0[1] + 3 * p1[1] - 3 * p2[1] + p3[1]) * t3); + + ctx.lineTo(x, y); + } + } +} diff --git a/packages/math/src/curve.ts b/packages/math/src/curve.ts index a79fb43a1..5404619c7 100644 --- a/packages/math/src/curve.ts +++ b/packages/math/src/curve.ts @@ -3,6 +3,8 @@ import type { Bounds } from "@excalidraw/element/bounds"; import { isPoint, pointDistance, pointFrom } from "./point"; import { rectangle, rectangleIntersectLineSegment } from "./rectangle"; +import { vector } from "./vector"; + import type { Curve, GlobalPoint, LineSegment, LocalPoint } from "./types"; /** @@ -82,7 +84,7 @@ function solve( return [t0, s0]; } -const bezierEquation = ( +export const bezierEquation = ( c: Curve, t: number, ) => @@ -274,6 +276,26 @@ export function isCurve

( ); } +export function curveTangent( + [p0, p1, p2, p3]: Curve, + t: number, +) { + return vector( + -3 * (1 - t) * (1 - t) * p0[0] + + 3 * (1 - t) * (1 - t) * p1[0] - + 6 * t * (1 - t) * p1[0] - + 3 * t * t * p2[0] + + 6 * t * (1 - t) * p2[0] + + 3 * t * t * p3[0], + -3 * (1 - t) * (1 - t) * p0[1] + + 3 * (1 - t) * (1 - t) * p1[1] - + 6 * t * (1 - t) * p1[1] - + 3 * t * t * p2[1] + + 6 * t * (1 - t) * p2[1] + + 3 * t * t * p3[1], + ); +} + function curveBounds( c: Curve, ): Bounds { diff --git a/packages/math/src/vector.ts b/packages/math/src/vector.ts index 246722067..12682fcd9 100644 --- a/packages/math/src/vector.ts +++ b/packages/math/src/vector.ts @@ -143,3 +143,8 @@ export const vectorNormalize = (v: Vector): Vector => { return vector(v[0] / m, v[1] / m); }; + +/** + * Calculate the right-hand normal of the vector. + */ +export const vectorNormal = (v: Vector): Vector => vector(v[1], -v[0]); From 769c47af86d0a83e57d722f18972628470785f19 Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Thu, 24 Apr 2025 20:09:29 +0200 Subject: [PATCH 18/28] Restore test Signed-off-by: Mark Tolmacs --- packages/element/tests/elbowArrow.test.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/element/tests/elbowArrow.test.tsx b/packages/element/tests/elbowArrow.test.tsx index 9e71e47ce..b5592a108 100644 --- a/packages/element/tests/elbowArrow.test.tsx +++ b/packages/element/tests/elbowArrow.test.tsx @@ -78,9 +78,9 @@ describe("elbow arrow segment move", () => { expect(arrow.points).toCloselyEqualPoints([ [0, 0], - [115, 0], - [115, 199.9], - [195, 199.9], + [110, 0], + [110, 200], + [190, 200], ]); mouse.reset(); From 043c95fcc04e8752c3ee1994c7662214d8c09239 Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Thu, 24 Apr 2025 21:11:55 +0200 Subject: [PATCH 19/28] Diamond highligh for sharp corners Signed-off-by: Mark Tolmacs --- .../excalidraw/renderer/interactiveScene.ts | 198 ++++++++---------- 1 file changed, 93 insertions(+), 105 deletions(-) diff --git a/packages/excalidraw/renderer/interactiveScene.ts b/packages/excalidraw/renderer/interactiveScene.ts index 0c98fda21..4a084e231 100644 --- a/packages/excalidraw/renderer/interactiveScene.ts +++ b/packages/excalidraw/renderer/interactiveScene.ts @@ -207,65 +207,59 @@ const strokeDiamondWithRotation = ( context.save(); context.translate(x, y); context.rotate(element.angle); - + padding = 45; { context.beginPath(); const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] = getDiamondPoints(element); - if (element.roundness) { - const verticalRadius = getCornerRadius(Math.abs(topX - leftX), element); - const horizontalRadius = getCornerRadius( - Math.abs(rightY - topY), - element, - ); - const topApprox = offsetBezier( - pointFrom(topX - verticalRadius, topY + horizontalRadius), - pointFrom(topX, topY), - pointFrom(topX, topY), - pointFrom(topX + verticalRadius, topY + horizontalRadius), - padding, - ); - const rightApprox = offsetBezier( - pointFrom(rightX - verticalRadius, rightY - horizontalRadius), - pointFrom(rightX, rightY), - pointFrom(rightX, rightY), - pointFrom(rightX - verticalRadius, rightY + horizontalRadius), - padding, - ); - const bottomApprox = offsetBezier( - pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius), - pointFrom(bottomX, bottomY), - pointFrom(bottomX, bottomY), - pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius), - padding, - ); - const leftApprox = offsetBezier( - pointFrom(leftX + verticalRadius, leftY + horizontalRadius), - pointFrom(leftX, leftY), - pointFrom(leftX, leftY), - pointFrom(leftX + verticalRadius, leftY - horizontalRadius), - padding, - ); + const verticalRadius = element.roundness + ? getCornerRadius(Math.abs(topX - leftX), element) + : 1; + const horizontalRadius = element.roundness + ? getCornerRadius(Math.abs(rightY - topY), element) + : 1; + const topApprox = offsetBezier( + pointFrom(topX - verticalRadius, topY + horizontalRadius), + pointFrom(topX, topY), + pointFrom(topX, topY), + pointFrom(topX + verticalRadius, topY + horizontalRadius), + padding, + ); + const rightApprox = offsetBezier( + pointFrom(rightX - verticalRadius, rightY - horizontalRadius), + pointFrom(rightX, rightY), + pointFrom(rightX, rightY), + pointFrom(rightX - verticalRadius, rightY + horizontalRadius), + padding, + ); + const bottomApprox = offsetBezier( + pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius), + pointFrom(bottomX, bottomY), + pointFrom(bottomX, bottomY), + pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius), + padding, + ); + const leftApprox = offsetBezier( + pointFrom(leftX + verticalRadius, leftY + horizontalRadius), + pointFrom(leftX, leftY), + pointFrom(leftX, leftY), + pointFrom(leftX + verticalRadius, leftY - horizontalRadius), + padding, + ); - context.moveTo( - topApprox[topApprox.length - 1][0], - topApprox[topApprox.length - 1][1], - ); - context.lineTo(rightApprox[0][0], rightApprox[0][1]); - drawCatmullRom(context, rightApprox); - context.lineTo(bottomApprox[0][0], bottomApprox[0][1]); - drawCatmullRom(context, bottomApprox); - context.lineTo(leftApprox[0][0], leftApprox[0][1]); - drawCatmullRom(context, leftApprox); - context.lineTo(topApprox[0][0], topApprox[0][1]); - drawCatmullRom(context, topApprox); - } else { - context.moveTo(topX, topY - padding); - context.lineTo(rightX + padding, rightY); - context.lineTo(bottomX, bottomY + padding); - context.lineTo(leftX - padding, leftY); - } + context.moveTo( + topApprox[topApprox.length - 1][0], + topApprox[topApprox.length - 1][1], + ); + context.lineTo(rightApprox[0][0], rightApprox[0][1]); + drawCatmullRom(context, rightApprox); + context.lineTo(bottomApprox[0][0], bottomApprox[0][1]); + drawCatmullRom(context, bottomApprox); + context.lineTo(leftApprox[0][0], leftApprox[0][1]); + drawCatmullRom(context, leftApprox); + context.lineTo(topApprox[0][0], topApprox[0][1]); + drawCatmullRom(context, topApprox); } // Counter-clockwise for the cutout in the middle. We need to have an "inverse @@ -274,59 +268,53 @@ const strokeDiamondWithRotation = ( { const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] = getDiamondPoints(element); - if (element.roundness) { - const verticalRadius = getCornerRadius(Math.abs(topX - leftX), element); - const horizontalRadius = getCornerRadius( - Math.abs(rightY - topY), - element, - ); - const topApprox = offsetBezier( - pointFrom(topX + verticalRadius, topY + horizontalRadius), - pointFrom(topX, topY), - pointFrom(topX, topY), - pointFrom(topX - verticalRadius, topY + horizontalRadius), - -5, - ); - const rightApprox = offsetBezier( - pointFrom(rightX - verticalRadius, rightY + horizontalRadius), - pointFrom(rightX, rightY), - pointFrom(rightX, rightY), - pointFrom(rightX - verticalRadius, rightY - horizontalRadius), - -5, - ); - const bottomApprox = offsetBezier( - pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius), - pointFrom(bottomX, bottomY), - pointFrom(bottomX, bottomY), - pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius), - -5, - ); - const leftApprox = offsetBezier( - pointFrom(leftX + verticalRadius, leftY - horizontalRadius), - pointFrom(leftX, leftY), - pointFrom(leftX, leftY), - pointFrom(leftX + verticalRadius, leftY + horizontalRadius), - -5, - ); + const verticalRadius = element.roundness + ? getCornerRadius(Math.abs(topX - leftX), element) + : 1; + const horizontalRadius = element.roundness + ? getCornerRadius(Math.abs(rightY - topY), element) + : 1; + const topApprox = offsetBezier( + pointFrom(topX + verticalRadius, topY + horizontalRadius), + pointFrom(topX, topY), + pointFrom(topX, topY), + pointFrom(topX - verticalRadius, topY + horizontalRadius), + -5, + ); + const rightApprox = offsetBezier( + pointFrom(rightX - verticalRadius, rightY + horizontalRadius), + pointFrom(rightX, rightY), + pointFrom(rightX, rightY), + pointFrom(rightX - verticalRadius, rightY - horizontalRadius), + -5, + ); + const bottomApprox = offsetBezier( + pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius), + pointFrom(bottomX, bottomY), + pointFrom(bottomX, bottomY), + pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius), + -5, + ); + const leftApprox = offsetBezier( + pointFrom(leftX + verticalRadius, leftY - horizontalRadius), + pointFrom(leftX, leftY), + pointFrom(leftX, leftY), + pointFrom(leftX + verticalRadius, leftY + horizontalRadius), + -5, + ); - context.moveTo( - topApprox[topApprox.length - 1][0], - topApprox[topApprox.length - 1][1], - ); - context.lineTo(leftApprox[0][0], leftApprox[0][1]); - drawCatmullRom(context, leftApprox); - context.lineTo(bottomApprox[0][0], bottomApprox[0][1]); - drawCatmullRom(context, bottomApprox); - context.lineTo(rightApprox[0][0], rightApprox[0][1]); - drawCatmullRom(context, rightApprox); - context.lineTo(topApprox[0][0], topApprox[0][1]); - drawCatmullRom(context, topApprox); - } else { - context.moveTo(topX, topY - 5); - context.lineTo(leftX + 5, leftY); - context.lineTo(bottomX, bottomY + 5); - context.lineTo(rightX - 5, rightY); - } + context.moveTo( + topApprox[topApprox.length - 1][0], + topApprox[topApprox.length - 1][1], + ); + context.lineTo(leftApprox[0][0], leftApprox[0][1]); + drawCatmullRom(context, leftApprox); + context.lineTo(bottomApprox[0][0], bottomApprox[0][1]); + drawCatmullRom(context, bottomApprox); + context.lineTo(rightApprox[0][0], rightApprox[0][1]); + drawCatmullRom(context, rightApprox); + context.lineTo(topApprox[0][0], topApprox[0][1]); + drawCatmullRom(context, topApprox); } context.closePath(); context.fill(); From 7d2253b75f51b26296e75ce812260b9190dde043 Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Thu, 24 Apr 2025 21:15:11 +0200 Subject: [PATCH 20/28] Fix corners Signed-off-by: Mark Tolmacs --- packages/excalidraw/renderer/interactiveScene.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/excalidraw/renderer/interactiveScene.ts b/packages/excalidraw/renderer/interactiveScene.ts index 4a084e231..419a315f3 100644 --- a/packages/excalidraw/renderer/interactiveScene.ts +++ b/packages/excalidraw/renderer/interactiveScene.ts @@ -207,7 +207,7 @@ const strokeDiamondWithRotation = ( context.save(); context.translate(x, y); context.rotate(element.angle); - padding = 45; + { context.beginPath(); @@ -215,10 +215,10 @@ const strokeDiamondWithRotation = ( getDiamondPoints(element); const verticalRadius = element.roundness ? getCornerRadius(Math.abs(topX - leftX), element) - : 1; + : (topX - leftX) * 0.01; const horizontalRadius = element.roundness ? getCornerRadius(Math.abs(rightY - topY), element) - : 1; + : (rightY - topY) * 0.01; const topApprox = offsetBezier( pointFrom(topX - verticalRadius, topY + horizontalRadius), pointFrom(topX, topY), @@ -270,10 +270,10 @@ const strokeDiamondWithRotation = ( getDiamondPoints(element); const verticalRadius = element.roundness ? getCornerRadius(Math.abs(topX - leftX), element) - : 1; + : (topX - leftX) * 0.01; const horizontalRadius = element.roundness ? getCornerRadius(Math.abs(rightY - topY), element) - : 1; + : (rightY - topY) * 0.01; const topApprox = offsetBezier( pointFrom(topX + verticalRadius, topY + horizontalRadius), pointFrom(topX, topY), From 1f6cb8d576af3f625edede246fdab8d9b53e39b9 Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Thu, 24 Apr 2025 21:26:56 +0200 Subject: [PATCH 21/28] More fix tests Signed-off-by: Mark Tolmacs --- packages/element/tests/elbowArrow.test.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/element/tests/elbowArrow.test.tsx b/packages/element/tests/elbowArrow.test.tsx index b5592a108..25f64072e 100644 --- a/packages/element/tests/elbowArrow.test.tsx +++ b/packages/element/tests/elbowArrow.test.tsx @@ -89,9 +89,9 @@ describe("elbow arrow segment move", () => { expect(arrow.points).toCloselyEqualPoints([ [0, 0], - [115, 0], - [115, 199.9], - [195, 199.9], + [110, 0], + [110, 200], + [190, 200], ]); }); From 086d012b0542e7b5f65556d84e90cee512c4f743 Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Thu, 24 Apr 2025 21:44:07 +0200 Subject: [PATCH 22/28] Fix errorneous corner snap for sharp diamonds Signed-off-by: Mark Tolmacs --- packages/element/src/binding.ts | 7 +++---- packages/excalidraw/renderer/interactiveScene.ts | 14 +++++++------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index 0033bdd1b..2e01b2950 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -108,7 +108,6 @@ export const isBindingEnabled = (appState: AppState): boolean => { export const FIXED_BINDING_DISTANCE = 5; const BINDING_HIGHLIGHT_THICKNESS = 10; -export const BINDING_HIGHLIGHT_OFFSET = 4; const getNonDeletedElements = ( scene: Scene, @@ -1105,7 +1104,7 @@ export const snapToMid = ( if ( element.type === "diamond" - ? nonRotated[0] <= x + width * (element.roundness ? 0.035 : 1) + ? nonRotated[0] <= x + width * (element.roundness ? 0.035 : 0) : nonRotated[0] <= x + width / 2 && nonRotated[1] > center[1] - verticalThrehsold && nonRotated[1] < center[1] + verticalThrehsold @@ -1118,7 +1117,7 @@ export const snapToMid = ( ); } else if ( element.type === "diamond" - ? nonRotated[1] <= y + height * (element.roundness ? 0.035 : 1) + ? nonRotated[1] <= y + height * (element.roundness ? 0.035 : 0) : nonRotated[1] <= y + height / 2 && nonRotated[0] > center[0] - horizontalThrehsold && nonRotated[0] < center[0] + horizontalThrehsold @@ -1528,7 +1527,7 @@ export const maxBindingGap = ( // bigger bindable boundary for bigger elements Math.min(0.25 * smallerDimension, 32), // keep in sync with the zoomed highlight - BINDING_HIGHLIGHT_THICKNESS / zoomValue + BINDING_HIGHLIGHT_OFFSET, + BINDING_HIGHLIGHT_THICKNESS / zoomValue + FIXED_BINDING_DISTANCE, ); }; diff --git a/packages/excalidraw/renderer/interactiveScene.ts b/packages/excalidraw/renderer/interactiveScene.ts index 419a315f3..438823300 100644 --- a/packages/excalidraw/renderer/interactiveScene.ts +++ b/packages/excalidraw/renderer/interactiveScene.ts @@ -17,10 +17,7 @@ import { throttleRAF, } from "@excalidraw/common"; -import { - BINDING_HIGHLIGHT_OFFSET, - maxBindingGap, -} from "@excalidraw/element/binding"; +import { maxBindingGap } from "@excalidraw/element/binding"; import { LinearElementEditor } from "@excalidraw/element/linearElementEditor"; import { getOmitSidesForDevice, @@ -373,9 +370,12 @@ const renderBindingHighlightForBindableElement = ( context.strokeStyle = "rgba(0,0,0,.05)"; context.fillStyle = "rgba(0,0,0,.05)"; // When zooming out, make line width greater for visibility - context.lineWidth = - maxBindingGap(element, element.width, element.height, zoom) - - BINDING_HIGHLIGHT_OFFSET; + context.lineWidth = maxBindingGap( + element, + element.width, + element.height, + zoom, + ); // To ensure the binding highlight doesn't overlap the element itself const padding = maxBindingGap(element, element.width, element.height, zoom); //context.lineWidth / 2 + BINDING_HIGHLIGHT_OFFSET; From 72752636868ba824654d17fb4ebc93f036c490ac Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Fri, 2 May 2025 16:36:15 +0200 Subject: [PATCH 23/28] Rectangle highlight Signed-off-by: Mark Tolmacs --- .../excalidraw/renderer/interactiveScene.ts | 213 ++++++++++++++++-- 1 file changed, 190 insertions(+), 23 deletions(-) diff --git a/packages/excalidraw/renderer/interactiveScene.ts b/packages/excalidraw/renderer/interactiveScene.ts index 438823300..aa811af0f 100644 --- a/packages/excalidraw/renderer/interactiveScene.ts +++ b/packages/excalidraw/renderer/interactiveScene.ts @@ -17,7 +17,10 @@ import { throttleRAF, } from "@excalidraw/common"; -import { maxBindingGap } from "@excalidraw/element/binding"; +import { + FIXED_BINDING_DISTANCE, + maxBindingGap, +} from "@excalidraw/element/binding"; import { LinearElementEditor } from "@excalidraw/element/linearElementEditor"; import { getOmitSidesForDevice, @@ -69,6 +72,7 @@ import type { ExcalidrawFrameLikeElement, ExcalidrawImageElement, ExcalidrawLinearElement, + ExcalidrawRectanguloidElement, ExcalidrawTextElement, GroupId, NonDeleted, @@ -161,6 +165,186 @@ const highlightPoint = ( ); }; +const drawHighlightForRectWithRotation = ( + context: CanvasRenderingContext2D, + element: ExcalidrawRectanguloidElement, + padding: number, +) => { + const [x, y] = pointRotateRads( + pointFrom(element.x, element.y), + elementCenterPoint(element), + element.angle, + ); + + context.save(); + context.translate(x, y); + context.rotate(element.angle); + + const radius = getCornerRadius( + Math.min(element.width, element.height), + element, + ); + + context.beginPath(); + + // { + // const topLeftApprox = offsetBezier( + // pointFrom(0, 0 + radius), + // pointFrom(0, 0), + // pointFrom(0, 0), + // pointFrom(0 + radius, 0), + // padding, + // ); + // const topRightApprox = offsetBezier( + // pointFrom(element.width - radius, 0), + // pointFrom(element.width, 0), + // pointFrom(element.width, 0), + // pointFrom(element.width, radius), + // padding, + // ); + // const bottomRightApprox = offsetBezier( + // pointFrom(element.width, element.height - radius), + // pointFrom(element.width, element.height), + // pointFrom(element.width, element.height), + // pointFrom(element.width - radius, element.height), + // padding, + // ); + // const bottomLeftApprox = offsetBezier( + // pointFrom(radius, element.height), + // pointFrom(0, element.height), + // pointFrom(0, element.height), + // pointFrom(0, element.height - radius), + // padding, + // ); + + // context.moveTo( + // topLeftApprox[topLeftApprox.length - 1][0], + // topLeftApprox[topLeftApprox.length - 1][1], + // ); + // context.lineTo(topRightApprox[0][0], topRightApprox[0][1]); + // drawCatmullRom(context, topRightApprox); + // context.lineTo(bottomRightApprox[0][0], bottomRightApprox[0][1]); + // drawCatmullRom(context, bottomRightApprox); + // context.lineTo(bottomLeftApprox[0][0], bottomLeftApprox[0][1]); + // drawCatmullRom(context, bottomLeftApprox); + // context.lineTo(topLeftApprox[0][0], topLeftApprox[0][1]); + // drawCatmullRom(context, topLeftApprox); + // } + + context.moveTo(-padding + radius, -padding); + context.lineTo(element.width + padding - radius, -padding); + context.quadraticCurveTo( + element.width + padding, + -padding, + element.width + padding, + -padding + radius, + ); + context.lineTo(element.width + padding, element.height + padding - radius); + context.quadraticCurveTo( + element.width + padding, + element.height + padding, + element.width + padding - radius, + element.height + padding, + ); + context.lineTo(-padding + radius, element.height + padding); + context.quadraticCurveTo( + -padding, + element.height + padding, + -padding, + element.height + padding - radius, + ); + context.lineTo(-padding, -padding + radius); + context.quadraticCurveTo(-padding, -padding, -padding + radius, -padding); + + context.moveTo(-FIXED_BINDING_DISTANCE + radius, -FIXED_BINDING_DISTANCE); + context.quadraticCurveTo( + -FIXED_BINDING_DISTANCE, + -FIXED_BINDING_DISTANCE, + -FIXED_BINDING_DISTANCE, + -FIXED_BINDING_DISTANCE + radius, + ); + context.lineTo( + -FIXED_BINDING_DISTANCE, + element.height + FIXED_BINDING_DISTANCE - radius, + ); + context.quadraticCurveTo( + -FIXED_BINDING_DISTANCE, + element.height + FIXED_BINDING_DISTANCE, + -FIXED_BINDING_DISTANCE + radius, + element.height + FIXED_BINDING_DISTANCE, + ); + context.lineTo( + element.width + FIXED_BINDING_DISTANCE - radius, + element.height + FIXED_BINDING_DISTANCE, + ); + context.quadraticCurveTo( + element.width + FIXED_BINDING_DISTANCE, + element.height + FIXED_BINDING_DISTANCE, + element.width + FIXED_BINDING_DISTANCE, + element.height + FIXED_BINDING_DISTANCE - radius, + ); + context.lineTo( + element.width + FIXED_BINDING_DISTANCE, + -FIXED_BINDING_DISTANCE + radius, + ); + context.quadraticCurveTo( + element.width + FIXED_BINDING_DISTANCE, + -FIXED_BINDING_DISTANCE, + element.width + FIXED_BINDING_DISTANCE - radius, + -FIXED_BINDING_DISTANCE, + ); + context.lineTo(-FIXED_BINDING_DISTANCE + radius, -FIXED_BINDING_DISTANCE); + + // { + // const topLeftApprox = offsetBezier( + // pointFrom(0 + radius, 0), + // pointFrom(0, 0), + // pointFrom(0, 0), + // pointFrom(0, 0 + radius), + // -FIXED_BINDING_DISTANCE, + // ); + // const topRightApprox = offsetBezier( + // pointFrom(element.width, radius), + // pointFrom(element.width, 0), + // pointFrom(element.width, 0), + // pointFrom(element.width - radius, 0), + // -FIXED_BINDING_DISTANCE, + // ); + // const bottomRightApprox = offsetBezier( + // pointFrom(element.width - radius, element.height), + // pointFrom(element.width, element.height), + // pointFrom(element.width, element.height), + // pointFrom(element.width, element.height - radius), + // -FIXED_BINDING_DISTANCE, + // ); + // const bottomLeftApprox = offsetBezier( + // pointFrom(0, element.height - radius), + // pointFrom(0, element.height), + // pointFrom(0, element.height), + // pointFrom(radius, element.height), + // -FIXED_BINDING_DISTANCE, + // ); + + // context.moveTo( + // topLeftApprox[topLeftApprox.length - 1][0], + // topLeftApprox[topLeftApprox.length - 1][1], + // ); + // context.lineTo(bottomLeftApprox[0][0], bottomLeftApprox[0][1]); + // drawCatmullRom(context, bottomLeftApprox); + // context.lineTo(bottomRightApprox[0][0], bottomRightApprox[0][1]); + // drawCatmullRom(context, bottomRightApprox); + // context.lineTo(topRightApprox[0][0], topRightApprox[0][1]); + // drawCatmullRom(context, topRightApprox); + // context.lineTo(topLeftApprox[0][0], topLeftApprox[0][1]); + // drawCatmullRom(context, topLeftApprox); + // } + + context.closePath(); + context.fill(); + + context.restore(); +}; + const strokeRectWithRotation = ( context: CanvasRenderingContext2D, x: number, @@ -276,28 +460,28 @@ const strokeDiamondWithRotation = ( pointFrom(topX, topY), pointFrom(topX, topY), pointFrom(topX - verticalRadius, topY + horizontalRadius), - -5, + -FIXED_BINDING_DISTANCE, ); const rightApprox = offsetBezier( pointFrom(rightX - verticalRadius, rightY + horizontalRadius), pointFrom(rightX, rightY), pointFrom(rightX, rightY), pointFrom(rightX - verticalRadius, rightY - horizontalRadius), - -5, + -FIXED_BINDING_DISTANCE, ); const bottomApprox = offsetBezier( pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius), pointFrom(bottomX, bottomY), pointFrom(bottomX, bottomY), pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius), - -5, + -FIXED_BINDING_DISTANCE, ); const leftApprox = offsetBezier( pointFrom(leftX + verticalRadius, leftY - horizontalRadius), pointFrom(leftX, leftY), pointFrom(leftX, leftY), pointFrom(leftX + verticalRadius, leftY + horizontalRadius), - -5, + -FIXED_BINDING_DISTANCE, ); context.moveTo( @@ -378,12 +562,6 @@ const renderBindingHighlightForBindableElement = ( ); // To ensure the binding highlight doesn't overlap the element itself const padding = maxBindingGap(element, element.width, element.height, zoom); - //context.lineWidth / 2 + BINDING_HIGHLIGHT_OFFSET; - - const radius = getCornerRadius( - Math.min(element.width, element.height), - element, - ); switch (element.type) { case "rectangle": @@ -393,18 +571,7 @@ const renderBindingHighlightForBindableElement = ( case "embeddable": case "frame": case "magicframe": - strokeRectWithRotation( - context, - x1 - padding, - y1 - padding, - width + padding * 2, - height + padding * 2, - x1 + width / 2, - y1 + height / 2, - element.angle, - undefined, - radius, - ); + drawHighlightForRectWithRotation(context, element, padding); break; case "diamond": strokeDiamondWithRotation(context, padding, element); From 436a0568fafd78273792f7051bbd3ebd8bd1dc66 Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Fri, 2 May 2025 18:05:32 +0200 Subject: [PATCH 24/28] Precise rectanguloid when rounded --- packages/element/src/bounds.ts | 30 +- .../excalidraw/renderer/interactiveScene.ts | 347 +++++++++--------- 2 files changed, 212 insertions(+), 165 deletions(-) diff --git a/packages/element/src/bounds.ts b/packages/element/src/bounds.ts index 4b27843ac..ff02e285b 100644 --- a/packages/element/src/bounds.ts +++ b/packages/element/src/bounds.ts @@ -21,6 +21,7 @@ import { vectorNormal, vectorScale, pointFromVector, + vector, } from "@excalidraw/math"; import { getCurvePathOps } from "@excalidraw/utils/shape"; @@ -1154,7 +1155,7 @@ export const doBoundsIntersect = ( return minX1 < maxX2 && maxX1 > minX2 && minY1 < maxY2 && maxY1 > minY2; }; -export function offsetBezier( +export function offsetCubicBezier( p0: GlobalPoint, p1: GlobalPoint, p2: GlobalPoint, @@ -1176,3 +1177,30 @@ export function offsetBezier( return offsetPoints; } + +export function offsetQuadraticBezier( + p0: GlobalPoint, + p1: GlobalPoint, + p2: GlobalPoint, + offsetDist: number, + steps = 20, +) { + const offsetPoints = []; + + for (let i = 0; i <= steps; i++) { + const t = i / steps; + const t1 = 1 - t; + const point = pointFrom( + t1 * t1 * p0[0] + 2 * t1 * t * p1[0] + t * t * p2[0], + t1 * t1 * p0[1] + 2 * t1 * t * p1[1] + t * t * p2[1], + ); + const tangentX = 2 * (1 - t) * (p1[0] - p0[0]) + 2 * t * (p2[0] - p1[0]); + const tangentY = 2 * (1 - t) * (p1[1] - p0[1]) + 2 * t * (p2[1] - p1[1]); + const tangent = vectorNormalize(vector(tangentX, tangentY)); + const normal = vectorNormal(tangent); + + offsetPoints.push(pointFromVector(vectorScale(normal, offsetDist), point)); + } + + return offsetPoints; +} diff --git a/packages/excalidraw/renderer/interactiveScene.ts b/packages/excalidraw/renderer/interactiveScene.ts index aa811af0f..2d5eb4fac 100644 --- a/packages/excalidraw/renderer/interactiveScene.ts +++ b/packages/excalidraw/renderer/interactiveScene.ts @@ -51,7 +51,8 @@ import { getCommonBounds, getDiamondPoints, getElementAbsoluteCoords, - offsetBezier, + offsetCubicBezier, + offsetQuadraticBezier, } from "@excalidraw/element/bounds"; import type { @@ -187,157 +188,149 @@ const drawHighlightForRectWithRotation = ( context.beginPath(); - // { - // const topLeftApprox = offsetBezier( - // pointFrom(0, 0 + radius), - // pointFrom(0, 0), - // pointFrom(0, 0), - // pointFrom(0 + radius, 0), - // padding, - // ); - // const topRightApprox = offsetBezier( - // pointFrom(element.width - radius, 0), - // pointFrom(element.width, 0), - // pointFrom(element.width, 0), - // pointFrom(element.width, radius), - // padding, - // ); - // const bottomRightApprox = offsetBezier( - // pointFrom(element.width, element.height - radius), - // pointFrom(element.width, element.height), - // pointFrom(element.width, element.height), - // pointFrom(element.width - radius, element.height), - // padding, - // ); - // const bottomLeftApprox = offsetBezier( - // pointFrom(radius, element.height), - // pointFrom(0, element.height), - // pointFrom(0, element.height), - // pointFrom(0, element.height - radius), - // padding, - // ); + { + const topLeftApprox = offsetQuadraticBezier( + pointFrom(0, 0 + radius), + pointFrom(0, 0), + pointFrom(0 + radius, 0), + padding, + ); + const topRightApprox = offsetQuadraticBezier( + pointFrom(element.width - radius, 0), + pointFrom(element.width, 0), + pointFrom(element.width, radius), + padding, + ); + const bottomRightApprox = offsetQuadraticBezier( + pointFrom(element.width, element.height - radius), + pointFrom(element.width, element.height), + pointFrom(element.width - radius, element.height), + padding, + ); + const bottomLeftApprox = offsetQuadraticBezier( + pointFrom(radius, element.height), + pointFrom(0, element.height), + pointFrom(0, element.height - radius), + padding, + ); - // context.moveTo( - // topLeftApprox[topLeftApprox.length - 1][0], - // topLeftApprox[topLeftApprox.length - 1][1], - // ); - // context.lineTo(topRightApprox[0][0], topRightApprox[0][1]); - // drawCatmullRom(context, topRightApprox); - // context.lineTo(bottomRightApprox[0][0], bottomRightApprox[0][1]); - // drawCatmullRom(context, bottomRightApprox); - // context.lineTo(bottomLeftApprox[0][0], bottomLeftApprox[0][1]); - // drawCatmullRom(context, bottomLeftApprox); - // context.lineTo(topLeftApprox[0][0], topLeftApprox[0][1]); - // drawCatmullRom(context, topLeftApprox); - // } + context.moveTo( + topLeftApprox[topLeftApprox.length - 1][0], + topLeftApprox[topLeftApprox.length - 1][1], + ); + context.lineTo(topRightApprox[0][0], topRightApprox[0][1]); + drawCatmullRomQuadraticApprox(context, topRightApprox); + context.lineTo(bottomRightApprox[0][0], bottomRightApprox[0][1]); + drawCatmullRomQuadraticApprox(context, bottomRightApprox); + context.lineTo(bottomLeftApprox[0][0], bottomLeftApprox[0][1]); + drawCatmullRomQuadraticApprox(context, bottomLeftApprox); + context.lineTo(topLeftApprox[0][0], topLeftApprox[0][1]); + drawCatmullRomQuadraticApprox(context, topLeftApprox); + } - context.moveTo(-padding + radius, -padding); - context.lineTo(element.width + padding - radius, -padding); - context.quadraticCurveTo( - element.width + padding, - -padding, - element.width + padding, - -padding + radius, - ); - context.lineTo(element.width + padding, element.height + padding - radius); - context.quadraticCurveTo( - element.width + padding, - element.height + padding, - element.width + padding - radius, - element.height + padding, - ); - context.lineTo(-padding + radius, element.height + padding); - context.quadraticCurveTo( - -padding, - element.height + padding, - -padding, - element.height + padding - radius, - ); - context.lineTo(-padding, -padding + radius); - context.quadraticCurveTo(-padding, -padding, -padding + radius, -padding); + // context.moveTo(-padding + radius, -padding); + // context.lineTo(element.width + padding - radius, -padding); + // context.quadraticCurveTo( + // element.width + padding, + // -padding, + // element.width + padding, + // -padding + radius, + // ); + // context.lineTo(element.width + padding, element.height + padding - radius); + // context.quadraticCurveTo( + // element.width + padding, + // element.height + padding, + // element.width + padding - radius, + // element.height + padding, + // ); + // context.lineTo(-padding + radius, element.height + padding); + // context.quadraticCurveTo( + // -padding, + // element.height + padding, + // -padding, + // element.height + padding - radius, + // ); + // context.lineTo(-padding, -padding + radius); + // context.quadraticCurveTo(-padding, -padding, -padding + radius, -padding); - context.moveTo(-FIXED_BINDING_DISTANCE + radius, -FIXED_BINDING_DISTANCE); - context.quadraticCurveTo( - -FIXED_BINDING_DISTANCE, - -FIXED_BINDING_DISTANCE, - -FIXED_BINDING_DISTANCE, - -FIXED_BINDING_DISTANCE + radius, - ); - context.lineTo( - -FIXED_BINDING_DISTANCE, - element.height + FIXED_BINDING_DISTANCE - radius, - ); - context.quadraticCurveTo( - -FIXED_BINDING_DISTANCE, - element.height + FIXED_BINDING_DISTANCE, - -FIXED_BINDING_DISTANCE + radius, - element.height + FIXED_BINDING_DISTANCE, - ); - context.lineTo( - element.width + FIXED_BINDING_DISTANCE - radius, - element.height + FIXED_BINDING_DISTANCE, - ); - context.quadraticCurveTo( - element.width + FIXED_BINDING_DISTANCE, - element.height + FIXED_BINDING_DISTANCE, - element.width + FIXED_BINDING_DISTANCE, - element.height + FIXED_BINDING_DISTANCE - radius, - ); - context.lineTo( - element.width + FIXED_BINDING_DISTANCE, - -FIXED_BINDING_DISTANCE + radius, - ); - context.quadraticCurveTo( - element.width + FIXED_BINDING_DISTANCE, - -FIXED_BINDING_DISTANCE, - element.width + FIXED_BINDING_DISTANCE - radius, - -FIXED_BINDING_DISTANCE, - ); - context.lineTo(-FIXED_BINDING_DISTANCE + radius, -FIXED_BINDING_DISTANCE); + // context.moveTo(-FIXED_BINDING_DISTANCE + radius, -FIXED_BINDING_DISTANCE); + // context.quadraticCurveTo( + // -FIXED_BINDING_DISTANCE, + // -FIXED_BINDING_DISTANCE, + // -FIXED_BINDING_DISTANCE, + // -FIXED_BINDING_DISTANCE + radius, + // ); + // context.lineTo( + // -FIXED_BINDING_DISTANCE, + // element.height + FIXED_BINDING_DISTANCE - radius, + // ); + // context.quadraticCurveTo( + // -FIXED_BINDING_DISTANCE, + // element.height + FIXED_BINDING_DISTANCE, + // -FIXED_BINDING_DISTANCE + radius, + // element.height + FIXED_BINDING_DISTANCE, + // ); + // context.lineTo( + // element.width + FIXED_BINDING_DISTANCE - radius, + // element.height + FIXED_BINDING_DISTANCE, + // ); + // context.quadraticCurveTo( + // element.width + FIXED_BINDING_DISTANCE, + // element.height + FIXED_BINDING_DISTANCE, + // element.width + FIXED_BINDING_DISTANCE, + // element.height + FIXED_BINDING_DISTANCE - radius, + // ); + // context.lineTo( + // element.width + FIXED_BINDING_DISTANCE, + // -FIXED_BINDING_DISTANCE + radius, + // ); + // context.quadraticCurveTo( + // element.width + FIXED_BINDING_DISTANCE, + // -FIXED_BINDING_DISTANCE, + // element.width + FIXED_BINDING_DISTANCE - radius, + // -FIXED_BINDING_DISTANCE, + // ); + // context.lineTo(-FIXED_BINDING_DISTANCE + radius, -FIXED_BINDING_DISTANCE); - // { - // const topLeftApprox = offsetBezier( - // pointFrom(0 + radius, 0), - // pointFrom(0, 0), - // pointFrom(0, 0), - // pointFrom(0, 0 + radius), - // -FIXED_BINDING_DISTANCE, - // ); - // const topRightApprox = offsetBezier( - // pointFrom(element.width, radius), - // pointFrom(element.width, 0), - // pointFrom(element.width, 0), - // pointFrom(element.width - radius, 0), - // -FIXED_BINDING_DISTANCE, - // ); - // const bottomRightApprox = offsetBezier( - // pointFrom(element.width - radius, element.height), - // pointFrom(element.width, element.height), - // pointFrom(element.width, element.height), - // pointFrom(element.width, element.height - radius), - // -FIXED_BINDING_DISTANCE, - // ); - // const bottomLeftApprox = offsetBezier( - // pointFrom(0, element.height - radius), - // pointFrom(0, element.height), - // pointFrom(0, element.height), - // pointFrom(radius, element.height), - // -FIXED_BINDING_DISTANCE, - // ); + { + const topLeftApprox = offsetQuadraticBezier( + pointFrom(0 + radius, 0), + pointFrom(0, 0), + pointFrom(0, 0 + radius), + -FIXED_BINDING_DISTANCE, + ); + const topRightApprox = offsetQuadraticBezier( + pointFrom(element.width, radius), + pointFrom(element.width, 0), + pointFrom(element.width - radius, 0), + -FIXED_BINDING_DISTANCE, + ); + const bottomRightApprox = offsetQuadraticBezier( + pointFrom(element.width - radius, element.height), + pointFrom(element.width, element.height), + pointFrom(element.width, element.height - radius), + -FIXED_BINDING_DISTANCE, + ); + const bottomLeftApprox = offsetQuadraticBezier( + pointFrom(0, element.height - radius), + pointFrom(0, element.height), + pointFrom(radius, element.height), + -FIXED_BINDING_DISTANCE, + ); - // context.moveTo( - // topLeftApprox[topLeftApprox.length - 1][0], - // topLeftApprox[topLeftApprox.length - 1][1], - // ); - // context.lineTo(bottomLeftApprox[0][0], bottomLeftApprox[0][1]); - // drawCatmullRom(context, bottomLeftApprox); - // context.lineTo(bottomRightApprox[0][0], bottomRightApprox[0][1]); - // drawCatmullRom(context, bottomRightApprox); - // context.lineTo(topRightApprox[0][0], topRightApprox[0][1]); - // drawCatmullRom(context, topRightApprox); - // context.lineTo(topLeftApprox[0][0], topLeftApprox[0][1]); - // drawCatmullRom(context, topLeftApprox); - // } + context.moveTo( + topLeftApprox[topLeftApprox.length - 1][0], + topLeftApprox[topLeftApprox.length - 1][1], + ); + context.lineTo(bottomLeftApprox[0][0], bottomLeftApprox[0][1]); + drawCatmullRomQuadraticApprox(context, bottomLeftApprox); + context.lineTo(bottomRightApprox[0][0], bottomRightApprox[0][1]); + drawCatmullRomQuadraticApprox(context, bottomRightApprox); + context.lineTo(topRightApprox[0][0], topRightApprox[0][1]); + drawCatmullRomQuadraticApprox(context, topRightApprox); + context.lineTo(topLeftApprox[0][0], topLeftApprox[0][1]); + drawCatmullRomQuadraticApprox(context, topLeftApprox); + } context.closePath(); context.fill(); @@ -400,28 +393,28 @@ const strokeDiamondWithRotation = ( const horizontalRadius = element.roundness ? getCornerRadius(Math.abs(rightY - topY), element) : (rightY - topY) * 0.01; - const topApprox = offsetBezier( + const topApprox = offsetCubicBezier( pointFrom(topX - verticalRadius, topY + horizontalRadius), pointFrom(topX, topY), pointFrom(topX, topY), pointFrom(topX + verticalRadius, topY + horizontalRadius), padding, ); - const rightApprox = offsetBezier( + const rightApprox = offsetCubicBezier( pointFrom(rightX - verticalRadius, rightY - horizontalRadius), pointFrom(rightX, rightY), pointFrom(rightX, rightY), pointFrom(rightX - verticalRadius, rightY + horizontalRadius), padding, ); - const bottomApprox = offsetBezier( + const bottomApprox = offsetCubicBezier( pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius), pointFrom(bottomX, bottomY), pointFrom(bottomX, bottomY), pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius), padding, ); - const leftApprox = offsetBezier( + const leftApprox = offsetCubicBezier( pointFrom(leftX + verticalRadius, leftY + horizontalRadius), pointFrom(leftX, leftY), pointFrom(leftX, leftY), @@ -434,13 +427,13 @@ const strokeDiamondWithRotation = ( topApprox[topApprox.length - 1][1], ); context.lineTo(rightApprox[0][0], rightApprox[0][1]); - drawCatmullRom(context, rightApprox); + drawCatmullRomCubicApprox(context, rightApprox); context.lineTo(bottomApprox[0][0], bottomApprox[0][1]); - drawCatmullRom(context, bottomApprox); + drawCatmullRomCubicApprox(context, bottomApprox); context.lineTo(leftApprox[0][0], leftApprox[0][1]); - drawCatmullRom(context, leftApprox); + drawCatmullRomCubicApprox(context, leftApprox); context.lineTo(topApprox[0][0], topApprox[0][1]); - drawCatmullRom(context, topApprox); + drawCatmullRomCubicApprox(context, topApprox); } // Counter-clockwise for the cutout in the middle. We need to have an "inverse @@ -455,28 +448,28 @@ const strokeDiamondWithRotation = ( const horizontalRadius = element.roundness ? getCornerRadius(Math.abs(rightY - topY), element) : (rightY - topY) * 0.01; - const topApprox = offsetBezier( + const topApprox = offsetCubicBezier( pointFrom(topX + verticalRadius, topY + horizontalRadius), pointFrom(topX, topY), pointFrom(topX, topY), pointFrom(topX - verticalRadius, topY + horizontalRadius), -FIXED_BINDING_DISTANCE, ); - const rightApprox = offsetBezier( + const rightApprox = offsetCubicBezier( pointFrom(rightX - verticalRadius, rightY + horizontalRadius), pointFrom(rightX, rightY), pointFrom(rightX, rightY), pointFrom(rightX - verticalRadius, rightY - horizontalRadius), -FIXED_BINDING_DISTANCE, ); - const bottomApprox = offsetBezier( + const bottomApprox = offsetCubicBezier( pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius), pointFrom(bottomX, bottomY), pointFrom(bottomX, bottomY), pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius), -FIXED_BINDING_DISTANCE, ); - const leftApprox = offsetBezier( + const leftApprox = offsetCubicBezier( pointFrom(leftX + verticalRadius, leftY - horizontalRadius), pointFrom(leftX, leftY), pointFrom(leftX, leftY), @@ -489,13 +482,13 @@ const strokeDiamondWithRotation = ( topApprox[topApprox.length - 1][1], ); context.lineTo(leftApprox[0][0], leftApprox[0][1]); - drawCatmullRom(context, leftApprox); + drawCatmullRomCubicApprox(context, leftApprox); context.lineTo(bottomApprox[0][0], bottomApprox[0][1]); - drawCatmullRom(context, bottomApprox); + drawCatmullRomCubicApprox(context, bottomApprox); context.lineTo(rightApprox[0][0], rightApprox[0][1]); - drawCatmullRom(context, rightApprox); + drawCatmullRomCubicApprox(context, rightApprox); context.lineTo(topApprox[0][0], topApprox[0][1]); - drawCatmullRom(context, topApprox); + drawCatmullRomCubicApprox(context, topApprox); } context.closePath(); context.fill(); @@ -1512,7 +1505,33 @@ export const renderInteractiveScene = < return ret as T extends true ? void : ReturnType; }; -function drawCatmullRom( +function drawCatmullRomQuadraticApprox( + ctx: CanvasRenderingContext2D, + points: GlobalPoint[], + segments = 20, +) { + ctx.lineTo(points[0][0], points[0][1]); + + for (let i = 0; i < points.length - 1; i++) { + const p0 = points[i - 1 < 0 ? 0 : i - 1]; + const p1 = points[i]; + const p2 = points[i + 1 >= points.length ? points.length - 1 : i + 1]; + + for (let t = 0; t <= 1; t += 1 / segments) { + const t2 = t * t; + + const x = + (1 - t) * (1 - t) * p0[0] + 2 * (1 - t) * t * p1[0] + t2 * p2[0]; + + const y = + (1 - t) * (1 - t) * p0[1] + 2 * (1 - t) * t * p1[1] + t2 * p2[1]; + + ctx.lineTo(x, y); + } + } +} + +function drawCatmullRomCubicApprox( ctx: CanvasRenderingContext2D, points: GlobalPoint[], segments = 20, From cdb7349111be7d828c7973017b4f06c5480aeede Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Fri, 2 May 2025 18:09:06 +0200 Subject: [PATCH 25/28] Remove commented out code and make it work for sharp rectanguloid --- .../excalidraw/renderer/interactiveScene.ts | 73 ++----------------- 1 file changed, 6 insertions(+), 67 deletions(-) diff --git a/packages/excalidraw/renderer/interactiveScene.ts b/packages/excalidraw/renderer/interactiveScene.ts index 2d5eb4fac..36e93fd4c 100644 --- a/packages/excalidraw/renderer/interactiveScene.ts +++ b/packages/excalidraw/renderer/interactiveScene.ts @@ -181,10 +181,13 @@ const drawHighlightForRectWithRotation = ( context.translate(x, y); context.rotate(element.angle); - const radius = getCornerRadius( + let radius = getCornerRadius( Math.min(element.width, element.height), element, ); + if (radius === 0) { + radius = 0.01; + } context.beginPath(); @@ -228,70 +231,6 @@ const drawHighlightForRectWithRotation = ( drawCatmullRomQuadraticApprox(context, topLeftApprox); } - // context.moveTo(-padding + radius, -padding); - // context.lineTo(element.width + padding - radius, -padding); - // context.quadraticCurveTo( - // element.width + padding, - // -padding, - // element.width + padding, - // -padding + radius, - // ); - // context.lineTo(element.width + padding, element.height + padding - radius); - // context.quadraticCurveTo( - // element.width + padding, - // element.height + padding, - // element.width + padding - radius, - // element.height + padding, - // ); - // context.lineTo(-padding + radius, element.height + padding); - // context.quadraticCurveTo( - // -padding, - // element.height + padding, - // -padding, - // element.height + padding - radius, - // ); - // context.lineTo(-padding, -padding + radius); - // context.quadraticCurveTo(-padding, -padding, -padding + radius, -padding); - - // context.moveTo(-FIXED_BINDING_DISTANCE + radius, -FIXED_BINDING_DISTANCE); - // context.quadraticCurveTo( - // -FIXED_BINDING_DISTANCE, - // -FIXED_BINDING_DISTANCE, - // -FIXED_BINDING_DISTANCE, - // -FIXED_BINDING_DISTANCE + radius, - // ); - // context.lineTo( - // -FIXED_BINDING_DISTANCE, - // element.height + FIXED_BINDING_DISTANCE - radius, - // ); - // context.quadraticCurveTo( - // -FIXED_BINDING_DISTANCE, - // element.height + FIXED_BINDING_DISTANCE, - // -FIXED_BINDING_DISTANCE + radius, - // element.height + FIXED_BINDING_DISTANCE, - // ); - // context.lineTo( - // element.width + FIXED_BINDING_DISTANCE - radius, - // element.height + FIXED_BINDING_DISTANCE, - // ); - // context.quadraticCurveTo( - // element.width + FIXED_BINDING_DISTANCE, - // element.height + FIXED_BINDING_DISTANCE, - // element.width + FIXED_BINDING_DISTANCE, - // element.height + FIXED_BINDING_DISTANCE - radius, - // ); - // context.lineTo( - // element.width + FIXED_BINDING_DISTANCE, - // -FIXED_BINDING_DISTANCE + radius, - // ); - // context.quadraticCurveTo( - // element.width + FIXED_BINDING_DISTANCE, - // -FIXED_BINDING_DISTANCE, - // element.width + FIXED_BINDING_DISTANCE - radius, - // -FIXED_BINDING_DISTANCE, - // ); - // context.lineTo(-FIXED_BINDING_DISTANCE + radius, -FIXED_BINDING_DISTANCE); - { const topLeftApprox = offsetQuadraticBezier( pointFrom(0 + radius, 0), @@ -368,7 +307,7 @@ const strokeRectWithRotation = ( context.restore(); }; -const strokeDiamondWithRotation = ( +const drawHighlightForDiamondWithRotation = ( context: CanvasRenderingContext2D, padding: number, element: ExcalidrawDiamondElement, @@ -567,7 +506,7 @@ const renderBindingHighlightForBindableElement = ( drawHighlightForRectWithRotation(context, element, padding); break; case "diamond": - strokeDiamondWithRotation(context, padding, element); + drawHighlightForDiamondWithRotation(context, padding, element); break; case "ellipse": strokeEllipseWithRotation( From 2d6219f3994c5fda141170d3363f7cac1b2ef795 Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Fri, 2 May 2025 18:19:10 +0200 Subject: [PATCH 26/28] Make ellipses binding highlights precise as well --- .../excalidraw/renderer/interactiveScene.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/excalidraw/renderer/interactiveScene.ts b/packages/excalidraw/renderer/interactiveScene.ts index 36e93fd4c..8c5f58ea1 100644 --- a/packages/excalidraw/renderer/interactiveScene.ts +++ b/packages/excalidraw/renderer/interactiveScene.ts @@ -231,6 +231,9 @@ const drawHighlightForRectWithRotation = ( drawCatmullRomQuadraticApprox(context, topLeftApprox); } + // Counter-clockwise for the cutout in the middle. We need to have an "inverse + // mask" on a filled shape for the diamond highlight, because stroking creates + // sharp inset edges on line joins < 90 degrees. { const topLeftApprox = offsetQuadraticBezier( pointFrom(0 + radius, 0), @@ -485,13 +488,7 @@ const renderBindingHighlightForBindableElement = ( context.strokeStyle = "rgba(0,0,0,.05)"; context.fillStyle = "rgba(0,0,0,.05)"; - // When zooming out, make line width greater for visibility - context.lineWidth = maxBindingGap( - element, - element.width, - element.height, - zoom, - ); + // To ensure the binding highlight doesn't overlap the element itself const padding = maxBindingGap(element, element.width, element.height, zoom); @@ -509,10 +506,14 @@ const renderBindingHighlightForBindableElement = ( drawHighlightForDiamondWithRotation(context, padding, element); break; case "ellipse": + context.lineWidth = + maxBindingGap(element, element.width, element.height, zoom) - + FIXED_BINDING_DISTANCE; + strokeEllipseWithRotation( context, - width + padding * 2, - height + padding * 2, + width + padding + FIXED_BINDING_DISTANCE, + height + padding + FIXED_BINDING_DISTANCE, x1 + width / 2, y1 + height / 2, element.angle, From 473449e9995caf8a415f78187627e41f48a7bb5d Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Fri, 2 May 2025 19:10:28 +0200 Subject: [PATCH 27/28] Move binding highlight precision into separate PR --- packages/element/src/bounds.ts | 58 --- .../excalidraw/renderer/interactiveScene.ts | 370 +++--------------- packages/math/src/curve.ts | 24 +- packages/math/src/vector.ts | 5 - 4 files changed, 53 insertions(+), 404 deletions(-) diff --git a/packages/element/src/bounds.ts b/packages/element/src/bounds.ts index ff02e285b..d0c071f2c 100644 --- a/packages/element/src/bounds.ts +++ b/packages/element/src/bounds.ts @@ -14,14 +14,6 @@ import { pointDistance, pointFromArray, pointRotateRads, - bezierEquation, - curve, - curveTangent, - vectorNormalize, - vectorNormal, - vectorScale, - pointFromVector, - vector, } from "@excalidraw/math"; import { getCurvePathOps } from "@excalidraw/utils/shape"; @@ -1154,53 +1146,3 @@ export const doBoundsIntersect = ( return minX1 < maxX2 && maxX1 > minX2 && minY1 < maxY2 && maxY1 > minY2; }; - -export function offsetCubicBezier( - p0: GlobalPoint, - p1: GlobalPoint, - p2: GlobalPoint, - p3: GlobalPoint, - offsetDist: number, - steps = 20, -) { - const offsetPoints = []; - - for (let i = 0; i <= steps; i++) { - const t = i / steps; - const c = curve(p0, p1, p2, p3); - const point = bezierEquation(c, t); - const tangent = vectorNormalize(curveTangent(c, t)); - const normal = vectorNormal(tangent); - - offsetPoints.push(pointFromVector(vectorScale(normal, offsetDist), point)); - } - - return offsetPoints; -} - -export function offsetQuadraticBezier( - p0: GlobalPoint, - p1: GlobalPoint, - p2: GlobalPoint, - offsetDist: number, - steps = 20, -) { - const offsetPoints = []; - - for (let i = 0; i <= steps; i++) { - const t = i / steps; - const t1 = 1 - t; - const point = pointFrom( - t1 * t1 * p0[0] + 2 * t1 * t * p1[0] + t * t * p2[0], - t1 * t1 * p0[1] + 2 * t1 * t * p1[1] + t * t * p2[1], - ); - const tangentX = 2 * (1 - t) * (p1[0] - p0[0]) + 2 * t * (p2[0] - p1[0]); - const tangentY = 2 * (1 - t) * (p1[1] - p0[1]) + 2 * t * (p2[1] - p1[1]); - const tangent = vectorNormalize(vector(tangentX, tangentY)); - const normal = vectorNormal(tangent); - - offsetPoints.push(pointFromVector(vectorScale(normal, offsetDist), point)); - } - - return offsetPoints; -} diff --git a/packages/excalidraw/renderer/interactiveScene.ts b/packages/excalidraw/renderer/interactiveScene.ts index 8c5f58ea1..920f5d200 100644 --- a/packages/excalidraw/renderer/interactiveScene.ts +++ b/packages/excalidraw/renderer/interactiveScene.ts @@ -1,7 +1,6 @@ import oc from "open-color"; import { pointFrom, - pointRotateRads, type GlobalPoint, type LocalPoint, type Radians, @@ -12,15 +11,11 @@ import { FRAME_STYLE, THEME, arrayToMap, - elementCenterPoint, invariant, throttleRAF, } from "@excalidraw/common"; -import { - FIXED_BINDING_DISTANCE, - maxBindingGap, -} from "@excalidraw/element/binding"; +import { maxBindingGap } from "@excalidraw/element/binding"; import { LinearElementEditor } from "@excalidraw/element/linearElementEditor"; import { getOmitSidesForDevice, @@ -49,10 +44,7 @@ import { import { getCommonBounds, - getDiamondPoints, getElementAbsoluteCoords, - offsetCubicBezier, - offsetQuadraticBezier, } from "@excalidraw/element/bounds"; import type { @@ -68,12 +60,10 @@ import type { import type { ElementsMap, ExcalidrawBindableElement, - ExcalidrawDiamondElement, ExcalidrawElement, ExcalidrawFrameLikeElement, ExcalidrawImageElement, ExcalidrawLinearElement, - ExcalidrawRectanguloidElement, ExcalidrawTextElement, GroupId, NonDeleted, @@ -102,6 +92,9 @@ import type { RenderableElementsMap, } from "../scene/types"; +const BINDING_HIGHLIGHT_OFFSET = 4; +const BINDING_HIGHLIGHT_THICKNESS = 10; + const renderElbowArrowMidPointHighlight = ( context: CanvasRenderingContext2D, appState: InteractiveCanvasAppState, @@ -166,120 +159,6 @@ const highlightPoint = ( ); }; -const drawHighlightForRectWithRotation = ( - context: CanvasRenderingContext2D, - element: ExcalidrawRectanguloidElement, - padding: number, -) => { - const [x, y] = pointRotateRads( - pointFrom(element.x, element.y), - elementCenterPoint(element), - element.angle, - ); - - context.save(); - context.translate(x, y); - context.rotate(element.angle); - - let radius = getCornerRadius( - Math.min(element.width, element.height), - element, - ); - if (radius === 0) { - radius = 0.01; - } - - context.beginPath(); - - { - const topLeftApprox = offsetQuadraticBezier( - pointFrom(0, 0 + radius), - pointFrom(0, 0), - pointFrom(0 + radius, 0), - padding, - ); - const topRightApprox = offsetQuadraticBezier( - pointFrom(element.width - radius, 0), - pointFrom(element.width, 0), - pointFrom(element.width, radius), - padding, - ); - const bottomRightApprox = offsetQuadraticBezier( - pointFrom(element.width, element.height - radius), - pointFrom(element.width, element.height), - pointFrom(element.width - radius, element.height), - padding, - ); - const bottomLeftApprox = offsetQuadraticBezier( - pointFrom(radius, element.height), - pointFrom(0, element.height), - pointFrom(0, element.height - radius), - padding, - ); - - context.moveTo( - topLeftApprox[topLeftApprox.length - 1][0], - topLeftApprox[topLeftApprox.length - 1][1], - ); - context.lineTo(topRightApprox[0][0], topRightApprox[0][1]); - drawCatmullRomQuadraticApprox(context, topRightApprox); - context.lineTo(bottomRightApprox[0][0], bottomRightApprox[0][1]); - drawCatmullRomQuadraticApprox(context, bottomRightApprox); - context.lineTo(bottomLeftApprox[0][0], bottomLeftApprox[0][1]); - drawCatmullRomQuadraticApprox(context, bottomLeftApprox); - context.lineTo(topLeftApprox[0][0], topLeftApprox[0][1]); - drawCatmullRomQuadraticApprox(context, topLeftApprox); - } - - // Counter-clockwise for the cutout in the middle. We need to have an "inverse - // mask" on a filled shape for the diamond highlight, because stroking creates - // sharp inset edges on line joins < 90 degrees. - { - const topLeftApprox = offsetQuadraticBezier( - pointFrom(0 + radius, 0), - pointFrom(0, 0), - pointFrom(0, 0 + radius), - -FIXED_BINDING_DISTANCE, - ); - const topRightApprox = offsetQuadraticBezier( - pointFrom(element.width, radius), - pointFrom(element.width, 0), - pointFrom(element.width - radius, 0), - -FIXED_BINDING_DISTANCE, - ); - const bottomRightApprox = offsetQuadraticBezier( - pointFrom(element.width - radius, element.height), - pointFrom(element.width, element.height), - pointFrom(element.width, element.height - radius), - -FIXED_BINDING_DISTANCE, - ); - const bottomLeftApprox = offsetQuadraticBezier( - pointFrom(0, element.height - radius), - pointFrom(0, element.height), - pointFrom(radius, element.height), - -FIXED_BINDING_DISTANCE, - ); - - context.moveTo( - topLeftApprox[topLeftApprox.length - 1][0], - topLeftApprox[topLeftApprox.length - 1][1], - ); - context.lineTo(bottomLeftApprox[0][0], bottomLeftApprox[0][1]); - drawCatmullRomQuadraticApprox(context, bottomLeftApprox); - context.lineTo(bottomRightApprox[0][0], bottomRightApprox[0][1]); - drawCatmullRomQuadraticApprox(context, bottomRightApprox); - context.lineTo(topRightApprox[0][0], topRightApprox[0][1]); - drawCatmullRomQuadraticApprox(context, topRightApprox); - context.lineTo(topLeftApprox[0][0], topLeftApprox[0][1]); - drawCatmullRomQuadraticApprox(context, topLeftApprox); - } - - context.closePath(); - context.fill(); - - context.restore(); -}; - const strokeRectWithRotation = ( context: CanvasRenderingContext2D, x: number, @@ -310,130 +189,24 @@ const strokeRectWithRotation = ( context.restore(); }; -const drawHighlightForDiamondWithRotation = ( +const strokeDiamondWithRotation = ( context: CanvasRenderingContext2D, - padding: number, - element: ExcalidrawDiamondElement, + width: number, + height: number, + cx: number, + cy: number, + angle: number, ) => { - const [x, y] = pointRotateRads( - pointFrom(element.x, element.y), - elementCenterPoint(element), - element.angle, - ); context.save(); - context.translate(x, y); - context.rotate(element.angle); - - { - context.beginPath(); - - const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] = - getDiamondPoints(element); - const verticalRadius = element.roundness - ? getCornerRadius(Math.abs(topX - leftX), element) - : (topX - leftX) * 0.01; - const horizontalRadius = element.roundness - ? getCornerRadius(Math.abs(rightY - topY), element) - : (rightY - topY) * 0.01; - const topApprox = offsetCubicBezier( - pointFrom(topX - verticalRadius, topY + horizontalRadius), - pointFrom(topX, topY), - pointFrom(topX, topY), - pointFrom(topX + verticalRadius, topY + horizontalRadius), - padding, - ); - const rightApprox = offsetCubicBezier( - pointFrom(rightX - verticalRadius, rightY - horizontalRadius), - pointFrom(rightX, rightY), - pointFrom(rightX, rightY), - pointFrom(rightX - verticalRadius, rightY + horizontalRadius), - padding, - ); - const bottomApprox = offsetCubicBezier( - pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius), - pointFrom(bottomX, bottomY), - pointFrom(bottomX, bottomY), - pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius), - padding, - ); - const leftApprox = offsetCubicBezier( - pointFrom(leftX + verticalRadius, leftY + horizontalRadius), - pointFrom(leftX, leftY), - pointFrom(leftX, leftY), - pointFrom(leftX + verticalRadius, leftY - horizontalRadius), - padding, - ); - - context.moveTo( - topApprox[topApprox.length - 1][0], - topApprox[topApprox.length - 1][1], - ); - context.lineTo(rightApprox[0][0], rightApprox[0][1]); - drawCatmullRomCubicApprox(context, rightApprox); - context.lineTo(bottomApprox[0][0], bottomApprox[0][1]); - drawCatmullRomCubicApprox(context, bottomApprox); - context.lineTo(leftApprox[0][0], leftApprox[0][1]); - drawCatmullRomCubicApprox(context, leftApprox); - context.lineTo(topApprox[0][0], topApprox[0][1]); - drawCatmullRomCubicApprox(context, topApprox); - } - - // Counter-clockwise for the cutout in the middle. We need to have an "inverse - // mask" on a filled shape for the diamond highlight, because stroking creates - // sharp inset edges on line joins < 90 degrees. - { - const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] = - getDiamondPoints(element); - const verticalRadius = element.roundness - ? getCornerRadius(Math.abs(topX - leftX), element) - : (topX - leftX) * 0.01; - const horizontalRadius = element.roundness - ? getCornerRadius(Math.abs(rightY - topY), element) - : (rightY - topY) * 0.01; - const topApprox = offsetCubicBezier( - pointFrom(topX + verticalRadius, topY + horizontalRadius), - pointFrom(topX, topY), - pointFrom(topX, topY), - pointFrom(topX - verticalRadius, topY + horizontalRadius), - -FIXED_BINDING_DISTANCE, - ); - const rightApprox = offsetCubicBezier( - pointFrom(rightX - verticalRadius, rightY + horizontalRadius), - pointFrom(rightX, rightY), - pointFrom(rightX, rightY), - pointFrom(rightX - verticalRadius, rightY - horizontalRadius), - -FIXED_BINDING_DISTANCE, - ); - const bottomApprox = offsetCubicBezier( - pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius), - pointFrom(bottomX, bottomY), - pointFrom(bottomX, bottomY), - pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius), - -FIXED_BINDING_DISTANCE, - ); - const leftApprox = offsetCubicBezier( - pointFrom(leftX + verticalRadius, leftY - horizontalRadius), - pointFrom(leftX, leftY), - pointFrom(leftX, leftY), - pointFrom(leftX + verticalRadius, leftY + horizontalRadius), - -FIXED_BINDING_DISTANCE, - ); - - context.moveTo( - topApprox[topApprox.length - 1][0], - topApprox[topApprox.length - 1][1], - ); - context.lineTo(leftApprox[0][0], leftApprox[0][1]); - drawCatmullRomCubicApprox(context, leftApprox); - context.lineTo(bottomApprox[0][0], bottomApprox[0][1]); - drawCatmullRomCubicApprox(context, bottomApprox); - context.lineTo(rightApprox[0][0], rightApprox[0][1]); - drawCatmullRomCubicApprox(context, rightApprox); - context.lineTo(topApprox[0][0], topApprox[0][1]); - drawCatmullRomCubicApprox(context, topApprox); - } + context.translate(cx, cy); + context.rotate(angle); + context.beginPath(); + context.moveTo(0, height / 2); + context.lineTo(width / 2, 0); + context.lineTo(0, -height / 2); + context.lineTo(-width / 2, 0); context.closePath(); - context.fill(); + context.stroke(); context.restore(); }; @@ -487,10 +260,16 @@ const renderBindingHighlightForBindableElement = ( const height = y2 - y1; context.strokeStyle = "rgba(0,0,0,.05)"; - context.fillStyle = "rgba(0,0,0,.05)"; - + // When zooming out, make line width greater for visibility + const zoomValue = zoom.value < 1 ? zoom.value : 1; + context.lineWidth = BINDING_HIGHLIGHT_THICKNESS / zoomValue; // To ensure the binding highlight doesn't overlap the element itself - const padding = maxBindingGap(element, element.width, element.height, zoom); + const padding = context.lineWidth / 2 + BINDING_HIGHLIGHT_OFFSET; + + const radius = getCornerRadius( + Math.min(element.width, element.height), + element, + ); switch (element.type) { case "rectangle": @@ -500,20 +279,37 @@ const renderBindingHighlightForBindableElement = ( case "embeddable": case "frame": case "magicframe": - drawHighlightForRectWithRotation(context, element, padding); + strokeRectWithRotation( + context, + x1 - padding, + y1 - padding, + width + padding * 2, + height + padding * 2, + x1 + width / 2, + y1 + height / 2, + element.angle, + undefined, + radius, + ); break; case "diamond": - drawHighlightForDiamondWithRotation(context, padding, element); + const side = Math.hypot(width, height); + const wPadding = (padding * side) / height; + const hPadding = (padding * side) / width; + strokeDiamondWithRotation( + context, + width + wPadding * 2, + height + hPadding * 2, + x1 + width / 2, + y1 + height / 2, + element.angle, + ); break; case "ellipse": - context.lineWidth = - maxBindingGap(element, element.width, element.height, zoom) - - FIXED_BINDING_DISTANCE; - strokeEllipseWithRotation( context, - width + padding + FIXED_BINDING_DISTANCE, - height + padding + FIXED_BINDING_DISTANCE, + width + padding * 2, + height + padding * 2, x1 + width / 2, y1 + height / 2, element.angle, @@ -1444,65 +1240,3 @@ export const renderInteractiveScene = < renderConfig.callback(ret); return ret as T extends true ? void : ReturnType; }; - -function drawCatmullRomQuadraticApprox( - ctx: CanvasRenderingContext2D, - points: GlobalPoint[], - segments = 20, -) { - ctx.lineTo(points[0][0], points[0][1]); - - for (let i = 0; i < points.length - 1; i++) { - const p0 = points[i - 1 < 0 ? 0 : i - 1]; - const p1 = points[i]; - const p2 = points[i + 1 >= points.length ? points.length - 1 : i + 1]; - - for (let t = 0; t <= 1; t += 1 / segments) { - const t2 = t * t; - - const x = - (1 - t) * (1 - t) * p0[0] + 2 * (1 - t) * t * p1[0] + t2 * p2[0]; - - const y = - (1 - t) * (1 - t) * p0[1] + 2 * (1 - t) * t * p1[1] + t2 * p2[1]; - - ctx.lineTo(x, y); - } - } -} - -function drawCatmullRomCubicApprox( - ctx: CanvasRenderingContext2D, - points: GlobalPoint[], - segments = 20, -) { - ctx.lineTo(points[0][0], points[0][1]); - - for (let i = 0; i < points.length - 1; i++) { - const p0 = points[i - 1 < 0 ? 0 : i - 1]; - const p1 = points[i]; - const p2 = points[i + 1 >= points.length ? points.length - 1 : i + 1]; - const p3 = points[i + 2 >= points.length ? points.length - 1 : i + 2]; - - for (let t = 0; t <= 1; t += 1 / segments) { - const t2 = t * t; - const t3 = t2 * t; - - const x = - 0.5 * - (2 * p1[0] + - (-p0[0] + p2[0]) * t + - (2 * p0[0] - 5 * p1[0] + 4 * p2[0] - p3[0]) * t2 + - (-p0[0] + 3 * p1[0] - 3 * p2[0] + p3[0]) * t3); - - const y = - 0.5 * - (2 * p1[1] + - (-p0[1] + p2[1]) * t + - (2 * p0[1] - 5 * p1[1] + 4 * p2[1] - p3[1]) * t2 + - (-p0[1] + 3 * p1[1] - 3 * p2[1] + p3[1]) * t3); - - ctx.lineTo(x, y); - } - } -} diff --git a/packages/math/src/curve.ts b/packages/math/src/curve.ts index 5404619c7..a79fb43a1 100644 --- a/packages/math/src/curve.ts +++ b/packages/math/src/curve.ts @@ -3,8 +3,6 @@ import type { Bounds } from "@excalidraw/element/bounds"; import { isPoint, pointDistance, pointFrom } from "./point"; import { rectangle, rectangleIntersectLineSegment } from "./rectangle"; -import { vector } from "./vector"; - import type { Curve, GlobalPoint, LineSegment, LocalPoint } from "./types"; /** @@ -84,7 +82,7 @@ function solve( return [t0, s0]; } -export const bezierEquation = ( +const bezierEquation = ( c: Curve, t: number, ) => @@ -276,26 +274,6 @@ export function isCurve

( ); } -export function curveTangent( - [p0, p1, p2, p3]: Curve, - t: number, -) { - return vector( - -3 * (1 - t) * (1 - t) * p0[0] + - 3 * (1 - t) * (1 - t) * p1[0] - - 6 * t * (1 - t) * p1[0] - - 3 * t * t * p2[0] + - 6 * t * (1 - t) * p2[0] + - 3 * t * t * p3[0], - -3 * (1 - t) * (1 - t) * p0[1] + - 3 * (1 - t) * (1 - t) * p1[1] - - 6 * t * (1 - t) * p1[1] - - 3 * t * t * p2[1] + - 6 * t * (1 - t) * p2[1] + - 3 * t * t * p3[1], - ); -} - function curveBounds( c: Curve, ): Bounds { diff --git a/packages/math/src/vector.ts b/packages/math/src/vector.ts index 12682fcd9..246722067 100644 --- a/packages/math/src/vector.ts +++ b/packages/math/src/vector.ts @@ -143,8 +143,3 @@ export const vectorNormalize = (v: Vector): Vector => { return vector(v[0] / m, v[1] / m); }; - -/** - * Calculate the right-hand normal of the vector. - */ -export const vectorNormal = (v: Vector): Vector => vector(v[1], -v[0]); From 868ec3f505cf2dd827aa52640eedc1037b479158 Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Fri, 2 May 2025 19:51:48 +0200 Subject: [PATCH 28/28] Fix zooming out arrow type change binds --- .../excalidraw/actions/actionProperties.tsx | 72 +++---------------- 1 file changed, 11 insertions(+), 61 deletions(-) diff --git a/packages/excalidraw/actions/actionProperties.tsx b/packages/excalidraw/actions/actionProperties.tsx index df07960af..e2482dfa1 100644 --- a/packages/excalidraw/actions/actionProperties.tsx +++ b/packages/excalidraw/actions/actionProperties.tsx @@ -18,7 +18,6 @@ import { arrayToMap, getFontFamilyString, getShortcutKey, - tupleToCoors, getLineHeight, } from "@excalidraw/common"; @@ -26,9 +25,7 @@ import { getNonDeletedElements } from "@excalidraw/element"; import { bindLinearElement, - bindPointToSnapToElementOutline, calculateFixedPointForElbowArrowBinding, - getHoveredElementForBinding, updateBoundElements, } from "@excalidraw/element/binding"; @@ -1607,63 +1604,16 @@ export const actionChangeArrowType = register({ -1, elementsMap, ); - const startHoveredElement = - !newElement.startBinding && - getHoveredElementForBinding( - tupleToCoors(startGlobalPoint), - elements, - elementsMap, - appState.zoom, - false, - true, - ); - const endHoveredElement = - !newElement.endBinding && - getHoveredElementForBinding( - tupleToCoors(endGlobalPoint), - elements, - elementsMap, - appState.zoom, - false, - true, - ); - const startElement = startHoveredElement - ? startHoveredElement - : newElement.startBinding && - (elementsMap.get( - newElement.startBinding.elementId, - ) as ExcalidrawBindableElement); - const endElement = endHoveredElement - ? endHoveredElement - : newElement.endBinding && - (elementsMap.get( - newElement.endBinding.elementId, - ) as ExcalidrawBindableElement); - - const finalStartPoint = startHoveredElement - ? bindPointToSnapToElementOutline( - newElement, - startHoveredElement, - "start", - ) - : startGlobalPoint; - const finalEndPoint = endHoveredElement - ? bindPointToSnapToElementOutline( - newElement, - endHoveredElement, - "end", - ) - : endGlobalPoint; - - startHoveredElement && - bindLinearElement( - newElement, - startHoveredElement, - "start", - app.scene, - ); - endHoveredElement && - bindLinearElement(newElement, endHoveredElement, "end", app.scene); + const startElement = + newElement.startBinding && + (elementsMap.get( + newElement.startBinding.elementId, + ) as ExcalidrawBindableElement); + const endElement = + newElement.endBinding && + (elementsMap.get( + newElement.endBinding.elementId, + ) as ExcalidrawBindableElement); const startBinding = startElement && newElement.startBinding @@ -1695,7 +1645,7 @@ export const actionChangeArrowType = register({ startBinding, endBinding, ...updateElbowArrowPoints(newElement, elementsMap, { - points: [finalStartPoint, finalEndPoint].map( + points: [startGlobalPoint, endGlobalPoint].map( (p): LocalPoint => pointFrom(p[0] - newElement.x, p[1] - newElement.y), ),