refactor: decoupling global Scene state part-1 (#7577)

This commit is contained in:
David Luzar 2024-01-22 00:23:02 +01:00 committed by GitHub
parent 740a165452
commit 0415c616b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 630 additions and 384 deletions

View file

@ -5,6 +5,7 @@ import {
ExcalidrawFreeDrawElement,
NonDeleted,
ExcalidrawTextElementWithContainer,
ElementsMapOrArray,
} from "./types";
import { distance2d, rotate, rotatePoint } from "../math";
import rough from "roughjs/bin/rough";
@ -161,7 +162,11 @@ export const getElementAbsoluteCoords = (
includeBoundText,
);
} else if (isTextElement(element)) {
const container = getContainerElement(element);
const elementsMap =
Scene.getScene(element)?.getElementsMapIncludingDeleted();
const container = elementsMap
? getContainerElement(element, elementsMap)
: null;
if (isArrowElement(container)) {
const coords = LinearElementEditor.getBoundTextElementPosition(
container,
@ -729,10 +734,8 @@ const getLinearElementRotatedBounds = (
export const getElementBounds = (element: ExcalidrawElement): Bounds => {
return ElementBounds.getBounds(element);
};
export const getCommonBounds = (
elements: readonly ExcalidrawElement[],
): Bounds => {
if (!elements.length) {
export const getCommonBounds = (elements: ElementsMapOrArray): Bounds => {
if ("size" in elements ? !elements.size : !elements.length) {
return [0, 0, 0, 0];
}

View file

@ -5,17 +5,12 @@ import { ExcalidrawProps } from "../types";
import { getFontString, updateActiveTool } from "../utils";
import { setCursorForShape } from "../cursor";
import { newTextElement } from "./newElement";
import { getContainerElement, wrapText } from "./textElement";
import {
isFrameLikeElement,
isIframeElement,
isIframeLikeElement,
} from "./typeChecks";
import { wrapText } from "./textElement";
import { isIframeElement } from "./typeChecks";
import {
ExcalidrawElement,
ExcalidrawIframeLikeElement,
IframeData,
NonDeletedExcalidrawElement,
} from "./types";
const embeddedLinkCache = new Map<string, IframeData>();
@ -217,21 +212,6 @@ export const getEmbedLink = (
return { link, intrinsicSize: aspectRatio, type };
};
export const isIframeLikeOrItsLabel = (
element: NonDeletedExcalidrawElement,
): Boolean => {
if (isIframeLikeElement(element)) {
return true;
}
if (element.type === "text") {
const container = getContainerElement(element);
if (container && isFrameLikeElement(container)) {
return true;
}
}
return false;
};
export const createPlaceholderEmbeddableLabel = (
element: ExcalidrawIframeLikeElement,
): ExcalidrawElement => {

View file

@ -31,7 +31,6 @@ import { getElementAbsoluteCoords } from ".";
import { adjustXYWithRotation } from "../math";
import { getResizedElementAbsoluteCoords } from "./bounds";
import {
getContainerElement,
measureText,
normalizeText,
wrapText,
@ -333,12 +332,12 @@ const getAdjustedDimensions = (
export const refreshTextDimensions = (
textElement: ExcalidrawTextElement,
container: ExcalidrawTextContainer | null,
text = textElement.text,
) => {
if (textElement.isDeleted) {
return;
}
const container = getContainerElement(textElement);
if (container) {
text = wrapText(
text,
@ -352,6 +351,7 @@ export const refreshTextDimensions = (
export const updateTextElement = (
textElement: ExcalidrawTextElement,
container: ExcalidrawTextContainer | null,
{
text,
isDeleted,
@ -365,7 +365,7 @@ export const updateTextElement = (
return newElementWith(textElement, {
originalText,
isDeleted: isDeleted ?? textElement.isDeleted,
...refreshTextDimensions(textElement, originalText),
...refreshTextDimensions(textElement, container, originalText),
});
};

View file

@ -15,6 +15,7 @@ import {
ExcalidrawElement,
ExcalidrawTextElementWithContainer,
ExcalidrawImageElement,
ElementsMap,
} from "./types";
import type { Mutable } from "../utility-types";
import {
@ -41,7 +42,7 @@ import {
MaybeTransformHandleType,
TransformHandleDirection,
} from "./transformHandles";
import { AppState, Point, PointerDownState } from "../types";
import { Point, PointerDownState } from "../types";
import Scene from "../scene/Scene";
import {
getApproxMinLineWidth,
@ -68,10 +69,10 @@ export const normalizeAngle = (angle: number): number => {
// Returns true when transform (resizing/rotation) happened
export const transformElements = (
pointerDownState: PointerDownState,
originalElements: PointerDownState["originalElements"],
transformHandleType: MaybeTransformHandleType,
selectedElements: readonly NonDeletedExcalidrawElement[],
resizeArrowDirection: "origin" | "end",
elementsMap: ElementsMap,
shouldRotateWithDiscreteAngle: boolean,
shouldResizeFromCenter: boolean,
shouldMaintainAspectRatio: boolean,
@ -79,7 +80,6 @@ export const transformElements = (
pointerY: number,
centerX: number,
centerY: number,
appState: AppState,
) => {
if (selectedElements.length === 1) {
const [element] = selectedElements;
@ -89,7 +89,6 @@ export const transformElements = (
pointerX,
pointerY,
shouldRotateWithDiscreteAngle,
pointerDownState.originalElements,
);
updateBoundElements(element);
} else if (
@ -101,6 +100,7 @@ export const transformElements = (
) {
resizeSingleTextElement(
element,
elementsMap,
transformHandleType,
shouldResizeFromCenter,
pointerX,
@ -109,9 +109,10 @@ export const transformElements = (
updateBoundElements(element);
} else if (transformHandleType) {
resizeSingleElement(
pointerDownState.originalElements,
originalElements,
shouldMaintainAspectRatio,
element,
elementsMap,
transformHandleType,
shouldResizeFromCenter,
pointerX,
@ -123,7 +124,7 @@ export const transformElements = (
} else if (selectedElements.length > 1) {
if (transformHandleType === "rotation") {
rotateMultipleElements(
pointerDownState,
originalElements,
selectedElements,
pointerX,
pointerY,
@ -139,8 +140,9 @@ export const transformElements = (
transformHandleType === "se"
) {
resizeMultipleElements(
pointerDownState,
originalElements,
selectedElements,
elementsMap,
transformHandleType,
shouldResizeFromCenter,
pointerX,
@ -157,7 +159,6 @@ const rotateSingleElement = (
pointerX: number,
pointerY: number,
shouldRotateWithDiscreteAngle: boolean,
originalElements: Map<string, NonDeleted<ExcalidrawElement>>,
) => {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
const cx = (x1 + x2) / 2;
@ -207,6 +208,7 @@ const rescalePointsInElement = (
const measureFontSizeFromWidth = (
element: NonDeleted<ExcalidrawTextElement>,
elementsMap: ElementsMap,
nextWidth: number,
nextHeight: number,
): { size: number; baseline: number } | null => {
@ -215,7 +217,7 @@ const measureFontSizeFromWidth = (
const hasContainer = isBoundToContainer(element);
if (hasContainer) {
const container = getContainerElement(element);
const container = getContainerElement(element, elementsMap);
if (container) {
width = getBoundTextMaxWidth(container);
}
@ -257,6 +259,7 @@ const getSidesForTransformHandle = (
const resizeSingleTextElement = (
element: NonDeleted<ExcalidrawTextElement>,
elementsMap: ElementsMap,
transformHandleType: "nw" | "ne" | "sw" | "se",
shouldResizeFromCenter: boolean,
pointerX: number,
@ -303,7 +306,12 @@ const resizeSingleTextElement = (
if (scale > 0) {
const nextWidth = element.width * scale;
const nextHeight = element.height * scale;
const metrics = measureFontSizeFromWidth(element, nextWidth, nextHeight);
const metrics = measureFontSizeFromWidth(
element,
elementsMap,
nextWidth,
nextHeight,
);
if (metrics === null) {
return;
}
@ -342,6 +350,7 @@ export const resizeSingleElement = (
originalElements: PointerDownState["originalElements"],
shouldMaintainAspectRatio: boolean,
element: NonDeletedExcalidrawElement,
elementsMap: ElementsMap,
transformHandleDirection: TransformHandleDirection,
shouldResizeFromCenter: boolean,
pointerX: number,
@ -448,6 +457,7 @@ export const resizeSingleElement = (
const nextFont = measureFontSizeFromWidth(
boundTextElement,
elementsMap,
getBoundTextMaxWidth(updatedElement),
getBoundTextMaxHeight(updatedElement, boundTextElement),
);
@ -637,8 +647,9 @@ export const resizeSingleElement = (
};
export const resizeMultipleElements = (
pointerDownState: PointerDownState,
originalElements: PointerDownState["originalElements"],
selectedElements: readonly NonDeletedExcalidrawElement[],
elementsMap: ElementsMap,
transformHandleType: "nw" | "ne" | "sw" | "se",
shouldResizeFromCenter: boolean,
pointerX: number,
@ -658,7 +669,7 @@ export const resizeMultipleElements = (
}[],
element,
) => {
const origElement = pointerDownState.originalElements.get(element.id);
const origElement = originalElements.get(element.id);
if (origElement) {
acc.push({ orig: origElement, latest: element });
}
@ -679,7 +690,7 @@ export const resizeMultipleElements = (
if (!textId) {
return acc;
}
const text = pointerDownState.originalElements.get(textId) ?? null;
const text = originalElements.get(textId) ?? null;
if (!isBoundToContainer(text)) {
return acc;
}
@ -825,7 +836,12 @@ export const resizeMultipleElements = (
}
if (isTextElement(orig)) {
const metrics = measureFontSizeFromWidth(orig, width, height);
const metrics = measureFontSizeFromWidth(
orig,
elementsMap,
width,
height,
);
if (!metrics) {
return;
}
@ -833,7 +849,7 @@ export const resizeMultipleElements = (
update.baseline = metrics.baseline;
}
const boundTextElement = pointerDownState.originalElements.get(
const boundTextElement = originalElements.get(
getBoundTextElementId(orig) ?? "",
) as ExcalidrawTextElementWithContainer | undefined;
@ -884,7 +900,7 @@ export const resizeMultipleElements = (
};
const rotateMultipleElements = (
pointerDownState: PointerDownState,
originalElements: PointerDownState["originalElements"],
elements: readonly NonDeletedExcalidrawElement[],
pointerX: number,
pointerY: number,
@ -906,8 +922,7 @@ const rotateMultipleElements = (
const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2;
const origAngle =
pointerDownState.originalElements.get(element.id)?.angle ??
element.angle;
originalElements.get(element.id)?.angle ?? element.angle;
const [rotatedCX, rotatedCY] = rotate(
cx,
cy,

View file

@ -1,5 +1,6 @@
import { getFontString, arrayToMap, isTestEnv, normalizeEOL } from "../utils";
import {
ElementsMap,
ExcalidrawElement,
ExcalidrawElementType,
ExcalidrawTextContainer,
@ -682,17 +683,15 @@ export const getBoundTextElement = (element: ExcalidrawElement | null) => {
};
export const getContainerElement = (
element:
| (ExcalidrawElement & {
containerId: ExcalidrawElement["id"] | null;
})
| null,
) => {
element: ExcalidrawTextElement | null,
elementsMap: ElementsMap,
): ExcalidrawTextContainer | null => {
if (!element) {
return null;
}
if (element.containerId) {
return Scene.getScene(element)?.getElement(element.containerId) || null;
return (elementsMap.get(element.containerId) ||
null) as ExcalidrawTextContainer | null;
}
return null;
};
@ -752,28 +751,16 @@ export const getContainerCoords = (container: NonDeletedExcalidrawElement) => {
};
};
export const getTextElementAngle = (textElement: ExcalidrawTextElement) => {
const container = getContainerElement(textElement);
export const getTextElementAngle = (
textElement: ExcalidrawTextElement,
container: ExcalidrawTextContainer | null,
) => {
if (!container || isArrowElement(container)) {
return textElement.angle;
}
return container.angle;
};
export const getBoundTextElementOffset = (
boundTextElement: ExcalidrawTextElement | null,
) => {
const container = getContainerElement(boundTextElement);
if (!container || !boundTextElement) {
return 0;
}
if (isArrowElement(container)) {
return BOUND_TEXT_PADDING * 8;
}
return BOUND_TEXT_PADDING;
};
export const getBoundTextElementPosition = (
container: ExcalidrawElement,
boundTextElement: ExcalidrawTextElementWithContainer,
@ -788,12 +775,12 @@ export const getBoundTextElementPosition = (
export const shouldAllowVerticalAlign = (
selectedElements: NonDeletedExcalidrawElement[],
elementsMap: ElementsMap,
) => {
return selectedElements.some((element) => {
const hasBoundContainer = isBoundToContainer(element);
if (hasBoundContainer) {
const container = getContainerElement(element);
if (isTextElement(element) && isArrowElement(container)) {
if (isBoundToContainer(element)) {
const container = getContainerElement(element, elementsMap);
if (isArrowElement(container)) {
return false;
}
return true;
@ -804,12 +791,12 @@ export const shouldAllowVerticalAlign = (
export const suppportsHorizontalAlign = (
selectedElements: NonDeletedExcalidrawElement[],
elementsMap: ElementsMap,
) => {
return selectedElements.some((element) => {
const hasBoundContainer = isBoundToContainer(element);
if (hasBoundContainer) {
const container = getContainerElement(element);
if (isTextElement(element) && isArrowElement(container)) {
if (isBoundToContainer(element)) {
const container = getContainerElement(element, elementsMap);
if (isArrowElement(container)) {
return false;
}
return true;

View file

@ -153,7 +153,10 @@ export const textWysiwyg = ({
if (updatedTextElement && isTextElement(updatedTextElement)) {
let coordX = updatedTextElement.x;
let coordY = updatedTextElement.y;
const container = getContainerElement(updatedTextElement);
const container = getContainerElement(
updatedTextElement,
app.scene.getElementsMapIncludingDeleted(),
);
let maxWidth = updatedTextElement.width;
let maxHeight = updatedTextElement.height;
@ -277,7 +280,7 @@ export const textWysiwyg = ({
transform: getTransform(
textElementWidth,
textElementHeight,
getTextElementAngle(updatedTextElement),
getTextElementAngle(updatedTextElement, container),
appState,
maxWidth,
editorMaxHeight,
@ -348,7 +351,10 @@ export const textWysiwyg = ({
if (!data) {
return;
}
const container = getContainerElement(element);
const container = getContainerElement(
element,
app.scene.getElementsMapIncludingDeleted(),
);
const font = getFontString({
fontSize: app.state.currentItemFontSize,
@ -528,7 +534,10 @@ export const textWysiwyg = ({
return;
}
let text = editable.value;
const container = getContainerElement(updateElement);
const container = getContainerElement(
updateElement,
app.scene.getElementsMapIncludingDeleted(),
);
if (container) {
text = updateElement.text;

View file

@ -6,7 +6,7 @@ import {
THEME,
VERTICAL_ALIGN,
} from "../constants";
import { MarkNonNullable, ValueOf } from "../utility-types";
import { MakeBrand, MarkNonNullable, ValueOf } from "../utility-types";
import { MagicCacheData } from "../data/magic";
export type ChartType = "bar" | "line";
@ -254,3 +254,31 @@ export type ExcalidrawFreeDrawElement = _ExcalidrawElementBase &
export type FileId = string & { _brand: "FileId" };
export type ExcalidrawElementType = ExcalidrawElement["type"];
/**
* Map of excalidraw elements.
* Unspecified whether deleted or non-deleted.
* Can be a subset of Scene elements.
*/
export type ElementsMap = Map<ExcalidrawElement["id"], ExcalidrawElement>;
/**
* Map of non-deleted elements.
* Can be a subset of Scene elements.
*/
export type NonDeletedElementsMap = Map<
ExcalidrawElement["id"],
NonDeletedExcalidrawElement
> &
MakeBrand<"NonDeletedElementsMap">;
/**
* Map of all excalidraw Scene elements, including deleted.
* Not a subset. Use this type when you need access to current Scene elements.
*/
export type SceneElementsMap = Map<ExcalidrawElement["id"], ExcalidrawElement> &
MakeBrand<"SceneElementsMap">;
export type ElementsMapOrArray =
| readonly ExcalidrawElement[]
| Readonly<ElementsMap>;