mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-04-14 16:40:58 -04:00
refactor: decoupling global Scene state part-1 (#7577)
This commit is contained in:
parent
740a165452
commit
0415c616b1
31 changed files with 630 additions and 384 deletions
|
@ -1,9 +1,13 @@
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import { ExcalidrawElement, NonDeleted } from "../element/types";
|
import {
|
||||||
|
ExcalidrawElement,
|
||||||
|
NonDeleted,
|
||||||
|
NonDeletedElementsMap,
|
||||||
|
} from "../element/types";
|
||||||
import { resizeMultipleElements } from "../element/resizeElements";
|
import { resizeMultipleElements } from "../element/resizeElements";
|
||||||
import { AppState, PointerDownState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
import { CODES, KEYS } from "../keys";
|
import { CODES, KEYS } from "../keys";
|
||||||
import { getCommonBoundingBox } from "../element/bounds";
|
import { getCommonBoundingBox } from "../element/bounds";
|
||||||
|
@ -20,7 +24,12 @@ export const actionFlipHorizontal = register({
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
elements: updateFrameMembershipOfSelectedElements(
|
elements: updateFrameMembershipOfSelectedElements(
|
||||||
flipSelectedElements(elements, appState, "horizontal"),
|
flipSelectedElements(
|
||||||
|
elements,
|
||||||
|
app.scene.getNonDeletedElementsMap(),
|
||||||
|
appState,
|
||||||
|
"horizontal",
|
||||||
|
),
|
||||||
appState,
|
appState,
|
||||||
app,
|
app,
|
||||||
),
|
),
|
||||||
|
@ -38,7 +47,12 @@ export const actionFlipVertical = register({
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
elements: updateFrameMembershipOfSelectedElements(
|
elements: updateFrameMembershipOfSelectedElements(
|
||||||
flipSelectedElements(elements, appState, "vertical"),
|
flipSelectedElements(
|
||||||
|
elements,
|
||||||
|
app.scene.getNonDeletedElementsMap(),
|
||||||
|
appState,
|
||||||
|
"vertical",
|
||||||
|
),
|
||||||
appState,
|
appState,
|
||||||
app,
|
app,
|
||||||
),
|
),
|
||||||
|
@ -53,6 +67,7 @@ export const actionFlipVertical = register({
|
||||||
|
|
||||||
const flipSelectedElements = (
|
const flipSelectedElements = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
|
elementsMap: NonDeletedElementsMap,
|
||||||
appState: Readonly<AppState>,
|
appState: Readonly<AppState>,
|
||||||
flipDirection: "horizontal" | "vertical",
|
flipDirection: "horizontal" | "vertical",
|
||||||
) => {
|
) => {
|
||||||
|
@ -67,6 +82,7 @@ const flipSelectedElements = (
|
||||||
|
|
||||||
const updatedElements = flipElements(
|
const updatedElements = flipElements(
|
||||||
selectedElements,
|
selectedElements,
|
||||||
|
elementsMap,
|
||||||
appState,
|
appState,
|
||||||
flipDirection,
|
flipDirection,
|
||||||
);
|
);
|
||||||
|
@ -79,15 +95,17 @@ const flipSelectedElements = (
|
||||||
};
|
};
|
||||||
|
|
||||||
const flipElements = (
|
const flipElements = (
|
||||||
elements: NonDeleted<ExcalidrawElement>[],
|
selectedElements: NonDeleted<ExcalidrawElement>[],
|
||||||
|
elementsMap: NonDeletedElementsMap,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
flipDirection: "horizontal" | "vertical",
|
flipDirection: "horizontal" | "vertical",
|
||||||
): ExcalidrawElement[] => {
|
): ExcalidrawElement[] => {
|
||||||
const { minX, minY, maxX, maxY } = getCommonBoundingBox(elements);
|
const { minX, minY, maxX, maxY } = getCommonBoundingBox(selectedElements);
|
||||||
|
|
||||||
resizeMultipleElements(
|
resizeMultipleElements(
|
||||||
{ originalElements: arrayToMap(elements) } as PointerDownState,
|
elementsMap,
|
||||||
elements,
|
selectedElements,
|
||||||
|
elementsMap,
|
||||||
"nw",
|
"nw",
|
||||||
true,
|
true,
|
||||||
flipDirection === "horizontal" ? maxX : minX,
|
flipDirection === "horizontal" ? maxX : minX,
|
||||||
|
@ -96,7 +114,7 @@ const flipElements = (
|
||||||
|
|
||||||
(isBindingEnabled(appState)
|
(isBindingEnabled(appState)
|
||||||
? bindOrUnbindSelectedElements
|
? bindOrUnbindSelectedElements
|
||||||
: unbindLinearElements)(elements);
|
: unbindLinearElements)(selectedElements);
|
||||||
|
|
||||||
return elements;
|
return selectedElements;
|
||||||
};
|
};
|
||||||
|
|
|
@ -63,11 +63,7 @@ export const actionRemoveAllElementsFromFrame = register({
|
||||||
|
|
||||||
if (isFrameLikeElement(selectedElement)) {
|
if (isFrameLikeElement(selectedElement)) {
|
||||||
return {
|
return {
|
||||||
elements: removeAllElementsFromFrame(
|
elements: removeAllElementsFromFrame(elements, selectedElement),
|
||||||
elements,
|
|
||||||
selectedElement,
|
|
||||||
appState,
|
|
||||||
),
|
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
selectedElementIds: {
|
selectedElementIds: {
|
||||||
|
|
|
@ -105,11 +105,7 @@ export const actionGroup = register({
|
||||||
const frameElementsMap = groupByFrameLikes(selectedElements);
|
const frameElementsMap = groupByFrameLikes(selectedElements);
|
||||||
|
|
||||||
frameElementsMap.forEach((elementsInFrame, frameId) => {
|
frameElementsMap.forEach((elementsInFrame, frameId) => {
|
||||||
nextElements = removeElementsFromFrame(
|
removeElementsFromFrame(elementsInFrame);
|
||||||
nextElements,
|
|
||||||
elementsInFrame,
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,7 +225,6 @@ export const actionUngroup = register({
|
||||||
nextElements,
|
nextElements,
|
||||||
getElementsInResizingFrame(nextElements, frame, appState),
|
getElementsInResizingFrame(nextElements, frame, appState),
|
||||||
frame,
|
frame,
|
||||||
appState,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { AppState, Primitive } from "../types";
|
import { AppClassProperties, AppState, Primitive } from "../types";
|
||||||
import {
|
import {
|
||||||
DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
|
DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
|
||||||
DEFAULT_ELEMENT_BACKGROUND_PICKS,
|
DEFAULT_ELEMENT_BACKGROUND_PICKS,
|
||||||
|
@ -66,7 +66,6 @@ import {
|
||||||
import { mutateElement, newElementWith } from "../element/mutateElement";
|
import { mutateElement, newElementWith } from "../element/mutateElement";
|
||||||
import {
|
import {
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getContainerElement,
|
|
||||||
getDefaultLineHeight,
|
getDefaultLineHeight,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import {
|
import {
|
||||||
|
@ -189,6 +188,7 @@ const offsetElementAfterFontResize = (
|
||||||
const changeFontSize = (
|
const changeFontSize = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
|
app: AppClassProperties,
|
||||||
getNewFontSize: (element: ExcalidrawTextElement) => number,
|
getNewFontSize: (element: ExcalidrawTextElement) => number,
|
||||||
fallbackValue?: ExcalidrawTextElement["fontSize"],
|
fallbackValue?: ExcalidrawTextElement["fontSize"],
|
||||||
) => {
|
) => {
|
||||||
|
@ -206,7 +206,10 @@ const changeFontSize = (
|
||||||
let newElement: ExcalidrawTextElement = newElementWith(oldElement, {
|
let newElement: ExcalidrawTextElement = newElementWith(oldElement, {
|
||||||
fontSize: newFontSize,
|
fontSize: newFontSize,
|
||||||
});
|
});
|
||||||
redrawTextBoundingBox(newElement, getContainerElement(oldElement));
|
redrawTextBoundingBox(
|
||||||
|
newElement,
|
||||||
|
app.scene.getContainerElement(oldElement),
|
||||||
|
);
|
||||||
|
|
||||||
newElement = offsetElementAfterFontResize(oldElement, newElement);
|
newElement = offsetElementAfterFontResize(oldElement, newElement);
|
||||||
|
|
||||||
|
@ -600,8 +603,8 @@ export const actionChangeOpacity = register({
|
||||||
export const actionChangeFontSize = register({
|
export const actionChangeFontSize = register({
|
||||||
name: "changeFontSize",
|
name: "changeFontSize",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
perform: (elements, appState, value) => {
|
perform: (elements, appState, value, app) => {
|
||||||
return changeFontSize(elements, appState, () => value, value);
|
return changeFontSize(elements, appState, app, () => value, value);
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
|
@ -663,8 +666,8 @@ export const actionChangeFontSize = register({
|
||||||
export const actionDecreaseFontSize = register({
|
export const actionDecreaseFontSize = register({
|
||||||
name: "decreaseFontSize",
|
name: "decreaseFontSize",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
perform: (elements, appState, value) => {
|
perform: (elements, appState, value, app) => {
|
||||||
return changeFontSize(elements, appState, (element) =>
|
return changeFontSize(elements, appState, app, (element) =>
|
||||||
Math.round(
|
Math.round(
|
||||||
// get previous value before relative increase (doesn't work fully
|
// get previous value before relative increase (doesn't work fully
|
||||||
// due to rounding and float precision issues)
|
// due to rounding and float precision issues)
|
||||||
|
@ -685,8 +688,8 @@ export const actionDecreaseFontSize = register({
|
||||||
export const actionIncreaseFontSize = register({
|
export const actionIncreaseFontSize = register({
|
||||||
name: "increaseFontSize",
|
name: "increaseFontSize",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
perform: (elements, appState, value) => {
|
perform: (elements, appState, value, app) => {
|
||||||
return changeFontSize(elements, appState, (element) =>
|
return changeFontSize(elements, appState, app, (element) =>
|
||||||
Math.round(element.fontSize * (1 + FONT_SIZE_RELATIVE_INCREASE_STEP)),
|
Math.round(element.fontSize * (1 + FONT_SIZE_RELATIVE_INCREASE_STEP)),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -703,7 +706,7 @@ export const actionIncreaseFontSize = register({
|
||||||
export const actionChangeFontFamily = register({
|
export const actionChangeFontFamily = register({
|
||||||
name: "changeFontFamily",
|
name: "changeFontFamily",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
perform: (elements, appState, value) => {
|
perform: (elements, appState, value, app) => {
|
||||||
return {
|
return {
|
||||||
elements: changeProperty(
|
elements: changeProperty(
|
||||||
elements,
|
elements,
|
||||||
|
@ -717,7 +720,10 @@ export const actionChangeFontFamily = register({
|
||||||
lineHeight: getDefaultLineHeight(value),
|
lineHeight: getDefaultLineHeight(value),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
redrawTextBoundingBox(newElement, getContainerElement(oldElement));
|
redrawTextBoundingBox(
|
||||||
|
newElement,
|
||||||
|
app.scene.getContainerElement(oldElement),
|
||||||
|
);
|
||||||
return newElement;
|
return newElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -795,7 +801,7 @@ export const actionChangeFontFamily = register({
|
||||||
export const actionChangeTextAlign = register({
|
export const actionChangeTextAlign = register({
|
||||||
name: "changeTextAlign",
|
name: "changeTextAlign",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
perform: (elements, appState, value) => {
|
perform: (elements, appState, value, app) => {
|
||||||
return {
|
return {
|
||||||
elements: changeProperty(
|
elements: changeProperty(
|
||||||
elements,
|
elements,
|
||||||
|
@ -806,7 +812,10 @@ export const actionChangeTextAlign = register({
|
||||||
oldElement,
|
oldElement,
|
||||||
{ textAlign: value },
|
{ textAlign: value },
|
||||||
);
|
);
|
||||||
redrawTextBoundingBox(newElement, getContainerElement(oldElement));
|
redrawTextBoundingBox(
|
||||||
|
newElement,
|
||||||
|
app.scene.getContainerElement(oldElement),
|
||||||
|
);
|
||||||
return newElement;
|
return newElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -875,7 +884,7 @@ export const actionChangeTextAlign = register({
|
||||||
export const actionChangeVerticalAlign = register({
|
export const actionChangeVerticalAlign = register({
|
||||||
name: "changeVerticalAlign",
|
name: "changeVerticalAlign",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState, value) => {
|
perform: (elements, appState, value, app) => {
|
||||||
return {
|
return {
|
||||||
elements: changeProperty(
|
elements: changeProperty(
|
||||||
elements,
|
elements,
|
||||||
|
@ -887,7 +896,10 @@ export const actionChangeVerticalAlign = register({
|
||||||
{ verticalAlign: value },
|
{ verticalAlign: value },
|
||||||
);
|
);
|
||||||
|
|
||||||
redrawTextBoundingBox(newElement, getContainerElement(oldElement));
|
redrawTextBoundingBox(
|
||||||
|
newElement,
|
||||||
|
app.scene.getContainerElement(oldElement),
|
||||||
|
);
|
||||||
return newElement;
|
return newElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import React, { useState } from "react";
|
import { useState } from "react";
|
||||||
import { ActionManager } from "../actions/manager";
|
import { ActionManager } from "../actions/manager";
|
||||||
import { getNonDeletedElements } from "../element";
|
import { ExcalidrawElementType, NonDeletedElementsMap } from "../element/types";
|
||||||
import { ExcalidrawElement, ExcalidrawElementType } from "../element/types";
|
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useDevice } from "./App";
|
import { useDevice } from "./App";
|
||||||
import {
|
import {
|
||||||
|
@ -44,17 +43,14 @@ import { useTunnels } from "../context/tunnels";
|
||||||
|
|
||||||
export const SelectedShapeActions = ({
|
export const SelectedShapeActions = ({
|
||||||
appState,
|
appState,
|
||||||
elements,
|
elementsMap,
|
||||||
renderAction,
|
renderAction,
|
||||||
}: {
|
}: {
|
||||||
appState: UIAppState;
|
appState: UIAppState;
|
||||||
elements: readonly ExcalidrawElement[];
|
elementsMap: NonDeletedElementsMap;
|
||||||
renderAction: ActionManager["renderAction"];
|
renderAction: ActionManager["renderAction"];
|
||||||
}) => {
|
}) => {
|
||||||
const targetElements = getTargetElements(
|
const targetElements = getTargetElements(elementsMap, appState);
|
||||||
getNonDeletedElements(elements),
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
|
|
||||||
let isSingleElementBoundContainer = false;
|
let isSingleElementBoundContainer = false;
|
||||||
if (
|
if (
|
||||||
|
@ -137,12 +133,12 @@ export const SelectedShapeActions = ({
|
||||||
{renderAction("changeFontFamily")}
|
{renderAction("changeFontFamily")}
|
||||||
|
|
||||||
{(appState.activeTool.type === "text" ||
|
{(appState.activeTool.type === "text" ||
|
||||||
suppportsHorizontalAlign(targetElements)) &&
|
suppportsHorizontalAlign(targetElements, elementsMap)) &&
|
||||||
renderAction("changeTextAlign")}
|
renderAction("changeTextAlign")}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{shouldAllowVerticalAlign(targetElements) &&
|
{shouldAllowVerticalAlign(targetElements, elementsMap) &&
|
||||||
renderAction("changeVerticalAlign")}
|
renderAction("changeVerticalAlign")}
|
||||||
{(canHaveArrowheads(appState.activeTool.type) ||
|
{(canHaveArrowheads(appState.activeTool.type) ||
|
||||||
targetElements.some((element) => canHaveArrowheads(element.type))) && (
|
targetElements.some((element) => canHaveArrowheads(element.type))) && (
|
||||||
|
|
|
@ -1417,7 +1417,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
const { renderTopRightUI, renderCustomStats } = this.props;
|
const { renderTopRightUI, renderCustomStats } = this.props;
|
||||||
|
|
||||||
const versionNonce = this.scene.getVersionNonce();
|
const versionNonce = this.scene.getVersionNonce();
|
||||||
const { canvasElements, visibleElements } =
|
const { elementsMap, visibleElements } =
|
||||||
this.renderer.getRenderableElements({
|
this.renderer.getRenderableElements({
|
||||||
versionNonce,
|
versionNonce,
|
||||||
zoom: this.state.zoom,
|
zoom: this.state.zoom,
|
||||||
|
@ -1627,7 +1627,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
<StaticCanvas
|
<StaticCanvas
|
||||||
canvas={this.canvas}
|
canvas={this.canvas}
|
||||||
rc={this.rc}
|
rc={this.rc}
|
||||||
elements={canvasElements}
|
elementsMap={elementsMap}
|
||||||
visibleElements={visibleElements}
|
visibleElements={visibleElements}
|
||||||
versionNonce={versionNonce}
|
versionNonce={versionNonce}
|
||||||
selectionNonce={
|
selectionNonce={
|
||||||
|
@ -1648,7 +1648,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
<InteractiveCanvas
|
<InteractiveCanvas
|
||||||
containerRef={this.excalidrawContainerRef}
|
containerRef={this.excalidrawContainerRef}
|
||||||
canvas={this.interactiveCanvas}
|
canvas={this.interactiveCanvas}
|
||||||
elements={canvasElements}
|
elementsMap={elementsMap}
|
||||||
visibleElements={visibleElements}
|
visibleElements={visibleElements}
|
||||||
selectedElements={selectedElements}
|
selectedElements={selectedElements}
|
||||||
versionNonce={versionNonce}
|
versionNonce={versionNonce}
|
||||||
|
@ -2780,7 +2780,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
private renderInteractiveSceneCallback = ({
|
private renderInteractiveSceneCallback = ({
|
||||||
atLeastOneVisibleElement,
|
atLeastOneVisibleElement,
|
||||||
scrollBars,
|
scrollBars,
|
||||||
elements,
|
elementsMap,
|
||||||
}: RenderInteractiveSceneCallback) => {
|
}: RenderInteractiveSceneCallback) => {
|
||||||
if (scrollBars) {
|
if (scrollBars) {
|
||||||
currentScrollBars = scrollBars;
|
currentScrollBars = scrollBars;
|
||||||
|
@ -2789,7 +2789,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
// hide when editing text
|
// hide when editing text
|
||||||
isTextElement(this.state.editingElement)
|
isTextElement(this.state.editingElement)
|
||||||
? false
|
? false
|
||||||
: !atLeastOneVisibleElement && elements.length > 0;
|
: !atLeastOneVisibleElement && elementsMap.size > 0;
|
||||||
if (this.state.scrolledOutside !== scrolledOutside) {
|
if (this.state.scrolledOutside !== scrolledOutside) {
|
||||||
this.setState({ scrolledOutside });
|
this.setState({ scrolledOutside });
|
||||||
}
|
}
|
||||||
|
@ -3119,7 +3119,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
|
|
||||||
newElements.forEach((newElement) => {
|
newElements.forEach((newElement) => {
|
||||||
if (isTextElement(newElement) && isBoundToContainer(newElement)) {
|
if (isTextElement(newElement) && isBoundToContainer(newElement)) {
|
||||||
const container = getContainerElement(newElement);
|
const container = getContainerElement(
|
||||||
|
newElement,
|
||||||
|
this.scene.getElementsMapIncludingDeleted(),
|
||||||
|
);
|
||||||
redrawTextBoundingBox(newElement, container);
|
redrawTextBoundingBox(newElement, container);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -4183,11 +4186,18 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.scene.replaceAllElements([
|
this.scene.replaceAllElements([
|
||||||
...this.scene.getElementsIncludingDeleted().map((_element) => {
|
...this.scene.getElementsIncludingDeleted().map((_element) => {
|
||||||
if (_element.id === element.id && isTextElement(_element)) {
|
if (_element.id === element.id && isTextElement(_element)) {
|
||||||
return updateTextElement(_element, {
|
return updateTextElement(
|
||||||
text,
|
_element,
|
||||||
isDeleted,
|
getContainerElement(
|
||||||
originalText,
|
_element,
|
||||||
});
|
this.scene.getElementsMapIncludingDeleted(),
|
||||||
|
),
|
||||||
|
{
|
||||||
|
text,
|
||||||
|
isDeleted,
|
||||||
|
originalText,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return _element;
|
return _element;
|
||||||
}),
|
}),
|
||||||
|
@ -7700,13 +7710,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
groupIds: [],
|
groupIds: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
this.scene.replaceAllElements(
|
removeElementsFromFrame([linearElement]);
|
||||||
removeElementsFromFrame(
|
|
||||||
this.scene.getElementsIncludingDeleted(),
|
this.scene.informMutation();
|
||||||
[linearElement],
|
|
||||||
this.state,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7716,7 +7722,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.getTopLayerFrameAtSceneCoords(sceneCoords);
|
this.getTopLayerFrameAtSceneCoords(sceneCoords);
|
||||||
|
|
||||||
const selectedElements = this.scene.getSelectedElements(this.state);
|
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||||
let nextElements = this.scene.getElementsIncludingDeleted();
|
let nextElements = this.scene.getElementsMapIncludingDeleted();
|
||||||
|
|
||||||
const updateGroupIdsAfterEditingGroup = (
|
const updateGroupIdsAfterEditingGroup = (
|
||||||
elements: ExcalidrawElement[],
|
elements: ExcalidrawElement[],
|
||||||
|
@ -7809,7 +7815,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
|
|
||||||
this.scene.replaceAllElements(
|
this.scene.replaceAllElements(
|
||||||
addElementsToFrame(
|
addElementsToFrame(
|
||||||
this.scene.getElementsIncludingDeleted(),
|
this.scene.getElementsMapIncludingDeleted(),
|
||||||
elementsInsideFrame,
|
elementsInsideFrame,
|
||||||
draggingElement,
|
draggingElement,
|
||||||
),
|
),
|
||||||
|
@ -7857,7 +7863,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.state,
|
this.state,
|
||||||
),
|
),
|
||||||
frame,
|
frame,
|
||||||
this.state,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9137,10 +9142,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
|
|
||||||
if (
|
if (
|
||||||
transformElements(
|
transformElements(
|
||||||
pointerDownState,
|
pointerDownState.originalElements,
|
||||||
transformHandleType,
|
transformHandleType,
|
||||||
selectedElements,
|
selectedElements,
|
||||||
pointerDownState.resize.arrowDirection,
|
this.scene.getElementsMapIncludingDeleted(),
|
||||||
shouldRotateWithDiscreteAngle(event),
|
shouldRotateWithDiscreteAngle(event),
|
||||||
shouldResizeFromCenter(event),
|
shouldResizeFromCenter(event),
|
||||||
selectedElements.length === 1 && isImageElement(selectedElements[0])
|
selectedElements.length === 1 && isImageElement(selectedElements[0])
|
||||||
|
@ -9150,7 +9155,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
resizeY,
|
resizeY,
|
||||||
pointerDownState.resize.center.x,
|
pointerDownState.resize.center.x,
|
||||||
pointerDownState.resize.center.y,
|
pointerDownState.resize.center.y,
|
||||||
this.state,
|
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
this.maybeSuggestBindingForAll(selectedElements);
|
this.maybeSuggestBindingForAll(selectedElements);
|
||||||
|
|
|
@ -226,7 +226,7 @@ const LayerUI = ({
|
||||||
>
|
>
|
||||||
<SelectedShapeActions
|
<SelectedShapeActions
|
||||||
appState={appState}
|
appState={appState}
|
||||||
elements={elements}
|
elementsMap={app.scene.getNonDeletedElementsMap()}
|
||||||
renderAction={actionManager.renderAction}
|
renderAction={actionManager.renderAction}
|
||||||
/>
|
/>
|
||||||
</Island>
|
</Island>
|
||||||
|
|
|
@ -183,7 +183,7 @@ export const MobileMenu = ({
|
||||||
<Section className="App-mobile-menu" heading="selectedShapeActions">
|
<Section className="App-mobile-menu" heading="selectedShapeActions">
|
||||||
<SelectedShapeActions
|
<SelectedShapeActions
|
||||||
appState={appState}
|
appState={appState}
|
||||||
elements={elements}
|
elementsMap={app.scene.getNonDeletedElementsMap()}
|
||||||
renderAction={actionManager.renderAction}
|
renderAction={actionManager.renderAction}
|
||||||
/>
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
|
@ -7,6 +7,7 @@ import type { DOMAttributes } from "react";
|
||||||
import type { AppState, InteractiveCanvasAppState } from "../../types";
|
import type { AppState, InteractiveCanvasAppState } from "../../types";
|
||||||
import type {
|
import type {
|
||||||
InteractiveCanvasRenderConfig,
|
InteractiveCanvasRenderConfig,
|
||||||
|
RenderableElementsMap,
|
||||||
RenderInteractiveSceneCallback,
|
RenderInteractiveSceneCallback,
|
||||||
} from "../../scene/types";
|
} from "../../scene/types";
|
||||||
import type { NonDeletedExcalidrawElement } from "../../element/types";
|
import type { NonDeletedExcalidrawElement } from "../../element/types";
|
||||||
|
@ -15,7 +16,7 @@ import { isRenderThrottlingEnabled } from "../../reactUtils";
|
||||||
type InteractiveCanvasProps = {
|
type InteractiveCanvasProps = {
|
||||||
containerRef: React.RefObject<HTMLDivElement>;
|
containerRef: React.RefObject<HTMLDivElement>;
|
||||||
canvas: HTMLCanvasElement | null;
|
canvas: HTMLCanvasElement | null;
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elementsMap: RenderableElementsMap;
|
||||||
visibleElements: readonly NonDeletedExcalidrawElement[];
|
visibleElements: readonly NonDeletedExcalidrawElement[];
|
||||||
selectedElements: readonly NonDeletedExcalidrawElement[];
|
selectedElements: readonly NonDeletedExcalidrawElement[];
|
||||||
versionNonce: number | undefined;
|
versionNonce: number | undefined;
|
||||||
|
@ -113,7 +114,7 @@ const InteractiveCanvas = (props: InteractiveCanvasProps) => {
|
||||||
renderInteractiveScene(
|
renderInteractiveScene(
|
||||||
{
|
{
|
||||||
canvas: props.canvas,
|
canvas: props.canvas,
|
||||||
elements: props.elements,
|
elementsMap: props.elementsMap,
|
||||||
visibleElements: props.visibleElements,
|
visibleElements: props.visibleElements,
|
||||||
selectedElements: props.selectedElements,
|
selectedElements: props.selectedElements,
|
||||||
scale: window.devicePixelRatio,
|
scale: window.devicePixelRatio,
|
||||||
|
@ -201,10 +202,10 @@ const areEqual = (
|
||||||
prevProps.selectionNonce !== nextProps.selectionNonce ||
|
prevProps.selectionNonce !== nextProps.selectionNonce ||
|
||||||
prevProps.versionNonce !== nextProps.versionNonce ||
|
prevProps.versionNonce !== nextProps.versionNonce ||
|
||||||
prevProps.scale !== nextProps.scale ||
|
prevProps.scale !== nextProps.scale ||
|
||||||
// we need to memoize on element arrays because they may have renewed
|
// we need to memoize on elementsMap because they may have renewed
|
||||||
// even if versionNonce didn't change (e.g. we filter elements out based
|
// even if versionNonce didn't change (e.g. we filter elements out based
|
||||||
// on appState)
|
// on appState)
|
||||||
prevProps.elements !== nextProps.elements ||
|
prevProps.elementsMap !== nextProps.elementsMap ||
|
||||||
prevProps.visibleElements !== nextProps.visibleElements ||
|
prevProps.visibleElements !== nextProps.visibleElements ||
|
||||||
prevProps.selectedElements !== nextProps.selectedElements
|
prevProps.selectedElements !== nextProps.selectedElements
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -3,14 +3,17 @@ import { RoughCanvas } from "roughjs/bin/canvas";
|
||||||
import { renderStaticScene } from "../../renderer/renderScene";
|
import { renderStaticScene } from "../../renderer/renderScene";
|
||||||
import { isShallowEqual } from "../../utils";
|
import { isShallowEqual } from "../../utils";
|
||||||
import type { AppState, StaticCanvasAppState } from "../../types";
|
import type { AppState, StaticCanvasAppState } from "../../types";
|
||||||
import type { StaticCanvasRenderConfig } from "../../scene/types";
|
import type {
|
||||||
|
RenderableElementsMap,
|
||||||
|
StaticCanvasRenderConfig,
|
||||||
|
} from "../../scene/types";
|
||||||
import type { NonDeletedExcalidrawElement } from "../../element/types";
|
import type { NonDeletedExcalidrawElement } from "../../element/types";
|
||||||
import { isRenderThrottlingEnabled } from "../../reactUtils";
|
import { isRenderThrottlingEnabled } from "../../reactUtils";
|
||||||
|
|
||||||
type StaticCanvasProps = {
|
type StaticCanvasProps = {
|
||||||
canvas: HTMLCanvasElement;
|
canvas: HTMLCanvasElement;
|
||||||
rc: RoughCanvas;
|
rc: RoughCanvas;
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elementsMap: RenderableElementsMap;
|
||||||
visibleElements: readonly NonDeletedExcalidrawElement[];
|
visibleElements: readonly NonDeletedExcalidrawElement[];
|
||||||
versionNonce: number | undefined;
|
versionNonce: number | undefined;
|
||||||
selectionNonce: number | undefined;
|
selectionNonce: number | undefined;
|
||||||
|
@ -63,7 +66,7 @@ const StaticCanvas = (props: StaticCanvasProps) => {
|
||||||
canvas,
|
canvas,
|
||||||
rc: props.rc,
|
rc: props.rc,
|
||||||
scale: props.scale,
|
scale: props.scale,
|
||||||
elements: props.elements,
|
elementsMap: props.elementsMap,
|
||||||
visibleElements: props.visibleElements,
|
visibleElements: props.visibleElements,
|
||||||
appState: props.appState,
|
appState: props.appState,
|
||||||
renderConfig: props.renderConfig,
|
renderConfig: props.renderConfig,
|
||||||
|
@ -106,10 +109,10 @@ const areEqual = (
|
||||||
if (
|
if (
|
||||||
prevProps.versionNonce !== nextProps.versionNonce ||
|
prevProps.versionNonce !== nextProps.versionNonce ||
|
||||||
prevProps.scale !== nextProps.scale ||
|
prevProps.scale !== nextProps.scale ||
|
||||||
// we need to memoize on element arrays because they may have renewed
|
// we need to memoize on elementsMap because they may have renewed
|
||||||
// even if versionNonce didn't change (e.g. we filter elements out based
|
// even if versionNonce didn't change (e.g. we filter elements out based
|
||||||
// on appState)
|
// on appState)
|
||||||
prevProps.elements !== nextProps.elements ||
|
prevProps.elementsMap !== nextProps.elementsMap ||
|
||||||
prevProps.visibleElements !== nextProps.visibleElements
|
prevProps.visibleElements !== nextProps.visibleElements
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -40,6 +40,7 @@ import { arrayToMap } from "../utils";
|
||||||
import { MarkOptional, Mutable } from "../utility-types";
|
import { MarkOptional, Mutable } from "../utility-types";
|
||||||
import {
|
import {
|
||||||
detectLineHeight,
|
detectLineHeight,
|
||||||
|
getContainerElement,
|
||||||
getDefaultLineHeight,
|
getDefaultLineHeight,
|
||||||
measureBaseline,
|
measureBaseline,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
|
@ -179,7 +180,6 @@ const restoreElementWithProperties = <
|
||||||
|
|
||||||
const restoreElement = (
|
const restoreElement = (
|
||||||
element: Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
|
element: Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
|
||||||
refreshDimensions = false,
|
|
||||||
): typeof element | null => {
|
): typeof element | null => {
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
case "text":
|
case "text":
|
||||||
|
@ -232,10 +232,6 @@ const restoreElement = (
|
||||||
element = bumpVersion(element);
|
element = bumpVersion(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (refreshDimensions) {
|
|
||||||
element = { ...element, ...refreshTextDimensions(element) };
|
|
||||||
}
|
|
||||||
|
|
||||||
return element;
|
return element;
|
||||||
case "freedraw": {
|
case "freedraw": {
|
||||||
return restoreElementWithProperties(element, {
|
return restoreElementWithProperties(element, {
|
||||||
|
@ -426,10 +422,7 @@ export const restoreElements = (
|
||||||
// filtering out selection, which is legacy, no longer kept in elements,
|
// filtering out selection, which is legacy, no longer kept in elements,
|
||||||
// and causing issues if retained
|
// and causing issues if retained
|
||||||
if (element.type !== "selection" && !isInvisiblySmallElement(element)) {
|
if (element.type !== "selection" && !isInvisiblySmallElement(element)) {
|
||||||
let migratedElement: ExcalidrawElement | null = restoreElement(
|
let migratedElement: ExcalidrawElement | null = restoreElement(element);
|
||||||
element,
|
|
||||||
opts?.refreshDimensions,
|
|
||||||
);
|
|
||||||
if (migratedElement) {
|
if (migratedElement) {
|
||||||
const localElement = localElementsMap?.get(element.id);
|
const localElement = localElementsMap?.get(element.id);
|
||||||
if (localElement && localElement.version > migratedElement.version) {
|
if (localElement && localElement.version > migratedElement.version) {
|
||||||
|
@ -462,6 +455,16 @@ export const restoreElements = (
|
||||||
} else if (element.boundElements) {
|
} else if (element.boundElements) {
|
||||||
repairContainerElement(element, restoredElementsMap);
|
repairContainerElement(element, restoredElementsMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (opts.refreshDimensions && isTextElement(element)) {
|
||||||
|
Object.assign(
|
||||||
|
element,
|
||||||
|
refreshTextDimensions(
|
||||||
|
element,
|
||||||
|
getContainerElement(element, restoredElementsMap),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return restoredElements;
|
return restoredElements;
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
ExcalidrawFreeDrawElement,
|
ExcalidrawFreeDrawElement,
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
|
ElementsMapOrArray,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { distance2d, rotate, rotatePoint } from "../math";
|
import { distance2d, rotate, rotatePoint } from "../math";
|
||||||
import rough from "roughjs/bin/rough";
|
import rough from "roughjs/bin/rough";
|
||||||
|
@ -161,7 +162,11 @@ export const getElementAbsoluteCoords = (
|
||||||
includeBoundText,
|
includeBoundText,
|
||||||
);
|
);
|
||||||
} else if (isTextElement(element)) {
|
} else if (isTextElement(element)) {
|
||||||
const container = getContainerElement(element);
|
const elementsMap =
|
||||||
|
Scene.getScene(element)?.getElementsMapIncludingDeleted();
|
||||||
|
const container = elementsMap
|
||||||
|
? getContainerElement(element, elementsMap)
|
||||||
|
: null;
|
||||||
if (isArrowElement(container)) {
|
if (isArrowElement(container)) {
|
||||||
const coords = LinearElementEditor.getBoundTextElementPosition(
|
const coords = LinearElementEditor.getBoundTextElementPosition(
|
||||||
container,
|
container,
|
||||||
|
@ -729,10 +734,8 @@ const getLinearElementRotatedBounds = (
|
||||||
export const getElementBounds = (element: ExcalidrawElement): Bounds => {
|
export const getElementBounds = (element: ExcalidrawElement): Bounds => {
|
||||||
return ElementBounds.getBounds(element);
|
return ElementBounds.getBounds(element);
|
||||||
};
|
};
|
||||||
export const getCommonBounds = (
|
export const getCommonBounds = (elements: ElementsMapOrArray): Bounds => {
|
||||||
elements: readonly ExcalidrawElement[],
|
if ("size" in elements ? !elements.size : !elements.length) {
|
||||||
): Bounds => {
|
|
||||||
if (!elements.length) {
|
|
||||||
return [0, 0, 0, 0];
|
return [0, 0, 0, 0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,17 +5,12 @@ import { ExcalidrawProps } from "../types";
|
||||||
import { getFontString, updateActiveTool } from "../utils";
|
import { getFontString, updateActiveTool } from "../utils";
|
||||||
import { setCursorForShape } from "../cursor";
|
import { setCursorForShape } from "../cursor";
|
||||||
import { newTextElement } from "./newElement";
|
import { newTextElement } from "./newElement";
|
||||||
import { getContainerElement, wrapText } from "./textElement";
|
import { wrapText } from "./textElement";
|
||||||
import {
|
import { isIframeElement } from "./typeChecks";
|
||||||
isFrameLikeElement,
|
|
||||||
isIframeElement,
|
|
||||||
isIframeLikeElement,
|
|
||||||
} from "./typeChecks";
|
|
||||||
import {
|
import {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawIframeLikeElement,
|
ExcalidrawIframeLikeElement,
|
||||||
IframeData,
|
IframeData,
|
||||||
NonDeletedExcalidrawElement,
|
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
const embeddedLinkCache = new Map<string, IframeData>();
|
const embeddedLinkCache = new Map<string, IframeData>();
|
||||||
|
@ -217,21 +212,6 @@ export const getEmbedLink = (
|
||||||
return { link, intrinsicSize: aspectRatio, type };
|
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 = (
|
export const createPlaceholderEmbeddableLabel = (
|
||||||
element: ExcalidrawIframeLikeElement,
|
element: ExcalidrawIframeLikeElement,
|
||||||
): ExcalidrawElement => {
|
): ExcalidrawElement => {
|
||||||
|
|
|
@ -31,7 +31,6 @@ import { getElementAbsoluteCoords } from ".";
|
||||||
import { adjustXYWithRotation } from "../math";
|
import { adjustXYWithRotation } from "../math";
|
||||||
import { getResizedElementAbsoluteCoords } from "./bounds";
|
import { getResizedElementAbsoluteCoords } from "./bounds";
|
||||||
import {
|
import {
|
||||||
getContainerElement,
|
|
||||||
measureText,
|
measureText,
|
||||||
normalizeText,
|
normalizeText,
|
||||||
wrapText,
|
wrapText,
|
||||||
|
@ -333,12 +332,12 @@ const getAdjustedDimensions = (
|
||||||
|
|
||||||
export const refreshTextDimensions = (
|
export const refreshTextDimensions = (
|
||||||
textElement: ExcalidrawTextElement,
|
textElement: ExcalidrawTextElement,
|
||||||
|
container: ExcalidrawTextContainer | null,
|
||||||
text = textElement.text,
|
text = textElement.text,
|
||||||
) => {
|
) => {
|
||||||
if (textElement.isDeleted) {
|
if (textElement.isDeleted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const container = getContainerElement(textElement);
|
|
||||||
if (container) {
|
if (container) {
|
||||||
text = wrapText(
|
text = wrapText(
|
||||||
text,
|
text,
|
||||||
|
@ -352,6 +351,7 @@ export const refreshTextDimensions = (
|
||||||
|
|
||||||
export const updateTextElement = (
|
export const updateTextElement = (
|
||||||
textElement: ExcalidrawTextElement,
|
textElement: ExcalidrawTextElement,
|
||||||
|
container: ExcalidrawTextContainer | null,
|
||||||
{
|
{
|
||||||
text,
|
text,
|
||||||
isDeleted,
|
isDeleted,
|
||||||
|
@ -365,7 +365,7 @@ export const updateTextElement = (
|
||||||
return newElementWith(textElement, {
|
return newElementWith(textElement, {
|
||||||
originalText,
|
originalText,
|
||||||
isDeleted: isDeleted ?? textElement.isDeleted,
|
isDeleted: isDeleted ?? textElement.isDeleted,
|
||||||
...refreshTextDimensions(textElement, originalText),
|
...refreshTextDimensions(textElement, container, originalText),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
ExcalidrawImageElement,
|
ExcalidrawImageElement,
|
||||||
|
ElementsMap,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import type { Mutable } from "../utility-types";
|
import type { Mutable } from "../utility-types";
|
||||||
import {
|
import {
|
||||||
|
@ -41,7 +42,7 @@ import {
|
||||||
MaybeTransformHandleType,
|
MaybeTransformHandleType,
|
||||||
TransformHandleDirection,
|
TransformHandleDirection,
|
||||||
} from "./transformHandles";
|
} from "./transformHandles";
|
||||||
import { AppState, Point, PointerDownState } from "../types";
|
import { Point, PointerDownState } from "../types";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import {
|
import {
|
||||||
getApproxMinLineWidth,
|
getApproxMinLineWidth,
|
||||||
|
@ -68,10 +69,10 @@ export const normalizeAngle = (angle: number): number => {
|
||||||
|
|
||||||
// Returns true when transform (resizing/rotation) happened
|
// Returns true when transform (resizing/rotation) happened
|
||||||
export const transformElements = (
|
export const transformElements = (
|
||||||
pointerDownState: PointerDownState,
|
originalElements: PointerDownState["originalElements"],
|
||||||
transformHandleType: MaybeTransformHandleType,
|
transformHandleType: MaybeTransformHandleType,
|
||||||
selectedElements: readonly NonDeletedExcalidrawElement[],
|
selectedElements: readonly NonDeletedExcalidrawElement[],
|
||||||
resizeArrowDirection: "origin" | "end",
|
elementsMap: ElementsMap,
|
||||||
shouldRotateWithDiscreteAngle: boolean,
|
shouldRotateWithDiscreteAngle: boolean,
|
||||||
shouldResizeFromCenter: boolean,
|
shouldResizeFromCenter: boolean,
|
||||||
shouldMaintainAspectRatio: boolean,
|
shouldMaintainAspectRatio: boolean,
|
||||||
|
@ -79,7 +80,6 @@ export const transformElements = (
|
||||||
pointerY: number,
|
pointerY: number,
|
||||||
centerX: number,
|
centerX: number,
|
||||||
centerY: number,
|
centerY: number,
|
||||||
appState: AppState,
|
|
||||||
) => {
|
) => {
|
||||||
if (selectedElements.length === 1) {
|
if (selectedElements.length === 1) {
|
||||||
const [element] = selectedElements;
|
const [element] = selectedElements;
|
||||||
|
@ -89,7 +89,6 @@ export const transformElements = (
|
||||||
pointerX,
|
pointerX,
|
||||||
pointerY,
|
pointerY,
|
||||||
shouldRotateWithDiscreteAngle,
|
shouldRotateWithDiscreteAngle,
|
||||||
pointerDownState.originalElements,
|
|
||||||
);
|
);
|
||||||
updateBoundElements(element);
|
updateBoundElements(element);
|
||||||
} else if (
|
} else if (
|
||||||
|
@ -101,6 +100,7 @@ export const transformElements = (
|
||||||
) {
|
) {
|
||||||
resizeSingleTextElement(
|
resizeSingleTextElement(
|
||||||
element,
|
element,
|
||||||
|
elementsMap,
|
||||||
transformHandleType,
|
transformHandleType,
|
||||||
shouldResizeFromCenter,
|
shouldResizeFromCenter,
|
||||||
pointerX,
|
pointerX,
|
||||||
|
@ -109,9 +109,10 @@ export const transformElements = (
|
||||||
updateBoundElements(element);
|
updateBoundElements(element);
|
||||||
} else if (transformHandleType) {
|
} else if (transformHandleType) {
|
||||||
resizeSingleElement(
|
resizeSingleElement(
|
||||||
pointerDownState.originalElements,
|
originalElements,
|
||||||
shouldMaintainAspectRatio,
|
shouldMaintainAspectRatio,
|
||||||
element,
|
element,
|
||||||
|
elementsMap,
|
||||||
transformHandleType,
|
transformHandleType,
|
||||||
shouldResizeFromCenter,
|
shouldResizeFromCenter,
|
||||||
pointerX,
|
pointerX,
|
||||||
|
@ -123,7 +124,7 @@ export const transformElements = (
|
||||||
} else if (selectedElements.length > 1) {
|
} else if (selectedElements.length > 1) {
|
||||||
if (transformHandleType === "rotation") {
|
if (transformHandleType === "rotation") {
|
||||||
rotateMultipleElements(
|
rotateMultipleElements(
|
||||||
pointerDownState,
|
originalElements,
|
||||||
selectedElements,
|
selectedElements,
|
||||||
pointerX,
|
pointerX,
|
||||||
pointerY,
|
pointerY,
|
||||||
|
@ -139,8 +140,9 @@ export const transformElements = (
|
||||||
transformHandleType === "se"
|
transformHandleType === "se"
|
||||||
) {
|
) {
|
||||||
resizeMultipleElements(
|
resizeMultipleElements(
|
||||||
pointerDownState,
|
originalElements,
|
||||||
selectedElements,
|
selectedElements,
|
||||||
|
elementsMap,
|
||||||
transformHandleType,
|
transformHandleType,
|
||||||
shouldResizeFromCenter,
|
shouldResizeFromCenter,
|
||||||
pointerX,
|
pointerX,
|
||||||
|
@ -157,7 +159,6 @@ const rotateSingleElement = (
|
||||||
pointerX: number,
|
pointerX: number,
|
||||||
pointerY: number,
|
pointerY: number,
|
||||||
shouldRotateWithDiscreteAngle: boolean,
|
shouldRotateWithDiscreteAngle: boolean,
|
||||||
originalElements: Map<string, NonDeleted<ExcalidrawElement>>,
|
|
||||||
) => {
|
) => {
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||||
const cx = (x1 + x2) / 2;
|
const cx = (x1 + x2) / 2;
|
||||||
|
@ -207,6 +208,7 @@ const rescalePointsInElement = (
|
||||||
|
|
||||||
const measureFontSizeFromWidth = (
|
const measureFontSizeFromWidth = (
|
||||||
element: NonDeleted<ExcalidrawTextElement>,
|
element: NonDeleted<ExcalidrawTextElement>,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
nextWidth: number,
|
nextWidth: number,
|
||||||
nextHeight: number,
|
nextHeight: number,
|
||||||
): { size: number; baseline: number } | null => {
|
): { size: number; baseline: number } | null => {
|
||||||
|
@ -215,7 +217,7 @@ const measureFontSizeFromWidth = (
|
||||||
|
|
||||||
const hasContainer = isBoundToContainer(element);
|
const hasContainer = isBoundToContainer(element);
|
||||||
if (hasContainer) {
|
if (hasContainer) {
|
||||||
const container = getContainerElement(element);
|
const container = getContainerElement(element, elementsMap);
|
||||||
if (container) {
|
if (container) {
|
||||||
width = getBoundTextMaxWidth(container);
|
width = getBoundTextMaxWidth(container);
|
||||||
}
|
}
|
||||||
|
@ -257,6 +259,7 @@ const getSidesForTransformHandle = (
|
||||||
|
|
||||||
const resizeSingleTextElement = (
|
const resizeSingleTextElement = (
|
||||||
element: NonDeleted<ExcalidrawTextElement>,
|
element: NonDeleted<ExcalidrawTextElement>,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
transformHandleType: "nw" | "ne" | "sw" | "se",
|
transformHandleType: "nw" | "ne" | "sw" | "se",
|
||||||
shouldResizeFromCenter: boolean,
|
shouldResizeFromCenter: boolean,
|
||||||
pointerX: number,
|
pointerX: number,
|
||||||
|
@ -303,7 +306,12 @@ const resizeSingleTextElement = (
|
||||||
if (scale > 0) {
|
if (scale > 0) {
|
||||||
const nextWidth = element.width * scale;
|
const nextWidth = element.width * scale;
|
||||||
const nextHeight = element.height * scale;
|
const nextHeight = element.height * scale;
|
||||||
const metrics = measureFontSizeFromWidth(element, nextWidth, nextHeight);
|
const metrics = measureFontSizeFromWidth(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
nextWidth,
|
||||||
|
nextHeight,
|
||||||
|
);
|
||||||
if (metrics === null) {
|
if (metrics === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -342,6 +350,7 @@ export const resizeSingleElement = (
|
||||||
originalElements: PointerDownState["originalElements"],
|
originalElements: PointerDownState["originalElements"],
|
||||||
shouldMaintainAspectRatio: boolean,
|
shouldMaintainAspectRatio: boolean,
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
transformHandleDirection: TransformHandleDirection,
|
transformHandleDirection: TransformHandleDirection,
|
||||||
shouldResizeFromCenter: boolean,
|
shouldResizeFromCenter: boolean,
|
||||||
pointerX: number,
|
pointerX: number,
|
||||||
|
@ -448,6 +457,7 @@ export const resizeSingleElement = (
|
||||||
|
|
||||||
const nextFont = measureFontSizeFromWidth(
|
const nextFont = measureFontSizeFromWidth(
|
||||||
boundTextElement,
|
boundTextElement,
|
||||||
|
elementsMap,
|
||||||
getBoundTextMaxWidth(updatedElement),
|
getBoundTextMaxWidth(updatedElement),
|
||||||
getBoundTextMaxHeight(updatedElement, boundTextElement),
|
getBoundTextMaxHeight(updatedElement, boundTextElement),
|
||||||
);
|
);
|
||||||
|
@ -637,8 +647,9 @@ export const resizeSingleElement = (
|
||||||
};
|
};
|
||||||
|
|
||||||
export const resizeMultipleElements = (
|
export const resizeMultipleElements = (
|
||||||
pointerDownState: PointerDownState,
|
originalElements: PointerDownState["originalElements"],
|
||||||
selectedElements: readonly NonDeletedExcalidrawElement[],
|
selectedElements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
elementsMap: ElementsMap,
|
||||||
transformHandleType: "nw" | "ne" | "sw" | "se",
|
transformHandleType: "nw" | "ne" | "sw" | "se",
|
||||||
shouldResizeFromCenter: boolean,
|
shouldResizeFromCenter: boolean,
|
||||||
pointerX: number,
|
pointerX: number,
|
||||||
|
@ -658,7 +669,7 @@ export const resizeMultipleElements = (
|
||||||
}[],
|
}[],
|
||||||
element,
|
element,
|
||||||
) => {
|
) => {
|
||||||
const origElement = pointerDownState.originalElements.get(element.id);
|
const origElement = originalElements.get(element.id);
|
||||||
if (origElement) {
|
if (origElement) {
|
||||||
acc.push({ orig: origElement, latest: element });
|
acc.push({ orig: origElement, latest: element });
|
||||||
}
|
}
|
||||||
|
@ -679,7 +690,7 @@ export const resizeMultipleElements = (
|
||||||
if (!textId) {
|
if (!textId) {
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
const text = pointerDownState.originalElements.get(textId) ?? null;
|
const text = originalElements.get(textId) ?? null;
|
||||||
if (!isBoundToContainer(text)) {
|
if (!isBoundToContainer(text)) {
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
|
@ -825,7 +836,12 @@ export const resizeMultipleElements = (
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTextElement(orig)) {
|
if (isTextElement(orig)) {
|
||||||
const metrics = measureFontSizeFromWidth(orig, width, height);
|
const metrics = measureFontSizeFromWidth(
|
||||||
|
orig,
|
||||||
|
elementsMap,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
);
|
||||||
if (!metrics) {
|
if (!metrics) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -833,7 +849,7 @@ export const resizeMultipleElements = (
|
||||||
update.baseline = metrics.baseline;
|
update.baseline = metrics.baseline;
|
||||||
}
|
}
|
||||||
|
|
||||||
const boundTextElement = pointerDownState.originalElements.get(
|
const boundTextElement = originalElements.get(
|
||||||
getBoundTextElementId(orig) ?? "",
|
getBoundTextElementId(orig) ?? "",
|
||||||
) as ExcalidrawTextElementWithContainer | undefined;
|
) as ExcalidrawTextElementWithContainer | undefined;
|
||||||
|
|
||||||
|
@ -884,7 +900,7 @@ export const resizeMultipleElements = (
|
||||||
};
|
};
|
||||||
|
|
||||||
const rotateMultipleElements = (
|
const rotateMultipleElements = (
|
||||||
pointerDownState: PointerDownState,
|
originalElements: PointerDownState["originalElements"],
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
pointerX: number,
|
pointerX: number,
|
||||||
pointerY: number,
|
pointerY: number,
|
||||||
|
@ -906,8 +922,7 @@ const rotateMultipleElements = (
|
||||||
const cx = (x1 + x2) / 2;
|
const cx = (x1 + x2) / 2;
|
||||||
const cy = (y1 + y2) / 2;
|
const cy = (y1 + y2) / 2;
|
||||||
const origAngle =
|
const origAngle =
|
||||||
pointerDownState.originalElements.get(element.id)?.angle ??
|
originalElements.get(element.id)?.angle ?? element.angle;
|
||||||
element.angle;
|
|
||||||
const [rotatedCX, rotatedCY] = rotate(
|
const [rotatedCX, rotatedCY] = rotate(
|
||||||
cx,
|
cx,
|
||||||
cy,
|
cy,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { getFontString, arrayToMap, isTestEnv, normalizeEOL } from "../utils";
|
import { getFontString, arrayToMap, isTestEnv, normalizeEOL } from "../utils";
|
||||||
import {
|
import {
|
||||||
|
ElementsMap,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawElementType,
|
ExcalidrawElementType,
|
||||||
ExcalidrawTextContainer,
|
ExcalidrawTextContainer,
|
||||||
|
@ -682,17 +683,15 @@ export const getBoundTextElement = (element: ExcalidrawElement | null) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getContainerElement = (
|
export const getContainerElement = (
|
||||||
element:
|
element: ExcalidrawTextElement | null,
|
||||||
| (ExcalidrawElement & {
|
elementsMap: ElementsMap,
|
||||||
containerId: ExcalidrawElement["id"] | null;
|
): ExcalidrawTextContainer | null => {
|
||||||
})
|
|
||||||
| null,
|
|
||||||
) => {
|
|
||||||
if (!element) {
|
if (!element) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (element.containerId) {
|
if (element.containerId) {
|
||||||
return Scene.getScene(element)?.getElement(element.containerId) || null;
|
return (elementsMap.get(element.containerId) ||
|
||||||
|
null) as ExcalidrawTextContainer | null;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
@ -752,28 +751,16 @@ export const getContainerCoords = (container: NonDeletedExcalidrawElement) => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTextElementAngle = (textElement: ExcalidrawTextElement) => {
|
export const getTextElementAngle = (
|
||||||
const container = getContainerElement(textElement);
|
textElement: ExcalidrawTextElement,
|
||||||
|
container: ExcalidrawTextContainer | null,
|
||||||
|
) => {
|
||||||
if (!container || isArrowElement(container)) {
|
if (!container || isArrowElement(container)) {
|
||||||
return textElement.angle;
|
return textElement.angle;
|
||||||
}
|
}
|
||||||
return container.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 = (
|
export const getBoundTextElementPosition = (
|
||||||
container: ExcalidrawElement,
|
container: ExcalidrawElement,
|
||||||
boundTextElement: ExcalidrawTextElementWithContainer,
|
boundTextElement: ExcalidrawTextElementWithContainer,
|
||||||
|
@ -788,12 +775,12 @@ export const getBoundTextElementPosition = (
|
||||||
|
|
||||||
export const shouldAllowVerticalAlign = (
|
export const shouldAllowVerticalAlign = (
|
||||||
selectedElements: NonDeletedExcalidrawElement[],
|
selectedElements: NonDeletedExcalidrawElement[],
|
||||||
|
elementsMap: ElementsMap,
|
||||||
) => {
|
) => {
|
||||||
return selectedElements.some((element) => {
|
return selectedElements.some((element) => {
|
||||||
const hasBoundContainer = isBoundToContainer(element);
|
if (isBoundToContainer(element)) {
|
||||||
if (hasBoundContainer) {
|
const container = getContainerElement(element, elementsMap);
|
||||||
const container = getContainerElement(element);
|
if (isArrowElement(container)) {
|
||||||
if (isTextElement(element) && isArrowElement(container)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -804,12 +791,12 @@ export const shouldAllowVerticalAlign = (
|
||||||
|
|
||||||
export const suppportsHorizontalAlign = (
|
export const suppportsHorizontalAlign = (
|
||||||
selectedElements: NonDeletedExcalidrawElement[],
|
selectedElements: NonDeletedExcalidrawElement[],
|
||||||
|
elementsMap: ElementsMap,
|
||||||
) => {
|
) => {
|
||||||
return selectedElements.some((element) => {
|
return selectedElements.some((element) => {
|
||||||
const hasBoundContainer = isBoundToContainer(element);
|
if (isBoundToContainer(element)) {
|
||||||
if (hasBoundContainer) {
|
const container = getContainerElement(element, elementsMap);
|
||||||
const container = getContainerElement(element);
|
if (isArrowElement(container)) {
|
||||||
if (isTextElement(element) && isArrowElement(container)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -153,7 +153,10 @@ export const textWysiwyg = ({
|
||||||
if (updatedTextElement && isTextElement(updatedTextElement)) {
|
if (updatedTextElement && isTextElement(updatedTextElement)) {
|
||||||
let coordX = updatedTextElement.x;
|
let coordX = updatedTextElement.x;
|
||||||
let coordY = updatedTextElement.y;
|
let coordY = updatedTextElement.y;
|
||||||
const container = getContainerElement(updatedTextElement);
|
const container = getContainerElement(
|
||||||
|
updatedTextElement,
|
||||||
|
app.scene.getElementsMapIncludingDeleted(),
|
||||||
|
);
|
||||||
let maxWidth = updatedTextElement.width;
|
let maxWidth = updatedTextElement.width;
|
||||||
|
|
||||||
let maxHeight = updatedTextElement.height;
|
let maxHeight = updatedTextElement.height;
|
||||||
|
@ -277,7 +280,7 @@ export const textWysiwyg = ({
|
||||||
transform: getTransform(
|
transform: getTransform(
|
||||||
textElementWidth,
|
textElementWidth,
|
||||||
textElementHeight,
|
textElementHeight,
|
||||||
getTextElementAngle(updatedTextElement),
|
getTextElementAngle(updatedTextElement, container),
|
||||||
appState,
|
appState,
|
||||||
maxWidth,
|
maxWidth,
|
||||||
editorMaxHeight,
|
editorMaxHeight,
|
||||||
|
@ -348,7 +351,10 @@ export const textWysiwyg = ({
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const container = getContainerElement(element);
|
const container = getContainerElement(
|
||||||
|
element,
|
||||||
|
app.scene.getElementsMapIncludingDeleted(),
|
||||||
|
);
|
||||||
|
|
||||||
const font = getFontString({
|
const font = getFontString({
|
||||||
fontSize: app.state.currentItemFontSize,
|
fontSize: app.state.currentItemFontSize,
|
||||||
|
@ -528,7 +534,10 @@ export const textWysiwyg = ({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let text = editable.value;
|
let text = editable.value;
|
||||||
const container = getContainerElement(updateElement);
|
const container = getContainerElement(
|
||||||
|
updateElement,
|
||||||
|
app.scene.getElementsMapIncludingDeleted(),
|
||||||
|
);
|
||||||
|
|
||||||
if (container) {
|
if (container) {
|
||||||
text = updateElement.text;
|
text = updateElement.text;
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {
|
||||||
THEME,
|
THEME,
|
||||||
VERTICAL_ALIGN,
|
VERTICAL_ALIGN,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { MarkNonNullable, ValueOf } from "../utility-types";
|
import { MakeBrand, MarkNonNullable, ValueOf } from "../utility-types";
|
||||||
import { MagicCacheData } from "../data/magic";
|
import { MagicCacheData } from "../data/magic";
|
||||||
|
|
||||||
export type ChartType = "bar" | "line";
|
export type ChartType = "bar" | "line";
|
||||||
|
@ -254,3 +254,31 @@ export type ExcalidrawFreeDrawElement = _ExcalidrawElementBase &
|
||||||
export type FileId = string & { _brand: "FileId" };
|
export type FileId = string & { _brand: "FileId" };
|
||||||
|
|
||||||
export type ExcalidrawElementType = ExcalidrawElement["type"];
|
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>;
|
||||||
|
|
|
@ -4,6 +4,8 @@ import {
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./element";
|
} from "./element";
|
||||||
import {
|
import {
|
||||||
|
ElementsMap,
|
||||||
|
ElementsMapOrArray,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawFrameLikeElement,
|
ExcalidrawFrameLikeElement,
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
|
@ -26,6 +28,7 @@ import {
|
||||||
elementsOverlappingBBox,
|
elementsOverlappingBBox,
|
||||||
} from "../utils/export";
|
} from "../utils/export";
|
||||||
import { isFrameElement, isFrameLikeElement } from "./element/typeChecks";
|
import { isFrameElement, isFrameLikeElement } from "./element/typeChecks";
|
||||||
|
import { ReadonlySetLike } from "./utility-types";
|
||||||
|
|
||||||
// --------------------------- Frame State ------------------------------------
|
// --------------------------- Frame State ------------------------------------
|
||||||
export const bindElementsToFramesAfterDuplication = (
|
export const bindElementsToFramesAfterDuplication = (
|
||||||
|
@ -211,9 +214,17 @@ export const groupByFrameLikes = (elements: readonly ExcalidrawElement[]) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFrameChildren = (
|
export const getFrameChildren = (
|
||||||
allElements: ExcalidrawElementsIncludingDeleted,
|
allElements: ElementsMapOrArray,
|
||||||
frameId: string,
|
frameId: string,
|
||||||
) => allElements.filter((element) => element.frameId === frameId);
|
) => {
|
||||||
|
const frameChildren: ExcalidrawElement[] = [];
|
||||||
|
for (const element of allElements.values()) {
|
||||||
|
if (element.frameId === frameId) {
|
||||||
|
frameChildren.push(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return frameChildren;
|
||||||
|
};
|
||||||
|
|
||||||
export const getFrameLikeElements = (
|
export const getFrameLikeElements = (
|
||||||
allElements: ExcalidrawElementsIncludingDeleted,
|
allElements: ExcalidrawElementsIncludingDeleted,
|
||||||
|
@ -425,23 +436,20 @@ export const filterElementsEligibleAsFrameChildren = (
|
||||||
* Retains (or repairs for target frame) the ordering invriant where children
|
* Retains (or repairs for target frame) the ordering invriant where children
|
||||||
* elements come right before the parent frame:
|
* elements come right before the parent frame:
|
||||||
* [el, el, child, child, frame, el]
|
* [el, el, child, child, frame, el]
|
||||||
|
*
|
||||||
|
* @returns mutated allElements (same data structure)
|
||||||
*/
|
*/
|
||||||
export const addElementsToFrame = (
|
export const addElementsToFrame = <T extends ElementsMapOrArray>(
|
||||||
allElements: ExcalidrawElementsIncludingDeleted,
|
allElements: T,
|
||||||
elementsToAdd: NonDeletedExcalidrawElement[],
|
elementsToAdd: NonDeletedExcalidrawElement[],
|
||||||
frame: ExcalidrawFrameLikeElement,
|
frame: ExcalidrawFrameLikeElement,
|
||||||
) => {
|
): T => {
|
||||||
const { currTargetFrameChildrenMap } = allElements.reduce(
|
const currTargetFrameChildrenMap = new Map<ExcalidrawElement["id"], true>();
|
||||||
(acc, element, index) => {
|
for (const element of allElements.values()) {
|
||||||
if (element.frameId === frame.id) {
|
if (element.frameId === frame.id) {
|
||||||
acc.currTargetFrameChildrenMap.set(element.id, true);
|
currTargetFrameChildrenMap.set(element.id, true);
|
||||||
}
|
}
|
||||||
return acc;
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
currTargetFrameChildrenMap: new Map<ExcalidrawElement["id"], true>(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const suppliedElementsToAddSet = new Set(elementsToAdd.map((el) => el.id));
|
const suppliedElementsToAddSet = new Set(elementsToAdd.map((el) => el.id));
|
||||||
|
|
||||||
|
@ -492,13 +500,12 @@ export const addElementsToFrame = (
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return allElements.slice();
|
|
||||||
|
return allElements;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const removeElementsFromFrame = (
|
export const removeElementsFromFrame = (
|
||||||
allElements: ExcalidrawElementsIncludingDeleted,
|
elementsToRemove: ReadonlySetLike<NonDeletedExcalidrawElement>,
|
||||||
elementsToRemove: NonDeletedExcalidrawElement[],
|
|
||||||
appState: AppState,
|
|
||||||
) => {
|
) => {
|
||||||
const _elementsToRemove = new Map<
|
const _elementsToRemove = new Map<
|
||||||
ExcalidrawElement["id"],
|
ExcalidrawElement["id"],
|
||||||
|
@ -536,35 +543,34 @@ export const removeElementsFromFrame = (
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return allElements.slice();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const removeAllElementsFromFrame = (
|
export const removeAllElementsFromFrame = <T extends ExcalidrawElement>(
|
||||||
allElements: ExcalidrawElementsIncludingDeleted,
|
allElements: readonly T[],
|
||||||
frame: ExcalidrawFrameLikeElement,
|
frame: ExcalidrawFrameLikeElement,
|
||||||
appState: AppState,
|
|
||||||
) => {
|
) => {
|
||||||
const elementsInFrame = getFrameChildren(allElements, frame.id);
|
const elementsInFrame = getFrameChildren(allElements, frame.id);
|
||||||
return removeElementsFromFrame(allElements, elementsInFrame, appState);
|
removeElementsFromFrame(elementsInFrame);
|
||||||
|
return allElements;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const replaceAllElementsInFrame = (
|
export const replaceAllElementsInFrame = <T extends ExcalidrawElement>(
|
||||||
allElements: ExcalidrawElementsIncludingDeleted,
|
allElements: readonly T[],
|
||||||
nextElementsInFrame: ExcalidrawElement[],
|
nextElementsInFrame: ExcalidrawElement[],
|
||||||
frame: ExcalidrawFrameLikeElement,
|
frame: ExcalidrawFrameLikeElement,
|
||||||
appState: AppState,
|
): T[] => {
|
||||||
) => {
|
|
||||||
return addElementsToFrame(
|
return addElementsToFrame(
|
||||||
removeAllElementsFromFrame(allElements, frame, appState),
|
removeAllElementsFromFrame(allElements, frame),
|
||||||
nextElementsInFrame,
|
nextElementsInFrame,
|
||||||
frame,
|
frame,
|
||||||
);
|
).slice();
|
||||||
};
|
};
|
||||||
|
|
||||||
/** does not mutate elements, but returns new ones */
|
/** does not mutate elements, but returns new ones */
|
||||||
export const updateFrameMembershipOfSelectedElements = (
|
export const updateFrameMembershipOfSelectedElements = <
|
||||||
allElements: ExcalidrawElementsIncludingDeleted,
|
T extends ElementsMapOrArray,
|
||||||
|
>(
|
||||||
|
allElements: T,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
app: AppClassProperties,
|
app: AppClassProperties,
|
||||||
) => {
|
) => {
|
||||||
|
@ -589,19 +595,22 @@ export const updateFrameMembershipOfSelectedElements = (
|
||||||
|
|
||||||
const elementsToRemove = new Set<ExcalidrawElement>();
|
const elementsToRemove = new Set<ExcalidrawElement>();
|
||||||
|
|
||||||
|
const elementsMap = arrayToMap(allElements);
|
||||||
|
|
||||||
elementsToFilter.forEach((element) => {
|
elementsToFilter.forEach((element) => {
|
||||||
if (
|
if (
|
||||||
element.frameId &&
|
element.frameId &&
|
||||||
!isFrameLikeElement(element) &&
|
!isFrameLikeElement(element) &&
|
||||||
!isElementInFrame(element, allElements, appState)
|
!isElementInFrame(element, elementsMap, appState)
|
||||||
) {
|
) {
|
||||||
elementsToRemove.add(element);
|
elementsToRemove.add(element);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return elementsToRemove.size > 0
|
if (elementsToRemove.size > 0) {
|
||||||
? removeElementsFromFrame(allElements, [...elementsToRemove], appState)
|
removeElementsFromFrame(elementsToRemove);
|
||||||
: allElements;
|
}
|
||||||
|
return allElements;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -609,14 +618,16 @@ export const updateFrameMembershipOfSelectedElements = (
|
||||||
* anywhere in the group tree
|
* anywhere in the group tree
|
||||||
*/
|
*/
|
||||||
export const omitGroupsContainingFrameLikes = (
|
export const omitGroupsContainingFrameLikes = (
|
||||||
allElements: ExcalidrawElementsIncludingDeleted,
|
allElements: ElementsMapOrArray,
|
||||||
/** subset of elements you want to filter. Optional perf optimization so we
|
/** subset of elements you want to filter. Optional perf optimization so we
|
||||||
* don't have to filter all elements unnecessarily
|
* don't have to filter all elements unnecessarily
|
||||||
*/
|
*/
|
||||||
selectedElements?: readonly ExcalidrawElement[],
|
selectedElements?: readonly ExcalidrawElement[],
|
||||||
) => {
|
) => {
|
||||||
const uniqueGroupIds = new Set<string>();
|
const uniqueGroupIds = new Set<string>();
|
||||||
for (const el of selectedElements || allElements) {
|
const elements = selectedElements || allElements;
|
||||||
|
|
||||||
|
for (const el of elements.values()) {
|
||||||
const topMostGroupId = el.groupIds[el.groupIds.length - 1];
|
const topMostGroupId = el.groupIds[el.groupIds.length - 1];
|
||||||
if (topMostGroupId) {
|
if (topMostGroupId) {
|
||||||
uniqueGroupIds.add(topMostGroupId);
|
uniqueGroupIds.add(topMostGroupId);
|
||||||
|
@ -634,9 +645,15 @@ export const omitGroupsContainingFrameLikes = (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (selectedElements || allElements).filter(
|
const ret: ExcalidrawElement[] = [];
|
||||||
(el) => !rejectedGroupIds.has(el.groupIds[el.groupIds.length - 1]),
|
|
||||||
);
|
for (const element of elements.values()) {
|
||||||
|
if (!rejectedGroupIds.has(element.groupIds[element.groupIds.length - 1])) {
|
||||||
|
ret.push(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -645,10 +662,11 @@ export const omitGroupsContainingFrameLikes = (
|
||||||
*/
|
*/
|
||||||
export const getTargetFrame = (
|
export const getTargetFrame = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
appState: StaticCanvasAppState,
|
appState: StaticCanvasAppState,
|
||||||
) => {
|
) => {
|
||||||
const _element = isTextElement(element)
|
const _element = isTextElement(element)
|
||||||
? getContainerElement(element) || element
|
? getContainerElement(element, elementsMap) || element
|
||||||
: element;
|
: element;
|
||||||
|
|
||||||
return appState.selectedElementIds[_element.id] &&
|
return appState.selectedElementIds[_element.id] &&
|
||||||
|
@ -661,12 +679,12 @@ export const getTargetFrame = (
|
||||||
// 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: ElementsMap,
|
||||||
appState: StaticCanvasAppState,
|
appState: StaticCanvasAppState,
|
||||||
) => {
|
) => {
|
||||||
const frame = getTargetFrame(element, appState);
|
const frame = getTargetFrame(element, allElements, appState);
|
||||||
const _element = isTextElement(element)
|
const _element = isTextElement(element)
|
||||||
? getContainerElement(element) || element
|
? getContainerElement(element, allElements) || element
|
||||||
: element;
|
: element;
|
||||||
|
|
||||||
if (frame) {
|
if (frame) {
|
||||||
|
|
|
@ -3,6 +3,7 @@ import {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
|
ElementsMapOrArray,
|
||||||
} from "./element/types";
|
} from "./element/types";
|
||||||
import {
|
import {
|
||||||
AppClassProperties,
|
AppClassProperties,
|
||||||
|
@ -270,9 +271,17 @@ export const isElementInGroup = (element: ExcalidrawElement, groupId: string) =>
|
||||||
element.groupIds.includes(groupId);
|
element.groupIds.includes(groupId);
|
||||||
|
|
||||||
export const getElementsInGroup = (
|
export const getElementsInGroup = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: ElementsMapOrArray,
|
||||||
groupId: string,
|
groupId: string,
|
||||||
) => elements.filter((element) => isElementInGroup(element, groupId));
|
) => {
|
||||||
|
const elementsInGroup: ExcalidrawElement[] = [];
|
||||||
|
for (const element of elements.values()) {
|
||||||
|
if (isElementInGroup(element, groupId)) {
|
||||||
|
elementsInGroup.push(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return elementsInGroup;
|
||||||
|
};
|
||||||
|
|
||||||
export const getSelectedGroupIdForElement = (
|
export const getSelectedGroupIdForElement = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
|
|
|
@ -21,7 +21,11 @@ import type { RoughCanvas } from "roughjs/bin/canvas";
|
||||||
import type { Drawable } from "roughjs/bin/core";
|
import type { Drawable } from "roughjs/bin/core";
|
||||||
import type { RoughSVG } from "roughjs/bin/svg";
|
import type { RoughSVG } from "roughjs/bin/svg";
|
||||||
|
|
||||||
import { SVGRenderConfig, StaticCanvasRenderConfig } from "../scene/types";
|
import {
|
||||||
|
SVGRenderConfig,
|
||||||
|
StaticCanvasRenderConfig,
|
||||||
|
RenderableElementsMap,
|
||||||
|
} from "../scene/types";
|
||||||
import {
|
import {
|
||||||
distance,
|
distance,
|
||||||
getFontString,
|
getFontString,
|
||||||
|
@ -611,6 +615,7 @@ export const renderSelectionElement = (
|
||||||
|
|
||||||
export const renderElement = (
|
export const renderElement = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
|
elementsMap: RenderableElementsMap,
|
||||||
rc: RoughCanvas,
|
rc: RoughCanvas,
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
renderConfig: StaticCanvasRenderConfig,
|
renderConfig: StaticCanvasRenderConfig,
|
||||||
|
@ -715,7 +720,7 @@ export const renderElement = (
|
||||||
let shiftX = (x2 - x1) / 2 - (element.x - x1);
|
let shiftX = (x2 - x1) / 2 - (element.x - x1);
|
||||||
let shiftY = (y2 - y1) / 2 - (element.y - y1);
|
let shiftY = (y2 - y1) / 2 - (element.y - y1);
|
||||||
if (isTextElement(element)) {
|
if (isTextElement(element)) {
|
||||||
const container = getContainerElement(element);
|
const container = getContainerElement(element, elementsMap);
|
||||||
if (isArrowElement(container)) {
|
if (isArrowElement(container)) {
|
||||||
const boundTextCoords =
|
const boundTextCoords =
|
||||||
LinearElementEditor.getBoundTextElementPosition(
|
LinearElementEditor.getBoundTextElementPosition(
|
||||||
|
@ -900,6 +905,7 @@ const maybeWrapNodesInFrameClipPath = (
|
||||||
|
|
||||||
export const renderElementToSvg = (
|
export const renderElementToSvg = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
|
elementsMap: RenderableElementsMap,
|
||||||
rsvg: RoughSVG,
|
rsvg: RoughSVG,
|
||||||
svgRoot: SVGElement,
|
svgRoot: SVGElement,
|
||||||
files: BinaryFiles,
|
files: BinaryFiles,
|
||||||
|
@ -912,7 +918,7 @@ export const renderElementToSvg = (
|
||||||
let cx = (x2 - x1) / 2 - (element.x - x1);
|
let cx = (x2 - x1) / 2 - (element.x - x1);
|
||||||
let cy = (y2 - y1) / 2 - (element.y - y1);
|
let cy = (y2 - y1) / 2 - (element.y - y1);
|
||||||
if (isTextElement(element)) {
|
if (isTextElement(element)) {
|
||||||
const container = getContainerElement(element);
|
const container = getContainerElement(element, elementsMap);
|
||||||
if (isArrowElement(container)) {
|
if (isArrowElement(container)) {
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(container);
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(container);
|
||||||
|
|
||||||
|
@ -1013,6 +1019,7 @@ export const renderElementToSvg = (
|
||||||
createPlaceholderEmbeddableLabel(element);
|
createPlaceholderEmbeddableLabel(element);
|
||||||
renderElementToSvg(
|
renderElementToSvg(
|
||||||
label,
|
label,
|
||||||
|
elementsMap,
|
||||||
rsvg,
|
rsvg,
|
||||||
root,
|
root,
|
||||||
files,
|
files,
|
||||||
|
|
|
@ -33,6 +33,7 @@ import {
|
||||||
SVGRenderConfig,
|
SVGRenderConfig,
|
||||||
StaticCanvasRenderConfig,
|
StaticCanvasRenderConfig,
|
||||||
StaticSceneRenderConfig,
|
StaticSceneRenderConfig,
|
||||||
|
RenderableElementsMap,
|
||||||
} from "../scene/types";
|
} from "../scene/types";
|
||||||
import {
|
import {
|
||||||
getScrollBars,
|
getScrollBars,
|
||||||
|
@ -61,7 +62,7 @@ import {
|
||||||
TransformHandles,
|
TransformHandles,
|
||||||
TransformHandleType,
|
TransformHandleType,
|
||||||
} from "../element/transformHandles";
|
} from "../element/transformHandles";
|
||||||
import { throttleRAF } from "../utils";
|
import { arrayToMap, throttleRAF } from "../utils";
|
||||||
import { UserIdleState } from "../types";
|
import { UserIdleState } from "../types";
|
||||||
import { FRAME_STYLE, THEME_FILTER } from "../constants";
|
import { FRAME_STYLE, THEME_FILTER } from "../constants";
|
||||||
import {
|
import {
|
||||||
|
@ -75,10 +76,7 @@ import {
|
||||||
isIframeLikeElement,
|
isIframeLikeElement,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
} from "../element/typeChecks";
|
} from "../element/typeChecks";
|
||||||
import {
|
import { createPlaceholderEmbeddableLabel } from "../element/embeddable";
|
||||||
isIframeLikeOrItsLabel,
|
|
||||||
createPlaceholderEmbeddableLabel,
|
|
||||||
} from "../element/embeddable";
|
|
||||||
import {
|
import {
|
||||||
elementOverlapsWithFrame,
|
elementOverlapsWithFrame,
|
||||||
getTargetFrame,
|
getTargetFrame,
|
||||||
|
@ -446,7 +444,7 @@ const bootstrapCanvas = ({
|
||||||
|
|
||||||
const _renderInteractiveScene = ({
|
const _renderInteractiveScene = ({
|
||||||
canvas,
|
canvas,
|
||||||
elements,
|
elementsMap,
|
||||||
visibleElements,
|
visibleElements,
|
||||||
selectedElements,
|
selectedElements,
|
||||||
scale,
|
scale,
|
||||||
|
@ -454,7 +452,7 @@ const _renderInteractiveScene = ({
|
||||||
renderConfig,
|
renderConfig,
|
||||||
}: InteractiveSceneRenderConfig) => {
|
}: InteractiveSceneRenderConfig) => {
|
||||||
if (canvas === null) {
|
if (canvas === null) {
|
||||||
return { atLeastOneVisibleElement: false, elements };
|
return { atLeastOneVisibleElement: false, elementsMap };
|
||||||
}
|
}
|
||||||
|
|
||||||
const [normalizedWidth, normalizedHeight] = getNormalizedCanvasDimensions(
|
const [normalizedWidth, normalizedHeight] = getNormalizedCanvasDimensions(
|
||||||
|
@ -562,75 +560,64 @@ const _renderInteractiveScene = ({
|
||||||
|
|
||||||
if (showBoundingBox) {
|
if (showBoundingBox) {
|
||||||
// Optimisation for finding quickly relevant element ids
|
// Optimisation for finding quickly relevant element ids
|
||||||
const locallySelectedIds = selectedElements.reduce(
|
const locallySelectedIds = arrayToMap(selectedElements);
|
||||||
(acc: Record<string, boolean>, element) => {
|
|
||||||
acc[element.id] = true;
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
|
|
||||||
const selections = elements.reduce(
|
const selections: {
|
||||||
(
|
angle: number;
|
||||||
acc: {
|
elementX1: number;
|
||||||
angle: number;
|
elementY1: number;
|
||||||
elementX1: number;
|
elementX2: number;
|
||||||
elementY1: number;
|
elementY2: number;
|
||||||
elementX2: number;
|
selectionColors: string[];
|
||||||
elementY2: number;
|
dashed?: boolean;
|
||||||
selectionColors: string[];
|
cx: number;
|
||||||
dashed?: boolean;
|
cy: number;
|
||||||
cx: number;
|
activeEmbeddable: boolean;
|
||||||
cy: number;
|
}[] = [];
|
||||||
activeEmbeddable: boolean;
|
|
||||||
}[],
|
|
||||||
element,
|
|
||||||
) => {
|
|
||||||
const selectionColors = [];
|
|
||||||
// local user
|
|
||||||
if (
|
|
||||||
locallySelectedIds[element.id] &&
|
|
||||||
!isSelectedViaGroup(appState, element)
|
|
||||||
) {
|
|
||||||
selectionColors.push(selectionColor);
|
|
||||||
}
|
|
||||||
// remote users
|
|
||||||
if (renderConfig.remoteSelectedElementIds[element.id]) {
|
|
||||||
selectionColors.push(
|
|
||||||
...renderConfig.remoteSelectedElementIds[element.id].map(
|
|
||||||
(socketId: string) => {
|
|
||||||
const background = getClientColor(socketId);
|
|
||||||
return background;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectionColors.length) {
|
for (const element of elementsMap.values()) {
|
||||||
const [elementX1, elementY1, elementX2, elementY2, cx, cy] =
|
const selectionColors = [];
|
||||||
getElementAbsoluteCoords(element, true);
|
// local user
|
||||||
acc.push({
|
if (
|
||||||
angle: element.angle,
|
locallySelectedIds.has(element.id) &&
|
||||||
elementX1,
|
!isSelectedViaGroup(appState, element)
|
||||||
elementY1,
|
) {
|
||||||
elementX2,
|
selectionColors.push(selectionColor);
|
||||||
elementY2,
|
}
|
||||||
selectionColors,
|
// remote users
|
||||||
dashed: !!renderConfig.remoteSelectedElementIds[element.id],
|
if (renderConfig.remoteSelectedElementIds[element.id]) {
|
||||||
cx,
|
selectionColors.push(
|
||||||
cy,
|
...renderConfig.remoteSelectedElementIds[element.id].map(
|
||||||
activeEmbeddable:
|
(socketId: string) => {
|
||||||
appState.activeEmbeddable?.element === element &&
|
const background = getClientColor(socketId);
|
||||||
appState.activeEmbeddable.state === "active",
|
return background;
|
||||||
});
|
},
|
||||||
}
|
),
|
||||||
return acc;
|
);
|
||||||
},
|
}
|
||||||
[],
|
|
||||||
);
|
if (selectionColors.length) {
|
||||||
|
const [elementX1, elementY1, elementX2, elementY2, cx, cy] =
|
||||||
|
getElementAbsoluteCoords(element, true);
|
||||||
|
selections.push({
|
||||||
|
angle: element.angle,
|
||||||
|
elementX1,
|
||||||
|
elementY1,
|
||||||
|
elementX2,
|
||||||
|
elementY2,
|
||||||
|
selectionColors,
|
||||||
|
dashed: !!renderConfig.remoteSelectedElementIds[element.id],
|
||||||
|
cx,
|
||||||
|
cy,
|
||||||
|
activeEmbeddable:
|
||||||
|
appState.activeEmbeddable?.element === element &&
|
||||||
|
appState.activeEmbeddable.state === "active",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const addSelectionForGroupId = (groupId: GroupId) => {
|
const addSelectionForGroupId = (groupId: GroupId) => {
|
||||||
const groupElements = getElementsInGroup(elements, groupId);
|
const groupElements = getElementsInGroup(elementsMap, groupId);
|
||||||
const [elementX1, elementY1, elementX2, elementY2] =
|
const [elementX1, elementY1, elementX2, elementY2] =
|
||||||
getCommonBounds(groupElements);
|
getCommonBounds(groupElements);
|
||||||
selections.push({
|
selections.push({
|
||||||
|
@ -870,7 +857,7 @@ const _renderInteractiveScene = ({
|
||||||
let scrollBars;
|
let scrollBars;
|
||||||
if (renderConfig.renderScrollbars) {
|
if (renderConfig.renderScrollbars) {
|
||||||
scrollBars = getScrollBars(
|
scrollBars = getScrollBars(
|
||||||
elements,
|
elementsMap,
|
||||||
normalizedWidth,
|
normalizedWidth,
|
||||||
normalizedHeight,
|
normalizedHeight,
|
||||||
appState,
|
appState,
|
||||||
|
@ -897,14 +884,14 @@ const _renderInteractiveScene = ({
|
||||||
return {
|
return {
|
||||||
scrollBars,
|
scrollBars,
|
||||||
atLeastOneVisibleElement: visibleElements.length > 0,
|
atLeastOneVisibleElement: visibleElements.length > 0,
|
||||||
elements,
|
elementsMap,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const _renderStaticScene = ({
|
const _renderStaticScene = ({
|
||||||
canvas,
|
canvas,
|
||||||
rc,
|
rc,
|
||||||
elements,
|
elementsMap,
|
||||||
visibleElements,
|
visibleElements,
|
||||||
scale,
|
scale,
|
||||||
appState,
|
appState,
|
||||||
|
@ -965,7 +952,7 @@ const _renderStaticScene = ({
|
||||||
|
|
||||||
// Paint visible elements
|
// Paint visible elements
|
||||||
visibleElements
|
visibleElements
|
||||||
.filter((el) => !isIframeLikeOrItsLabel(el))
|
.filter((el) => !isIframeLikeElement(el))
|
||||||
.forEach((element) => {
|
.forEach((element) => {
|
||||||
try {
|
try {
|
||||||
const frameId = element.frameId || appState.frameToHighlight?.id;
|
const frameId = element.frameId || appState.frameToHighlight?.id;
|
||||||
|
@ -977,16 +964,30 @@ const _renderStaticScene = ({
|
||||||
) {
|
) {
|
||||||
context.save();
|
context.save();
|
||||||
|
|
||||||
const frame = getTargetFrame(element, appState);
|
const frame = getTargetFrame(element, elementsMap, appState);
|
||||||
|
|
||||||
// TODO do we need to check isElementInFrame here?
|
// TODO do we need to check isElementInFrame here?
|
||||||
if (frame && isElementInFrame(element, elements, appState)) {
|
if (frame && isElementInFrame(element, elementsMap, appState)) {
|
||||||
frameClip(frame, context, renderConfig, appState);
|
frameClip(frame, context, renderConfig, appState);
|
||||||
}
|
}
|
||||||
renderElement(element, rc, context, renderConfig, appState);
|
renderElement(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
rc,
|
||||||
|
context,
|
||||||
|
renderConfig,
|
||||||
|
appState,
|
||||||
|
);
|
||||||
context.restore();
|
context.restore();
|
||||||
} else {
|
} else {
|
||||||
renderElement(element, rc, context, renderConfig, appState);
|
renderElement(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
rc,
|
||||||
|
context,
|
||||||
|
renderConfig,
|
||||||
|
appState,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (!isExporting) {
|
if (!isExporting) {
|
||||||
renderLinkIcon(element, context, appState);
|
renderLinkIcon(element, context, appState);
|
||||||
|
@ -998,11 +999,18 @@ const _renderStaticScene = ({
|
||||||
|
|
||||||
// render embeddables on top
|
// render embeddables on top
|
||||||
visibleElements
|
visibleElements
|
||||||
.filter((el) => isIframeLikeOrItsLabel(el))
|
.filter((el) => isIframeLikeElement(el))
|
||||||
.forEach((element) => {
|
.forEach((element) => {
|
||||||
try {
|
try {
|
||||||
const render = () => {
|
const render = () => {
|
||||||
renderElement(element, rc, context, renderConfig, appState);
|
renderElement(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
rc,
|
||||||
|
context,
|
||||||
|
renderConfig,
|
||||||
|
appState,
|
||||||
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isIframeLikeElement(element) &&
|
isIframeLikeElement(element) &&
|
||||||
|
@ -1014,7 +1022,14 @@ const _renderStaticScene = ({
|
||||||
element.height
|
element.height
|
||||||
) {
|
) {
|
||||||
const label = createPlaceholderEmbeddableLabel(element);
|
const label = createPlaceholderEmbeddableLabel(element);
|
||||||
renderElement(label, rc, context, renderConfig, appState);
|
renderElement(
|
||||||
|
label,
|
||||||
|
elementsMap,
|
||||||
|
rc,
|
||||||
|
context,
|
||||||
|
renderConfig,
|
||||||
|
appState,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (!isExporting) {
|
if (!isExporting) {
|
||||||
renderLinkIcon(element, context, appState);
|
renderLinkIcon(element, context, appState);
|
||||||
|
@ -1032,9 +1047,9 @@ const _renderStaticScene = ({
|
||||||
) {
|
) {
|
||||||
context.save();
|
context.save();
|
||||||
|
|
||||||
const frame = getTargetFrame(element, appState);
|
const frame = getTargetFrame(element, elementsMap, appState);
|
||||||
|
|
||||||
if (frame && isElementInFrame(element, elements, appState)) {
|
if (frame && isElementInFrame(element, elementsMap, appState)) {
|
||||||
frameClip(frame, context, renderConfig, appState);
|
frameClip(frame, context, renderConfig, appState);
|
||||||
}
|
}
|
||||||
render();
|
render();
|
||||||
|
@ -1448,6 +1463,7 @@ const renderLinkIcon = (
|
||||||
// This should be only called for exporting purposes
|
// This should be only called for exporting purposes
|
||||||
export const renderSceneToSvg = (
|
export const renderSceneToSvg = (
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
elementsMap: RenderableElementsMap,
|
||||||
rsvg: RoughSVG,
|
rsvg: RoughSVG,
|
||||||
svgRoot: SVGElement,
|
svgRoot: SVGElement,
|
||||||
files: BinaryFiles,
|
files: BinaryFiles,
|
||||||
|
@ -1459,12 +1475,13 @@ export const renderSceneToSvg = (
|
||||||
|
|
||||||
// render elements
|
// render elements
|
||||||
elements
|
elements
|
||||||
.filter((el) => !isIframeLikeOrItsLabel(el))
|
.filter((el) => !isIframeLikeElement(el))
|
||||||
.forEach((element) => {
|
.forEach((element) => {
|
||||||
if (!element.isDeleted) {
|
if (!element.isDeleted) {
|
||||||
try {
|
try {
|
||||||
renderElementToSvg(
|
renderElementToSvg(
|
||||||
element,
|
element,
|
||||||
|
elementsMap,
|
||||||
rsvg,
|
rsvg,
|
||||||
svgRoot,
|
svgRoot,
|
||||||
files,
|
files,
|
||||||
|
@ -1486,6 +1503,7 @@ export const renderSceneToSvg = (
|
||||||
try {
|
try {
|
||||||
renderElementToSvg(
|
renderElementToSvg(
|
||||||
element,
|
element,
|
||||||
|
elementsMap,
|
||||||
rsvg,
|
rsvg,
|
||||||
svgRoot,
|
svgRoot,
|
||||||
files,
|
files,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { isTextElement, refreshTextDimensions } from "../element";
|
import { isTextElement, refreshTextDimensions } from "../element";
|
||||||
import { newElementWith } from "../element/mutateElement";
|
import { newElementWith } from "../element/mutateElement";
|
||||||
|
import { getContainerElement } from "../element/textElement";
|
||||||
import { isBoundToContainer } from "../element/typeChecks";
|
import { isBoundToContainer } from "../element/typeChecks";
|
||||||
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
|
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
|
||||||
import { getFontString } from "../utils";
|
import { getFontString } from "../utils";
|
||||||
|
@ -57,7 +58,13 @@ export class Fonts {
|
||||||
ShapeCache.delete(element);
|
ShapeCache.delete(element);
|
||||||
didUpdate = true;
|
didUpdate = true;
|
||||||
return newElementWith(element, {
|
return newElementWith(element, {
|
||||||
...refreshTextDimensions(element),
|
...refreshTextDimensions(
|
||||||
|
element,
|
||||||
|
getContainerElement(
|
||||||
|
element,
|
||||||
|
this.scene.getElementsMapIncludingDeleted(),
|
||||||
|
),
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return element;
|
return element;
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
import { isElementInViewport } from "../element/sizeHelpers";
|
import { isElementInViewport } from "../element/sizeHelpers";
|
||||||
import { isImageElement } from "../element/typeChecks";
|
import { isImageElement } from "../element/typeChecks";
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import {
|
||||||
|
NonDeletedElementsMap,
|
||||||
|
NonDeletedExcalidrawElement,
|
||||||
|
} from "../element/types";
|
||||||
import { cancelRender } from "../renderer/renderScene";
|
import { cancelRender } from "../renderer/renderScene";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { memoize } from "../utils";
|
import { memoize, toBrandedType } from "../utils";
|
||||||
import Scene from "./Scene";
|
import Scene from "./Scene";
|
||||||
|
import { RenderableElementsMap } from "./types";
|
||||||
|
|
||||||
export class Renderer {
|
export class Renderer {
|
||||||
private scene: Scene;
|
private scene: Scene;
|
||||||
|
@ -15,7 +19,7 @@ export class Renderer {
|
||||||
|
|
||||||
public getRenderableElements = (() => {
|
public getRenderableElements = (() => {
|
||||||
const getVisibleCanvasElements = ({
|
const getVisibleCanvasElements = ({
|
||||||
elements,
|
elementsMap,
|
||||||
zoom,
|
zoom,
|
||||||
offsetLeft,
|
offsetLeft,
|
||||||
offsetTop,
|
offsetTop,
|
||||||
|
@ -24,7 +28,7 @@ export class Renderer {
|
||||||
height,
|
height,
|
||||||
width,
|
width,
|
||||||
}: {
|
}: {
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elementsMap: NonDeletedElementsMap;
|
||||||
zoom: AppState["zoom"];
|
zoom: AppState["zoom"];
|
||||||
offsetLeft: AppState["offsetLeft"];
|
offsetLeft: AppState["offsetLeft"];
|
||||||
offsetTop: AppState["offsetTop"];
|
offsetTop: AppState["offsetTop"];
|
||||||
|
@ -33,43 +37,55 @@ export class Renderer {
|
||||||
height: AppState["height"];
|
height: AppState["height"];
|
||||||
width: AppState["width"];
|
width: AppState["width"];
|
||||||
}): readonly NonDeletedExcalidrawElement[] => {
|
}): readonly NonDeletedExcalidrawElement[] => {
|
||||||
return elements.filter((element) =>
|
const visibleElements: NonDeletedExcalidrawElement[] = [];
|
||||||
isElementInViewport(element, width, height, {
|
for (const element of elementsMap.values()) {
|
||||||
zoom,
|
if (
|
||||||
offsetLeft,
|
isElementInViewport(element, width, height, {
|
||||||
offsetTop,
|
zoom,
|
||||||
scrollX,
|
offsetLeft,
|
||||||
scrollY,
|
offsetTop,
|
||||||
}),
|
scrollX,
|
||||||
);
|
scrollY,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
visibleElements.push(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return visibleElements;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCanvasElements = ({
|
const getRenderableElements = ({
|
||||||
editingElement,
|
|
||||||
elements,
|
elements,
|
||||||
|
editingElement,
|
||||||
pendingImageElementId,
|
pendingImageElementId,
|
||||||
}: {
|
}: {
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
editingElement: AppState["editingElement"];
|
editingElement: AppState["editingElement"];
|
||||||
pendingImageElementId: AppState["pendingImageElementId"];
|
pendingImageElementId: AppState["pendingImageElementId"];
|
||||||
}) => {
|
}) => {
|
||||||
return elements.filter((element) => {
|
const elementsMap = toBrandedType<RenderableElementsMap>(new Map());
|
||||||
|
|
||||||
|
for (const element of elements) {
|
||||||
if (isImageElement(element)) {
|
if (isImageElement(element)) {
|
||||||
if (
|
if (
|
||||||
// => not placed on canvas yet (but in elements array)
|
// => not placed on canvas yet (but in elements array)
|
||||||
pendingImageElementId === element.id
|
pendingImageElementId === element.id
|
||||||
) {
|
) {
|
||||||
return false;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// we don't want to render text element that's being currently edited
|
// we don't want to render text element that's being currently edited
|
||||||
// (it's rendered on remote only)
|
// (it's rendered on remote only)
|
||||||
return (
|
if (
|
||||||
!editingElement ||
|
!editingElement ||
|
||||||
editingElement.type !== "text" ||
|
editingElement.type !== "text" ||
|
||||||
element.id !== editingElement.id
|
element.id !== editingElement.id
|
||||||
);
|
) {
|
||||||
});
|
elementsMap.set(element.id, element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return elementsMap;
|
||||||
};
|
};
|
||||||
|
|
||||||
return memoize(
|
return memoize(
|
||||||
|
@ -100,14 +116,14 @@ export class Renderer {
|
||||||
}) => {
|
}) => {
|
||||||
const elements = this.scene.getNonDeletedElements();
|
const elements = this.scene.getNonDeletedElements();
|
||||||
|
|
||||||
const canvasElements = getCanvasElements({
|
const elementsMap = getRenderableElements({
|
||||||
elements,
|
elements,
|
||||||
editingElement,
|
editingElement,
|
||||||
pendingImageElementId,
|
pendingImageElementId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const visibleElements = getVisibleCanvasElements({
|
const visibleElements = getVisibleCanvasElements({
|
||||||
elements: canvasElements,
|
elementsMap,
|
||||||
zoom,
|
zoom,
|
||||||
offsetLeft,
|
offsetLeft,
|
||||||
offsetTop,
|
offsetTop,
|
||||||
|
@ -117,7 +133,7 @@ export class Renderer {
|
||||||
width,
|
width,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { canvasElements, visibleElements };
|
return { elementsMap, visibleElements };
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -3,14 +3,18 @@ import {
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
ExcalidrawFrameLikeElement,
|
ExcalidrawFrameLikeElement,
|
||||||
|
ElementsMapOrArray,
|
||||||
|
NonDeletedElementsMap,
|
||||||
|
SceneElementsMap,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { getNonDeletedElements, isNonDeletedElement } from "../element";
|
import { isNonDeletedElement } from "../element";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
import { isFrameLikeElement } from "../element/typeChecks";
|
import { isFrameLikeElement } from "../element/typeChecks";
|
||||||
import { getSelectedElements } from "./selection";
|
import { getSelectedElements } from "./selection";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { Assert, SameType } from "../utility-types";
|
import { Assert, SameType } from "../utility-types";
|
||||||
import { randomInteger } from "../random";
|
import { randomInteger } from "../random";
|
||||||
|
import { toBrandedType } from "../utils";
|
||||||
|
|
||||||
type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"];
|
type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"];
|
||||||
type ElementKey = ExcalidrawElement | ElementIdKey;
|
type ElementKey = ExcalidrawElement | ElementIdKey;
|
||||||
|
@ -20,6 +24,20 @@ type SceneStateCallbackRemover = () => void;
|
||||||
|
|
||||||
type SelectionHash = string & { __brand: "selectionHash" };
|
type SelectionHash = string & { __brand: "selectionHash" };
|
||||||
|
|
||||||
|
const getNonDeletedElements = <T extends ExcalidrawElement>(
|
||||||
|
allElements: readonly T[],
|
||||||
|
) => {
|
||||||
|
const elementsMap = new Map() as NonDeletedElementsMap;
|
||||||
|
const elements: T[] = [];
|
||||||
|
for (const element of allElements) {
|
||||||
|
if (!element.isDeleted) {
|
||||||
|
elements.push(element as NonDeleted<T>);
|
||||||
|
elementsMap.set(element.id, element as NonDeletedExcalidrawElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { elementsMap, elements };
|
||||||
|
};
|
||||||
|
|
||||||
const hashSelectionOpts = (
|
const hashSelectionOpts = (
|
||||||
opts: Parameters<InstanceType<typeof Scene>["getSelectedElements"]>[0],
|
opts: Parameters<InstanceType<typeof Scene>["getSelectedElements"]>[0],
|
||||||
) => {
|
) => {
|
||||||
|
@ -102,11 +120,13 @@ class Scene {
|
||||||
private callbacks: Set<SceneStateCallback> = new Set();
|
private callbacks: Set<SceneStateCallback> = new Set();
|
||||||
|
|
||||||
private nonDeletedElements: readonly NonDeletedExcalidrawElement[] = [];
|
private nonDeletedElements: readonly NonDeletedExcalidrawElement[] = [];
|
||||||
|
private nonDeletedElementsMap: NonDeletedElementsMap =
|
||||||
|
new Map() as NonDeletedElementsMap;
|
||||||
private elements: readonly ExcalidrawElement[] = [];
|
private elements: readonly ExcalidrawElement[] = [];
|
||||||
private nonDeletedFramesLikes: readonly NonDeleted<ExcalidrawFrameLikeElement>[] =
|
private nonDeletedFramesLikes: readonly NonDeleted<ExcalidrawFrameLikeElement>[] =
|
||||||
[];
|
[];
|
||||||
private frames: readonly ExcalidrawFrameLikeElement[] = [];
|
private frames: readonly ExcalidrawFrameLikeElement[] = [];
|
||||||
private elementsMap = new Map<ExcalidrawElement["id"], ExcalidrawElement>();
|
private elementsMap = toBrandedType<SceneElementsMap>(new Map());
|
||||||
private selectedElementsCache: {
|
private selectedElementsCache: {
|
||||||
selectedElementIds: AppState["selectedElementIds"] | null;
|
selectedElementIds: AppState["selectedElementIds"] | null;
|
||||||
elements: readonly NonDeletedExcalidrawElement[] | null;
|
elements: readonly NonDeletedExcalidrawElement[] | null;
|
||||||
|
@ -118,6 +138,14 @@ class Scene {
|
||||||
};
|
};
|
||||||
private versionNonce: number | undefined;
|
private versionNonce: number | undefined;
|
||||||
|
|
||||||
|
getElementsMapIncludingDeleted() {
|
||||||
|
return this.elementsMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
getNonDeletedElementsMap() {
|
||||||
|
return this.nonDeletedElementsMap;
|
||||||
|
}
|
||||||
|
|
||||||
getElementsIncludingDeleted() {
|
getElementsIncludingDeleted() {
|
||||||
return this.elements;
|
return this.elements;
|
||||||
}
|
}
|
||||||
|
@ -138,7 +166,7 @@ class Scene {
|
||||||
* scene state. This in effect will likely result in cache-miss, and
|
* scene state. This in effect will likely result in cache-miss, and
|
||||||
* the cache won't be updated in this case.
|
* the cache won't be updated in this case.
|
||||||
*/
|
*/
|
||||||
elements?: readonly ExcalidrawElement[];
|
elements?: ElementsMapOrArray;
|
||||||
// selection-related options
|
// selection-related options
|
||||||
includeBoundTextElement?: boolean;
|
includeBoundTextElement?: boolean;
|
||||||
includeElementsInFrames?: boolean;
|
includeElementsInFrames?: boolean;
|
||||||
|
@ -227,23 +255,27 @@ class Scene {
|
||||||
return didChange;
|
return didChange;
|
||||||
}
|
}
|
||||||
|
|
||||||
replaceAllElements(
|
replaceAllElements(nextElements: ElementsMapOrArray, mapElementIds = true) {
|
||||||
nextElements: readonly ExcalidrawElement[],
|
this.elements =
|
||||||
mapElementIds = true,
|
// ts doesn't like `Array.isArray` of `instanceof Map`
|
||||||
) {
|
nextElements instanceof Array
|
||||||
this.elements = nextElements;
|
? nextElements
|
||||||
|
: Array.from(nextElements.values());
|
||||||
const nextFrameLikes: ExcalidrawFrameLikeElement[] = [];
|
const nextFrameLikes: ExcalidrawFrameLikeElement[] = [];
|
||||||
this.elementsMap.clear();
|
this.elementsMap.clear();
|
||||||
nextElements.forEach((element) => {
|
this.elements.forEach((element) => {
|
||||||
if (isFrameLikeElement(element)) {
|
if (isFrameLikeElement(element)) {
|
||||||
nextFrameLikes.push(element);
|
nextFrameLikes.push(element);
|
||||||
}
|
}
|
||||||
this.elementsMap.set(element.id, element);
|
this.elementsMap.set(element.id, element);
|
||||||
Scene.mapElementToScene(element, this);
|
Scene.mapElementToScene(element, this, mapElementIds);
|
||||||
});
|
});
|
||||||
this.nonDeletedElements = getNonDeletedElements(this.elements);
|
const nonDeletedElements = getNonDeletedElements(this.elements);
|
||||||
|
this.nonDeletedElements = nonDeletedElements.elements;
|
||||||
|
this.nonDeletedElementsMap = nonDeletedElements.elementsMap;
|
||||||
|
|
||||||
this.frames = nextFrameLikes;
|
this.frames = nextFrameLikes;
|
||||||
this.nonDeletedFramesLikes = getNonDeletedElements(this.frames);
|
this.nonDeletedFramesLikes = getNonDeletedElements(this.frames).elements;
|
||||||
|
|
||||||
this.informMutation();
|
this.informMutation();
|
||||||
}
|
}
|
||||||
|
@ -332,6 +364,22 @@ class Scene {
|
||||||
getElementIndex(elementId: string) {
|
getElementIndex(elementId: string) {
|
||||||
return this.elements.findIndex((element) => element.id === elementId);
|
return this.elements.findIndex((element) => element.id === elementId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getContainerElement = (
|
||||||
|
element:
|
||||||
|
| (ExcalidrawElement & {
|
||||||
|
containerId: ExcalidrawElement["id"] | null;
|
||||||
|
})
|
||||||
|
| null,
|
||||||
|
) => {
|
||||||
|
if (!element) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (element.containerId) {
|
||||||
|
return this.getElement(element.containerId) || null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Scene;
|
export default Scene;
|
||||||
|
|
|
@ -11,7 +11,13 @@ import {
|
||||||
getElementAbsoluteCoords,
|
getElementAbsoluteCoords,
|
||||||
} from "../element/bounds";
|
} from "../element/bounds";
|
||||||
import { renderSceneToSvg, renderStaticScene } from "../renderer/renderScene";
|
import { renderSceneToSvg, renderStaticScene } from "../renderer/renderScene";
|
||||||
import { cloneJSON, distance, getFontString } from "../utils";
|
import {
|
||||||
|
arrayToMap,
|
||||||
|
cloneJSON,
|
||||||
|
distance,
|
||||||
|
getFontString,
|
||||||
|
toBrandedType,
|
||||||
|
} from "../utils";
|
||||||
import { AppState, BinaryFiles } from "../types";
|
import { AppState, BinaryFiles } from "../types";
|
||||||
import {
|
import {
|
||||||
DEFAULT_EXPORT_PADDING,
|
DEFAULT_EXPORT_PADDING,
|
||||||
|
@ -37,6 +43,7 @@ import { Mutable } from "../utility-types";
|
||||||
import { newElementWith } from "../element/mutateElement";
|
import { newElementWith } from "../element/mutateElement";
|
||||||
import Scene from "./Scene";
|
import Scene from "./Scene";
|
||||||
import { isFrameElement, isFrameLikeElement } from "../element/typeChecks";
|
import { isFrameElement, isFrameLikeElement } from "../element/typeChecks";
|
||||||
|
import { RenderableElementsMap } from "./types";
|
||||||
|
|
||||||
const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
|
const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
|
||||||
|
|
||||||
|
@ -59,7 +66,7 @@ const __createSceneForElementsHack__ = (
|
||||||
// ids to Scene instances so that we don't override the editor elements
|
// ids to Scene instances so that we don't override the editor elements
|
||||||
// mapping.
|
// mapping.
|
||||||
// We still need to clone the objects themselves to regen references.
|
// We still need to clone the objects themselves to regen references.
|
||||||
scene.replaceAllElements(cloneJSON(elements), false);
|
scene.replaceAllElements(cloneJSON(elements));
|
||||||
return scene;
|
return scene;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -241,10 +248,14 @@ export const exportToCanvas = async (
|
||||||
files,
|
files,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const elementsMap = toBrandedType<RenderableElementsMap>(
|
||||||
|
arrayToMap(elementsForRender),
|
||||||
|
);
|
||||||
|
|
||||||
renderStaticScene({
|
renderStaticScene({
|
||||||
canvas,
|
canvas,
|
||||||
rc: rough.canvas(canvas),
|
rc: rough.canvas(canvas),
|
||||||
elements: elementsForRender,
|
elementsMap,
|
||||||
visibleElements: elementsForRender,
|
visibleElements: elementsForRender,
|
||||||
scale,
|
scale,
|
||||||
appState: {
|
appState: {
|
||||||
|
@ -432,22 +443,29 @@ export const exportToSvg = async (
|
||||||
|
|
||||||
const renderEmbeddables = opts?.renderEmbeddables ?? false;
|
const renderEmbeddables = opts?.renderEmbeddables ?? false;
|
||||||
|
|
||||||
renderSceneToSvg(elementsForRender, rsvg, svgRoot, files || {}, {
|
renderSceneToSvg(
|
||||||
offsetX,
|
elementsForRender,
|
||||||
offsetY,
|
toBrandedType<RenderableElementsMap>(arrayToMap(elementsForRender)),
|
||||||
isExporting: true,
|
rsvg,
|
||||||
exportWithDarkMode,
|
svgRoot,
|
||||||
renderEmbeddables,
|
files || {},
|
||||||
frameRendering,
|
{
|
||||||
canvasBackgroundColor: viewBackgroundColor,
|
offsetX,
|
||||||
embedsValidationStatus: renderEmbeddables
|
offsetY,
|
||||||
? new Map(
|
isExporting: true,
|
||||||
elementsForRender
|
exportWithDarkMode,
|
||||||
.filter((element) => isFrameLikeElement(element))
|
renderEmbeddables,
|
||||||
.map((element) => [element.id, true]),
|
frameRendering,
|
||||||
)
|
canvasBackgroundColor: viewBackgroundColor,
|
||||||
: new Map(),
|
embedsValidationStatus: renderEmbeddables
|
||||||
});
|
? new Map(
|
||||||
|
elementsForRender
|
||||||
|
.filter((element) => isFrameLikeElement(element))
|
||||||
|
.map((element) => [element.id, true]),
|
||||||
|
)
|
||||||
|
: new Map(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
tempScene.destroy();
|
tempScene.destroy();
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { ExcalidrawElement } from "../element/types";
|
|
||||||
import { getCommonBounds } from "../element";
|
import { getCommonBounds } from "../element";
|
||||||
import { InteractiveCanvasAppState } from "../types";
|
import { InteractiveCanvasAppState } from "../types";
|
||||||
import { ScrollBars } from "./types";
|
import { RenderableElementsMap, ScrollBars } from "./types";
|
||||||
import { getGlobalCSSVariable } from "../utils";
|
import { getGlobalCSSVariable } from "../utils";
|
||||||
import { getLanguage } from "../i18n";
|
import { getLanguage } from "../i18n";
|
||||||
|
|
||||||
|
@ -10,12 +9,12 @@ export const SCROLLBAR_WIDTH = 6;
|
||||||
export const SCROLLBAR_COLOR = "rgba(0,0,0,0.3)";
|
export const SCROLLBAR_COLOR = "rgba(0,0,0,0.3)";
|
||||||
|
|
||||||
export const getScrollBars = (
|
export const getScrollBars = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: RenderableElementsMap,
|
||||||
viewportWidth: number,
|
viewportWidth: number,
|
||||||
viewportHeight: number,
|
viewportHeight: number,
|
||||||
appState: InteractiveCanvasAppState,
|
appState: InteractiveCanvasAppState,
|
||||||
): ScrollBars => {
|
): ScrollBars => {
|
||||||
if (elements.length === 0) {
|
if (!elements.size) {
|
||||||
return {
|
return {
|
||||||
horizontal: null,
|
horizontal: null,
|
||||||
vertical: null,
|
vertical: null,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import {
|
import {
|
||||||
|
ElementsMapOrArray,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
|
@ -166,26 +167,28 @@ export const getCommonAttributeOfSelectedElements = <T>(
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSelectedElements = (
|
export const getSelectedElements = (
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: ElementsMapOrArray,
|
||||||
appState: Pick<InteractiveCanvasAppState, "selectedElementIds">,
|
appState: Pick<InteractiveCanvasAppState, "selectedElementIds">,
|
||||||
opts?: {
|
opts?: {
|
||||||
includeBoundTextElement?: boolean;
|
includeBoundTextElement?: boolean;
|
||||||
includeElementsInFrames?: boolean;
|
includeElementsInFrames?: boolean;
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
const selectedElements = elements.filter((element) => {
|
const selectedElements: ExcalidrawElement[] = [];
|
||||||
|
for (const element of elements.values()) {
|
||||||
if (appState.selectedElementIds[element.id]) {
|
if (appState.selectedElementIds[element.id]) {
|
||||||
return element;
|
selectedElements.push(element);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
opts?.includeBoundTextElement &&
|
opts?.includeBoundTextElement &&
|
||||||
isBoundToContainer(element) &&
|
isBoundToContainer(element) &&
|
||||||
appState.selectedElementIds[element?.containerId]
|
appState.selectedElementIds[element?.containerId]
|
||||||
) {
|
) {
|
||||||
return element;
|
selectedElements.push(element);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
return null;
|
}
|
||||||
});
|
|
||||||
|
|
||||||
if (opts?.includeElementsInFrames) {
|
if (opts?.includeElementsInFrames) {
|
||||||
const elementsToInclude: ExcalidrawElement[] = [];
|
const elementsToInclude: ExcalidrawElement[] = [];
|
||||||
|
@ -205,7 +208,7 @@ export const getSelectedElements = (
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTargetElements = (
|
export const getTargetElements = (
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: ElementsMapOrArray,
|
||||||
appState: Pick<AppState, "selectedElementIds" | "editingElement">,
|
appState: Pick<AppState, "selectedElementIds" | "editingElement">,
|
||||||
) =>
|
) =>
|
||||||
appState.editingElement
|
appState.editingElement
|
||||||
|
|
|
@ -2,6 +2,7 @@ import type { RoughCanvas } from "roughjs/bin/canvas";
|
||||||
import { Drawable } from "roughjs/bin/core";
|
import { Drawable } from "roughjs/bin/core";
|
||||||
import {
|
import {
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
|
NonDeletedElementsMap,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import {
|
import {
|
||||||
|
@ -12,6 +13,10 @@ import {
|
||||||
InteractiveCanvasAppState,
|
InteractiveCanvasAppState,
|
||||||
StaticCanvasAppState,
|
StaticCanvasAppState,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
import { MakeBrand } from "../utility-types";
|
||||||
|
|
||||||
|
export type RenderableElementsMap = NonDeletedElementsMap &
|
||||||
|
MakeBrand<"RenderableElementsMap">;
|
||||||
|
|
||||||
export type StaticCanvasRenderConfig = {
|
export type StaticCanvasRenderConfig = {
|
||||||
canvasBackgroundColor: AppState["viewBackgroundColor"];
|
canvasBackgroundColor: AppState["viewBackgroundColor"];
|
||||||
|
@ -53,14 +58,14 @@ export type InteractiveCanvasRenderConfig = {
|
||||||
|
|
||||||
export type RenderInteractiveSceneCallback = {
|
export type RenderInteractiveSceneCallback = {
|
||||||
atLeastOneVisibleElement: boolean;
|
atLeastOneVisibleElement: boolean;
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elementsMap: RenderableElementsMap;
|
||||||
scrollBars?: ScrollBars;
|
scrollBars?: ScrollBars;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StaticSceneRenderConfig = {
|
export type StaticSceneRenderConfig = {
|
||||||
canvas: HTMLCanvasElement;
|
canvas: HTMLCanvasElement;
|
||||||
rc: RoughCanvas;
|
rc: RoughCanvas;
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elementsMap: RenderableElementsMap;
|
||||||
visibleElements: readonly NonDeletedExcalidrawElement[];
|
visibleElements: readonly NonDeletedExcalidrawElement[];
|
||||||
scale: number;
|
scale: number;
|
||||||
appState: StaticCanvasAppState;
|
appState: StaticCanvasAppState;
|
||||||
|
@ -69,7 +74,7 @@ export type StaticSceneRenderConfig = {
|
||||||
|
|
||||||
export type InteractiveSceneRenderConfig = {
|
export type InteractiveSceneRenderConfig = {
|
||||||
canvas: HTMLCanvasElement | null;
|
canvas: HTMLCanvasElement | null;
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elementsMap: RenderableElementsMap;
|
||||||
visibleElements: readonly NonDeletedExcalidrawElement[];
|
visibleElements: readonly NonDeletedExcalidrawElement[];
|
||||||
selectedElements: readonly NonDeletedExcalidrawElement[];
|
selectedElements: readonly NonDeletedExcalidrawElement[];
|
||||||
scale: number;
|
scale: number;
|
||||||
|
|
|
@ -54,3 +54,11 @@ export type Assert<T extends true> = T;
|
||||||
export type NestedKeyOf<T, K = keyof T> = K extends keyof T & (string | number)
|
export type NestedKeyOf<T, K = keyof T> = K extends keyof T & (string | number)
|
||||||
? `${K}` | (T[K] extends object ? `${K}.${NestedKeyOf<T[K]>}` : never)
|
? `${K}` | (T[K] extends object ? `${K}.${NestedKeyOf<T[K]>}` : never)
|
||||||
: never;
|
: never;
|
||||||
|
|
||||||
|
export type SetLike<T> = Set<T> | T[];
|
||||||
|
export type ReadonlySetLike<T> = ReadonlySet<T> | readonly T[];
|
||||||
|
|
||||||
|
export type MakeBrand<T extends string> = {
|
||||||
|
/** @private using ~ to sort last in intellisense */
|
||||||
|
[K in `~brand~${T}`]: T;
|
||||||
|
};
|
||||||
|
|
|
@ -650,8 +650,11 @@ export const getUpdatedTimestamp = () => (isTestEnv() ? 1 : Date.now());
|
||||||
* or array of ids (strings), into a Map, keyd by `id`.
|
* or array of ids (strings), into a Map, keyd by `id`.
|
||||||
*/
|
*/
|
||||||
export const arrayToMap = <T extends { id: string } | string>(
|
export const arrayToMap = <T extends { id: string } | string>(
|
||||||
items: readonly T[],
|
items: readonly T[] | Map<string, T>,
|
||||||
) => {
|
) => {
|
||||||
|
if (items instanceof Map) {
|
||||||
|
return items;
|
||||||
|
}
|
||||||
return items.reduce((acc: Map<string, T>, element) => {
|
return items.reduce((acc: Map<string, T>, element) => {
|
||||||
acc.set(typeof element === "string" ? element : element.id, element);
|
acc.set(typeof element === "string" ? element : element.id, element);
|
||||||
return acc;
|
return acc;
|
||||||
|
@ -1050,3 +1053,40 @@ export function getSvgPathFromStroke(points: number[][], closed = true) {
|
||||||
export const normalizeEOL = (str: string) => {
|
export const normalizeEOL = (str: string) => {
|
||||||
return str.replace(/\r?\n|\r/g, "\n");
|
return str.replace(/\r?\n|\r/g, "\n");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
type HasBrand<T> = {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
[K in keyof T]: K extends `~brand${infer _}` ? true : never;
|
||||||
|
}[keyof T];
|
||||||
|
|
||||||
|
type RemoveAllBrands<T> = HasBrand<T> extends true
|
||||||
|
? {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
[K in keyof T as K extends `~brand~${infer _}` ? never : K]: T[K];
|
||||||
|
}
|
||||||
|
: never;
|
||||||
|
|
||||||
|
// adapted from https://github.com/colinhacks/zod/discussions/1994#discussioncomment-6068940
|
||||||
|
// currently does not cover all types (e.g. tuples, promises...)
|
||||||
|
type Unbrand<T> = T extends Map<infer E, infer F>
|
||||||
|
? Map<E, F>
|
||||||
|
: T extends Set<infer E>
|
||||||
|
? Set<E>
|
||||||
|
: T extends Array<infer E>
|
||||||
|
? Array<E>
|
||||||
|
: RemoveAllBrands<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes type into a branded type, ensuring that value is assignable to
|
||||||
|
* the base ubranded type. Optionally you can explicitly supply current value
|
||||||
|
* type to combine both (useful for composite branded types. Make sure you
|
||||||
|
* compose branded types which are not composite themselves.)
|
||||||
|
*/
|
||||||
|
export const toBrandedType = <BrandedType, CurrentType = BrandedType>(
|
||||||
|
value: Unbrand<BrandedType>,
|
||||||
|
) => {
|
||||||
|
return value as CurrentType & BrandedType;
|
||||||
|
};
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
Loading…
Add table
Reference in a new issue