fix: make getBoundTextElement and related helpers pure (#7601)

* fix: make getBoundTextElement pure

* updating args

* fix

* pass boundTextElement to getBoundTextMaxWidth

* fix labelled arrows

* lint

* pass elementsMap to removeElementsFromFrame

* pass elementsMap to getMaximumGroups, alignElements and distributeElements

* lint

* pass allElementsMap to renderElement

* lint

* feat: make more typesafe

* fix: remove unnecessary assertion

* fix: remove unused params

---------

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
pull/7457/head^2
Aakansha Doshi 1 year ago committed by GitHub
parent 2789d08154
commit 10bd08ef19
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -40,8 +40,13 @@ const alignSelectedElements = (
alignment: Alignment,
) => {
const selectedElements = app.scene.getSelectedElements(appState);
const elementsMap = arrayToMap(elements);
const updatedElements = alignElements(selectedElements, alignment);
const updatedElements = alignElements(
selectedElements,
elementsMap,
alignment,
);
const updatedElementsMap = arrayToMap(updatedElements);

@ -45,8 +45,9 @@ export const actionUnbindText = register({
},
perform: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
const elementsMap = app.scene.getNonDeletedElementsMap();
selectedElements.forEach((element) => {
const boundTextElement = getBoundTextElement(element);
const boundTextElement = getBoundTextElement(element, elementsMap);
if (boundTextElement) {
const { width, height, baseline } = measureText(
boundTextElement.originalText,
@ -106,7 +107,10 @@ export const actionBindText = register({
if (
textElement &&
bindingContainer &&
getBoundTextElement(bindingContainer) === null
getBoundTextElement(
bindingContainer,
app.scene.getNonDeletedElementsMap(),
) === null
) {
return true;
}

@ -32,7 +32,11 @@ const distributeSelectedElements = (
) => {
const selectedElements = app.scene.getSelectedElements(appState);
const updatedElements = distributeElements(selectedElements, distribution);
const updatedElements = distributeElements(
selectedElements,
app.scene.getNonDeletedElementsMap(),
distribution,
);
const updatedElementsMap = arrayToMap(updatedElements);

@ -139,7 +139,7 @@ const duplicateElements = (
continue;
}
const boundTextElement = getBoundTextElement(element);
const boundTextElement = getBoundTextElement(element, arrayToMap(elements));
const isElementAFrameLike = isFrameLikeElement(element);
if (idsOfElementsToDuplicate.get(element.id)) {

@ -5,6 +5,7 @@ import {
ExcalidrawElement,
NonDeleted,
NonDeletedElementsMap,
NonDeletedSceneElementsMap,
} from "../element/types";
import { resizeMultipleElements } from "../element/resizeElements";
import { AppState } from "../types";
@ -67,7 +68,7 @@ export const actionFlipVertical = register({
const flipSelectedElements = (
elements: readonly ExcalidrawElement[],
elementsMap: NonDeletedElementsMap,
elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap,
appState: Readonly<AppState>,
flipDirection: "horizontal" | "vertical",
) => {
@ -96,7 +97,7 @@ const flipSelectedElements = (
const flipElements = (
selectedElements: NonDeleted<ExcalidrawElement>[],
elementsMap: NonDeletedElementsMap,
elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap,
appState: AppState,
flipDirection: "horizontal" | "vertical",
): ExcalidrawElement[] => {

@ -105,7 +105,10 @@ export const actionGroup = register({
const frameElementsMap = groupByFrameLikes(selectedElements);
frameElementsMap.forEach((elementsInFrame, frameId) => {
removeElementsFromFrame(elementsInFrame);
removeElementsFromFrame(
elementsInFrame,
app.scene.getNonDeletedElementsMap(),
);
});
}
@ -225,6 +228,7 @@ export const actionUngroup = register({
nextElements,
getElementsInResizingFrame(nextElements, frame, appState),
frame,
app,
);
}
});

@ -606,7 +606,7 @@ export const actionChangeFontSize = register({
perform: (elements, appState, value, app) => {
return changeFontSize(elements, appState, app, () => value, value);
},
PanelComponent: ({ elements, appState, updateData }) => (
PanelComponent: ({ elements, appState, updateData, app }) => (
<fieldset>
<legend>{t("labels.fontSize")}</legend>
<ButtonIconSelect
@ -644,14 +644,21 @@ export const actionChangeFontSize = register({
if (isTextElement(element)) {
return element.fontSize;
}
const boundTextElement = getBoundTextElement(element);
const boundTextElement = getBoundTextElement(
element,
app.scene.getNonDeletedElementsMap(),
);
if (boundTextElement) {
return boundTextElement.fontSize;
}
return null;
},
(element) =>
isTextElement(element) || getBoundTextElement(element) !== null,
isTextElement(element) ||
getBoundTextElement(
element,
app.scene.getNonDeletedElementsMap(),
) !== null,
(hasSelection) =>
hasSelection
? null
@ -738,7 +745,7 @@ export const actionChangeFontFamily = register({
commitToHistory: true,
};
},
PanelComponent: ({ elements, appState, updateData }) => {
PanelComponent: ({ elements, appState, updateData, app }) => {
const options: {
value: FontFamilyValues;
text: string;
@ -778,14 +785,21 @@ export const actionChangeFontFamily = register({
if (isTextElement(element)) {
return element.fontFamily;
}
const boundTextElement = getBoundTextElement(element);
const boundTextElement = getBoundTextElement(
element,
app.scene.getNonDeletedElementsMap(),
);
if (boundTextElement) {
return boundTextElement.fontFamily;
}
return null;
},
(element) =>
isTextElement(element) || getBoundTextElement(element) !== null,
isTextElement(element) ||
getBoundTextElement(
element,
app.scene.getNonDeletedElementsMap(),
) !== null,
(hasSelection) =>
hasSelection
? null
@ -830,7 +844,8 @@ export const actionChangeTextAlign = register({
commitToHistory: true,
};
},
PanelComponent: ({ elements, appState, updateData }) => {
PanelComponent: ({ elements, appState, updateData, app }) => {
const elementsMap = app.scene.getNonDeletedElementsMap();
return (
<fieldset>
<legend>{t("labels.textAlign")}</legend>
@ -863,14 +878,18 @@ export const actionChangeTextAlign = register({
if (isTextElement(element)) {
return element.textAlign;
}
const boundTextElement = getBoundTextElement(element);
const boundTextElement = getBoundTextElement(
element,
elementsMap,
);
if (boundTextElement) {
return boundTextElement.textAlign;
}
return null;
},
(element) =>
isTextElement(element) || getBoundTextElement(element) !== null,
isTextElement(element) ||
getBoundTextElement(element, elementsMap) !== null,
(hasSelection) =>
hasSelection ? null : appState.currentItemTextAlign,
)}
@ -913,7 +932,7 @@ export const actionChangeVerticalAlign = register({
commitToHistory: true,
};
},
PanelComponent: ({ elements, appState, updateData }) => {
PanelComponent: ({ elements, appState, updateData, app }) => {
return (
<fieldset>
<ButtonIconSelect<VerticalAlign | false>
@ -945,14 +964,21 @@ export const actionChangeVerticalAlign = register({
if (isTextElement(element) && element.containerId) {
return element.verticalAlign;
}
const boundTextElement = getBoundTextElement(element);
const boundTextElement = getBoundTextElement(
element,
app.scene.getNonDeletedElementsMap(),
);
if (boundTextElement) {
return boundTextElement.verticalAlign;
}
return null;
},
(element) =>
isTextElement(element) || getBoundTextElement(element) !== null,
isTextElement(element) ||
getBoundTextElement(
element,
app.scene.getNonDeletedElementsMap(),
) !== null,
(hasSelection) => (hasSelection ? null : VERTICAL_ALIGN.MIDDLE),
)}
onChange={(value) => updateData(value)}

@ -32,12 +32,15 @@ export let copiedStyles: string = "{}";
export const actionCopyStyles = register({
name: "copyStyles",
trackEvent: { category: "element" },
perform: (elements, appState) => {
perform: (elements, appState, formData, app) => {
const elementsCopied = [];
const element = elements.find((el) => appState.selectedElementIds[el.id]);
elementsCopied.push(element);
if (element && hasBoundTextElement(element)) {
const boundTextElement = getBoundTextElement(element);
const boundTextElement = getBoundTextElement(
element,
app.scene.getNonDeletedElementsMap(),
);
elementsCopied.push(boundTextElement);
}
if (element) {
@ -59,7 +62,7 @@ export const actionCopyStyles = register({
export const actionPasteStyles = register({
name: "pasteStyles",
trackEvent: { category: "element" },
perform: (elements, appState) => {
perform: (elements, appState, formData, app) => {
const elementsCopied = JSON.parse(copiedStyles);
const pastedElement = elementsCopied[0];
const boundTextElement = elementsCopied[1];

@ -1,4 +1,4 @@
import { ExcalidrawElement } from "./element/types";
import { ElementsMap, ExcalidrawElement } from "./element/types";
import { newElementWith } from "./element/mutateElement";
import { BoundingBox, getCommonBoundingBox } from "./element/bounds";
import { getMaximumGroups } from "./groups";
@ -10,10 +10,13 @@ export interface Alignment {
export const alignElements = (
selectedElements: ExcalidrawElement[],
elementsMap: ElementsMap,
alignment: Alignment,
): ExcalidrawElement[] => {
const groups: ExcalidrawElement[][] = getMaximumGroups(selectedElements);
const groups: ExcalidrawElement[][] = getMaximumGroups(
selectedElements,
elementsMap,
);
const selectionBoundingBox = getCommonBoundingBox(selectedElements);
return groups.flatMap((group) => {

@ -1,6 +1,10 @@
import { useState } from "react";
import { ActionManager } from "../actions/manager";
import { ExcalidrawElementType, NonDeletedElementsMap } from "../element/types";
import {
ExcalidrawElementType,
NonDeletedElementsMap,
NonDeletedSceneElementsMap,
} from "../element/types";
import { t } from "../i18n";
import { useDevice } from "./App";
import {
@ -47,7 +51,7 @@ export const SelectedShapeActions = ({
renderAction,
}: {
appState: UIAppState;
elementsMap: NonDeletedElementsMap;
elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap;
renderAction: ActionManager["renderAction"];
}) => {
const targetElements = getTargetElements(elementsMap, appState);

@ -1431,6 +1431,8 @@ class App extends React.Component<AppProps, AppState> {
pendingImageElementId: this.state.pendingImageElementId,
});
const allElementsMap = this.scene.getNonDeletedElementsMap();
const shouldBlockPointerEvents =
!(
this.state.editingElement && isLinearElement(this.state.editingElement)
@ -1628,6 +1630,7 @@ class App extends React.Component<AppProps, AppState> {
canvas={this.canvas}
rc={this.rc}
elementsMap={elementsMap}
allElementsMap={allElementsMap}
visibleElements={visibleElements}
versionNonce={versionNonce}
selectionNonce={
@ -3869,7 +3872,11 @@ class App extends React.Component<AppProps, AppState> {
if (!isTextElement(selectedElement)) {
container = selectedElement as ExcalidrawTextContainer;
}
const midPoint = getContainerCenter(selectedElement, this.state);
const midPoint = getContainerCenter(
selectedElement,
this.state,
this.scene.getNonDeletedElementsMap(),
);
const sceneX = midPoint.x;
const sceneY = midPoint.y;
this.startTextEditing({
@ -4333,6 +4340,7 @@ class App extends React.Component<AppProps, AppState> {
this.frameNameBoundsCache,
x,
y,
this.scene.getNonDeletedElementsMap(),
)
? allHitElements[allHitElements.length - 2]
: elementWithHighestZIndex;
@ -4362,7 +4370,14 @@ class App extends React.Component<AppProps, AppState> {
);
return getElementsAtPosition(elements, (element) =>
hitTest(element, this.state, this.frameNameBoundsCache, x, y),
hitTest(
element,
this.state,
this.frameNameBoundsCache,
x,
y,
this.scene.getNonDeletedElementsMap(),
),
).filter((element) => {
// hitting a frame's element from outside the frame is not considered a hit
const containingFrame = getContainingFrame(element);
@ -4399,7 +4414,10 @@ class App extends React.Component<AppProps, AppState> {
container,
);
if (container && parentCenterPosition) {
const boundTextElementToContainer = getBoundTextElement(container);
const boundTextElementToContainer = getBoundTextElement(
container,
this.scene.getNonDeletedElementsMap(),
);
if (!boundTextElementToContainer) {
shouldBindToContainer = true;
}
@ -4412,7 +4430,10 @@ class App extends React.Component<AppProps, AppState> {
if (isTextElement(selectedElements[0])) {
existingTextElement = selectedElements[0];
} else if (container) {
existingTextElement = getBoundTextElement(selectedElements[0]);
existingTextElement = getBoundTextElement(
selectedElements[0],
this.scene.getNonDeletedElementsMap(),
);
} else {
existingTextElement = this.getTextElementAtPosition(sceneX, sceneY);
}
@ -4621,7 +4642,11 @@ class App extends React.Component<AppProps, AppState> {
[sceneX, sceneY],
)
) {
const midPoint = getContainerCenter(container, this.state);
const midPoint = getContainerCenter(
container,
this.state,
this.scene.getNonDeletedElementsMap(),
);
sceneX = midPoint.x;
sceneY = midPoint.y;
@ -5257,8 +5282,8 @@ class App extends React.Component<AppProps, AppState> {
const element = LinearElementEditor.getElement(
linearElementEditor.elementId,
);
const boundTextElement = getBoundTextElement(element);
const elementsMap = this.scene.getNonDeletedElementsMap();
const boundTextElement = getBoundTextElement(element, elementsMap);
if (!element) {
return;
@ -5285,6 +5310,7 @@ class App extends React.Component<AppProps, AppState> {
linearElementEditor,
{ x: scenePointerX, y: scenePointerY },
this.state,
this.scene.getNonDeletedElementsMap(),
);
if (hoverPointIndex >= 0 || segmentMidPointHoveredCoords) {
@ -5300,6 +5326,7 @@ class App extends React.Component<AppProps, AppState> {
this.frameNameBoundsCache,
scenePointerX,
scenePointerY,
elementsMap,
)
) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
@ -5311,6 +5338,7 @@ class App extends React.Component<AppProps, AppState> {
this.frameNameBoundsCache,
scenePointerX,
scenePointerY,
this.scene.getNonDeletedElementsMap(),
)
) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
@ -6060,6 +6088,7 @@ class App extends React.Component<AppProps, AppState> {
this.history,
pointerDownState.origin,
linearElementEditor,
this.scene.getNonDeletedElementsMap(),
);
if (ret.hitElement) {
pointerDownState.hit.element = ret.hitElement;
@ -6995,6 +7024,7 @@ class App extends React.Component<AppProps, AppState> {
);
},
linearElementEditor,
this.scene.getNonDeletedElementsMap(),
);
if (didDrag) {
pointerDownState.lastCoords.x = pointerCoords.x;
@ -7713,7 +7743,10 @@ class App extends React.Component<AppProps, AppState> {
groupIds: [],
});
removeElementsFromFrame([linearElement]);
removeElementsFromFrame(
[linearElement],
this.scene.getNonDeletedElementsMap(),
);
this.scene.informMutation();
}
@ -7866,6 +7899,7 @@ class App extends React.Component<AppProps, AppState> {
this.state,
),
frame,
this,
);
}
@ -8093,6 +8127,7 @@ class App extends React.Component<AppProps, AppState> {
this.frameNameBoundsCache,
pointerDownState.origin.x,
pointerDownState.origin.y,
this.scene.getNonDeletedElementsMap(),
)) ||
(!hitElement &&
pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements))
@ -9334,7 +9369,11 @@ class App extends React.Component<AppProps, AppState> {
let elementCenterX = container.x + container.width / 2;
let elementCenterY = container.y + container.height / 2;
const elementCenter = getContainerCenter(container, appState);
const elementCenter = getContainerCenter(
container,
appState,
this.scene.getNonDeletedElementsMap(),
);
if (elementCenter) {
elementCenterX = elementCenter.x;
elementCenterY = elementCenter.y;

@ -7,13 +7,17 @@ import type {
RenderableElementsMap,
StaticCanvasRenderConfig,
} from "../../scene/types";
import type { NonDeletedExcalidrawElement } from "../../element/types";
import type {
NonDeletedExcalidrawElement,
NonDeletedSceneElementsMap,
} from "../../element/types";
import { isRenderThrottlingEnabled } from "../../reactUtils";
type StaticCanvasProps = {
canvas: HTMLCanvasElement;
rc: RoughCanvas;
elementsMap: RenderableElementsMap;
allElementsMap: NonDeletedSceneElementsMap;
visibleElements: readonly NonDeletedExcalidrawElement[];
versionNonce: number | undefined;
selectionNonce: number | undefined;
@ -67,6 +71,7 @@ const StaticCanvas = (props: StaticCanvasProps) => {
rc: props.rc,
scale: props.scale,
elementsMap: props.elementsMap,
allElementsMap: props.allElementsMap,
visibleElements: props.visibleElements,
appState: props.appState,
renderConfig: props.renderConfig,

@ -24,6 +24,7 @@ import {
normalizeText,
} from "../element/textElement";
import {
ElementsMap,
ExcalidrawArrowElement,
ExcalidrawBindableElement,
ExcalidrawElement,
@ -42,7 +43,7 @@ import {
VerticalAlign,
} from "../element/types";
import { MarkOptional } from "../utility-types";
import { assertNever, cloneJSON, getFontString } from "../utils";
import { arrayToMap, assertNever, cloneJSON, getFontString } from "../utils";
import { getSizeFromPoints } from "../points";
import { randomId } from "../random";
@ -202,6 +203,7 @@ const DEFAULT_DIMENSION = 100;
const bindTextToContainer = (
container: ExcalidrawElement,
textProps: { text: string } & MarkOptional<ElementConstructorOpts, "x" | "y">,
elementsMap: ElementsMap,
) => {
const textElement: ExcalidrawTextElement = newTextElement({
x: 0,
@ -623,6 +625,7 @@ export const convertToExcalidrawElements = (
let [container, text] = bindTextToContainer(
excalidrawElement,
element?.label,
arrayToMap(elementStore.getElements()),
);
elementStore.add(container);
elementStore.add(text);

@ -1,7 +1,7 @@
import { ExcalidrawElement } from "./element/types";
import { newElementWith } from "./element/mutateElement";
import { getMaximumGroups } from "./groups";
import { getCommonBoundingBox } from "./element/bounds";
import type { ElementsMap, ExcalidrawElement } from "./element/types";
export interface Distribution {
space: "between";
@ -10,6 +10,7 @@ export interface Distribution {
export const distributeElements = (
selectedElements: ExcalidrawElement[],
elementsMap: ElementsMap,
distribution: Distribution,
): ExcalidrawElement[] => {
const [start, mid, end, extent] =
@ -18,7 +19,7 @@ export const distributeElements = (
: (["minY", "midY", "maxY", "height"] as const);
const bounds = getCommonBoundingBox(selectedElements);
const groups = getMaximumGroups(selectedElements)
const groups = getMaximumGroups(selectedElements, elementsMap)
.map((group) => [group, getCommonBoundingBox(group)] as const)
.sort((a, b) => a[1][mid] - b[1][mid]);

@ -321,9 +321,9 @@ export const updateBoundElements = (
const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds(
simultaneouslyUpdated,
);
const scene = Scene.getScene(changedElement)!;
getNonDeletedElements(
Scene.getScene(changedElement)!,
scene,
boundLinearElements.map((el) => el.id),
).forEach((element) => {
if (!isLinearElement(element)) {
@ -362,9 +362,12 @@ export const updateBoundElements = (
endBinding,
changedElement as ExcalidrawBindableElement,
);
const boundText = getBoundTextElement(element);
const boundText = getBoundTextElement(
element,
scene.getNonDeletedElementsMap(),
);
if (boundText) {
handleBindTextResize(element, false);
handleBindTextResize(element, scene.getNonDeletedElementsMap(), false);
}
});
};

@ -6,6 +6,7 @@ import {
NonDeleted,
ExcalidrawTextElementWithContainer,
ElementsMapOrArray,
ElementsMap,
} from "./types";
import { distance2d, rotate, rotatePoint } from "../math";
import rough from "roughjs/bin/rough";
@ -74,13 +75,16 @@ export class ElementBounds {
) {
return cachedBounds.bounds;
}
const bounds = ElementBounds.calculateBounds(element);
const scene = Scene.getScene(element);
const bounds = ElementBounds.calculateBounds(
element,
scene?.getNonDeletedElementsMap() || new Map(),
);
// hack to ensure that downstream checks could retrieve element Scene
// so as to have correctly calculated bounds
// FIXME remove when we get rid of all the id:Scene / element:Scene mapping
const shouldCache = Scene.getScene(element);
const shouldCache = !!scene;
if (shouldCache) {
ElementBounds.boundsCache.set(element, {
@ -92,7 +96,10 @@ export class ElementBounds {
return bounds;
}
private static calculateBounds(element: ExcalidrawElement): Bounds {
private static calculateBounds(
element: ExcalidrawElement,
elementsMap: ElementsMap,
): Bounds {
let bounds: Bounds;
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(element);
@ -111,7 +118,7 @@ export class ElementBounds {
maxY + element.y,
];
} else if (isLinearElement(element)) {
bounds = getLinearElementRotatedBounds(element, cx, cy);
bounds = getLinearElementRotatedBounds(element, cx, cy, elementsMap);
} else if (element.type === "diamond") {
const [x11, y11] = rotate(cx, y1, cx, cy, element.angle);
const [x12, y12] = rotate(cx, y2, cx, cy, element.angle);
@ -154,16 +161,17 @@ export const getElementAbsoluteCoords = (
element: ExcalidrawElement,
includeBoundText: boolean = false,
): [number, number, number, number, number, number] => {
const elementsMap =
Scene.getScene(element)?.getElementsMapIncludingDeleted() || new Map();
if (isFreeDrawElement(element)) {
return getFreeDrawElementAbsoluteCoords(element);
} else if (isLinearElement(element)) {
return LinearElementEditor.getElementAbsoluteCoords(
element,
elementsMap,
includeBoundText,
);
} else if (isTextElement(element)) {
const elementsMap =
Scene.getScene(element)?.getElementsMapIncludingDeleted();
const container = elementsMap
? getContainerElement(element, elementsMap)
: null;
@ -677,7 +685,10 @@ const getLinearElementRotatedBounds = (
element: ExcalidrawLinearElement,
cx: number,
cy: number,
elementsMap: ElementsMap,
): Bounds => {
const boundTextElement = getBoundTextElement(element, elementsMap);
if (element.points.length < 2) {
const [pointX, pointY] = element.points[0];
const [x, y] = rotate(
@ -689,7 +700,6 @@ const getLinearElementRotatedBounds = (
);
let coords: Bounds = [x, y, x, y];
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
element,
@ -714,7 +724,6 @@ const getLinearElementRotatedBounds = (
rotate(element.x + x, element.y + y, cx, cy, element.angle);
const res = getMinMaxXYFromCurvePathOps(ops, transformXY);
let coords: Bounds = [res[0], res[1], res[2], res[3]];
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
element,

@ -28,6 +28,7 @@ import {
StrokeRoundness,
ExcalidrawFrameLikeElement,
ExcalidrawIframeLikeElement,
ElementsMap,
} from "./types";
import {
@ -78,6 +79,7 @@ export const hitTest = (
frameNameBoundsCache: FrameNameBoundsCache,
x: number,
y: number,
elementsMap: ElementsMap,
): boolean => {
// How many pixels off the shape boundary we still consider a hit
const threshold = 10 / appState.zoom.value;
@ -95,7 +97,7 @@ export const hitTest = (
);
}
const boundTextElement = getBoundTextElement(element);
const boundTextElement = getBoundTextElement(element, elementsMap);
if (boundTextElement) {
const isHittingBoundTextElement = hitTest(
boundTextElement,
@ -103,6 +105,7 @@ export const hitTest = (
frameNameBoundsCache,
x,
y,
elementsMap,
);
if (isHittingBoundTextElement) {
return true;
@ -122,15 +125,16 @@ export const isHittingElementBoundingBoxWithoutHittingElement = (
frameNameBoundsCache: FrameNameBoundsCache,
x: number,
y: number,
elementsMap: ElementsMap,
): boolean => {
const threshold = 10 / appState.zoom.value;
// So that bound text element hit is considered within bounding box of container even if its outside actual bounding box of element
// eg for linear elements text can be outside the element bounding box
const boundTextElement = getBoundTextElement(element);
const boundTextElement = getBoundTextElement(element, elementsMap);
if (
boundTextElement &&
hitTest(boundTextElement, appState, frameNameBoundsCache, x, y)
hitTest(boundTextElement, appState, frameNameBoundsCache, x, y, elementsMap)
) {
return false;
}

@ -57,7 +57,10 @@ export const dragSelectedElements = (
// skip arrow labels since we calculate its position during render
!isArrowElement(element)
) {
const textElement = getBoundTextElement(element);
const textElement = getBoundTextElement(
element,
scene.getNonDeletedElementsMap(),
);
if (textElement) {
updateElementCoords(pointerDownState, textElement, adjustedOffset);
}

@ -5,6 +5,7 @@ import {
PointBinding,
ExcalidrawBindableElement,
ExcalidrawTextElementWithContainer,
ElementsMap,
} from "./types";
import {
distance2d,
@ -193,6 +194,7 @@ export class LinearElementEditor {
pointSceneCoords: { x: number; y: number }[],
) => void,
linearElementEditor: LinearElementEditor,
elementsMap: ElementsMap,
): boolean {
if (!linearElementEditor) {
return false;
@ -272,9 +274,9 @@ export class LinearElementEditor {
);
}
const boundTextElement = getBoundTextElement(element);
const boundTextElement = getBoundTextElement(element, elementsMap);
if (boundTextElement) {
handleBindTextResize(element, false);
handleBindTextResize(element, elementsMap, false);
}
// suggest bindings for first and last point if selected
@ -404,9 +406,10 @@ export class LinearElementEditor {
static getEditorMidPoints = (
element: NonDeleted<ExcalidrawLinearElement>,
elementsMap: ElementsMap,
appState: InteractiveCanvasAppState,
): typeof editorMidPointsCache["points"] => {
const boundText = getBoundTextElement(element);
const boundText = getBoundTextElement(element, elementsMap);
// Since its not needed outside editor unless 2 pointer lines or bound text
if (
@ -465,6 +468,7 @@ export class LinearElementEditor {
linearElementEditor: LinearElementEditor,
scenePointer: { x: number; y: number },
appState: AppState,
elementsMap: ElementsMap,
) => {
const { elementId } = linearElementEditor;
const element = LinearElementEditor.getElement(elementId);
@ -503,7 +507,7 @@ export class LinearElementEditor {
}
let index = 0;
const midPoints: typeof editorMidPointsCache["points"] =
LinearElementEditor.getEditorMidPoints(element, appState);
LinearElementEditor.getEditorMidPoints(element, elementsMap, appState);
while (index < midPoints.length) {
if (midPoints[index] !== null) {
const distance = distance2d(
@ -581,6 +585,7 @@ export class LinearElementEditor {
linearElementEditor: LinearElementEditor,
appState: AppState,
midPoint: Point,
elementsMap: ElementsMap,
) {
const element = LinearElementEditor.getElement(
linearElementEditor.elementId,
@ -588,7 +593,11 @@ export class LinearElementEditor {
if (!element) {
return -1;
}
const midPoints = LinearElementEditor.getEditorMidPoints(element, appState);
const midPoints = LinearElementEditor.getEditorMidPoints(
element,
elementsMap,
appState,
);
let index = 0;
while (index < midPoints.length) {
if (LinearElementEditor.arePointsEqual(midPoint, midPoints[index])) {
@ -605,6 +614,7 @@ export class LinearElementEditor {
history: History,
scenePointer: { x: number; y: number },
linearElementEditor: LinearElementEditor,
elementsMap: ElementsMap,
): {
didAddPoint: boolean;
hitElement: NonDeleted<ExcalidrawElement> | null;
@ -630,6 +640,7 @@ export class LinearElementEditor {
linearElementEditor,
scenePointer,
appState,
elementsMap,
);
let segmentMidpointIndex = null;
if (segmentMidpoint) {
@ -637,6 +648,7 @@ export class LinearElementEditor {
linearElementEditor,
appState,
segmentMidpoint,
elementsMap,
);
}
if (event.altKey && appState.editingLinearElement) {
@ -1418,6 +1430,7 @@ export class LinearElementEditor {
static getElementAbsoluteCoords = (
element: ExcalidrawLinearElement,
elementsMap: ElementsMap,
includeBoundText: boolean = false,
): [number, number, number, number, number, number] => {
let coords: [number, number, number, number, number, number];
@ -1462,7 +1475,7 @@ export class LinearElementEditor {
if (!includeBoundText) {
return coords;
}
const boundTextElement = getBoundTextElement(element);
const boundTextElement = getBoundTextElement(element, elementsMap);
if (boundTextElement) {
coords = LinearElementEditor.getMinMaxXYWithBoundText(
element,

@ -342,7 +342,7 @@ export const refreshTextDimensions = (
text = wrapText(
text,
getFontString(textElement),
getBoundTextMaxWidth(container),
getBoundTextMaxWidth(container, textElement),
);
}
const dimensions = getAdjustedDimensions(textElement, text);

@ -126,6 +126,7 @@ export const transformElements = (
rotateMultipleElements(
originalElements,
selectedElements,
elementsMap,
pointerX,
pointerY,
shouldRotateWithDiscreteAngle,
@ -219,7 +220,7 @@ const measureFontSizeFromWidth = (
if (hasContainer) {
const container = getContainerElement(element, elementsMap);
if (container) {
width = getBoundTextMaxWidth(container);
width = getBoundTextMaxWidth(container, element);
}
}
const nextFontSize = element.fontSize * (nextWidth / width);
@ -394,7 +395,7 @@ export const resizeSingleElement = (
let scaleY = atStartBoundsHeight / boundsCurrentHeight;
let boundTextFont: { fontSize?: number; baseline?: number } = {};
const boundTextElement = getBoundTextElement(element);
const boundTextElement = getBoundTextElement(element, elementsMap);
if (transformHandleDirection.includes("e")) {
scaleX = (rotatedPointer[0] - startTopLeft[0]) / boundsCurrentWidth;
@ -458,7 +459,7 @@ export const resizeSingleElement = (
const nextFont = measureFontSizeFromWidth(
boundTextElement,
elementsMap,
getBoundTextMaxWidth(updatedElement),
getBoundTextMaxWidth(updatedElement, boundTextElement),
getBoundTextMaxHeight(updatedElement, boundTextElement),
);
if (nextFont === null) {
@ -640,6 +641,7 @@ export const resizeSingleElement = (
}
handleBindTextResize(
element,
elementsMap,
transformHandleDirection,
shouldMaintainAspectRatio,
);
@ -882,7 +884,7 @@ export const resizeMultipleElements = (
newSize: { width, height },
});
const boundTextElement = getBoundTextElement(element);
const boundTextElement = getBoundTextElement(element, elementsMap);
if (boundTextElement && boundTextFontSize) {
mutateElement(
boundTextElement,
@ -892,7 +894,7 @@ export const resizeMultipleElements = (
},
false,
);
handleBindTextResize(element, transformHandleType, true);
handleBindTextResize(element, elementsMap, transformHandleType, true);
}
}
@ -902,6 +904,7 @@ export const resizeMultipleElements = (
const rotateMultipleElements = (
originalElements: PointerDownState["originalElements"],
elements: readonly NonDeletedExcalidrawElement[],
elementsMap: ElementsMap,
pointerX: number,
pointerY: number,
shouldRotateWithDiscreteAngle: boolean,
@ -941,7 +944,7 @@ const rotateMultipleElements = (
);
updateBoundElements(element, { simultaneouslyUpdated: elements });
const boundText = getBoundTextElement(element);
const boundText = getBoundTextElement(element, elementsMap);
if (boundText && !isArrowElement(element)) {
mutateElement(
boundText,

@ -319,17 +319,17 @@ describe("Test measureText", () => {
it("should return max width when container is rectangle", () => {
const container = API.createElement({ type: "rectangle", ...params });
expect(getBoundTextMaxWidth(container)).toBe(168);
expect(getBoundTextMaxWidth(container, null)).toBe(168);
});
it("should return max width when container is ellipse", () => {
const container = API.createElement({ type: "ellipse", ...params });
expect(getBoundTextMaxWidth(container)).toBe(116);
expect(getBoundTextMaxWidth(container, null)).toBe(116);
});
it("should return max width when container is diamond", () => {
const container = API.createElement({ type: "diamond", ...params });
expect(getBoundTextMaxWidth(container)).toBe(79);
expect(getBoundTextMaxWidth(container, null)).toBe(79);
});
});

@ -23,7 +23,6 @@ import {
VERTICAL_ALIGN,
} from "../constants";
import { MaybeTransformHandleType } from "./transformHandles";
import Scene from "../scene/Scene";
import { isTextElement } from ".";
import { isBoundToContainer, isArrowElement } from "./typeChecks";
import { LinearElementEditor } from "./linearElementEditor";
@ -89,7 +88,7 @@ export const redrawTextBoundingBox = (
container,
textElement as ExcalidrawTextElementWithContainer,
);
const maxContainerWidth = getBoundTextMaxWidth(container);
const maxContainerWidth = getBoundTextMaxWidth(container, textElement);
if (!isArrowElement(container) && metrics.height > maxContainerHeight) {
const nextHeight = computeContainerDimensionForBoundText(
@ -162,6 +161,7 @@ export const bindTextToShapeAfterDuplication = (
export const handleBindTextResize = (
container: NonDeletedExcalidrawElement,
elementsMap: ElementsMap,
transformHandleType: MaybeTransformHandleType,
shouldMaintainAspectRatio = false,
) => {
@ -170,25 +170,17 @@ export const handleBindTextResize = (
return;
}
resetOriginalContainerCache(container.id);
let textElement = Scene.getScene(container)!.getElement(
boundTextElementId,
) as ExcalidrawTextElement;
const textElement = getBoundTextElement(container, elementsMap);
if (textElement && textElement.text) {
if (!container) {
return;
}
textElement = Scene.getScene(container)!.getElement(
boundTextElementId,
) as ExcalidrawTextElement;
let text = textElement.text;
let nextHeight = textElement.height;
let nextWidth = textElement.width;
const maxWidth = getBoundTextMaxWidth(container);
const maxHeight = getBoundTextMaxHeight(
container,
textElement as ExcalidrawTextElementWithContainer,
);
const maxWidth = getBoundTextMaxWidth(container, textElement);
const maxHeight = getBoundTextMaxHeight(container, textElement);
let containerHeight = container.height;
let nextBaseLine = textElement.baseline;
if (
@ -243,10 +235,7 @@ export const handleBindTextResize = (
if (!isArrowElement(container)) {
mutateElement(
textElement,
computeBoundTextPosition(
container,
textElement as ExcalidrawTextElementWithContainer,
),
computeBoundTextPosition(container, textElement),
);
}
}
@ -264,7 +253,7 @@ export const computeBoundTextPosition = (
}
const containerCoords = getContainerCoords(container);
const maxContainerHeight = getBoundTextMaxHeight(container, boundTextElement);
const maxContainerWidth = getBoundTextMaxWidth(container);
const maxContainerWidth = getBoundTextMaxWidth(container, boundTextElement);
let x;
let y;
@ -667,17 +656,18 @@ export const getBoundTextElementId = (container: ExcalidrawElement | null) => {
: null;
};
export const getBoundTextElement = (element: ExcalidrawElement | null) => {
export const getBoundTextElement = (
element: ExcalidrawElement | null,
elementsMap: ElementsMap,
) => {
if (!element) {
return null;
}
const boundTextElementId = getBoundTextElementId(element);
if (boundTextElementId) {
return (
(Scene.getScene(element)?.getElement(
boundTextElementId,
) as ExcalidrawTextElementWithContainer) || null
);
return (elementsMap.get(boundTextElementId) ||
null) as ExcalidrawTextElementWithContainer | null;
}
return null;
};
@ -699,6 +689,7 @@ export const getContainerElement = (
export const getContainerCenter = (
container: ExcalidrawElement,
appState: AppState,
elementsMap: ElementsMap,
) => {
if (!isArrowElement(container)) {
return {
@ -718,6 +709,7 @@ export const getContainerCenter = (
const index = container.points.length / 2 - 1;
let midSegmentMidpoint = LinearElementEditor.getEditorMidPoints(
container,
elementsMap,
appState,
)[index];
if (!midSegmentMidpoint) {
@ -877,9 +869,7 @@ export const computeContainerDimensionForBoundText = (
export const getBoundTextMaxWidth = (
container: ExcalidrawElement,
boundTextElement: ExcalidrawTextElement | null = getBoundTextElement(
container,
),
boundTextElement: ExcalidrawTextElement | null,
) => {
const { width } = container;
if (isArrowElement(container)) {

@ -34,6 +34,7 @@ import {
computeContainerDimensionForBoundText,
detectLineHeight,
computeBoundTextPosition,
getBoundTextElement,
} from "./textElement";
import {
actionDecreaseFontSize,
@ -196,7 +197,8 @@ export const textWysiwyg = ({
}
}
maxWidth = getBoundTextMaxWidth(container);
maxWidth = getBoundTextMaxWidth(container, updatedTextElement);
maxHeight = getBoundTextMaxHeight(
container,
updatedTextElement as ExcalidrawTextElementWithContainer,
@ -361,10 +363,14 @@ export const textWysiwyg = ({
fontFamily: app.state.currentItemFontFamily,
});
if (container) {
const boundTextElement = getBoundTextElement(
container,
app.scene.getNonDeletedElementsMap(),
);
const wrappedText = wrapText(
`${editable.value}${data}`,
font,
getBoundTextMaxWidth(container),
getBoundTextMaxWidth(container, boundTextElement),
);
const width = getTextWidth(wrappedText, font);
editable.style.width = `${width}px`;

@ -279,6 +279,16 @@ export type NonDeletedElementsMap = Map<
export type SceneElementsMap = Map<ExcalidrawElement["id"], ExcalidrawElement> &
MakeBrand<"SceneElementsMap">;
/**
* Map of all non-deleted Scene elements.
* Not a subset. Use this type when you need access to current Scene elements.
*/
export type NonDeletedSceneElementsMap = Map<
ExcalidrawElement["id"],
NonDeletedExcalidrawElement
> &
MakeBrand<"NonDeletedSceneElementsMap">;
export type ElementsMapOrArray =
| readonly ExcalidrawElement[]
| Readonly<ElementsMap>;

@ -444,6 +444,7 @@ export const addElementsToFrame = <T extends ElementsMapOrArray>(
elementsToAdd: NonDeletedExcalidrawElement[],
frame: ExcalidrawFrameLikeElement,
): T => {
const elementsMap = arrayToMap(allElements);
const currTargetFrameChildrenMap = new Map<ExcalidrawElement["id"], true>();
for (const element of allElements.values()) {
if (element.frameId === frame.id) {
@ -481,7 +482,7 @@ export const addElementsToFrame = <T extends ElementsMapOrArray>(
finalElementsToAdd.push(element);
}
const boundTextElement = getBoundTextElement(element);
const boundTextElement = getBoundTextElement(element, elementsMap);
if (
boundTextElement &&
!suppliedElementsToAddSet.has(boundTextElement.id) &&
@ -506,6 +507,7 @@ export const addElementsToFrame = <T extends ElementsMapOrArray>(
export const removeElementsFromFrame = (
elementsToRemove: ReadonlySetLike<NonDeletedExcalidrawElement>,
elementsMap: ElementsMap,
) => {
const _elementsToRemove = new Map<
ExcalidrawElement["id"],
@ -524,7 +526,7 @@ export const removeElementsFromFrame = (
const arr = toRemoveElementsByFrame.get(element.frameId) || [];
arr.push(element);
const boundTextElement = getBoundTextElement(element);
const boundTextElement = getBoundTextElement(element, elementsMap);
if (boundTextElement) {
_elementsToRemove.set(boundTextElement.id, boundTextElement);
arr.push(boundTextElement);
@ -550,7 +552,7 @@ export const removeAllElementsFromFrame = <T extends ExcalidrawElement>(
frame: ExcalidrawFrameLikeElement,
) => {
const elementsInFrame = getFrameChildren(allElements, frame.id);
removeElementsFromFrame(elementsInFrame);
removeElementsFromFrame(elementsInFrame, arrayToMap(allElements));
return allElements;
};
@ -558,6 +560,7 @@ export const replaceAllElementsInFrame = <T extends ExcalidrawElement>(
allElements: readonly T[],
nextElementsInFrame: ExcalidrawElement[],
frame: ExcalidrawFrameLikeElement,
app: AppClassProperties,
): T[] => {
return addElementsToFrame(
removeAllElementsFromFrame(allElements, frame),
@ -608,7 +611,7 @@ export const updateFrameMembershipOfSelectedElements = <
});
if (elementsToRemove.size > 0) {
removeElementsFromFrame(elementsToRemove);
removeElementsFromFrame(elementsToRemove, elementsMap);
}
return allElements;
};

@ -4,6 +4,7 @@ import {
NonDeleted,
NonDeletedExcalidrawElement,
ElementsMapOrArray,
ElementsMap,
} from "./element/types";
import {
AppClassProperties,
@ -329,12 +330,12 @@ export const removeFromSelectedGroups = (
export const getMaximumGroups = (
elements: ExcalidrawElement[],
elementsMap: ElementsMap,
): ExcalidrawElement[][] => {
const groups: Map<String, ExcalidrawElement[]> = new Map<
String,
ExcalidrawElement[]
>();
elements.forEach((element: ExcalidrawElement) => {
const groupId =
element.groupIds.length === 0
@ -344,7 +345,7 @@ export const getMaximumGroups = (
const currentGroupMembers = groups.get(groupId) || [];
// Include bound text if present when grouping
const boundTextElement = getBoundTextElement(element);
const boundTextElement = getBoundTextElement(element, elementsMap);
if (boundTextElement) {
currentGroupMembers.push(boundTextElement);
}

@ -6,6 +6,7 @@ import {
ExcalidrawImageElement,
ExcalidrawTextElementWithContainer,
ExcalidrawFrameLikeElement,
NonDeletedSceneElementsMap,
} from "../element/types";
import {
isTextElement,
@ -190,6 +191,7 @@ const cappedElementCanvasSize = (
const generateElementCanvas = (
element: NonDeletedExcalidrawElement,
elementsMap: RenderableElementsMap,
zoom: Zoom,
renderConfig: StaticCanvasRenderConfig,
appState: StaticCanvasAppState,
@ -247,7 +249,8 @@ const generateElementCanvas = (
zoomValue: zoom.value,
canvasOffsetX,
canvasOffsetY,
boundTextElementVersion: getBoundTextElement(element)?.version || null,
boundTextElementVersion:
getBoundTextElement(element, elementsMap)?.version || null,
containingFrameOpacity: getContainingFrame(element)?.opacity || 100,
};
};
@ -407,6 +410,7 @@ export const elementWithCanvasCache = new WeakMap<
const generateElementWithCanvas = (
element: NonDeletedExcalidrawElement,
elementsMap: RenderableElementsMap,
renderConfig: StaticCanvasRenderConfig,
appState: StaticCanvasAppState,
) => {
@ -416,7 +420,9 @@ const generateElementWithCanvas = (
prevElementWithCanvas &&
prevElementWithCanvas.zoomValue !== zoom.value &&
!appState?.shouldCacheIgnoreZoom;
const boundTextElementVersion = getBoundTextElement(element)?.version || null;
const boundTextElementVersion =
getBoundTextElement(element, elementsMap)?.version || null;
const containingFrameOpacity = getContainingFrame(element)?.opacity || 100;
if (
@ -428,6 +434,7 @@ const generateElementWithCanvas = (
) {
const elementWithCanvas = generateElementCanvas(
element,
elementsMap,
zoom,
renderConfig,
appState,
@ -445,6 +452,7 @@ const drawElementFromCanvas = (
context: CanvasRenderingContext2D,
renderConfig: StaticCanvasRenderConfig,
appState: StaticCanvasAppState,
allElementsMap: NonDeletedSceneElementsMap,
) => {
const element = elementWithCanvas.element;
const padding = getCanvasPadding(element);
@ -464,7 +472,8 @@ const drawElementFromCanvas = (
context.save();
context.scale(1 / window.devicePixelRatio, 1 / window.devicePixelRatio);
const boundTextElement = getBoundTextElement(element);
const boundTextElement = getBoundTextElement(element, allElementsMap);
if (isArrowElement(element) && boundTextElement) {
const tempCanvas = document.createElement("canvas");
@ -511,7 +520,6 @@ const drawElementFromCanvas = (
offsetY -
padding * zoom;
tempCanvasContext.translate(-shiftX, -shiftY);
// Clear the bound text area
tempCanvasContext.clearRect(
-(boundTextElement.width / 2 + BOUND_TEXT_PADDING) *
@ -573,6 +581,7 @@ const drawElementFromCanvas = (
) {
const textElement = getBoundTextElement(
element,
allElementsMap,
) as ExcalidrawTextElementWithContainer;
const coords = getContainerCoords(element);
context.strokeStyle = "#c92a2a";
@ -580,7 +589,7 @@ const drawElementFromCanvas = (
context.strokeRect(
(coords.x + appState.scrollX) * window.devicePixelRatio,
(coords.y + appState.scrollY) * window.devicePixelRatio,
getBoundTextMaxWidth(element) * window.devicePixelRatio,
getBoundTextMaxWidth(element, textElement) * window.devicePixelRatio,
getBoundTextMaxHeight(element, textElement) * window.devicePixelRatio,
);
}
@ -616,6 +625,7 @@ export const renderSelectionElement = (
export const renderElement = (
element: NonDeletedExcalidrawElement,
elementsMap: RenderableElementsMap,
allElementsMap: NonDeletedSceneElementsMap,
rc: RoughCanvas,
context: CanvasRenderingContext2D,
renderConfig: StaticCanvasRenderConfig,
@ -687,6 +697,7 @@ export const renderElement = (
} else {
const elementWithCanvas = generateElementWithCanvas(
element,
elementsMap,
renderConfig,
appState,
);
@ -695,6 +706,7 @@ export const renderElement = (
context,
renderConfig,
appState,
allElementsMap,
);
}
@ -737,7 +749,7 @@ export const renderElement = (
if (shouldResetImageFilter(element, renderConfig, appState)) {
context.filter = "none";
}
const boundTextElement = getBoundTextElement(element);
const boundTextElement = getBoundTextElement(element, elementsMap);
if (isArrowElement(element) && boundTextElement) {
const tempCanvas = document.createElement("canvas");
@ -820,6 +832,7 @@ export const renderElement = (
} else {
const elementWithCanvas = generateElementWithCanvas(
element,
elementsMap,
renderConfig,
appState,
);
@ -851,6 +864,7 @@ export const renderElement = (
context,
renderConfig,
appState,
allElementsMap,
);
// reset
@ -1096,7 +1110,7 @@ export const renderElementToSvg = (
}
case "line":
case "arrow": {
const boundText = getBoundTextElement(element);
const boundText = getBoundTextElement(element, elementsMap);
const maskPath = svgRoot.ownerDocument!.createElementNS(SVG_NS, "mask");
if (boundText) {
maskPath.setAttribute("id", `mask-${element.id}`);

@ -246,6 +246,7 @@ const renderLinearPointHandles = (
context: CanvasRenderingContext2D,
appState: InteractiveCanvasAppState,
element: NonDeleted<ExcalidrawLinearElement>,
elementsMap: RenderableElementsMap,
) => {
if (!appState.selectedLinearElement) {
return;
@ -269,6 +270,7 @@ const renderLinearPointHandles = (
//Rendering segment mid points
const midPoints = LinearElementEditor.getEditorMidPoints(
element,
elementsMap,
appState,
).filter((midPoint) => midPoint !== null) as Point[];
@ -485,7 +487,12 @@ const _renderInteractiveScene = ({
});
if (editingLinearElement) {
renderLinearPointHandles(context, appState, editingLinearElement);
renderLinearPointHandles(
context,
appState,
editingLinearElement,
elementsMap,
);
}
// Paint selection element
@ -528,6 +535,7 @@ const _renderInteractiveScene = ({
context,
appState,
selectedElements[0] as NonDeleted<ExcalidrawLinearElement>,
elementsMap,
);
}
@ -553,6 +561,7 @@ const _renderInteractiveScene = ({
context,
appState,
selectedElements[0] as ExcalidrawLinearElement,
elementsMap,
);
}
const selectionColor = renderConfig.selectionColor || oc.black;
@ -891,6 +900,7 @@ const _renderStaticScene = ({
canvas,
rc,
elementsMap,
allElementsMap,
visibleElements,
scale,
appState,
@ -972,6 +982,7 @@ const _renderStaticScene = ({
renderElement(
element,
elementsMap,
allElementsMap,
rc,
context,
renderConfig,
@ -982,6 +993,7 @@ const _renderStaticScene = ({
renderElement(
element,
elementsMap,
allElementsMap,
rc,
context,
renderConfig,
@ -1005,6 +1017,7 @@ const _renderStaticScene = ({
renderElement(
element,
elementsMap,
allElementsMap,
rc,
context,
renderConfig,
@ -1024,6 +1037,7 @@ const _renderStaticScene = ({
renderElement(
label,
elementsMap,
allElementsMap,
rc,
context,
renderConfig,

@ -4,8 +4,8 @@ import {
NonDeleted,
ExcalidrawFrameLikeElement,
ElementsMapOrArray,
NonDeletedElementsMap,
SceneElementsMap,
NonDeletedSceneElementsMap,
} from "../element/types";
import { isNonDeletedElement } from "../element";
import { LinearElementEditor } from "../element/linearElementEditor";
@ -27,7 +27,7 @@ type SelectionHash = string & { __brand: "selectionHash" };
const getNonDeletedElements = <T extends ExcalidrawElement>(
allElements: readonly T[],
) => {
const elementsMap = new Map() as NonDeletedElementsMap;
const elementsMap = new Map() as NonDeletedSceneElementsMap;
const elements: T[] = [];
for (const element of allElements) {
if (!element.isDeleted) {
@ -120,8 +120,9 @@ class Scene {
private callbacks: Set<SceneStateCallback> = new Set();
private nonDeletedElements: readonly NonDeletedExcalidrawElement[] = [];
private nonDeletedElementsMap: NonDeletedElementsMap =
new Map() as NonDeletedElementsMap;
private nonDeletedElementsMap = toBrandedType<NonDeletedSceneElementsMap>(
new Map(),
);
private elements: readonly ExcalidrawElement[] = [];
private nonDeletedFramesLikes: readonly NonDeleted<ExcalidrawFrameLikeElement>[] =
[];

@ -4,6 +4,7 @@ import {
ExcalidrawFrameLikeElement,
ExcalidrawTextElement,
NonDeletedExcalidrawElement,
NonDeletedSceneElementsMap,
} from "../element/types";
import {
Bounds,
@ -248,14 +249,15 @@ export const exportToCanvas = async (
files,
});
const elementsMap = toBrandedType<RenderableElementsMap>(
arrayToMap(elementsForRender),
);
renderStaticScene({
canvas,
rc: rough.canvas(canvas),
elementsMap,
elementsMap: toBrandedType<RenderableElementsMap>(
arrayToMap(elementsForRender),
),
allElementsMap: toBrandedType<NonDeletedSceneElementsMap>(
arrayToMap(elements),
),
visibleElements: elementsForRender,
scale,
appState: {

@ -4,6 +4,7 @@ import {
ExcalidrawTextElement,
NonDeletedElementsMap,
NonDeletedExcalidrawElement,
NonDeletedSceneElementsMap,
} from "../element/types";
import {
AppClassProperties,
@ -66,6 +67,7 @@ export type StaticSceneRenderConfig = {
canvas: HTMLCanvasElement;
rc: RoughCanvas;
elementsMap: RenderableElementsMap;
allElementsMap: NonDeletedSceneElementsMap;
visibleElements: readonly NonDeletedExcalidrawElement[];
scale: number;
appState: StaticCanvasAppState;

@ -16,6 +16,7 @@ import { KEYS } from "./keys";
import { rangeIntersection, rangesOverlap, rotatePoint } from "./math";
import { getVisibleAndNonSelectedElements } from "./scene/selection";
import { AppState, KeyboardModifiersObject, Point } from "./types";
import { arrayToMap } from "./utils";
const SNAP_DISTANCE = 8;
@ -286,7 +287,10 @@ export const getVisibleGaps = (
appState,
);
const referenceBounds = getMaximumGroups(referenceElements)
const referenceBounds = getMaximumGroups(
referenceElements,
arrayToMap(elements),
)
.filter(
(elementsGroup) =>
!(elementsGroup.length === 1 && isBoundToContainer(elementsGroup[0])),
@ -572,7 +576,7 @@ export const getReferenceSnapPoints = (
appState,
);
return getMaximumGroups(referenceElements)
return getMaximumGroups(referenceElements, arrayToMap(elements))
.filter(
(elementsGroup) =>
!(elementsGroup.length === 1 && isBoundToContainer(elementsGroup[0])),

@ -24,6 +24,7 @@ import {
import * as textElementUtils from "../element/textElement";
import { ROUNDNESS, VERTICAL_ALIGN } from "../constants";
import { vi } from "vitest";
import { arrayToMap } from "../utils";
const renderInteractiveScene = vi.spyOn(Renderer, "renderInteractiveScene");
const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene");
@ -307,6 +308,7 @@ describe("Test Linear Elements", () => {
const midPointsWithSharpEdge = LinearElementEditor.getEditorMidPoints(
line,
h.app.scene.getNonDeletedElementsMap(),
h.state,
);
@ -320,6 +322,7 @@ describe("Test Linear Elements", () => {
const midPointsWithRoundEdge = LinearElementEditor.getEditorMidPoints(
h.elements[0] as ExcalidrawLinearElement,
h.app.scene.getNonDeletedElementsMap(),
h.state,
);
expect(midPointsWithRoundEdge[0]).not.toEqual(midPointsWithSharpEdge[0]);
@ -351,7 +354,11 @@ describe("Test Linear Elements", () => {
const points = LinearElementEditor.getPointsGlobalCoordinates(line);
expect([line.x, line.y]).toEqual(points[0]);
const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
const midPoints = LinearElementEditor.getEditorMidPoints(
line,
h.app.scene.getNonDeletedElementsMap(),
h.state,
);
const startPoint = centerPoint(points[0], midPoints[0] as Point);
const deltaX = 50;
@ -373,6 +380,7 @@ describe("Test Linear Elements", () => {
const newMidPoints = LinearElementEditor.getEditorMidPoints(
line,
h.app.scene.getNonDeletedElementsMap(),
h.state,
);
expect(midPoints[0]).not.toEqual(newMidPoints[0]);
@ -458,7 +466,11 @@ describe("Test Linear Elements", () => {
it("should update only the first segment midpoint when its point is dragged", async () => {
const points = LinearElementEditor.getPointsGlobalCoordinates(line);
const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
const midPoints = LinearElementEditor.getEditorMidPoints(
line,
h.app.scene.getNonDeletedElementsMap(),
h.state,
);
const hitCoords: Point = [points[0][0], points[0][1]];
@ -478,6 +490,7 @@ describe("Test Linear Elements", () => {
const newMidPoints = LinearElementEditor.getEditorMidPoints(
line,
h.app.scene.getNonDeletedElementsMap(),
h.state,
);
@ -487,7 +500,11 @@ describe("Test Linear Elements", () => {
it("should hide midpoints in the segment when points moved close", async () => {
const points = LinearElementEditor.getPointsGlobalCoordinates(line);
const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
const midPoints = LinearElementEditor.getEditorMidPoints(
line,
h.app.scene.getNonDeletedElementsMap(),
h.state,
);
const hitCoords: Point = [points[0][0], points[0][1]];
@ -507,6 +524,7 @@ describe("Test Linear Elements", () => {
const newMidPoints = LinearElementEditor.getEditorMidPoints(
line,
h.app.scene.getNonDeletedElementsMap(),
h.state,
);
// This midpoint is hidden since the points are too close
@ -526,7 +544,11 @@ describe("Test Linear Elements", () => {
]);
expect(line.points.length).toEqual(4);
const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
const midPoints = LinearElementEditor.getEditorMidPoints(
line,
h.app.scene.getNonDeletedElementsMap(),
h.state,
);
// delete 3rd point
deletePoint(points[2]);
@ -538,6 +560,7 @@ describe("Test Linear Elements", () => {
const newMidPoints = LinearElementEditor.getEditorMidPoints(
line,
h.app.scene.getNonDeletedElementsMap(),
h.state,
);
expect(newMidPoints.length).toEqual(2);
@ -615,7 +638,11 @@ describe("Test Linear Elements", () => {
it("should update all the midpoints when its point is dragged", async () => {
const points = LinearElementEditor.getPointsGlobalCoordinates(line);
const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
const midPoints = LinearElementEditor.getEditorMidPoints(
line,
h.app.scene.getNonDeletedElementsMap(),
h.state,
);
const hitCoords: Point = [points[0][0], points[0][1]];
@ -630,6 +657,7 @@ describe("Test Linear Elements", () => {
const newMidPoints = LinearElementEditor.getEditorMidPoints(
line,
h.app.scene.getNonDeletedElementsMap(),
h.state,
);
@ -651,7 +679,11 @@ describe("Test Linear Elements", () => {
it("should hide midpoints in the segment when points moved close", async () => {
const points = LinearElementEditor.getPointsGlobalCoordinates(line);
const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
const midPoints = LinearElementEditor.getEditorMidPoints(
line,
h.app.scene.getNonDeletedElementsMap(),
h.state,
);
const hitCoords: Point = [points[0][0], points[0][1]];
@ -671,6 +703,7 @@ describe("Test Linear Elements", () => {
const newMidPoints = LinearElementEditor.getEditorMidPoints(
line,
h.app.scene.getNonDeletedElementsMap(),
h.state,
);
// This mid point is hidden due to point being too close
@ -685,7 +718,11 @@ describe("Test Linear Elements", () => {
]);
expect(line.points.length).toEqual(4);
const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
const midPoints = LinearElementEditor.getEditorMidPoints(
line,
h.app.scene.getNonDeletedElementsMap(),
h.state,
);
const points = LinearElementEditor.getPointsGlobalCoordinates(line);
// delete 3rd point
@ -694,6 +731,7 @@ describe("Test Linear Elements", () => {
const newMidPoints = LinearElementEditor.getEditorMidPoints(
line,
h.app.scene.getNonDeletedElementsMap(),
h.state,
);
expect(newMidPoints.length).toEqual(2);
@ -762,7 +800,7 @@ describe("Test Linear Elements", () => {
type: "text",
x: 0,
y: 0,
text: wrapText(text, font, getBoundTextMaxWidth(container)),
text: wrapText(text, font, getBoundTextMaxWidth(container, null)),
containerId: container.id,
width: 30,
height: 20,
@ -986,8 +1024,13 @@ describe("Test Linear Elements", () => {
collaboration made
easy"
`);
expect(LinearElementEditor.getElementAbsoluteCoords(container, true))
.toMatchInlineSnapshot(`
expect(
LinearElementEditor.getElementAbsoluteCoords(
container,
h.app.scene.getNonDeletedElementsMap(),
true,
),
).toMatchInlineSnapshot(`
[
20,
20,
@ -1020,8 +1063,13 @@ describe("Test Linear Elements", () => {
"Online whiteboard
collaboration made easy"
`);
expect(LinearElementEditor.getElementAbsoluteCoords(container, true))
.toMatchInlineSnapshot(`
expect(
LinearElementEditor.getElementAbsoluteCoords(
container,
h.app.scene.getNonDeletedElementsMap(),
true,
),
).toMatchInlineSnapshot(`
[
20,
35,
@ -1121,7 +1169,11 @@ describe("Test Linear Elements", () => {
expect(rect.x).toBe(400);
expect(rect.y).toBe(0);
expect(
wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)),
wrapText(
textElement.originalText,
font,
getBoundTextMaxWidth(arrow, null),
),
).toMatchInlineSnapshot(`
"Online whiteboard
collaboration made easy"
@ -1140,11 +1192,17 @@ describe("Test Linear Elements", () => {
expect(rect.x).toBe(200);
expect(rect.y).toBe(0);
expect(handleBindTextResizeSpy).toHaveBeenCalledWith(
h.elements[1],
h.elements[0],
arrayToMap(h.elements),
"nw",
false,
);
expect(
wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)),
wrapText(
textElement.originalText,
font,
getBoundTextMaxWidth(arrow, null),
),
).toMatchInlineSnapshot(`
"Online whiteboard
collaboration made

Loading…
Cancel
Save