fix: Arrow with angle jittering

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
This commit is contained in:
Mark Tolmacs
2025-11-17 19:21:18 +01:00
parent d46c38cb27
commit 66d021a19e
3 changed files with 23 additions and 39 deletions

View File

@@ -1569,7 +1569,9 @@ export class LinearElementEditor {
const updatedOriginPoint = const updatedOriginPoint =
pointUpdates.get(0)?.point ?? pointFrom<LocalPoint>(0, 0); pointUpdates.get(0)?.point ?? pointFrom<LocalPoint>(0, 0);
const [offsetX, offsetY] = updatedOriginPoint; // Handle non-normalized points
const offsetX = element.points[0][0] - updatedOriginPoint[0];
const offsetY = element.points[0][1] - updatedOriginPoint[1];
const nextPoints = isElbowArrow(element) const nextPoints = isElbowArrow(element)
? [ ? [
@@ -1586,7 +1588,14 @@ export class LinearElementEditor {
idx !== points.length - 1 && idx !== points.length - 1 &&
!pointUpdates.has(idx) !pointUpdates.has(idx)
) { ) {
return pointFrom<LocalPoint>(current[0], current[1]); return current;
}
// Since we're subtracting the offset coming from the first point
// from all other points, we need to skip the first point here to avoid
// double-subtracting the offset.
if (idx === 0) {
return pointFrom<LocalPoint>(0, 0);
} }
return pointFrom<LocalPoint>( return pointFrom<LocalPoint>(

View File

@@ -4,7 +4,7 @@ import throttle from "lodash.throttle";
import { useEffect, useMemo, useState, memo } from "react"; import { useEffect, useMemo, useState, memo } from "react";
import { STATS_PANELS } from "@excalidraw/common"; import { STATS_PANELS } from "@excalidraw/common";
import { getCommonBounds, isBindingElement } from "@excalidraw/element"; import { getCommonBounds } from "@excalidraw/element";
import { getUncroppedWidthAndHeight } from "@excalidraw/element"; import { getUncroppedWidthAndHeight } from "@excalidraw/element";
import { isImageElement } from "@excalidraw/element"; import { isImageElement } from "@excalidraw/element";
@@ -333,16 +333,14 @@ export const StatsInner = memo(
appState={appState} appState={appState}
/> />
</StatsRow> </StatsRow>
{!isBindingElement(singleElement) && ( <StatsRow>
<StatsRow> <Angle
<Angle property="angle"
property="angle" element={singleElement}
element={singleElement} scene={scene}
scene={scene} appState={appState}
appState={appState} />
/> </StatsRow>
</StatsRow>
)}
<StatsRow> <StatsRow>
<FontSize <FontSize
property="fontSize" property="fontSize"

View File

@@ -1,4 +1,4 @@
import { isFiniteNumber, pointFrom, pointRotateRads } from "@excalidraw/math"; import { isFiniteNumber, pointFrom } from "@excalidraw/math";
import { import {
DEFAULT_FONT_FAMILY, DEFAULT_FONT_FAMILY,
@@ -20,7 +20,6 @@ import {
} from "@excalidraw/common"; } from "@excalidraw/common";
import { import {
calculateFixedPointForNonElbowArrowBinding, calculateFixedPointForNonElbowArrowBinding,
elementCenterPoint,
getNonDeletedElements, getNonDeletedElements,
isPointInElement, isPointInElement,
isValidPolygon, isValidPolygon,
@@ -393,35 +392,14 @@ export const restoreElement = (
...getSizeFromPoints(points), ...getSizeFromPoints(points),
}); });
case "arrow": { case "arrow": {
const { const { startArrowhead = null, endArrowhead = "arrow" } = element;
startArrowhead = null,
endArrowhead = "arrow",
angle = 0,
} = element;
const x: number | undefined = element.x; const x: number | undefined = element.x;
const y: number | undefined = element.y; const y: number | undefined = element.y;
let points: readonly LocalPoint[] | undefined = // migrate old arrow model to new one const points: readonly LocalPoint[] | undefined = // migrate old arrow model to new one
!Array.isArray(element.points) || element.points.length < 2 !Array.isArray(element.points) || element.points.length < 2
? [pointFrom(0, 0), pointFrom(element.width, element.height)] ? [pointFrom(0, 0), pointFrom(element.width, element.height)]
: element.points; : element.points;
if (angle !== 0) {
points = LinearElementEditor.getPointsGlobalCoordinates(
element,
elementsMap,
).map((point) =>
LinearElementEditor.pointFromAbsoluteCoords(
element as ExcalidrawArrowElement,
pointRotateRads(
point,
elementCenterPoint(element, elementsMap),
angle,
),
elementsMap,
),
);
}
const base = { const base = {
type: element.type, type: element.type,
startBinding: repairBinding( startBinding: repairBinding(
@@ -443,7 +421,6 @@ export const restoreElement = (
y, y,
elbowed: (element as ExcalidrawArrowElement).elbowed, elbowed: (element as ExcalidrawArrowElement).elbowed,
...getSizeFromPoints(points), ...getSizeFromPoints(points),
angle: 0 as Radians,
}; };
// TODO: Separate arrow from linear element // TODO: Separate arrow from linear element