fix polygon behavior when adding/removing/moving points within line editor

This commit is contained in:
dwelle 2025-05-08 22:59:44 +02:00
parent ac1ad31921
commit d1fa9005b9
4 changed files with 97 additions and 73 deletions

View File

@ -88,11 +88,41 @@ import type {
PointsPositionUpdates, PointsPositionUpdates,
} from "./types"; } from "./types";
/**
* Normalizes line points so that the start point is at [0,0]. This is
* expected in various parts of the codebase.
*
* Also returns the offsets - [0,0] if no normalization needed.
*
* @private
*/
const getNormalizedPoints = ({
points,
}: {
points: ExcalidrawLinearElement["points"];
}): {
points: LocalPoint[];
offsetX: number;
offsetY: number;
} => {
const offsetX = points[0][0];
const offsetY = points[0][1];
return {
points: points.map((p) => {
return pointFrom(p[0] - offsetX, p[1] - offsetY);
}),
offsetX,
offsetY,
};
};
const editorMidPointsCache: { const editorMidPointsCache: {
version: number | null; version: number | null;
points: (GlobalPoint | null)[]; points: (GlobalPoint | null)[];
zoom: number | null; zoom: number | null;
} = { version: null, points: [], zoom: null }; } = { version: null, points: [], zoom: null };
export class LinearElementEditor { export class LinearElementEditor {
public readonly elementId: ExcalidrawElement["id"] & { public readonly elementId: ExcalidrawElement["id"] & {
_brand: "excalidrawLinearElementId"; _brand: "excalidrawLinearElementId";
@ -135,7 +165,11 @@ export class LinearElementEditor {
}; };
if (!pointsEqual(element.points[0], pointFrom(0, 0))) { if (!pointsEqual(element.points[0], pointFrom(0, 0))) {
console.error("Linear element is not normalized", Error().stack); console.error("Linear element is not normalized", Error().stack);
LinearElementEditor.normalizePoints(element, elementsMap); mutateElement(
element,
elementsMap,
LinearElementEditor.getNormalizeElementPointsAndCoords(element),
);
} }
this.selectedPointsIndices = null; this.selectedPointsIndices = null;
this.lastUncommittedPoint = null; this.lastUncommittedPoint = null;
@ -1033,7 +1067,7 @@ export class LinearElementEditor {
]), ]),
); );
} else { } else {
LinearElementEditor.addPoints(element, app.scene, [{ point: newPoint }]); LinearElementEditor.addPoints(element, app.scene, [newPoint]);
} }
return { return {
...appState.editingLinearElement, ...appState.editingLinearElement,
@ -1176,40 +1210,23 @@ export class LinearElementEditor {
/** /**
* Normalizes line points so that the start point is at [0,0]. This is * Normalizes line points so that the start point is at [0,0]. This is
* expected in various parts of the codebase. Also returns new x/y to account * expected in various parts of the codebase.
* for the potential normalization. *
* Also returns normalized x and y coords to account for the normalization
* of the points.
*/ */
static getNormalizedPoints(element: ExcalidrawLinearElement): { static getNormalizeElementPointsAndCoords(element: ExcalidrawLinearElement) {
points: LocalPoint[]; const { points, offsetX, offsetY } = getNormalizedPoints(element);
x: number;
y: number;
} {
const { points } = element;
const offsetX = points[0][0];
const offsetY = points[0][1];
return { return {
points: points.map((p) => { points,
return pointFrom(p[0] - offsetX, p[1] - offsetY);
}),
x: element.x + offsetX, x: element.x + offsetX,
y: element.y + offsetY, y: element.y + offsetY,
}; };
} }
// element-mutating methods // element-mutating methods
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
static normalizePoints(
element: NonDeleted<ExcalidrawLinearElement>,
elementsMap: ElementsMap,
) {
mutateElement(
element,
elementsMap,
LinearElementEditor.getNormalizedPoints(element),
);
}
static duplicateSelectedPoints(appState: AppState, scene: Scene): AppState { static duplicateSelectedPoints(appState: AppState, scene: Scene): AppState {
invariant( invariant(
appState.editingLinearElement, appState.editingLinearElement,
@ -1291,51 +1308,43 @@ export class LinearElementEditor {
app: AppClassProperties, app: AppClassProperties,
pointIndices: readonly number[], pointIndices: readonly number[],
) { ) {
// Handle loop lock behavior const isUncommittedPoint =
app.state.editingLinearElement?.lastUncommittedPoint ===
element.points[element.points.length - 1];
// break polygon if deleting start/end point
if (isLineElement(element) && element.loopLock) { if (isLineElement(element) && element.loopLock) {
if ( if (
pointIndices.includes(0) || pointIndices.includes(0) ||
(pointIndices.includes(element.points.length - 1) && (pointIndices.includes(element.points.length - 1) &&
// don't disable polygon if cleaning up uncommitted point // don't disable polygon if cleaning up uncommitted point
app.state.editingLinearElement?.lastUncommittedPoint !== !isUncommittedPoint)
element.points[element.points.length - 1])
) { ) {
app.scene.mutateElement(element, { loopLock: false }); app.scene.mutateElement(element, { loopLock: false });
} }
} }
let offsetX = 0; const nextPoints = element.points.filter((_, idx) => {
let offsetY = 0; return !pointIndices.includes(idx);
});
const isDeletingOriginPoint = pointIndices.includes(0); if (isUncommittedPoint && isLineElement(element) && element.loopLock) {
nextPoints[0] = pointFrom(
// if deleting first point, make the next to be [0,0] and recalculate nextPoints[nextPoints.length - 1][0],
// positions of the rest with respect to it nextPoints[nextPoints.length - 1][1],
if (isDeletingOriginPoint) { );
const firstNonDeletedPoint = element.points.find((point, idx) => {
return !pointIndices.includes(idx);
});
if (firstNonDeletedPoint) {
offsetX = firstNonDeletedPoint[0];
offsetY = firstNonDeletedPoint[1];
}
} }
const nextPoints = element.points.reduce((acc: LocalPoint[], p, idx) => { const {
if (!pointIndices.includes(idx)) { points: normalizedPoints,
acc.push( offsetX,
!acc.length offsetY,
? pointFrom(0, 0) } = getNormalizedPoints({ points: nextPoints });
: pointFrom(p[0] - offsetX, p[1] - offsetY),
);
}
return acc;
}, []);
LinearElementEditor._updatePoints( LinearElementEditor._updatePoints(
element, element,
app.scene, app.scene,
nextPoints, normalizedPoints,
offsetX, offsetX,
offsetY, offsetY,
); );
@ -1344,16 +1353,27 @@ export class LinearElementEditor {
static addPoints( static addPoints(
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
scene: Scene, scene: Scene,
targetPoints: { point: LocalPoint }[], addedPoints: LocalPoint[],
) { ) {
const offsetX = 0; const nextPoints = [...element.points, ...addedPoints];
const offsetY = 0;
if (isLineElement(element) && element.loopLock) {
nextPoints[0] = pointFrom(
nextPoints[nextPoints.length - 1][0],
nextPoints[nextPoints.length - 1][1],
);
}
const {
points: normalizedPoints,
offsetX,
offsetY,
} = getNormalizedPoints({ points: nextPoints });
const nextPoints = [...element.points, ...targetPoints.map((x) => x.point)];
LinearElementEditor._updatePoints( LinearElementEditor._updatePoints(
element, element,
scene, scene,
nextPoints, normalizedPoints,
offsetX, offsetX,
offsetY, offsetY,
); );
@ -1396,12 +1416,11 @@ export class LinearElementEditor {
// all the other points in the opposite direction by delta to // all the other points in the opposite direction by delta to
// offset it. We do the same with actual element.x/y position, so // offset it. We do the same with actual element.x/y position, so
// this hacks are completely transparent to the user. // this hacks are completely transparent to the user.
const [deltaX, deltaY] =
const updatedOriginPoint =
pointUpdates.get(0)?.point ?? pointFrom<LocalPoint>(0, 0); pointUpdates.get(0)?.point ?? pointFrom<LocalPoint>(0, 0);
const [offsetX, offsetY] = pointFrom<LocalPoint>(
deltaX - points[0][0], const [offsetX, offsetY] = updatedOriginPoint;
deltaY - points[0][1],
);
const nextPoints = isElbowArrow(element) const nextPoints = isElbowArrow(element)
? [ ? [
@ -1571,6 +1590,7 @@ export class LinearElementEditor {
isDragging: options?.isDragging ?? false, isDragging: options?.isDragging ?? false,
}); });
} else { } else {
// TODO do we need to get precise coords here just to calc centers?
const nextCoords = getElementPointsCoords(element, nextPoints); const nextCoords = getElementPointsCoords(element, nextPoints);
const prevCoords = getElementPointsCoords(element, element.points); const prevCoords = getElementPointsCoords(element, element.points);
const nextCenterX = (nextCoords[0] + nextCoords[2]) / 2; const nextCenterX = (nextCoords[0] + nextCoords[2]) / 2;
@ -1579,7 +1599,7 @@ export class LinearElementEditor {
const prevCenterY = (prevCoords[1] + prevCoords[3]) / 2; const prevCenterY = (prevCoords[1] + prevCoords[3]) / 2;
const dX = prevCenterX - nextCenterX; const dX = prevCenterX - nextCenterX;
const dY = prevCenterY - nextCenterY; const dY = prevCenterY - nextCenterY;
const rotated = pointRotateRads( const rotatedOffset = pointRotateRads(
pointFrom(offsetX, offsetY), pointFrom(offsetX, offsetY),
pointFrom(dX, dY), pointFrom(dX, dY),
element.angle, element.angle,
@ -1587,8 +1607,8 @@ export class LinearElementEditor {
scene.mutateElement(element, { scene.mutateElement(element, {
...otherUpdates, ...otherUpdates,
points: nextPoints, points: nextPoints,
x: element.x + rotated[0], x: element.x + rotatedOffset[0],
y: element.y + rotated[1], y: element.y + rotatedOffset[1],
}); });
} }
} }

View File

@ -296,8 +296,10 @@ export type FixedPointBinding = Merge<
} }
>; >;
type Index = number;
export type PointsPositionUpdates = Map< export type PointsPositionUpdates = Map<
number, Index,
{ point: LocalPoint; isDragging?: boolean } { point: LocalPoint; isDragging?: boolean }
>; >;
@ -335,7 +337,7 @@ export type ExcalidrawLineElement = ExcalidrawLinearElement &
export type FixedSegment = { export type FixedSegment = {
start: LocalPoint; start: LocalPoint;
end: LocalPoint; end: LocalPoint;
index: number; index: Index;
}; };
export type ExcalidrawArrowElement = ExcalidrawLinearElement & export type ExcalidrawArrowElement = ExcalidrawLinearElement &

View File

@ -324,7 +324,8 @@ const restoreElement = (
: element.points; : element.points;
if (points[0][0] !== 0 || points[0][1] !== 0) { if (points[0][0] !== 0 || points[0][1] !== 0) {
({ points, x, y } = LinearElementEditor.getNormalizedPoints(element)); ({ points, x, y } =
LinearElementEditor.getNormalizeElementPointsAndCoords(element));
} }
return restoreElementWithProperties(element, { return restoreElementWithProperties(element, {
@ -355,7 +356,8 @@ const restoreElement = (
: element.points; : element.points;
if (points[0][0] !== 0 || points[0][1] !== 0) { if (points[0][0] !== 0 || points[0][1] !== 0) {
({ points, x, y } = LinearElementEditor.getNormalizedPoints(element)); ({ points, x, y } =
LinearElementEditor.getNormalizeElementPointsAndCoords(element));
} }
const base = { const base = {

View File

@ -469,7 +469,7 @@ const bindLinearElementToElement = (
Object.assign( Object.assign(
linearElement, linearElement,
LinearElementEditor.getNormalizedPoints({ LinearElementEditor.getNormalizeElementPointsAndCoords({
...linearElement, ...linearElement,
points: newPoints, points: newPoints,
}), }),