feat: remove bound-arrows from frames (#7157)

This commit is contained in:
David Luzar 2023-10-17 18:18:20 +02:00 committed by GitHub
parent 5b94cffc74
commit dde3dac931
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 81 additions and 16 deletions

View file

@ -155,7 +155,12 @@ const duplicateElements = (
groupId,
).flatMap((element) =>
isFrameElement(element)
? [...getFrameElements(elements, element.id), element]
? [
...getFrameElements(elements, element.id, {
includeBoundArrows: true,
}),
element,
]
: [element],
);
@ -181,7 +186,9 @@ const duplicateElements = (
continue;
}
if (isElementAFrame) {
const elementsInFrame = getFrameElements(sortedElements, element.id);
const elementsInFrame = getFrameElements(sortedElements, element.id, {
includeBoundArrows: true,
});
elementsWithClones.push(
...markAsProcessed([

View file

@ -2431,18 +2431,12 @@ class App extends React.Component<AppProps, AppState> {
const lineHeight = getDefaultLineHeight(textElementProps.fontFamily);
if (text.length) {
const topLayerFrame = this.getTopLayerFrameAtSceneCoords({
x,
y: currentY,
});
const element = newTextElement({
...textElementProps,
x,
y: currentY,
text,
lineHeight,
frameId: topLayerFrame ? topLayerFrame.id : null,
});
acc.push(element);
currentY += element.height + LINE_GAP;
@ -3456,6 +3450,11 @@ class App extends React.Component<AppProps, AppState> {
return getElementsAtPosition(elements, (element) =>
hitTest(element, this.state, this.frameNameBoundsCache, x, y),
).filter((element) => {
// arrows don't clip even if they're children of frames,
// so always allow hitbox regardless of beinging contained in frame
if (isArrowElement(element)) {
return true;
}
// hitting a frame's element from outside the frame is not considered a hit
const containingFrame = getContainingFrame(element);
return containingFrame &&

View file

@ -43,6 +43,7 @@ import {
measureBaseline,
} from "../element/textElement";
import { normalizeLink } from "./url";
import { isValidFrameChild } from "../frame";
type RestoredAppState = Omit<
AppState,
@ -396,7 +397,7 @@ const repairBoundElement = (
};
/**
* Remove an element's frameId if its containing frame is non-existent
* resets `frameId` if no longer applicable.
*
* NOTE mutates elements.
*/
@ -404,12 +405,16 @@ const repairFrameMembership = (
element: Mutable<ExcalidrawElement>,
elementsMap: Map<string, Mutable<ExcalidrawElement>>,
) => {
if (element.frameId) {
const containingFrame = elementsMap.get(element.frameId);
if (!element.frameId) {
return;
}
if (!containingFrame) {
element.frameId = null;
}
if (
!isValidFrameChild(element) ||
// target frame not exists
!elementsMap.get(element.frameId)
) {
element.frameId = null;
}
};
@ -453,6 +458,8 @@ export const restoreElements = (
// repair binding. Mutates elements.
const restoredElementsMap = arrayToMap(restoredElements);
for (const element of restoredElements) {
// repair frame membership *after* bindings we do in restoreElement()
// since we rely on bindings to be correct
if (element.frameId) {
repairFrameMembership(element, restoredElementsMap);
}

View file

@ -27,6 +27,7 @@ import { LinearElementEditor } from "./linearElementEditor";
import { arrayToMap, tupleToCoors } from "../utils";
import { KEYS } from "../keys";
import { getBoundTextElement, handleBindTextResize } from "./textElement";
import { isValidFrameChild } from "../frame";
export type SuggestedBinding =
| NonDeleted<ExcalidrawBindableElement>
@ -211,6 +212,15 @@ export const bindLinearElement = (
}),
});
}
if (linearElement.frameId && !isValidFrameChild(linearElement)) {
mutateElement(
linearElement,
{
frameId: null,
},
false,
);
}
};
// Don't bind both ends of a simple segment

View file

@ -323,7 +323,24 @@ export const groupByFrames = (elements: readonly ExcalidrawElement[]) => {
export const getFrameElements = (
allElements: ExcalidrawElementsIncludingDeleted,
frameId: string,
) => allElements.filter((element) => element.frameId === frameId);
opts?: { includeBoundArrows?: boolean },
) => {
return allElements.filter((element) => {
if (element.frameId === frameId) {
return true;
}
if (opts?.includeBoundArrows && element.type === "arrow") {
const bindingId = element.startBinding?.elementId;
if (bindingId) {
const boundElement = Scene.getScene(element)?.getElement(bindingId);
if (boundElement?.frameId === frameId) {
return true;
}
}
}
return false;
});
};
export const getElementsInResizingFrame = (
allElements: ExcalidrawElementsIncludingDeleted,
@ -451,6 +468,14 @@ export const getContainingFrame = (
return null;
};
export const isValidFrameChild = (element: ExcalidrawElement) => {
return (
element.type !== "frame" &&
// arrows that are bound to elements cannot be frame children
(element.type !== "arrow" || (!element.startBinding && !element.endBinding))
);
};
// --------------------------- Frame Operations -------------------------------
/**
@ -489,6 +514,9 @@ export const addElementsToFrame = (
elementsToAdd,
)) {
if (!currTargetFrameChildrenMap.has(element.id)) {
if (!isValidFrameChild(element)) {
continue;
}
finalElementsToAdd.push(element);
}

View file

@ -69,6 +69,7 @@ import {
} from "../element/Hyperlink";
import { renderSnaps } from "./renderSnaps";
import {
isArrowElement,
isEmbeddableElement,
isFrameElement,
isLinearElement,
@ -984,7 +985,10 @@ const _renderStaticScene = ({
// TODO do we need to check isElementInFrame here?
if (frame && isElementInFrame(element, elements, appState)) {
frameClip(frame, context, renderConfig, appState);
// do not clip arrows
if (!isArrowElement(element)) {
frameClip(frame, context, renderConfig, appState);
}
}
renderElement(element, rc, context, renderConfig, appState);
context.restore();