From 5947af5b504fede7c11400faa3a829fd350beca6 Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Fri, 7 Mar 2025 13:03:19 +0100 Subject: [PATCH] Start grid point arrow align --- packages/element/src/linearElementEditor.ts | 44 ++-- packages/excalidraw/components/App.tsx | 221 ++++++++++++-------- 2 files changed, 148 insertions(+), 117 deletions(-) diff --git a/packages/element/src/linearElementEditor.ts b/packages/element/src/linearElementEditor.ts index a177c0c8c5..bd3d53f369 100644 --- a/packages/element/src/linearElementEditor.ts +++ b/packages/element/src/linearElementEditor.ts @@ -238,14 +238,15 @@ export class LinearElementEditor { }); } - static getOutlineAvoidingPointOrNull( + static getOutlineAvoidingPoint( element: NonDeleted, - coords: { x: number; y: number }, + coords: GlobalPoint, pointIndex: number, app: AppClassProperties, - ) { + fallback?: GlobalPoint, + ): GlobalPoint { const hoveredElement = getHoveredElementForBinding( - coords, + { x: coords[0], y: coords[1] }, app.scene.getNonDeletedElements(), app.scene.getNonDeletedElementsMap(), app.state.zoom, @@ -254,11 +255,10 @@ export class LinearElementEditor { ); if (hoveredElement) { - const p = pointFrom(coords.x, coords.y); const newPoints = Array.from(element.points); newPoints[pointIndex] = pointFrom( - p[0] - element.x, - p[1] - element.y, + coords[0] - element.x, + coords[1] - element.y, ); return bindPointToSnapToElementOutline( @@ -272,27 +272,7 @@ export class LinearElementEditor { ); } - return null; - } - - static getOutlineAvoidingPoint( - element: NonDeleted, - coords: { x: number; y: number }, - pointIndex: number, - app: AppClassProperties, - ): GlobalPoint { - const p = LinearElementEditor.getOutlineAvoidingPointOrNull( - element, - coords, - pointIndex, - app, - ); - - if (p) { - return p; - } - - return pointFrom(coords.x, coords.y); + return fallback ?? coords; } /** @@ -411,10 +391,10 @@ export class LinearElementEditor { globalNewPointPosition = LinearElementEditor.getOutlineAvoidingPoint( element, - { - x: element.x + element.points[pointIndex][0] + deltaX, - y: element.y + element.points[pointIndex][1] + deltaY, - }, + pointFrom( + element.x + element.points[pointIndex][0] + deltaX, + element.y + element.points[pointIndex][1] + deltaY, + ), pointIndex, app, ); diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 3da087217a..be54673f82 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -5966,32 +5966,26 @@ class App extends React.Component { setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER); } - const outlineGlobalPoint = - LinearElementEditor.getOutlineAvoidingPointOrNull( - multiElement, - { - x: scenePointerX, - y: scenePointerY, - }, - multiElement.points.length - 1, - this, - ); - - const nextPoint = outlineGlobalPoint - ? pointFrom( - outlineGlobalPoint[0] - rx, - outlineGlobalPoint[1] - ry, - ) - : pointFrom( - lastCommittedX + dxFromLastCommitted, - lastCommittedY + dyFromLastCommitted, - ); - // update last uncommitted point mutateElement( multiElement, { - points: [...points.slice(0, -1), nextPoint], + points: [ + ...points.slice(0, -1), + pointTranslate( + LinearElementEditor.getOutlineAvoidingPoint( + multiElement, + pointFrom(scenePointerX, scenePointerY), + multiElement.points.length - 1, + this, + pointFrom( + multiElement.x + lastCommittedX + dxFromLastCommitted, + multiElement.y + lastCommittedY + dyFromLastCommitted, + ), + ), + vector(-multiElement.x, -multiElement.y), + ), + ], }, false, { @@ -7802,53 +7796,93 @@ class App extends React.Component { ? [currentItemStartArrowhead, currentItemEndArrowhead] : [null, null]; - const element = - elementType === "arrow" - ? newArrowElement({ - type: elementType, - x: gridX, - y: gridY, - strokeColor: this.state.currentItemStrokeColor, - backgroundColor: this.state.currentItemBackgroundColor, - fillStyle: this.state.currentItemFillStyle, - strokeWidth: this.state.currentItemStrokeWidth, - strokeStyle: this.state.currentItemStrokeStyle, - roughness: this.state.currentItemRoughness, - opacity: this.state.currentItemOpacity, - roundness: - this.state.currentItemArrowType === ARROW_TYPE.round - ? { type: ROUNDNESS.PROPORTIONAL_RADIUS } - : // note, roundness doesn't have any effect for elbow arrows, - // but it's best to set it to null as well - null, - startArrowhead, - endArrowhead, - locked: false, - frameId: topLayerFrame ? topLayerFrame.id : null, - elbowed: this.state.currentItemArrowType === ARROW_TYPE.elbow, - fixedSegments: - this.state.currentItemArrowType === ARROW_TYPE.elbow - ? [] - : null, - }) - : newLinearElement({ - type: elementType, - x: gridX, - y: gridY, - strokeColor: this.state.currentItemStrokeColor, - backgroundColor: this.state.currentItemBackgroundColor, - fillStyle: this.state.currentItemFillStyle, - strokeWidth: this.state.currentItemStrokeWidth, - strokeStyle: this.state.currentItemStrokeStyle, - roughness: this.state.currentItemRoughness, - opacity: this.state.currentItemOpacity, - roundness: - this.state.currentItemRoundness === "round" - ? { type: ROUNDNESS.PROPORTIONAL_RADIUS } - : null, - locked: false, - frameId: topLayerFrame ? topLayerFrame.id : null, - }); + let element: NonDeleted; + if (elementType === "arrow") { + const arrow: Mutable> = + newArrowElement({ + type: "arrow", + x: gridX, + y: gridY, + strokeColor: this.state.currentItemStrokeColor, + backgroundColor: this.state.currentItemBackgroundColor, + fillStyle: this.state.currentItemFillStyle, + strokeWidth: this.state.currentItemStrokeWidth, + strokeStyle: this.state.currentItemStrokeStyle, + roughness: this.state.currentItemRoughness, + opacity: this.state.currentItemOpacity, + roundness: + this.state.currentItemArrowType === ARROW_TYPE.round + ? { type: ROUNDNESS.PROPORTIONAL_RADIUS } + : // note, roundness doesn't have any effect for elbow arrows, + // but it's best to set it to null as well + null, + startArrowhead, + endArrowhead, + locked: false, + frameId: topLayerFrame ? topLayerFrame.id : null, + elbowed: this.state.currentItemArrowType === ARROW_TYPE.elbow, + fixedSegments: + this.state.currentItemArrowType === ARROW_TYPE.elbow ? [] : null, + }); + + const hoveredElement = getHoveredElementForBinding( + { x: gridX, y: gridY }, + this.scene.getNonDeletedElements(), + this.scene.getNonDeletedElementsMap(), + this.state.zoom, + true, + this.state.currentItemArrowType === ARROW_TYPE.elbow, + ); + + if (hoveredElement) { + [arrow.x, arrow.y] = + intersectElementWithLineSegment( + hoveredElement, + lineSegment( + pointFrom(gridX, gridY), + pointFrom( + gridX, + hoveredElement.y + hoveredElement.height / 2, + ), + ), + 2 * FIXED_BINDING_DISTANCE, + )[0] ?? + intersectElementWithLineSegment( + hoveredElement, + lineSegment( + pointFrom(gridX, gridY), + pointFrom( + hoveredElement.x + hoveredElement.width / 2, + gridY, + ), + ), + 2 * FIXED_BINDING_DISTANCE, + )[0] ?? + pointFrom(gridX, gridY); + } + + element = arrow; + } else { + element = newLinearElement({ + type: elementType, + x: gridX, + y: gridY, + strokeColor: this.state.currentItemStrokeColor, + backgroundColor: this.state.currentItemBackgroundColor, + fillStyle: this.state.currentItemFillStyle, + strokeWidth: this.state.currentItemStrokeWidth, + strokeStyle: this.state.currentItemStrokeStyle, + roughness: this.state.currentItemRoughness, + opacity: this.state.currentItemOpacity, + roundness: + this.state.currentItemRoundness === "round" + ? { type: ROUNDNESS.PROPORTIONAL_RADIUS } + : null, + locked: false, + frameId: topLayerFrame ? topLayerFrame.id : null, + }); + } + this.setState((prevState) => { const nextSelectedElementIds = { ...prevState.selectedElementIds, @@ -8163,12 +8197,6 @@ class App extends React.Component { this.laserTrails.addPointToPath(pointerCoords.x, pointerCoords.y); } - const [gridX, gridY] = getGridPoint( - pointerCoords.x, - pointerCoords.y, - event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(), - ); - // for arrows/lines, don't start dragging until a given threshold // to ensure we don't create a 2-point arrow by mistake when // user clicks mouse in a way that it moves a tiny bit (thus @@ -8610,6 +8638,11 @@ class App extends React.Component { } else if (isLinearElement(newElement)) { pointerDownState.drag.hasOccurred = true; const points = newElement.points; + const [gridX, gridY] = getGridPoint( + pointerCoords.x, + pointerCoords.y, + event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(), + ); let dx = gridX - newElement.x; let dy = gridY - newElement.y; @@ -8626,7 +8659,22 @@ class App extends React.Component { mutateElement( newElement, { - points: [...points, pointFrom(dx, dy)], + points: [ + ...points, + pointTranslate( + LinearElementEditor.getOutlineAvoidingPoint( + newElement, + pointFrom(pointerCoords.x, pointerCoords.y), + newElement.points.length - 1, + this, + pointFrom( + newElement.x + dx, + newElement.y + dy, + ), + ), + vector(-newElement.x, -newElement.y), + ), + ], }, false, ); @@ -8634,20 +8682,23 @@ class App extends React.Component { points.length === 2 || (points.length > 1 && isElbowArrow(newElement)) ) { - const globalPoint = LinearElementEditor.getOutlineAvoidingPoint( - newElement, - { x: newElement.x + dx, y: newElement.y + dy }, - 1, - this, - ); mutateElement( newElement, { points: [ ...points.slice(0, -1), - pointFrom( - globalPoint[0] - newElement.x, - globalPoint[1] - newElement.y, + pointTranslate( + LinearElementEditor.getOutlineAvoidingPoint( + newElement, + pointFrom(pointerCoords.x, pointerCoords.y), + newElement.points.length - 1, + this, + pointFrom( + newElement.x + dx, + newElement.y + dy, + ), + ), + vector(-newElement.x, -newElement.y), ), ], },