mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
perf: reduce unnecessary frame clippings (#8980)
* reduce unnecessary frame clippings * further optim
This commit is contained in:
parent
ec06fbc1fc
commit
dd1b45a25a
2 changed files with 152 additions and 46 deletions
|
@ -95,12 +95,11 @@ export const getElementsCompletelyInFrame = (
|
||||||
);
|
);
|
||||||
|
|
||||||
export const isElementContainingFrame = (
|
export const isElementContainingFrame = (
|
||||||
elements: readonly ExcalidrawElement[],
|
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
frame: ExcalidrawFrameLikeElement,
|
frame: ExcalidrawFrameLikeElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
) => {
|
) => {
|
||||||
return getElementsWithinSelection(elements, element, elementsMap).some(
|
return getElementsWithinSelection([frame], element, elementsMap).some(
|
||||||
(e) => e.id === frame.id,
|
(e) => e.id === frame.id,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -144,7 +143,7 @@ export const elementOverlapsWithFrame = (
|
||||||
return (
|
return (
|
||||||
elementsAreInFrameBounds([element], frame, elementsMap) ||
|
elementsAreInFrameBounds([element], frame, elementsMap) ||
|
||||||
isElementIntersectingFrame(element, frame, elementsMap) ||
|
isElementIntersectingFrame(element, frame, elementsMap) ||
|
||||||
isElementContainingFrame([frame], element, frame, elementsMap)
|
isElementContainingFrame(element, frame, elementsMap)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -283,7 +282,7 @@ export const getElementsInResizingFrame = (
|
||||||
const elementsCompletelyInFrame = new Set([
|
const elementsCompletelyInFrame = new Set([
|
||||||
...getElementsCompletelyInFrame(allElements, frame, elementsMap),
|
...getElementsCompletelyInFrame(allElements, frame, elementsMap),
|
||||||
...prevElementsInFrame.filter((element) =>
|
...prevElementsInFrame.filter((element) =>
|
||||||
isElementContainingFrame(allElements, element, frame, elementsMap),
|
isElementContainingFrame(element, frame, elementsMap),
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -695,13 +694,30 @@ export const isElementInFrame = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
allElementsMap: ElementsMap,
|
allElementsMap: ElementsMap,
|
||||||
appState: StaticCanvasAppState,
|
appState: StaticCanvasAppState,
|
||||||
|
opts?: {
|
||||||
|
targetFrame?: ExcalidrawFrameLikeElement;
|
||||||
|
checkedGroups?: Map<string, boolean>;
|
||||||
|
},
|
||||||
) => {
|
) => {
|
||||||
const frame = getTargetFrame(element, allElementsMap, appState);
|
const frame =
|
||||||
|
opts?.targetFrame ?? getTargetFrame(element, allElementsMap, appState);
|
||||||
|
|
||||||
|
if (!frame) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const _element = isTextElement(element)
|
const _element = isTextElement(element)
|
||||||
? getContainerElement(element, allElementsMap) || element
|
? getContainerElement(element, allElementsMap) || element
|
||||||
: element;
|
: element;
|
||||||
|
|
||||||
if (frame) {
|
const setGroupsInFrame = (isInFrame: boolean) => {
|
||||||
|
if (opts?.checkedGroups) {
|
||||||
|
_element.groupIds.forEach((groupId) => {
|
||||||
|
opts.checkedGroups?.set(groupId, isInFrame);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Perf improvement:
|
// Perf improvement:
|
||||||
// For an element that's already in a frame, if it's not being dragged
|
// For an element that's already in a frame, if it's not being dragged
|
||||||
// then there is no need to refer to geometry (which, yes, is slow) to check if it's in a frame.
|
// then there is no need to refer to geometry (which, yes, is slow) to check if it's in a frame.
|
||||||
|
@ -717,10 +733,21 @@ export const isElementInFrame = (
|
||||||
return elementOverlapsWithFrame(_element, frame, allElementsMap);
|
return elementOverlapsWithFrame(_element, frame, allElementsMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const gid of _element.groupIds) {
|
||||||
|
if (opts?.checkedGroups?.has(gid)) {
|
||||||
|
return opts.checkedGroups.get(gid)!!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const allElementsInGroup = new Set(
|
const allElementsInGroup = new Set(
|
||||||
_element.groupIds.flatMap((gid) =>
|
_element.groupIds
|
||||||
getElementsInGroup(allElementsMap, gid),
|
.filter((gid) => {
|
||||||
),
|
if (opts?.checkedGroups) {
|
||||||
|
return !opts.checkedGroups.has(gid);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.flatMap((gid) => getElementsInGroup(allElementsMap, gid)),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (appState.editingGroupId && appState.selectedElementsAreBeingDragged) {
|
if (appState.editingGroupId && appState.selectedElementsAreBeingDragged) {
|
||||||
|
@ -741,15 +768,76 @@ export const isElementInFrame = (
|
||||||
|
|
||||||
for (const elementInGroup of allElementsInGroup) {
|
for (const elementInGroup of allElementsInGroup) {
|
||||||
if (isFrameLikeElement(elementInGroup)) {
|
if (isFrameLikeElement(elementInGroup)) {
|
||||||
|
setGroupsInFrame(false);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const elementInGroup of allElementsInGroup) {
|
for (const elementInGroup of allElementsInGroup) {
|
||||||
if (elementOverlapsWithFrame(elementInGroup, frame, allElementsMap)) {
|
if (elementOverlapsWithFrame(elementInGroup, frame, allElementsMap)) {
|
||||||
|
setGroupsInFrame(true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const shouldApplyFrameClip = (
|
||||||
|
element: ExcalidrawElement,
|
||||||
|
frame: ExcalidrawFrameLikeElement,
|
||||||
|
appState: StaticCanvasAppState,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
checkedGroups?: Map<string, boolean>,
|
||||||
|
) => {
|
||||||
|
if (!appState.frameRendering || !appState.frameRendering.clip) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// for individual elements, only clip when the element is
|
||||||
|
// a. overlapping with the frame, or
|
||||||
|
// b. containing the frame, for example when an element is used as a background
|
||||||
|
// and is therefore bigger than the frame and completely contains the frame
|
||||||
|
const shouldClipElementItself =
|
||||||
|
isElementIntersectingFrame(element, frame, elementsMap) ||
|
||||||
|
isElementContainingFrame(element, frame, elementsMap);
|
||||||
|
|
||||||
|
if (shouldClipElementItself) {
|
||||||
|
for (const groupId of element.groupIds) {
|
||||||
|
checkedGroups?.set(groupId, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if an element is outside the frame, but is part of a group that has some elements
|
||||||
|
// "in" the frame, we should clip the element
|
||||||
|
if (
|
||||||
|
!shouldClipElementItself &&
|
||||||
|
element.groupIds.length > 0 &&
|
||||||
|
!elementsAreInFrameBounds([element], frame, elementsMap)
|
||||||
|
) {
|
||||||
|
let shouldClip = false;
|
||||||
|
|
||||||
|
// if no elements are being dragged, we can skip the geometry check
|
||||||
|
// because we know if the element is in the given frame or not
|
||||||
|
if (!appState.selectedElementsAreBeingDragged) {
|
||||||
|
shouldClip = element.frameId === frame.id;
|
||||||
|
for (const groupId of element.groupIds) {
|
||||||
|
checkedGroups?.set(groupId, shouldClip);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
shouldClip = isElementInFrame(element, elementsMap, appState, {
|
||||||
|
targetFrame: frame,
|
||||||
|
checkedGroups,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const groupId of element.groupIds) {
|
||||||
|
checkedGroups?.set(groupId, shouldClip);
|
||||||
|
}
|
||||||
|
|
||||||
|
return shouldClip;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { getElementAbsoluteCoords } from "../element";
|
||||||
import {
|
import {
|
||||||
elementOverlapsWithFrame,
|
elementOverlapsWithFrame,
|
||||||
getTargetFrame,
|
getTargetFrame,
|
||||||
isElementInFrame,
|
shouldApplyFrameClip,
|
||||||
} from "../frame";
|
} from "../frame";
|
||||||
import {
|
import {
|
||||||
isEmbeddableElement,
|
isEmbeddableElement,
|
||||||
|
@ -273,6 +273,8 @@ const _renderStaticScene = ({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const inFrameGroupsMap = new Map<string, boolean>();
|
||||||
|
|
||||||
// Paint visible elements
|
// Paint visible elements
|
||||||
visibleElements
|
visibleElements
|
||||||
.filter((el) => !isIframeLikeElement(el))
|
.filter((el) => !isIframeLikeElement(el))
|
||||||
|
@ -297,9 +299,16 @@ const _renderStaticScene = ({
|
||||||
appState.frameRendering.clip
|
appState.frameRendering.clip
|
||||||
) {
|
) {
|
||||||
const frame = getTargetFrame(element, elementsMap, appState);
|
const frame = getTargetFrame(element, elementsMap, appState);
|
||||||
|
if (
|
||||||
// TODO do we need to check isElementInFrame here?
|
frame &&
|
||||||
if (frame && isElementInFrame(element, elementsMap, appState)) {
|
shouldApplyFrameClip(
|
||||||
|
element,
|
||||||
|
frame,
|
||||||
|
appState,
|
||||||
|
elementsMap,
|
||||||
|
inFrameGroupsMap,
|
||||||
|
)
|
||||||
|
) {
|
||||||
frameClip(frame, context, renderConfig, appState);
|
frameClip(frame, context, renderConfig, appState);
|
||||||
}
|
}
|
||||||
renderElement(
|
renderElement(
|
||||||
|
@ -400,7 +409,16 @@ const _renderStaticScene = ({
|
||||||
|
|
||||||
const frame = getTargetFrame(element, elementsMap, appState);
|
const frame = getTargetFrame(element, elementsMap, appState);
|
||||||
|
|
||||||
if (frame && isElementInFrame(element, elementsMap, appState)) {
|
if (
|
||||||
|
frame &&
|
||||||
|
shouldApplyFrameClip(
|
||||||
|
element,
|
||||||
|
frame,
|
||||||
|
appState,
|
||||||
|
elementsMap,
|
||||||
|
inFrameGroupsMap,
|
||||||
|
)
|
||||||
|
) {
|
||||||
frameClip(frame, context, renderConfig, appState);
|
frameClip(frame, context, renderConfig, appState);
|
||||||
}
|
}
|
||||||
render();
|
render();
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue