perf(snapping): reduce line point snap scanning

This commit is contained in:
Ryan Di
2026-04-28 17:06:54 +10:00
parent 309849925d
commit 79e802d9ed
3 changed files with 174 additions and 105 deletions
@@ -1944,7 +1944,6 @@ export class LinearElementEditor {
const { snapOffset, snapLines: nextSnapLines } = snapLinearElementPoint( const { snapOffset, snapLines: nextSnapLines } = snapLinearElementPoint(
app.scene.getNonDeletedElements(), app.scene.getNonDeletedElements(),
element, element,
pointIndex,
pointFrom<GlobalPoint>(effectiveGridX, effectiveGridY), pointFrom<GlobalPoint>(effectiveGridX, effectiveGridY),
app, app,
event, event,
@@ -1997,7 +1996,6 @@ export class LinearElementEditor {
const { snapOffset, snapLines } = snapLinearElementPoint( const { snapOffset, snapLines } = snapLinearElementPoint(
app.scene.getNonDeletedElements(), app.scene.getNonDeletedElements(),
element, element,
pointIndex,
pointFrom(originalPointerX, originalPointerY), pointFrom(originalPointerX, originalPointerY),
app, app,
event, event,
+173 -101
View File
@@ -130,6 +130,11 @@ export type SnapLine = PointSnapLine | GapSnapLine | PointerSnapLine;
export class SnapCache { export class SnapCache {
private static referenceSnapPoints: GlobalPoint[] | null = null; private static referenceSnapPoints: GlobalPoint[] | null = null;
private static linearElementPointReferenceSnapPoints: {
editingElementId: ExcalidrawElement["id"];
snapPoints: GlobalPoint[];
} | null = null;
private static visibleGaps: { private static visibleGaps: {
verticalGaps: Gap[]; verticalGaps: Gap[];
horizontalGaps: Gap[]; horizontalGaps: Gap[];
@@ -143,6 +148,27 @@ export class SnapCache {
return SnapCache.referenceSnapPoints; return SnapCache.referenceSnapPoints;
}; };
public static setLinearElementPointReferenceSnapPoints = (
editingElementId: ExcalidrawElement["id"],
snapPoints: GlobalPoint[] | null,
) => {
SnapCache.linearElementPointReferenceSnapPoints = snapPoints
? {
editingElementId,
snapPoints,
}
: null;
};
public static getLinearElementPointReferenceSnapPoints = (
editingElementId: ExcalidrawElement["id"],
) => {
return SnapCache.linearElementPointReferenceSnapPoints?.editingElementId ===
editingElementId
? SnapCache.linearElementPointReferenceSnapPoints.snapPoints
: null;
};
public static setVisibleGaps = ( public static setVisibleGaps = (
gaps: { gaps: {
verticalGaps: Gap[]; verticalGaps: Gap[];
@@ -158,6 +184,7 @@ export class SnapCache {
public static destroy = () => { public static destroy = () => {
SnapCache.referenceSnapPoints = null; SnapCache.referenceSnapPoints = null;
SnapCache.linearElementPointReferenceSnapPoints = null;
SnapCache.visibleGaps = null; SnapCache.visibleGaps = null;
}; };
} }
@@ -654,10 +681,46 @@ export const getReferenceSnapPoints = (
.flatMap((elementGroup) => getElementsCorners(elementGroup, elementsMap)); .flatMap((elementGroup) => getElementsCorners(elementGroup, elementsMap));
}; };
const getExternalReferenceSnapPointsForLinearElementPoint = (
elements: readonly NonDeletedExcalidrawElement[],
editingElement: ExcalidrawLinearElement,
appState: AppState,
elementsMap: ElementsMap,
) => {
const cachedReferenceSnapPoints =
SnapCache.getLinearElementPointReferenceSnapPoints(editingElement.id);
const externalReferenceSnapPoints =
cachedReferenceSnapPoints ??
getReferenceSnapPoints(elements, [editingElement], appState, elementsMap);
if (!cachedReferenceSnapPoints) {
SnapCache.setLinearElementPointReferenceSnapPoints(
editingElement.id,
externalReferenceSnapPoints,
);
}
return externalReferenceSnapPoints;
};
const getSelfReferenceSnapPointsForLinearElementPoint = (
editingElement: ExcalidrawLinearElement,
elementsMap: ElementsMap,
selectedPointsIndices?: readonly number[],
) => {
return LinearElementEditor.getPointsGlobalCoordinates(
editingElement as NonDeleted<ExcalidrawLinearElement>,
elementsMap,
{
excludePointsIndices: selectedPointsIndices,
},
);
};
export const getReferenceSnapPointsForLinearElementPoint = ( export const getReferenceSnapPointsForLinearElementPoint = (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
editingElement: ExcalidrawLinearElement, editingElement: ExcalidrawLinearElement,
editingPointIndex: number,
appState: AppState, appState: AppState,
elementsMap: ElementsMap, elementsMap: ElementsMap,
options: { options: {
@@ -665,50 +728,77 @@ export const getReferenceSnapPointsForLinearElementPoint = (
selectedPointsIndices?: readonly number[]; selectedPointsIndices?: readonly number[];
} = {}, } = {},
) => { ) => {
const { includeSelfPoints = false } = options; const externalReferenceSnapPoints =
getExternalReferenceSnapPointsForLinearElementPoint(
// Get all reference elements (excluding the one being edited) elements,
const referenceElements = getReferenceElements( editingElement,
elements, appState,
[editingElement],
appState,
elementsMap,
);
const allSnapPoints: GlobalPoint[] = [];
// Add snap points from all reference elements
const referenceGroups = getMaximumGroups(
referenceElements,
elementsMap,
).filter(
(elementsGroup) =>
!(elementsGroup.length === 1 && isBoundToContainer(elementsGroup[0])),
);
for (const elementGroup of referenceGroups) {
allSnapPoints.push(...getElementsCorners(elementGroup, elementsMap));
}
// Include other points from the same linear element when creating new points or in editing mode
if (includeSelfPoints) {
const elementPoints = LinearElementEditor.getPointsGlobalCoordinates(
editingElement as NonDeleted<ExcalidrawLinearElement>,
elementsMap, elementsMap,
{
excludePointsIndices: options.selectedPointsIndices,
},
); );
allSnapPoints.push(...elementPoints);
if (!options.includeSelfPoints) {
return externalReferenceSnapPoints;
} }
return allSnapPoints; return externalReferenceSnapPoints.concat(
getSelfReferenceSnapPointsForLinearElementPoint(
editingElement,
elementsMap,
options.selectedPointsIndices,
),
);
};
const addNearestPointSnaps = (
referenceSnapPoints: readonly GlobalPoint[],
pointerPosition: GlobalPoint,
nearestSnapsX: Snaps,
nearestSnapsY: Snaps,
minOffset: Vector2D,
) => {
for (const referencePoint of referenceSnapPoints) {
const offsetX = referencePoint[0] - pointerPosition[0];
const offsetY = referencePoint[1] - pointerPosition[1];
const absOffsetX = Math.abs(offsetX);
const absOffsetY = Math.abs(offsetY);
if (absOffsetX > minOffset.x && absOffsetY > minOffset.y) {
continue;
}
if (absOffsetX <= minOffset.x) {
if (absOffsetX < minOffset.x) {
nearestSnapsX.length = 0;
}
nearestSnapsX.push({
type: "point",
points: [pointerPosition, referencePoint],
offset: offsetX,
});
minOffset.x = absOffsetX;
}
if (absOffsetY <= minOffset.y) {
if (absOffsetY < minOffset.y) {
nearestSnapsY.length = 0;
}
nearestSnapsY.push({
type: "point",
points: [pointerPosition, referencePoint],
offset: offsetY,
});
minOffset.y = absOffsetY;
}
}
}; };
export const snapLinearElementPoint = ( export const snapLinearElementPoint = (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
editingElement: ExcalidrawLinearElement, editingElement: ExcalidrawLinearElement,
editingPointIndex: number,
pointerPosition: GlobalPoint, pointerPosition: GlobalPoint,
app: AppClassProperties, app: AppClassProperties,
event: KeyboardModifiersObject, event: KeyboardModifiersObject,
@@ -737,48 +827,31 @@ export const snapLinearElementPoint = (
const nearestSnapsX: Snaps = []; const nearestSnapsX: Snaps = [];
const nearestSnapsY: Snaps = []; const nearestSnapsY: Snaps = [];
// Get reference snap points (all elements except the current point) addNearestPointSnaps(
const referenceSnapPoints = getReferenceSnapPointsForLinearElementPoint( getExternalReferenceSnapPointsForLinearElementPoint(
elements, elements,
editingElement, editingElement,
editingPointIndex, app.state,
app.state, elementsMap,
elementsMap, ),
options, pointerPosition,
nearestSnapsX,
nearestSnapsY,
minOffset,
); );
// Find nearest snaps if (options.includeSelfPoints) {
for (const referencePoint of referenceSnapPoints) { addNearestPointSnaps(
const offsetX = referencePoint[0] - pointerPosition[0]; getSelfReferenceSnapPointsForLinearElementPoint(
const offsetY = referencePoint[1] - pointerPosition[1]; editingElement,
elementsMap,
if (Math.abs(offsetX) <= minOffset.x) { options.selectedPointsIndices,
if (Math.abs(offsetX) < minOffset.x) { ),
nearestSnapsX.length = 0; pointerPosition,
} nearestSnapsX,
nearestSnapsY,
nearestSnapsX.push({ minOffset,
type: "point", );
points: [pointerPosition, referencePoint],
offset: offsetX,
});
minOffset.x = Math.abs(offsetX);
}
if (Math.abs(offsetY) <= minOffset.y) {
if (Math.abs(offsetY) < minOffset.y) {
nearestSnapsY.length = 0;
}
nearestSnapsY.push({
type: "point",
points: [pointerPosition, referencePoint],
offset: offsetY,
});
minOffset.y = Math.abs(offsetY);
}
} }
const snapOffset = { const snapOffset = {
@@ -790,37 +863,36 @@ export const snapLinearElementPoint = (
let pointSnapLines: SnapLine[] = []; let pointSnapLines: SnapLine[] = [];
if (snapOffset.x !== 0 || snapOffset.y !== 0) { if (snapOffset.x !== 0 || snapOffset.y !== 0) {
// Recalculate snap lines with the snapped position
const snappedPosition = pointFrom<GlobalPoint>( const snappedPosition = pointFrom<GlobalPoint>(
pointerPosition[0] + snapOffset.x, pointerPosition[0] + snapOffset.x,
pointerPosition[1] + snapOffset.y, pointerPosition[1] + snapOffset.y,
); );
const snappedSnapsX: Snaps = []; const snappedSnapsX = nearestSnapsX
const snappedSnapsY: Snaps = []; .filter(
(
snap,
): snap is PointSnap =>
snap.type === "point" && isCloseTo(snap.offset, snapOffset.x, 0.01),
)
.map((snap) => ({
type: "point" as const,
points: [snappedPosition, snap.points[1]] as [GlobalPoint, GlobalPoint],
offset: 0,
}));
// Find the reference points that we're snapping to const snappedSnapsY = nearestSnapsY
for (const referencePoint of referenceSnapPoints) { .filter(
const offsetX = referencePoint[0] - snappedPosition[0]; (
const offsetY = referencePoint[1] - snappedPosition[1]; snap,
): snap is PointSnap =>
// Only include points that we're actually snapping to snap.type === "point" && isCloseTo(snap.offset, snapOffset.y, 0.01),
if (isCloseTo(offsetX, 0, 0.01)) { )
snappedSnapsX.push({ .map((snap) => ({
type: "point", type: "point" as const,
points: [snappedPosition, referencePoint], points: [snappedPosition, snap.points[1]] as [GlobalPoint, GlobalPoint],
offset: 0, offset: 0,
}); }));
}
if (isCloseTo(offsetY, 0, 0.01)) {
snappedSnapsY.push({
type: "point",
points: [snappedPosition, referencePoint],
offset: 0,
});
}
}
pointSnapLines = createPointSnapLines(snappedSnapsX, snappedSnapsY); pointSnapLines = createPointSnapLines(snappedSnapsX, snappedSnapsY);
} }
+1 -2
View File
@@ -10462,8 +10462,7 @@ class App extends React.Component<AppProps, AppState> {
this.lassoTrail.endPath(); this.lassoTrail.endPath();
this.previousPointerMoveCoords = null; this.previousPointerMoveCoords = null;
SnapCache.setReferenceSnapPoints(null); SnapCache.destroy();
SnapCache.setVisibleGaps(null);
this.savePointer(childEvent.clientX, childEvent.clientY, "up"); this.savePointer(childEvent.clientX, childEvent.clientY, "up");