merge: with master

This commit is contained in:
Ryan Di
2025-06-24 21:00:06 +10:00
98 changed files with 7691 additions and 5935 deletions

View File

@ -9,6 +9,8 @@ import {
vectorFromPoint,
line,
linesIntersectAt,
curveLength,
curvePointAtLength,
} from "@excalidraw/math";
import { getCurvePathOps } from "@excalidraw/utils/shape";
@ -20,14 +22,16 @@ import {
getGridPoint,
invariant,
tupleToCoors,
viewportCoordsToSceneCoords,
} from "@excalidraw/common";
import {
type SnapLine,
deconstructLinearOrFreeDrawElement,
isPathALoop,
snapLinearElementPoint,
} from "@excalidraw/element/snapping";
import { ShapeCache, type Store } from "@excalidraw/element";
type SnapLine,
type Store,
} from "@excalidraw/element";
import type { Radians } from "@excalidraw/math";
@ -46,6 +50,7 @@ import {
bindOrUnbindLinearElement,
getHoveredElementForBinding,
isBindingEnabled,
maybeSuggestBindingsForLinearElementAtCoords,
} from "./binding";
import {
getElementAbsoluteCoords,
@ -62,14 +67,7 @@ import {
isFixedPointBinding,
} from "./typeChecks";
import {
isPathALoop,
getBezierCurveLength,
getControlPointsForBezierCurve,
mapIntervalToBezierT,
getBezierXY,
toggleLinePolygonState,
} from "./shapes";
import { ShapeCache, toggleLinePolygonState } from "./shape";
import { getLockedLinearCursorAlignSize } from "./sizeHelpers";
@ -155,7 +153,6 @@ export class LinearElementEditor {
public readonly segmentMidPointHoveredCoords: GlobalPoint | null;
public readonly elbowed: boolean;
public readonly customLineAngle: number | null;
public readonly snapLines: readonly SnapLine[];
constructor(
element: NonDeleted<ExcalidrawLinearElement>,
@ -194,7 +191,6 @@ export class LinearElementEditor {
this.segmentMidPointHoveredCoords = null;
this.elbowed = isElbowArrow(element) && element.elbowed;
this.customLineAngle = null;
this.snapLines = [];
}
// ---------------------------------------------------------------------------
@ -285,18 +281,13 @@ export class LinearElementEditor {
app: AppClassProperties,
scenePointerX: number,
scenePointerY: number,
maybeSuggestBinding: (
element: NonDeleted<ExcalidrawLinearElement>,
pointSceneCoords: { x: number; y: number }[],
) => void,
linearElementEditor: LinearElementEditor,
scene: Scene,
): LinearElementEditor | null {
): Pick<AppState, keyof AppState> | null {
if (!linearElementEditor) {
return null;
}
const { elementId } = linearElementEditor;
const elementsMap = scene.getNonDeletedElementsMap();
const elementsMap = app.scene.getNonDeletedElementsMap();
const element = LinearElementEditor.getElement(elementId, elementsMap);
let customLineAngle = linearElementEditor.customLineAngle;
if (!element) {
@ -386,7 +377,7 @@ export class LinearElementEditor {
if (!isElbowArrow(element)) {
const { snapOffset, snapLines } = snapLinearElementPoint(
scene.getNonDeletedElements(),
app.scene.getNonDeletedElements(),
element,
lastClickedPoint,
{ x: effectiveGridX, y: effectiveGridY },
@ -464,7 +455,7 @@ export class LinearElementEditor {
LinearElementEditor.movePoints(
element,
scene,
app.scene,
new Map([
[
selectedIndex,
@ -483,7 +474,7 @@ export class LinearElementEditor {
scenePointerY - linearElementEditor.pointerOffset.y;
const { snapOffset, snapLines } = snapLinearElementPoint(
scene.getNonDeletedElements(),
app.scene.getNonDeletedElements(),
element,
lastClickedPoint,
{ x: originalPointerX, y: originalPointerY },
@ -512,7 +503,7 @@ export class LinearElementEditor {
LinearElementEditor.movePoints(
element,
scene,
app.scene,
new Map(
selectedPointsIndices.map((pointIndex) => {
const newPointPosition: LocalPoint =
@ -536,46 +527,59 @@ export class LinearElementEditor {
const boundTextElement = getBoundTextElement(element, elementsMap);
if (boundTextElement) {
handleBindTextResize(element, scene, false);
handleBindTextResize(element, app.scene, false);
}
// suggest bindings for first and last point if selected
let suggestedBindings: ExcalidrawBindableElement[] = [];
if (isBindingElement(element, false)) {
const firstSelectedIndex = selectedPointsIndices[0] === 0;
const lastSelectedIndex =
selectedPointsIndices[selectedPointsIndices.length - 1] ===
element.points.length - 1;
const coords: { x: number; y: number }[] = [];
const firstSelectedIndex = selectedPointsIndices[0];
if (firstSelectedIndex === 0) {
coords.push(
tupleToCoors(
LinearElementEditor.getPointGlobalCoordinates(
element,
element.points[0],
elementsMap,
if (!firstSelectedIndex !== !lastSelectedIndex) {
coords.push({ x: scenePointerX, y: scenePointerY });
} else {
if (firstSelectedIndex) {
coords.push(
tupleToCoors(
LinearElementEditor.getPointGlobalCoordinates(
element,
element.points[0],
elementsMap,
),
),
),
);
}
);
}
const lastSelectedIndex =
selectedPointsIndices[selectedPointsIndices.length - 1];
if (lastSelectedIndex === element.points.length - 1) {
coords.push(
tupleToCoors(
LinearElementEditor.getPointGlobalCoordinates(
element,
element.points[lastSelectedIndex],
elementsMap,
if (lastSelectedIndex) {
coords.push(
tupleToCoors(
LinearElementEditor.getPointGlobalCoordinates(
element,
element.points[
selectedPointsIndices[selectedPointsIndices.length - 1]
],
elementsMap,
),
),
),
);
);
}
}
if (coords.length) {
maybeSuggestBinding(element, coords);
suggestedBindings = maybeSuggestBindingsForLinearElementAtCoords(
element,
coords,
app.scene,
app.state.zoom,
);
}
}
return {
const newLinearElementEditor = {
...linearElementEditor,
selectedPointsIndices,
segmentMidPointHoveredCoords:
@ -587,7 +591,6 @@ export class LinearElementEditor {
elementsMap,
)
: null,
snapLines: _snapLines,
hoverPointIndex:
lastClickedPoint === 0 ||
lastClickedPoint === element.points.length - 1
@ -596,6 +599,16 @@ export class LinearElementEditor {
isDragging: true,
customLineAngle,
};
return {
...app.state,
editingLinearElement: app.state.editingLinearElement
? newLinearElementEditor
: null,
selectedLinearElement: newLinearElementEditor,
suggestedBindings,
snapLines: _snapLines,
};
}
return null;
@ -609,6 +622,7 @@ export class LinearElementEditor {
): LinearElementEditor {
const elementsMap = scene.getNonDeletedElementsMap();
const elements = scene.getNonDeletedElements();
const pointerCoords = viewportCoordsToSceneCoords(event, appState);
const { elementId, selectedPointsIndices, isDragging, pointerDownState } =
editingLinearElement;
@ -664,13 +678,15 @@ export class LinearElementEditor {
const bindingElement = isBindingEnabled(appState)
? getHoveredElementForBinding(
tupleToCoors(
LinearElementEditor.getPointAtIndexGlobalCoordinates(
element,
selectedPoint!,
elementsMap,
),
),
(selectedPointsIndices?.length ?? 0) > 1
? tupleToCoors(
LinearElementEditor.getPointAtIndexGlobalCoordinates(
element,
selectedPoint!,
elementsMap,
),
)
: pointerCoords,
elements,
elementsMap,
appState.zoom,
@ -756,10 +772,7 @@ export class LinearElementEditor {
}
const segmentMidPoint = LinearElementEditor.getSegmentMidPoint(
element,
points[index],
points[index + 1],
index + 1,
elementsMap,
);
midpoints.push(segmentMidPoint);
index++;
@ -861,7 +874,18 @@ export class LinearElementEditor {
let distance = pointDistance(startPoint, endPoint);
if (element.points.length > 2 && element.roundness) {
distance = getBezierCurveLength(element, endPoint);
const [lines, curves] = deconstructLinearOrFreeDrawElement(element);
invariant(
lines.length === 0 && curves.length > 0,
"Only linears built out of curves are supported",
);
invariant(
lines.length + curves.length >= index,
"Invalid segment index while calculating mid point",
);
distance = curveLength<GlobalPoint>(curves[index]);
}
return distance * zoom.value < LinearElementEditor.POINT_HANDLE_SIZE * 4;
@ -869,39 +893,42 @@ export class LinearElementEditor {
static getSegmentMidPoint(
element: NonDeleted<ExcalidrawLinearElement>,
startPoint: GlobalPoint,
endPoint: GlobalPoint,
endPointIndex: number,
elementsMap: ElementsMap,
index: number,
): GlobalPoint {
let segmentMidPoint = pointCenter(startPoint, endPoint);
if (element.points.length > 2 && element.roundness) {
const controlPoints = getControlPointsForBezierCurve(
element,
element.points[endPointIndex],
if (isElbowArrow(element)) {
invariant(
element.points.length >= index,
"Invalid segment index while calculating elbow arrow mid point",
);
if (controlPoints) {
const t = mapIntervalToBezierT(
element,
element.points[endPointIndex],
0.5,
);
segmentMidPoint = LinearElementEditor.getPointGlobalCoordinates(
element,
getBezierXY(
controlPoints[0],
controlPoints[1],
controlPoints[2],
controlPoints[3],
t,
),
elementsMap,
);
}
const p = pointCenter(element.points[index - 1], element.points[index]);
return pointFrom<GlobalPoint>(element.x + p[0], element.y + p[1]);
}
return segmentMidPoint;
const [lines, curves] = deconstructLinearOrFreeDrawElement(element);
invariant(
(lines.length === 0 && curves.length > 0) ||
(lines.length > 0 && curves.length === 0),
"Only linears built out of either segments or curves are supported",
);
invariant(
lines.length + curves.length >= index,
"Invalid segment index while calculating mid point",
);
if (lines.length) {
const segment = lines[index - 1];
return pointCenter(segment[0], segment[1]);
}
if (curves.length) {
const segment = curves[index - 1];
return curvePointAtLength(segment, 0.5);
}
invariant(false, "Invalid segment type while calculating mid point");
}
static getSegmentMidPointIndex(
@ -1118,7 +1145,10 @@ export class LinearElementEditor {
scenePointerX: number,
scenePointerY: number,
app: AppClassProperties,
): LinearElementEditor | null {
): {
linearElementEditor: LinearElementEditor;
snapLines: readonly SnapLine[];
} | null {
const appState = app.state;
if (!appState.editingLinearElement) {
return null;
@ -1127,7 +1157,10 @@ export class LinearElementEditor {
const elementsMap = app.scene.getNonDeletedElementsMap();
const element = LinearElementEditor.getElement(elementId, elementsMap);
if (!element) {
return appState.editingLinearElement;
return {
linearElementEditor: appState.editingLinearElement,
snapLines: appState.snapLines,
};
}
const { points } = element;
@ -1138,8 +1171,12 @@ export class LinearElementEditor {
LinearElementEditor.deletePoints(element, app, [points.length - 1]);
}
return {
...appState.editingLinearElement,
lastUncommittedPoint: null,
linearElementEditor: {
...appState.editingLinearElement,
lastUncommittedPoint: null,
isDragging: false,
pointerOffset: { x: 0, y: 0 },
},
snapLines: [],
};
}
@ -1315,8 +1352,10 @@ export class LinearElementEditor {
}
return {
...appState.editingLinearElement,
lastUncommittedPoint: element.points[element.points.length - 1],
linearElementEditor: {
...appState.editingLinearElement,
lastUncommittedPoint: element.points[element.points.length - 1],
},
snapLines,
};
}
@ -1924,10 +1963,7 @@ export class LinearElementEditor {
const index = element.points.length / 2 - 1;
const midSegmentMidpoint = LinearElementEditor.getSegmentMidPoint(
element,
points[index],
points[index + 1],
index + 1,
elementsMap,
);
x = midSegmentMidpoint[0] - boundTextElement.width / 2;