|
|
@ -1,8 +1,4 @@
|
|
|
|
import {
|
|
|
|
import { getCommonBounds, getElementBounds, isTextElement } from "./element";
|
|
|
|
getCommonBounds,
|
|
|
|
|
|
|
|
getElementAbsoluteCoords,
|
|
|
|
|
|
|
|
isTextElement,
|
|
|
|
|
|
|
|
} from "./element";
|
|
|
|
|
|
|
|
import {
|
|
|
|
import {
|
|
|
|
ExcalidrawElement,
|
|
|
|
ExcalidrawElement,
|
|
|
|
ExcalidrawFrameElement,
|
|
|
|
ExcalidrawFrameElement,
|
|
|
@ -56,6 +52,7 @@ export const bindElementsToFramesAfterDuplication = (
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// --------------------------- Frame Geometry ---------------------------------
|
|
|
|
export function isElementIntersectingFrame(
|
|
|
|
export function isElementIntersectingFrame(
|
|
|
|
element: ExcalidrawElement,
|
|
|
|
element: ExcalidrawElement,
|
|
|
|
frame: ExcalidrawFrameElement,
|
|
|
|
frame: ExcalidrawFrameElement,
|
|
|
@ -85,36 +82,27 @@ export const getElementsCompletelyInFrame = (
|
|
|
|
element.frameId === frame.id,
|
|
|
|
element.frameId === frame.id,
|
|
|
|
);
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
export const isElementContainingFrame = (
|
|
|
|
|
|
|
|
elements: readonly ExcalidrawElement[],
|
|
|
|
|
|
|
|
element: ExcalidrawElement,
|
|
|
|
|
|
|
|
frame: ExcalidrawFrameElement,
|
|
|
|
|
|
|
|
) => {
|
|
|
|
|
|
|
|
return getElementsWithinSelection(elements, element).some(
|
|
|
|
|
|
|
|
(e) => e.id === frame.id,
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getElementsIntersectingFrame = (
|
|
|
|
export const getElementsIntersectingFrame = (
|
|
|
|
elements: readonly ExcalidrawElement[],
|
|
|
|
elements: readonly ExcalidrawElement[],
|
|
|
|
frame: ExcalidrawFrameElement,
|
|
|
|
frame: ExcalidrawFrameElement,
|
|
|
|
) => elements.filter((element) => isElementIntersectingFrame(element, frame));
|
|
|
|
) => elements.filter((element) => isElementIntersectingFrame(element, frame));
|
|
|
|
|
|
|
|
|
|
|
|
export const elementsAreInFrameBounds = (
|
|
|
|
export const elementsAreInBounds = (
|
|
|
|
elements: readonly ExcalidrawElement[],
|
|
|
|
elements: readonly ExcalidrawElement[],
|
|
|
|
frame: ExcalidrawFrameElement,
|
|
|
|
element: ExcalidrawElement,
|
|
|
|
|
|
|
|
tolerance = 0,
|
|
|
|
) => {
|
|
|
|
) => {
|
|
|
|
const [selectionX1, selectionY1, selectionX2, selectionY2] =
|
|
|
|
|
|
|
|
getElementAbsoluteCoords(frame);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const [elementX1, elementY1, elementX2, elementY2] =
|
|
|
|
const [elementX1, elementY1, elementX2, elementY2] =
|
|
|
|
|
|
|
|
getElementBounds(element);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const [elementsX1, elementsY1, elementsX2, elementsY2] =
|
|
|
|
getCommonBounds(elements);
|
|
|
|
getCommonBounds(elements);
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
return (
|
|
|
|
selectionX1 <= elementX1 &&
|
|
|
|
elementX1 <= elementsX1 - tolerance &&
|
|
|
|
selectionY1 <= elementY1 &&
|
|
|
|
elementY1 <= elementsY1 - tolerance &&
|
|
|
|
selectionX2 >= elementX2 &&
|
|
|
|
elementX2 >= elementsX2 + tolerance &&
|
|
|
|
selectionY2 >= elementY2
|
|
|
|
elementY2 >= elementsY2 + tolerance
|
|
|
|
);
|
|
|
|
);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
@ -123,9 +111,12 @@ export const elementOverlapsWithFrame = (
|
|
|
|
frame: ExcalidrawFrameElement,
|
|
|
|
frame: ExcalidrawFrameElement,
|
|
|
|
) => {
|
|
|
|
) => {
|
|
|
|
return (
|
|
|
|
return (
|
|
|
|
elementsAreInFrameBounds([element], frame) ||
|
|
|
|
// frame contains element
|
|
|
|
isElementIntersectingFrame(element, frame) ||
|
|
|
|
elementsAreInBounds([element], frame) ||
|
|
|
|
isElementContainingFrame([frame], element, frame)
|
|
|
|
// element contains frame
|
|
|
|
|
|
|
|
(elementsAreInBounds([frame], element) && element.frameId === frame.id) ||
|
|
|
|
|
|
|
|
// element intersects with frame
|
|
|
|
|
|
|
|
isElementIntersectingFrame(element, frame)
|
|
|
|
);
|
|
|
|
);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
@ -136,7 +127,7 @@ export const isCursorInFrame = (
|
|
|
|
},
|
|
|
|
},
|
|
|
|
frame: NonDeleted<ExcalidrawFrameElement>,
|
|
|
|
frame: NonDeleted<ExcalidrawFrameElement>,
|
|
|
|
) => {
|
|
|
|
) => {
|
|
|
|
const [fx1, fy1, fx2, fy2] = getElementAbsoluteCoords(frame);
|
|
|
|
const [fx1, fy1, fx2, fy2] = getElementBounds(frame);
|
|
|
|
|
|
|
|
|
|
|
|
return isPointWithinBounds(
|
|
|
|
return isPointWithinBounds(
|
|
|
|
[fx1, fy1],
|
|
|
|
[fx1, fy1],
|
|
|
@ -160,7 +151,7 @@ export const groupsAreAtLeastIntersectingTheFrame = (
|
|
|
|
|
|
|
|
|
|
|
|
return !!elementsInGroup.find(
|
|
|
|
return !!elementsInGroup.find(
|
|
|
|
(element) =>
|
|
|
|
(element) =>
|
|
|
|
elementsAreInFrameBounds([element], frame) ||
|
|
|
|
elementsAreInBounds([element], frame) ||
|
|
|
|
isElementIntersectingFrame(element, frame),
|
|
|
|
isElementIntersectingFrame(element, frame),
|
|
|
|
);
|
|
|
|
);
|
|
|
|
};
|
|
|
|
};
|
|
|
@ -181,7 +172,7 @@ export const groupsAreCompletelyOutOfFrame = (
|
|
|
|
return (
|
|
|
|
return (
|
|
|
|
elementsInGroup.find(
|
|
|
|
elementsInGroup.find(
|
|
|
|
(element) =>
|
|
|
|
(element) =>
|
|
|
|
elementsAreInFrameBounds([element], frame) ||
|
|
|
|
elementsAreInBounds([element], frame) ||
|
|
|
|
isElementIntersectingFrame(element, frame),
|
|
|
|
isElementIntersectingFrame(element, frame),
|
|
|
|
) === undefined
|
|
|
|
) === undefined
|
|
|
|
);
|
|
|
|
);
|
|
|
@ -249,12 +240,18 @@ export const getElementsInResizingFrame = (
|
|
|
|
const prevElementsInFrame = getFrameChildren(allElements, frame.id);
|
|
|
|
const prevElementsInFrame = getFrameChildren(allElements, frame.id);
|
|
|
|
const nextElementsInFrame = new Set<ExcalidrawElement>(prevElementsInFrame);
|
|
|
|
const nextElementsInFrame = new Set<ExcalidrawElement>(prevElementsInFrame);
|
|
|
|
|
|
|
|
|
|
|
|
const elementsCompletelyInFrame = new Set([
|
|
|
|
const elementsCompletelyInFrame = new Set<ExcalidrawElement>(
|
|
|
|
...getElementsCompletelyInFrame(allElements, frame),
|
|
|
|
getElementsCompletelyInFrame(allElements, frame),
|
|
|
|
...prevElementsInFrame.filter((element) =>
|
|
|
|
);
|
|
|
|
isElementContainingFrame(allElements, element, frame),
|
|
|
|
|
|
|
|
),
|
|
|
|
for (const element of prevElementsInFrame) {
|
|
|
|
]);
|
|
|
|
if (!elementsCompletelyInFrame.has(element)) {
|
|
|
|
|
|
|
|
// element contains the frame
|
|
|
|
|
|
|
|
if (elementsAreInBounds([frame], element)) {
|
|
|
|
|
|
|
|
elementsCompletelyInFrame.add(element);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const elementsNotCompletelyInFrame = prevElementsInFrame.filter(
|
|
|
|
const elementsNotCompletelyInFrame = prevElementsInFrame.filter(
|
|
|
|
(element) => !elementsCompletelyInFrame.has(element),
|
|
|
|
(element) => !elementsCompletelyInFrame.has(element),
|
|
|
@ -321,7 +318,7 @@ export const getElementsInResizingFrame = (
|
|
|
|
if (isSelected) {
|
|
|
|
if (isSelected) {
|
|
|
|
const elementsInGroup = getElementsInGroup(allElements, id);
|
|
|
|
const elementsInGroup = getElementsInGroup(allElements, id);
|
|
|
|
|
|
|
|
|
|
|
|
if (elementsAreInFrameBounds(elementsInGroup, frame)) {
|
|
|
|
if (elementsAreInBounds(elementsInGroup, frame)) {
|
|
|
|
for (const element of elementsInGroup) {
|
|
|
|
for (const element of elementsInGroup) {
|
|
|
|
nextElementsInFrame.add(element);
|
|
|
|
nextElementsInFrame.add(element);
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -509,12 +506,15 @@ export const updateFrameMembershipOfSelectedElements = (
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const elementsToRemove = new Set<ExcalidrawElement>();
|
|
|
|
const elementsToRemove = new Set<ExcalidrawElement>();
|
|
|
|
|
|
|
|
const processedGroupIds = new Map<string, boolean>();
|
|
|
|
|
|
|
|
|
|
|
|
elementsToFilter.forEach((element) => {
|
|
|
|
elementsToFilter.forEach((element) => {
|
|
|
|
if (
|
|
|
|
if (
|
|
|
|
element.frameId &&
|
|
|
|
element.frameId &&
|
|
|
|
!isFrameElement(element) &&
|
|
|
|
!isFrameElement(element) &&
|
|
|
|
!isElementInFrame(element, allElements, appState)
|
|
|
|
!isElementInFrame(element, allElements, appState, {
|
|
|
|
|
|
|
|
processedGroupIds,
|
|
|
|
|
|
|
|
})
|
|
|
|
) {
|
|
|
|
) {
|
|
|
|
elementsToRemove.add(element);
|
|
|
|
elementsToRemove.add(element);
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -576,27 +576,36 @@ export const getTargetFrame = (
|
|
|
|
: getContainingFrame(_element);
|
|
|
|
: getContainingFrame(_element);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// TODO: this a huge bottleneck for large scenes, optimise
|
|
|
|
|
|
|
|
// given an element, return if the element is in some frame
|
|
|
|
// given an element, return if the element is in some frame
|
|
|
|
export const isElementInFrame = (
|
|
|
|
export const isElementInFrame = (
|
|
|
|
element: ExcalidrawElement,
|
|
|
|
element: ExcalidrawElement,
|
|
|
|
allElements: ExcalidrawElementsIncludingDeleted,
|
|
|
|
allElements: ExcalidrawElementsIncludingDeleted,
|
|
|
|
appState: StaticCanvasAppState,
|
|
|
|
appState: StaticCanvasAppState,
|
|
|
|
targetFrame?: ExcalidrawFrameElement,
|
|
|
|
opts?: {
|
|
|
|
|
|
|
|
targetFrame?: ExcalidrawFrameElement;
|
|
|
|
|
|
|
|
processedGroupIds?: Map<string, boolean>;
|
|
|
|
|
|
|
|
},
|
|
|
|
) => {
|
|
|
|
) => {
|
|
|
|
const frame = targetFrame ?? getTargetFrame(element, appState);
|
|
|
|
const frame = opts?.targetFrame ?? getTargetFrame(element, appState);
|
|
|
|
const _element = isTextElement(element)
|
|
|
|
const _element = isTextElement(element)
|
|
|
|
? getContainerElement(element) || element
|
|
|
|
? getContainerElement(element) || element
|
|
|
|
: element;
|
|
|
|
: element;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const groupsInFrame = (yes: boolean) => {
|
|
|
|
|
|
|
|
if (opts?.processedGroupIds) {
|
|
|
|
|
|
|
|
_element.groupIds.forEach((gid) => {
|
|
|
|
|
|
|
|
opts.processedGroupIds?.set(gid, yes);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (frame) {
|
|
|
|
if (frame) {
|
|
|
|
// 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 selected
|
|
|
|
// then there is no need to refer to geometry (which, yes, is slow) to check if it's in a frame.
|
|
|
|
// and its frame is not being selected, it has to be in its containing frame.
|
|
|
|
// It has to be in its containing frame.
|
|
|
|
|
|
|
|
if (
|
|
|
|
if (
|
|
|
|
!appState.selectedElementIds[element.id] ||
|
|
|
|
!appState.selectedElementIds[element.id] &&
|
|
|
|
!appState.selectedElementsAreBeingDragged
|
|
|
|
!appState.selectedElementIds[frame.id]
|
|
|
|
) {
|
|
|
|
) {
|
|
|
|
return true;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -605,8 +614,21 @@ export const isElementInFrame = (
|
|
|
|
return elementOverlapsWithFrame(_element, frame);
|
|
|
|
return elementOverlapsWithFrame(_element, frame);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (const gid of _element.groupIds) {
|
|
|
|
|
|
|
|
if (opts?.processedGroupIds?.has(gid)) {
|
|
|
|
|
|
|
|
return opts.processedGroupIds.get(gid);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const allElementsInGroup = new Set(
|
|
|
|
const allElementsInGroup = new Set(
|
|
|
|
_element.groupIds.flatMap((gid) => getElementsInGroup(allElements, gid)),
|
|
|
|
_element.groupIds
|
|
|
|
|
|
|
|
.filter((gid) => {
|
|
|
|
|
|
|
|
if (opts?.processedGroupIds) {
|
|
|
|
|
|
|
|
return !opts.processedGroupIds.has(gid);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
.flatMap((gid) => getElementsInGroup(allElements, gid)),
|
|
|
|
);
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (appState.editingGroupId && appState.selectedElementsAreBeingDragged) {
|
|
|
|
if (appState.editingGroupId && appState.selectedElementsAreBeingDragged) {
|
|
|
@ -627,16 +649,22 @@ export const isElementInFrame = (
|
|
|
|
|
|
|
|
|
|
|
|
for (const elementInGroup of allElementsInGroup) {
|
|
|
|
for (const elementInGroup of allElementsInGroup) {
|
|
|
|
if (isFrameElement(elementInGroup)) {
|
|
|
|
if (isFrameElement(elementInGroup)) {
|
|
|
|
|
|
|
|
groupsInFrame(false);
|
|
|
|
return false;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (const elementInGroup of allElementsInGroup) {
|
|
|
|
for (const elementInGroup of allElementsInGroup) {
|
|
|
|
if (elementOverlapsWithFrame(elementInGroup, frame)) {
|
|
|
|
if (elementOverlapsWithFrame(elementInGroup, frame)) {
|
|
|
|
|
|
|
|
groupsInFrame(true);
|
|
|
|
return true;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (_element.groupIds.length > 0) {
|
|
|
|
|
|
|
|
groupsInFrame(false);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
return false;
|
|
|
|
};
|
|
|
|
};
|
|
|
|