Add NonDeleted<ExcalidrawElement> (#1068)
* add NonDeleted * make test:all script run tests without prompt * rename helper * replace with helper * make element contructors return nonDeleted elements * cache filtered elements where appliacable for better perf * rename manager element getter * remove unnecessary assertion * fix test * make element types in resizeElement into nonDeleted Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
parent
c714c778ab
commit
df0613d8ac
@ -110,6 +110,7 @@
|
|||||||
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
|
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
"test": "npm run test:app",
|
"test": "npm run test:app",
|
||||||
|
"test:all": "npm run test:typecheck && npm run test:code && npm run test:other && npm run test:app -- --watchAll=false",
|
||||||
"test:update": "npm run test:app -- --updateSnapshot --watchAll=false",
|
"test:update": "npm run test:app -- --updateSnapshot --watchAll=false",
|
||||||
"test:app": "react-scripts test --env=jsdom --passWithNoTests",
|
"test:app": "react-scripts test --env=jsdom --passWithNoTests",
|
||||||
"test:code": "eslint --max-warnings=0 --ignore-path .gitignore --ext .js,.ts,.tsx .",
|
"test:code": "eslint --max-warnings=0 --ignore-path .gitignore --ext .js,.ts,.tsx .",
|
||||||
|
@ -5,6 +5,7 @@ import React from "react";
|
|||||||
import { trash } from "../components/icons";
|
import { trash } from "../components/icons";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
import { getNonDeletedElements } from "../element";
|
||||||
|
|
||||||
export const actionDeleteSelected = register({
|
export const actionDeleteSelected = register({
|
||||||
name: "deleteSelectedElements",
|
name: "deleteSelectedElements",
|
||||||
@ -20,7 +21,10 @@ export const actionDeleteSelected = register({
|
|||||||
elementType: "selection",
|
elementType: "selection",
|
||||||
multiElement: null,
|
multiElement: null,
|
||||||
},
|
},
|
||||||
commitToHistory: isSomeElementSelected(elements, appState),
|
commitToHistory: isSomeElementSelected(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
contextItemLabel: "labels.delete",
|
contextItemLabel: "labels.delete",
|
||||||
@ -33,7 +37,7 @@ export const actionDeleteSelected = register({
|
|||||||
title={t("labels.delete")}
|
title={t("labels.delete")}
|
||||||
aria-label={t("labels.delete")}
|
aria-label={t("labels.delete")}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
visible={isSomeElementSelected(elements, appState)}
|
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
@ -2,7 +2,7 @@ import React from "react";
|
|||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { duplicateElement } from "../element";
|
import { duplicateElement, getNonDeletedElements } from "../element";
|
||||||
import { isSomeElementSelected } from "../scene";
|
import { isSomeElementSelected } from "../scene";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { clone } from "../components/icons";
|
import { clone } from "../components/icons";
|
||||||
@ -43,7 +43,7 @@ export const actionDuplicateSelection = register({
|
|||||||
)}`}
|
)}`}
|
||||||
aria-label={t("labels.duplicateSelection")}
|
aria-label={t("labels.duplicateSelection")}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
visible={isSomeElementSelected(elements, appState)}
|
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
@ -2,7 +2,7 @@ import React from "react";
|
|||||||
import { menu, palette } from "../components/icons";
|
import { menu, palette } from "../components/icons";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { showSelectedShapeActions } from "../element";
|
import { showSelectedShapeActions, getNonDeletedElements } from "../element";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { allowFullScreen, exitFullScreen, isFullScreen } from "../utils";
|
import { allowFullScreen, exitFullScreen, isFullScreen } from "../utils";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
@ -39,7 +39,10 @@ export const actionToggleEditMenu = register({
|
|||||||
}),
|
}),
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
visible={showSelectedShapeActions(appState, elements)}
|
visible={showSelectedShapeActions(
|
||||||
|
appState,
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={palette}
|
icon={palette}
|
||||||
aria-label={t("buttons.edit")}
|
aria-label={t("buttons.edit")}
|
||||||
|
@ -5,7 +5,11 @@ import {
|
|||||||
isSomeElementSelected,
|
isSomeElementSelected,
|
||||||
} from "../scene";
|
} from "../scene";
|
||||||
import { ButtonSelect } from "../components/ButtonSelect";
|
import { ButtonSelect } from "../components/ButtonSelect";
|
||||||
import { isTextElement, redrawTextBoundingBox } from "../element";
|
import {
|
||||||
|
isTextElement,
|
||||||
|
redrawTextBoundingBox,
|
||||||
|
getNonDeletedElements,
|
||||||
|
} from "../element";
|
||||||
import { ColorPicker } from "../components/ColorPicker";
|
import { ColorPicker } from "../components/ColorPicker";
|
||||||
import { AppState } from "../../src/types";
|
import { AppState } from "../../src/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
@ -33,10 +37,15 @@ const getFormValue = function <T>(
|
|||||||
defaultValue?: T,
|
defaultValue?: T,
|
||||||
): T | null {
|
): T | null {
|
||||||
const editingElement = appState.editingElement;
|
const editingElement = appState.editingElement;
|
||||||
|
const nonDeletedElements = getNonDeletedElements(elements);
|
||||||
return (
|
return (
|
||||||
(editingElement && getAttribute(editingElement)) ??
|
(editingElement && getAttribute(editingElement)) ??
|
||||||
(isSomeElementSelected(elements, appState)
|
(isSomeElementSelected(nonDeletedElements, appState)
|
||||||
? getCommonAttributeOfSelectedElements(elements, appState, getAttribute)
|
? getCommonAttributeOfSelectedElements(
|
||||||
|
nonDeletedElements,
|
||||||
|
appState,
|
||||||
|
getAttribute,
|
||||||
|
)
|
||||||
: defaultValue) ??
|
: defaultValue) ??
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
import { globalSceneState } from "../scene";
|
||||||
|
|
||||||
export class ActionManager implements ActionsManagerInterface {
|
export class ActionManager implements ActionsManagerInterface {
|
||||||
actions = {} as ActionsManagerInterface["actions"];
|
actions = {} as ActionsManagerInterface["actions"];
|
||||||
@ -17,16 +18,18 @@ export class ActionManager implements ActionsManagerInterface {
|
|||||||
|
|
||||||
getAppState: () => AppState;
|
getAppState: () => AppState;
|
||||||
|
|
||||||
getElements: () => readonly ExcalidrawElement[];
|
getElementsIncludingDeleted: () => readonly ExcalidrawElement[];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
updater: UpdaterFn,
|
updater: UpdaterFn,
|
||||||
getAppState: () => AppState,
|
getAppState: () => AppState,
|
||||||
getElements: () => readonly ExcalidrawElement[],
|
getElementsIncludingDeleted: () => ReturnType<
|
||||||
|
typeof globalSceneState["getElementsIncludingDeleted"]
|
||||||
|
>,
|
||||||
) {
|
) {
|
||||||
this.updater = updater;
|
this.updater = updater;
|
||||||
this.getAppState = getAppState;
|
this.getAppState = getAppState;
|
||||||
this.getElements = getElements;
|
this.getElementsIncludingDeleted = getElementsIncludingDeleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
registerAction(action: Action) {
|
registerAction(action: Action) {
|
||||||
@ -43,7 +46,11 @@ export class ActionManager implements ActionsManagerInterface {
|
|||||||
.filter(
|
.filter(
|
||||||
(action) =>
|
(action) =>
|
||||||
action.keyTest &&
|
action.keyTest &&
|
||||||
action.keyTest(event, this.getAppState(), this.getElements()),
|
action.keyTest(
|
||||||
|
event,
|
||||||
|
this.getAppState(),
|
||||||
|
this.getElementsIncludingDeleted(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
@ -51,12 +58,24 @@ export class ActionManager implements ActionsManagerInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.updater(data[0].perform(this.getElements(), this.getAppState(), null));
|
this.updater(
|
||||||
|
data[0].perform(
|
||||||
|
this.getElementsIncludingDeleted(),
|
||||||
|
this.getAppState(),
|
||||||
|
null,
|
||||||
|
),
|
||||||
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
executeAction(action: Action) {
|
executeAction(action: Action) {
|
||||||
this.updater(action.perform(this.getElements(), this.getAppState(), null));
|
this.updater(
|
||||||
|
action.perform(
|
||||||
|
this.getElementsIncludingDeleted(),
|
||||||
|
this.getAppState(),
|
||||||
|
null,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getContextMenuItems(actionFilter: ActionFilterFn = (action) => action) {
|
getContextMenuItems(actionFilter: ActionFilterFn = (action) => action) {
|
||||||
@ -72,7 +91,11 @@ export class ActionManager implements ActionsManagerInterface {
|
|||||||
label: action.contextItemLabel ? t(action.contextItemLabel) : "",
|
label: action.contextItemLabel ? t(action.contextItemLabel) : "",
|
||||||
action: () => {
|
action: () => {
|
||||||
this.updater(
|
this.updater(
|
||||||
action.perform(this.getElements(), this.getAppState(), null),
|
action.perform(
|
||||||
|
this.getElementsIncludingDeleted(),
|
||||||
|
this.getAppState(),
|
||||||
|
null,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
@ -84,13 +107,17 @@ export class ActionManager implements ActionsManagerInterface {
|
|||||||
const PanelComponent = action.PanelComponent!;
|
const PanelComponent = action.PanelComponent!;
|
||||||
const updateData = (formState?: any) => {
|
const updateData = (formState?: any) => {
|
||||||
this.updater(
|
this.updater(
|
||||||
action.perform(this.getElements(), this.getAppState(), formState),
|
action.perform(
|
||||||
|
this.getElementsIncludingDeleted(),
|
||||||
|
this.getAppState(),
|
||||||
|
formState,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelComponent
|
<PanelComponent
|
||||||
elements={this.getElements()}
|
elements={this.getElementsIncludingDeleted()}
|
||||||
appState={this.getAppState()}
|
appState={this.getAppState()}
|
||||||
updateData={updateData}
|
updateData={updateData}
|
||||||
/>
|
/>
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import { ExcalidrawElement } from "./element/types";
|
import {
|
||||||
|
ExcalidrawElement,
|
||||||
|
NonDeletedExcalidrawElement,
|
||||||
|
} from "./element/types";
|
||||||
import { getSelectedElements } from "./scene";
|
import { getSelectedElements } from "./scene";
|
||||||
import { AppState } from "./types";
|
import { AppState } from "./types";
|
||||||
import { SVG_EXPORT_TAG } from "./scene/export";
|
import { SVG_EXPORT_TAG } from "./scene/export";
|
||||||
@ -19,7 +22,7 @@ export const probablySupportsClipboardBlob =
|
|||||||
"toBlob" in HTMLCanvasElement.prototype;
|
"toBlob" in HTMLCanvasElement.prototype;
|
||||||
|
|
||||||
export async function copyToAppClipboard(
|
export async function copyToAppClipboard(
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
) {
|
) {
|
||||||
CLIPBOARD = JSON.stringify(getSelectedElements(elements, appState));
|
CLIPBOARD = JSON.stringify(getSelectedElements(elements, appState));
|
||||||
|
@ -9,6 +9,7 @@ import { ToolButton } from "./ToolButton";
|
|||||||
import { capitalizeString, setCursorForShape } from "../utils";
|
import { capitalizeString, setCursorForShape } from "../utils";
|
||||||
import Stack from "./Stack";
|
import Stack from "./Stack";
|
||||||
import useIsMobile from "../is-mobile";
|
import useIsMobile from "../is-mobile";
|
||||||
|
import { getNonDeletedElements } from "../element";
|
||||||
|
|
||||||
export function SelectedShapeActions({
|
export function SelectedShapeActions({
|
||||||
appState,
|
appState,
|
||||||
@ -21,7 +22,10 @@ export function SelectedShapeActions({
|
|||||||
renderAction: ActionManager["renderAction"];
|
renderAction: ActionManager["renderAction"];
|
||||||
elementType: ExcalidrawElement["type"];
|
elementType: ExcalidrawElement["type"];
|
||||||
}) {
|
}) {
|
||||||
const targetElements = getTargetElement(elements, appState);
|
const targetElements = getTargetElement(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
);
|
||||||
const isEditing = Boolean(appState.editingElement);
|
const isEditing = Boolean(appState.editingElement);
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
@ -82,13 +86,9 @@ export function SelectedShapeActions({
|
|||||||
export function ShapesSwitcher({
|
export function ShapesSwitcher({
|
||||||
elementType,
|
elementType,
|
||||||
setAppState,
|
setAppState,
|
||||||
setElements,
|
|
||||||
elements,
|
|
||||||
}: {
|
}: {
|
||||||
elementType: ExcalidrawElement["type"];
|
elementType: ExcalidrawElement["type"];
|
||||||
setAppState: any;
|
setAppState: any;
|
||||||
setElements: any;
|
|
||||||
elements: readonly ExcalidrawElement[];
|
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -20,7 +20,6 @@ import {
|
|||||||
getElementMap,
|
getElementMap,
|
||||||
getDrawingVersion,
|
getDrawingVersion,
|
||||||
getSyncableElements,
|
getSyncableElements,
|
||||||
hasNonDeletedElements,
|
|
||||||
newLinearElement,
|
newLinearElement,
|
||||||
ResizeArrowFnType,
|
ResizeArrowFnType,
|
||||||
resizeElements,
|
resizeElements,
|
||||||
@ -185,7 +184,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
this.actionManager = new ActionManager(
|
this.actionManager = new ActionManager(
|
||||||
this.syncActionResult,
|
this.syncActionResult,
|
||||||
() => this.state,
|
() => this.state,
|
||||||
() => globalSceneState.getAllElements(),
|
() => globalSceneState.getElementsIncludingDeleted(),
|
||||||
);
|
);
|
||||||
this.actionManager.registerAll(actions);
|
this.actionManager.registerAll(actions);
|
||||||
|
|
||||||
@ -209,10 +208,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
appState={this.state}
|
appState={this.state}
|
||||||
setAppState={this.setAppState}
|
setAppState={this.setAppState}
|
||||||
actionManager={this.actionManager}
|
actionManager={this.actionManager}
|
||||||
elements={globalSceneState.getAllElements().filter((element) => {
|
elements={globalSceneState.getElements()}
|
||||||
return !element.isDeleted;
|
|
||||||
})}
|
|
||||||
setElements={this.setElements}
|
|
||||||
onRoomCreate={this.openPortal}
|
onRoomCreate={this.openPortal}
|
||||||
onRoomDestroy={this.closePortal}
|
onRoomDestroy={this.closePortal}
|
||||||
onLockToggle={this.toggleLock}
|
onLockToggle={this.toggleLock}
|
||||||
@ -310,7 +306,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
try {
|
try {
|
||||||
await Promise.race([
|
await Promise.race([
|
||||||
document.fonts?.ready?.then(() => {
|
document.fonts?.ready?.then(() => {
|
||||||
globalSceneState.getAllElements().forEach((element) => {
|
globalSceneState.getElementsIncludingDeleted().forEach((element) => {
|
||||||
if (isTextElement(element)) {
|
if (isTextElement(element)) {
|
||||||
invalidateShapeForElement(element);
|
invalidateShapeForElement(element);
|
||||||
}
|
}
|
||||||
@ -431,7 +427,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
}
|
}
|
||||||
private onResize = withBatchedUpdates(() => {
|
private onResize = withBatchedUpdates(() => {
|
||||||
globalSceneState
|
globalSceneState
|
||||||
.getAllElements()
|
.getElementsIncludingDeleted()
|
||||||
.forEach((element) => invalidateShapeForElement(element));
|
.forEach((element) => invalidateShapeForElement(element));
|
||||||
this.setState({});
|
this.setState({});
|
||||||
});
|
});
|
||||||
@ -439,7 +435,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
private beforeUnload = withBatchedUpdates((event: BeforeUnloadEvent) => {
|
private beforeUnload = withBatchedUpdates((event: BeforeUnloadEvent) => {
|
||||||
if (
|
if (
|
||||||
this.state.isCollaborating &&
|
this.state.isCollaborating &&
|
||||||
hasNonDeletedElements(globalSceneState.getAllElements())
|
globalSceneState.getElements().length > 0
|
||||||
) {
|
) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
// NOTE: modern browsers no longer allow showing a custom message here
|
// NOTE: modern browsers no longer allow showing a custom message here
|
||||||
@ -484,8 +480,9 @@ export class App extends React.Component<any, AppState> {
|
|||||||
);
|
);
|
||||||
cursorButton[socketID] = user.button;
|
cursorButton[socketID] = user.button;
|
||||||
});
|
});
|
||||||
|
const elements = globalSceneState.getElements();
|
||||||
const { atLeastOneVisibleElement, scrollBars } = renderScene(
|
const { atLeastOneVisibleElement, scrollBars } = renderScene(
|
||||||
globalSceneState.getAllElements().filter((element) => {
|
elements.filter((element) => {
|
||||||
// don't render text element that's being currently edited (it's
|
// don't render text element that's being currently edited (it's
|
||||||
// rendered on remote only)
|
// rendered on remote only)
|
||||||
return (
|
return (
|
||||||
@ -517,22 +514,20 @@ export class App extends React.Component<any, AppState> {
|
|||||||
if (scrollBars) {
|
if (scrollBars) {
|
||||||
currentScrollBars = scrollBars;
|
currentScrollBars = scrollBars;
|
||||||
}
|
}
|
||||||
const scrolledOutside =
|
const scrolledOutside = !atLeastOneVisibleElement && elements.length > 0;
|
||||||
!atLeastOneVisibleElement &&
|
|
||||||
hasNonDeletedElements(globalSceneState.getAllElements());
|
|
||||||
if (this.state.scrolledOutside !== scrolledOutside) {
|
if (this.state.scrolledOutside !== scrolledOutside) {
|
||||||
this.setState({ scrolledOutside: scrolledOutside });
|
this.setState({ scrolledOutside: scrolledOutside });
|
||||||
}
|
}
|
||||||
this.saveDebounced();
|
this.saveDebounced();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
getDrawingVersion(globalSceneState.getAllElements()) >
|
getDrawingVersion(globalSceneState.getElementsIncludingDeleted()) >
|
||||||
this.lastBroadcastedOrReceivedSceneVersion
|
this.lastBroadcastedOrReceivedSceneVersion
|
||||||
) {
|
) {
|
||||||
this.broadcastScene("SCENE_UPDATE");
|
this.broadcastScene("SCENE_UPDATE");
|
||||||
}
|
}
|
||||||
|
|
||||||
history.record(this.state, globalSceneState.getAllElements());
|
history.record(this.state, globalSceneState.getElementsIncludingDeleted());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy/paste
|
// Copy/paste
|
||||||
@ -543,7 +538,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
}
|
}
|
||||||
this.copyAll();
|
this.copyAll();
|
||||||
const { elements: nextElements, appState } = deleteSelectedElements(
|
const { elements: nextElements, appState } = deleteSelectedElements(
|
||||||
globalSceneState.getAllElements(),
|
globalSceneState.getElementsIncludingDeleted(),
|
||||||
this.state,
|
this.state,
|
||||||
);
|
);
|
||||||
globalSceneState.replaceAllElements(nextElements);
|
globalSceneState.replaceAllElements(nextElements);
|
||||||
@ -561,19 +556,16 @@ export class App extends React.Component<any, AppState> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
private copyAll = () => {
|
private copyAll = () => {
|
||||||
copyToAppClipboard(globalSceneState.getAllElements(), this.state);
|
copyToAppClipboard(globalSceneState.getElements(), this.state);
|
||||||
};
|
};
|
||||||
|
|
||||||
private copyToClipboardAsPng = () => {
|
private copyToClipboardAsPng = () => {
|
||||||
const selectedElements = getSelectedElements(
|
const elements = globalSceneState.getElements();
|
||||||
globalSceneState.getAllElements(),
|
|
||||||
this.state,
|
const selectedElements = getSelectedElements(elements, this.state);
|
||||||
);
|
|
||||||
exportCanvas(
|
exportCanvas(
|
||||||
"clipboard",
|
"clipboard",
|
||||||
selectedElements.length
|
selectedElements.length ? selectedElements : elements,
|
||||||
? selectedElements
|
|
||||||
: globalSceneState.getAllElements(),
|
|
||||||
this.state,
|
this.state,
|
||||||
this.canvas!,
|
this.canvas!,
|
||||||
this.state,
|
this.state,
|
||||||
@ -582,14 +574,14 @@ export class App extends React.Component<any, AppState> {
|
|||||||
|
|
||||||
private copyToClipboardAsSvg = () => {
|
private copyToClipboardAsSvg = () => {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = getSelectedElements(
|
||||||
globalSceneState.getAllElements(),
|
globalSceneState.getElements(),
|
||||||
this.state,
|
this.state,
|
||||||
);
|
);
|
||||||
exportCanvas(
|
exportCanvas(
|
||||||
"clipboard-svg",
|
"clipboard-svg",
|
||||||
selectedElements.length
|
selectedElements.length
|
||||||
? selectedElements
|
? selectedElements
|
||||||
: globalSceneState.getAllElements(),
|
: globalSceneState.getElements(),
|
||||||
this.state,
|
this.state,
|
||||||
this.canvas!,
|
this.canvas!,
|
||||||
this.state,
|
this.state,
|
||||||
@ -669,7 +661,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
globalSceneState.replaceAllElements([
|
globalSceneState.replaceAllElements([
|
||||||
...globalSceneState.getAllElements(),
|
...globalSceneState.getElementsIncludingDeleted(),
|
||||||
...newElements,
|
...newElements,
|
||||||
]);
|
]);
|
||||||
history.resumeRecording();
|
history.resumeRecording();
|
||||||
@ -703,7 +695,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
globalSceneState.replaceAllElements([
|
globalSceneState.replaceAllElements([
|
||||||
...globalSceneState.getAllElements(),
|
...globalSceneState.getElementsIncludingDeleted(),
|
||||||
element,
|
element,
|
||||||
]);
|
]);
|
||||||
this.setState({ selectedElementIds: { [element.id]: true } });
|
this.setState({ selectedElementIds: { [element.id]: true } });
|
||||||
@ -789,15 +781,15 @@ export class App extends React.Component<any, AppState> {
|
|||||||
// elements with more staler versions than ours, ignore them
|
// elements with more staler versions than ours, ignore them
|
||||||
// and keep ours.
|
// and keep ours.
|
||||||
if (
|
if (
|
||||||
globalSceneState.getAllElements() == null ||
|
globalSceneState.getElementsIncludingDeleted() == null ||
|
||||||
globalSceneState.getAllElements().length === 0
|
globalSceneState.getElementsIncludingDeleted().length === 0
|
||||||
) {
|
) {
|
||||||
globalSceneState.replaceAllElements(remoteElements);
|
globalSceneState.replaceAllElements(remoteElements);
|
||||||
} else {
|
} else {
|
||||||
// create a map of ids so we don't have to iterate
|
// create a map of ids so we don't have to iterate
|
||||||
// over the array more than once.
|
// over the array more than once.
|
||||||
const localElementMap = getElementMap(
|
const localElementMap = getElementMap(
|
||||||
globalSceneState.getAllElements(),
|
globalSceneState.getElementsIncludingDeleted(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Reconcile
|
// Reconcile
|
||||||
@ -982,12 +974,14 @@ export class App extends React.Component<any, AppState> {
|
|||||||
const data: SocketUpdateDataSource[typeof sceneType] = {
|
const data: SocketUpdateDataSource[typeof sceneType] = {
|
||||||
type: sceneType,
|
type: sceneType,
|
||||||
payload: {
|
payload: {
|
||||||
elements: getSyncableElements(globalSceneState.getAllElements()),
|
elements: getSyncableElements(
|
||||||
|
globalSceneState.getElementsIncludingDeleted(),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
this.lastBroadcastedOrReceivedSceneVersion = Math.max(
|
this.lastBroadcastedOrReceivedSceneVersion = Math.max(
|
||||||
this.lastBroadcastedOrReceivedSceneVersion,
|
this.lastBroadcastedOrReceivedSceneVersion,
|
||||||
getDrawingVersion(globalSceneState.getAllElements()),
|
getDrawingVersion(globalSceneState.getElementsIncludingDeleted()),
|
||||||
);
|
);
|
||||||
return this._broadcastSocketData(
|
return this._broadcastSocketData(
|
||||||
data as typeof data & { _brand: "socketUpdateData" },
|
data as typeof data & { _brand: "socketUpdateData" },
|
||||||
@ -1063,7 +1057,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
? ELEMENT_SHIFT_TRANSLATE_AMOUNT
|
? ELEMENT_SHIFT_TRANSLATE_AMOUNT
|
||||||
: ELEMENT_TRANSLATE_AMOUNT;
|
: ELEMENT_TRANSLATE_AMOUNT;
|
||||||
globalSceneState.replaceAllElements(
|
globalSceneState.replaceAllElements(
|
||||||
globalSceneState.getAllElements().map((el) => {
|
globalSceneState.getElementsIncludingDeleted().map((el) => {
|
||||||
if (this.state.selectedElementIds[el.id]) {
|
if (this.state.selectedElementIds[el.id]) {
|
||||||
const update: { x?: number; y?: number } = {};
|
const update: { x?: number; y?: number } = {};
|
||||||
if (event.key === KEYS.ARROW_LEFT) {
|
if (event.key === KEYS.ARROW_LEFT) {
|
||||||
@ -1083,7 +1077,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
} else if (event.key === KEYS.ENTER) {
|
} else if (event.key === KEYS.ENTER) {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = getSelectedElements(
|
||||||
globalSceneState.getAllElements(),
|
globalSceneState.getElements(),
|
||||||
this.state,
|
this.state,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1188,7 +1182,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
|
|
||||||
const deleteElement = () => {
|
const deleteElement = () => {
|
||||||
globalSceneState.replaceAllElements([
|
globalSceneState.replaceAllElements([
|
||||||
...globalSceneState.getAllElements().map((_element) => {
|
...globalSceneState.getElementsIncludingDeleted().map((_element) => {
|
||||||
if (_element.id === element.id) {
|
if (_element.id === element.id) {
|
||||||
return newElementWith(_element, { isDeleted: true });
|
return newElementWith(_element, { isDeleted: true });
|
||||||
}
|
}
|
||||||
@ -1199,7 +1193,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
|
|
||||||
const updateElement = (text: string) => {
|
const updateElement = (text: string) => {
|
||||||
globalSceneState.replaceAllElements([
|
globalSceneState.replaceAllElements([
|
||||||
...globalSceneState.getAllElements().map((_element) => {
|
...globalSceneState.getElementsIncludingDeleted().map((_element) => {
|
||||||
if (_element.id === element.id) {
|
if (_element.id === element.id) {
|
||||||
return newTextElement({
|
return newTextElement({
|
||||||
...(_element as ExcalidrawTextElement),
|
...(_element as ExcalidrawTextElement),
|
||||||
@ -1271,7 +1265,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
centerIfPossible?: boolean;
|
centerIfPossible?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const elementAtPosition = getElementAtPosition(
|
const elementAtPosition = getElementAtPosition(
|
||||||
globalSceneState.getAllElements(),
|
globalSceneState.getElements(),
|
||||||
this.state,
|
this.state,
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
@ -1326,7 +1320,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
globalSceneState.replaceAllElements([
|
globalSceneState.replaceAllElements([
|
||||||
...globalSceneState.getAllElements(),
|
...globalSceneState.getElementsIncludingDeleted(),
|
||||||
element,
|
element,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -1503,13 +1497,12 @@ export class App extends React.Component<any, AppState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedElements = getSelectedElements(
|
const elements = globalSceneState.getElements();
|
||||||
globalSceneState.getAllElements(),
|
|
||||||
this.state,
|
const selectedElements = getSelectedElements(elements, this.state);
|
||||||
);
|
|
||||||
if (selectedElements.length === 1 && !isOverScrollBar) {
|
if (selectedElements.length === 1 && !isOverScrollBar) {
|
||||||
const elementWithResizeHandler = getElementWithResizeHandler(
|
const elementWithResizeHandler = getElementWithResizeHandler(
|
||||||
globalSceneState.getAllElements(),
|
elements,
|
||||||
this.state,
|
this.state,
|
||||||
{ x, y },
|
{ x, y },
|
||||||
this.state.zoom,
|
this.state.zoom,
|
||||||
@ -1538,7 +1531,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const hitElement = getElementAtPosition(
|
const hitElement = getElementAtPosition(
|
||||||
globalSceneState.getAllElements(),
|
elements,
|
||||||
this.state,
|
this.state,
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
@ -1737,13 +1730,11 @@ export class App extends React.Component<any, AppState> {
|
|||||||
let hitElement: ExcalidrawElement | null = null;
|
let hitElement: ExcalidrawElement | null = null;
|
||||||
let hitElementWasAddedToSelection = false;
|
let hitElementWasAddedToSelection = false;
|
||||||
if (this.state.elementType === "selection") {
|
if (this.state.elementType === "selection") {
|
||||||
const selectedElements = getSelectedElements(
|
const elements = globalSceneState.getElements();
|
||||||
globalSceneState.getAllElements(),
|
const selectedElements = getSelectedElements(elements, this.state);
|
||||||
this.state,
|
|
||||||
);
|
|
||||||
if (selectedElements.length === 1) {
|
if (selectedElements.length === 1) {
|
||||||
const elementWithResizeHandler = getElementWithResizeHandler(
|
const elementWithResizeHandler = getElementWithResizeHandler(
|
||||||
globalSceneState.getAllElements(),
|
elements,
|
||||||
this.state,
|
this.state,
|
||||||
{ x, y },
|
{ x, y },
|
||||||
this.state.zoom,
|
this.state.zoom,
|
||||||
@ -1781,7 +1772,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
}
|
}
|
||||||
if (!isResizingElements) {
|
if (!isResizingElements) {
|
||||||
hitElement = getElementAtPosition(
|
hitElement = getElementAtPosition(
|
||||||
globalSceneState.getAllElements(),
|
elements,
|
||||||
this.state,
|
this.state,
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
@ -1809,7 +1800,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
globalSceneState.replaceAllElements(
|
globalSceneState.replaceAllElements(
|
||||||
globalSceneState.getAllElements(),
|
globalSceneState.getElementsIncludingDeleted(),
|
||||||
);
|
);
|
||||||
hitElementWasAddedToSelection = true;
|
hitElementWasAddedToSelection = true;
|
||||||
}
|
}
|
||||||
@ -1820,7 +1811,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
// put the duplicates where the selected elements used to be.
|
// put the duplicates where the selected elements used to be.
|
||||||
const nextElements = [];
|
const nextElements = [];
|
||||||
const elementsToAppend = [];
|
const elementsToAppend = [];
|
||||||
for (const element of globalSceneState.getAllElements()) {
|
for (const element of globalSceneState.getElementsIncludingDeleted()) {
|
||||||
if (
|
if (
|
||||||
this.state.selectedElementIds[element.id] ||
|
this.state.selectedElementIds[element.id] ||
|
||||||
(element.id === hitElement.id && hitElementWasAddedToSelection)
|
(element.id === hitElement.id && hitElementWasAddedToSelection)
|
||||||
@ -1930,7 +1921,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
points: [...element.points, [0, 0]],
|
points: [...element.points, [0, 0]],
|
||||||
});
|
});
|
||||||
globalSceneState.replaceAllElements([
|
globalSceneState.replaceAllElements([
|
||||||
...globalSceneState.getAllElements(),
|
...globalSceneState.getElementsIncludingDeleted(),
|
||||||
element,
|
element,
|
||||||
]);
|
]);
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -1958,7 +1949,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
globalSceneState.replaceAllElements([
|
globalSceneState.replaceAllElements([
|
||||||
...globalSceneState.getAllElements(),
|
...globalSceneState.getElementsIncludingDeleted(),
|
||||||
element,
|
element,
|
||||||
]);
|
]);
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -2047,7 +2038,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
// if elements should be deselected on pointerup
|
// if elements should be deselected on pointerup
|
||||||
draggingOccurred = true;
|
draggingOccurred = true;
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = getSelectedElements(
|
||||||
globalSceneState.getAllElements(),
|
globalSceneState.getElements(),
|
||||||
this.state,
|
this.state,
|
||||||
);
|
);
|
||||||
if (selectedElements.length > 0) {
|
if (selectedElements.length > 0) {
|
||||||
@ -2123,14 +2114,12 @@ export class App extends React.Component<any, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.elementType === "selection") {
|
if (this.state.elementType === "selection") {
|
||||||
if (
|
const elements = globalSceneState.getElements();
|
||||||
!event.shiftKey &&
|
if (!event.shiftKey && isSomeElementSelected(elements, this.state)) {
|
||||||
isSomeElementSelected(globalSceneState.getAllElements(), this.state)
|
|
||||||
) {
|
|
||||||
this.setState({ selectedElementIds: {} });
|
this.setState({ selectedElementIds: {} });
|
||||||
}
|
}
|
||||||
const elementsWithinSelection = getElementsWithinSelection(
|
const elementsWithinSelection = getElementsWithinSelection(
|
||||||
globalSceneState.getAllElements(),
|
elements,
|
||||||
draggingElement,
|
draggingElement,
|
||||||
);
|
);
|
||||||
this.setState((prevState) => ({
|
this.setState((prevState) => ({
|
||||||
@ -2223,7 +2212,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
) {
|
) {
|
||||||
// remove invisible element which was added in onPointerDown
|
// remove invisible element which was added in onPointerDown
|
||||||
globalSceneState.replaceAllElements(
|
globalSceneState.replaceAllElements(
|
||||||
globalSceneState.getAllElements().slice(0, -1),
|
globalSceneState.getElementsIncludingDeleted().slice(0, -1),
|
||||||
);
|
);
|
||||||
this.setState({
|
this.setState({
|
||||||
draggingElement: null,
|
draggingElement: null,
|
||||||
@ -2240,7 +2229,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
if (resizingElement && isInvisiblySmallElement(resizingElement)) {
|
if (resizingElement && isInvisiblySmallElement(resizingElement)) {
|
||||||
globalSceneState.replaceAllElements(
|
globalSceneState.replaceAllElements(
|
||||||
globalSceneState
|
globalSceneState
|
||||||
.getAllElements()
|
.getElementsIncludingDeleted()
|
||||||
.filter((el) => el.id !== resizingElement.id),
|
.filter((el) => el.id !== resizingElement.id),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -2285,7 +2274,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
elementType !== "selection" ||
|
elementType !== "selection" ||
|
||||||
isSomeElementSelected(globalSceneState.getAllElements(), this.state)
|
isSomeElementSelected(globalSceneState.getElements(), this.state)
|
||||||
) {
|
) {
|
||||||
history.resumeRecording();
|
history.resumeRecording();
|
||||||
}
|
}
|
||||||
@ -2366,8 +2355,9 @@ export class App extends React.Component<any, AppState> {
|
|||||||
window.devicePixelRatio,
|
window.devicePixelRatio,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const elements = globalSceneState.getElements();
|
||||||
const element = getElementAtPosition(
|
const element = getElementAtPosition(
|
||||||
globalSceneState.getAllElements(),
|
elements,
|
||||||
this.state,
|
this.state,
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
@ -2381,12 +2371,12 @@ export class App extends React.Component<any, AppState> {
|
|||||||
action: () => this.pasteFromClipboard(null),
|
action: () => this.pasteFromClipboard(null),
|
||||||
},
|
},
|
||||||
probablySupportsClipboardBlob &&
|
probablySupportsClipboardBlob &&
|
||||||
hasNonDeletedElements(globalSceneState.getAllElements()) && {
|
elements.length > 0 && {
|
||||||
label: t("labels.copyAsPng"),
|
label: t("labels.copyAsPng"),
|
||||||
action: this.copyToClipboardAsPng,
|
action: this.copyToClipboardAsPng,
|
||||||
},
|
},
|
||||||
probablySupportsClipboardWriteText &&
|
probablySupportsClipboardWriteText &&
|
||||||
hasNonDeletedElements(globalSceneState.getAllElements()) && {
|
elements.length > 0 && {
|
||||||
label: t("labels.copyAsSvg"),
|
label: t("labels.copyAsSvg"),
|
||||||
action: this.copyToClipboardAsSvg,
|
action: this.copyToClipboardAsSvg,
|
||||||
},
|
},
|
||||||
@ -2468,7 +2458,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
scale: number,
|
scale: number,
|
||||||
) {
|
) {
|
||||||
const elementClickedInside = getElementContainingPosition(
|
const elementClickedInside = getElementContainingPosition(
|
||||||
globalSceneState.getAllElements(),
|
globalSceneState.getElementsIncludingDeleted(),
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
);
|
);
|
||||||
@ -2522,7 +2512,10 @@ export class App extends React.Component<any, AppState> {
|
|||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
private saveDebounced = debounce(() => {
|
private saveDebounced = debounce(() => {
|
||||||
saveToLocalStorage(globalSceneState.getAllElements(), this.state);
|
saveToLocalStorage(
|
||||||
|
globalSceneState.getElementsIncludingDeleted(),
|
||||||
|
this.state,
|
||||||
|
);
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2548,7 +2541,7 @@ if (process.env.NODE_ENV === "test" || process.env.NODE_ENV === "development") {
|
|||||||
Object.defineProperties(window.h, {
|
Object.defineProperties(window.h, {
|
||||||
elements: {
|
elements: {
|
||||||
get() {
|
get() {
|
||||||
return globalSceneState.getAllElements();
|
return globalSceneState.getElementsIncludingDeleted();
|
||||||
},
|
},
|
||||||
set(elements: ExcalidrawElement[]) {
|
set(elements: ExcalidrawElement[]) {
|
||||||
return globalSceneState.replaceAllElements(elements);
|
return globalSceneState.replaceAllElements(elements);
|
||||||
|
@ -4,7 +4,7 @@ import React, { useState, useEffect, useRef } from "react";
|
|||||||
|
|
||||||
import { ToolButton } from "./ToolButton";
|
import { ToolButton } from "./ToolButton";
|
||||||
import { clipboard, exportFile, link } from "./icons";
|
import { clipboard, exportFile, link } from "./icons";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { exportToCanvas } from "../scene/export";
|
import { exportToCanvas } from "../scene/export";
|
||||||
import { ActionsManagerInterface } from "../actions/types";
|
import { ActionsManagerInterface } from "../actions/types";
|
||||||
@ -20,7 +20,7 @@ const scales = [1, 2, 3];
|
|||||||
const defaultScale = scales.includes(devicePixelRatio) ? devicePixelRatio : 1;
|
const defaultScale = scales.includes(devicePixelRatio) ? devicePixelRatio : 1;
|
||||||
|
|
||||||
export type ExportCB = (
|
export type ExportCB = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
scale?: number,
|
scale?: number,
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
@ -35,7 +35,7 @@ function ExportModal({
|
|||||||
onExportToBackend,
|
onExportToBackend,
|
||||||
}: {
|
}: {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
exportPadding?: number;
|
exportPadding?: number;
|
||||||
actionManager: ActionsManagerInterface;
|
actionManager: ActionsManagerInterface;
|
||||||
onExportToPng: ExportCB;
|
onExportToPng: ExportCB;
|
||||||
@ -166,7 +166,7 @@ export function ExportDialog({
|
|||||||
onExportToBackend,
|
onExportToBackend,
|
||||||
}: {
|
}: {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
exportPadding?: number;
|
exportPadding?: number;
|
||||||
actionManager: ActionsManagerInterface;
|
actionManager: ActionsManagerInterface;
|
||||||
onExportToPng: ExportCB;
|
onExportToPng: ExportCB;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
|
|
||||||
import "./HintViewer.scss";
|
import "./HintViewer.scss";
|
||||||
@ -9,7 +9,7 @@ import { isLinearElement } from "../element/typeChecks";
|
|||||||
|
|
||||||
interface Hint {
|
interface Hint {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const getHints = ({ appState, elements }: Hint) => {
|
const getHints = ({ appState, elements }: Hint) => {
|
||||||
|
@ -4,7 +4,7 @@ import { calculateScrollCenter } from "../scene";
|
|||||||
import { exportCanvas } from "../data";
|
import { exportCanvas } from "../data";
|
||||||
|
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
|
|
||||||
import { ActionManager } from "../actions/manager";
|
import { ActionManager } from "../actions/manager";
|
||||||
import { Island } from "./Island";
|
import { Island } from "./Island";
|
||||||
@ -31,8 +31,7 @@ interface LayerUIProps {
|
|||||||
appState: AppState;
|
appState: AppState;
|
||||||
canvas: HTMLCanvasElement | null;
|
canvas: HTMLCanvasElement | null;
|
||||||
setAppState: any;
|
setAppState: any;
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
setElements: (elements: readonly ExcalidrawElement[]) => void;
|
|
||||||
onRoomCreate: () => void;
|
onRoomCreate: () => void;
|
||||||
onRoomDestroy: () => void;
|
onRoomDestroy: () => void;
|
||||||
onLockToggle: () => void;
|
onLockToggle: () => void;
|
||||||
@ -45,7 +44,6 @@ export const LayerUI = React.memo(
|
|||||||
setAppState,
|
setAppState,
|
||||||
canvas,
|
canvas,
|
||||||
elements,
|
elements,
|
||||||
setElements,
|
|
||||||
onRoomCreate,
|
onRoomCreate,
|
||||||
onRoomDestroy,
|
onRoomDestroy,
|
||||||
onLockToggle,
|
onLockToggle,
|
||||||
@ -96,7 +94,6 @@ export const LayerUI = React.memo(
|
|||||||
<MobileMenu
|
<MobileMenu
|
||||||
appState={appState}
|
appState={appState}
|
||||||
elements={elements}
|
elements={elements}
|
||||||
setElements={setElements}
|
|
||||||
actionManager={actionManager}
|
actionManager={actionManager}
|
||||||
exportButton={renderExportDialog()}
|
exportButton={renderExportDialog()}
|
||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
@ -170,8 +167,6 @@ export const LayerUI = React.memo(
|
|||||||
<ShapesSwitcher
|
<ShapesSwitcher
|
||||||
elementType={appState.elementType}
|
elementType={appState.elementType}
|
||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
setElements={setElements}
|
|
||||||
elements={elements}
|
|
||||||
/>
|
/>
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
</Island>
|
</Island>
|
||||||
|
@ -5,7 +5,7 @@ import { t, setLanguage } from "../i18n";
|
|||||||
import Stack from "./Stack";
|
import Stack from "./Stack";
|
||||||
import { LanguageList } from "./LanguageList";
|
import { LanguageList } from "./LanguageList";
|
||||||
import { showSelectedShapeActions } from "../element";
|
import { showSelectedShapeActions } from "../element";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
import { FixedSideContainer } from "./FixedSideContainer";
|
import { FixedSideContainer } from "./FixedSideContainer";
|
||||||
import { Island } from "./Island";
|
import { Island } from "./Island";
|
||||||
import { HintViewer } from "./HintViewer";
|
import { HintViewer } from "./HintViewer";
|
||||||
@ -22,8 +22,7 @@ type MobileMenuProps = {
|
|||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
exportButton: React.ReactNode;
|
exportButton: React.ReactNode;
|
||||||
setAppState: any;
|
setAppState: any;
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
setElements: any;
|
|
||||||
onRoomCreate: () => void;
|
onRoomCreate: () => void;
|
||||||
onRoomDestroy: () => void;
|
onRoomDestroy: () => void;
|
||||||
onLockToggle: () => void;
|
onLockToggle: () => void;
|
||||||
@ -32,7 +31,6 @@ type MobileMenuProps = {
|
|||||||
export function MobileMenu({
|
export function MobileMenu({
|
||||||
appState,
|
appState,
|
||||||
elements,
|
elements,
|
||||||
setElements,
|
|
||||||
actionManager,
|
actionManager,
|
||||||
exportButton,
|
exportButton,
|
||||||
setAppState,
|
setAppState,
|
||||||
@ -54,8 +52,6 @@ export function MobileMenu({
|
|||||||
<ShapesSwitcher
|
<ShapesSwitcher
|
||||||
elementType={appState.elementType}
|
elementType={appState.elementType}
|
||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
setElements={setElements}
|
|
||||||
elements={elements}
|
|
||||||
/>
|
/>
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
</Island>
|
</Island>
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import { ExcalidrawElement } from "../element/types";
|
import {
|
||||||
|
ExcalidrawElement,
|
||||||
|
NonDeletedExcalidrawElement,
|
||||||
|
} from "../element/types";
|
||||||
|
|
||||||
import { getDefaultAppState } from "../appState";
|
import { getDefaultAppState } from "../appState";
|
||||||
|
|
||||||
@ -16,7 +19,6 @@ import { serializeAsJSON } from "./json";
|
|||||||
import { ExportType } from "../scene/types";
|
import { ExportType } from "../scene/types";
|
||||||
import { restore } from "./restore";
|
import { restore } from "./restore";
|
||||||
import { restoreFromLocalStorage } from "./localStorage";
|
import { restoreFromLocalStorage } from "./localStorage";
|
||||||
import { hasNonDeletedElements } from "../element";
|
|
||||||
|
|
||||||
export { loadFromBlob } from "./blob";
|
export { loadFromBlob } from "./blob";
|
||||||
export { saveAsJSON, loadFromJSON } from "./json";
|
export { saveAsJSON, loadFromJSON } from "./json";
|
||||||
@ -283,7 +285,7 @@ export async function importFromBackend(
|
|||||||
|
|
||||||
export async function exportCanvas(
|
export async function exportCanvas(
|
||||||
type: ExportType,
|
type: ExportType,
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
canvas: HTMLCanvasElement,
|
canvas: HTMLCanvasElement,
|
||||||
{
|
{
|
||||||
@ -300,7 +302,7 @@ export async function exportCanvas(
|
|||||||
scale?: number;
|
scale?: number;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
if (!hasNonDeletedElements(elements)) {
|
if (elements.length === 0) {
|
||||||
return window.alert(t("alerts.cannotExportEmptyCanvas"));
|
return window.alert(t("alerts.cannotExportEmptyCanvas"));
|
||||||
}
|
}
|
||||||
if (type === "svg" || type === "clipboard-svg") {
|
if (type === "svg" || type === "clipboard-svg") {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { distanceBetweenPointAndSegment } from "../math";
|
import { distanceBetweenPointAndSegment } from "../math";
|
||||||
|
|
||||||
import { ExcalidrawElement } from "./types";
|
import { NonDeletedExcalidrawElement } from "./types";
|
||||||
|
|
||||||
import { getDiamondPoints, getElementAbsoluteCoords } from "./bounds";
|
import { getDiamondPoints, getElementAbsoluteCoords } from "./bounds";
|
||||||
import { Point } from "../types";
|
import { Point } from "../types";
|
||||||
@ -11,7 +11,7 @@ import { isLinearElement } from "./typeChecks";
|
|||||||
import { rotate } from "../math";
|
import { rotate } from "../math";
|
||||||
|
|
||||||
function isElementDraggableFromInside(
|
function isElementDraggableFromInside(
|
||||||
element: ExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
): boolean {
|
): boolean {
|
||||||
return (
|
return (
|
||||||
@ -21,7 +21,7 @@ function isElementDraggableFromInside(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function hitTest(
|
export function hitTest(
|
||||||
element: ExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ExcalidrawElement } from "./types";
|
import { ExcalidrawElement, NonDeletedExcalidrawElement } from "./types";
|
||||||
import { isInvisiblySmallElement } from "./sizeHelpers";
|
import { isInvisiblySmallElement } from "./sizeHelpers";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -63,6 +63,9 @@ export function getDrawingVersion(elements: readonly ExcalidrawElement[]) {
|
|||||||
return elements.reduce((acc, el) => acc + el.version, 0);
|
return elements.reduce((acc, el) => acc + el.version, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasNonDeletedElements(elements: readonly ExcalidrawElement[]) {
|
export function getNonDeletedElements(elements: readonly ExcalidrawElement[]) {
|
||||||
return elements.some((element) => !element.isDeleted);
|
return (
|
||||||
|
elements.filter((element) => !element.isDeleted) as
|
||||||
|
readonly NonDeletedExcalidrawElement[]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import {
|
|||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
ExcalidrawGenericElement,
|
ExcalidrawGenericElement,
|
||||||
|
NonDeleted,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { measureText } from "../utils";
|
import { measureText } from "../utils";
|
||||||
import { randomInteger, randomId } from "../random";
|
import { randomInteger, randomId } from "../random";
|
||||||
@ -56,7 +57,7 @@ function _newElementBase<T extends ExcalidrawElement>(
|
|||||||
seed: rest.seed ?? randomInteger(),
|
seed: rest.seed ?? randomInteger(),
|
||||||
version: rest.version || 1,
|
version: rest.version || 1,
|
||||||
versionNonce: rest.versionNonce ?? 0,
|
versionNonce: rest.versionNonce ?? 0,
|
||||||
isDeleted: rest.isDeleted ?? false,
|
isDeleted: false as false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,7 +65,7 @@ export function newElement(
|
|||||||
opts: {
|
opts: {
|
||||||
type: ExcalidrawGenericElement["type"];
|
type: ExcalidrawGenericElement["type"];
|
||||||
} & ElementConstructorOpts,
|
} & ElementConstructorOpts,
|
||||||
): ExcalidrawGenericElement {
|
): NonDeleted<ExcalidrawGenericElement> {
|
||||||
return _newElementBase<ExcalidrawGenericElement>(opts.type, opts);
|
return _newElementBase<ExcalidrawGenericElement>(opts.type, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,13 +74,12 @@ export function newTextElement(
|
|||||||
text: string;
|
text: string;
|
||||||
font: string;
|
font: string;
|
||||||
} & ElementConstructorOpts,
|
} & ElementConstructorOpts,
|
||||||
): ExcalidrawTextElement {
|
): NonDeleted<ExcalidrawTextElement> {
|
||||||
const { text, font } = opts;
|
const { text, font } = opts;
|
||||||
const metrics = measureText(text, font);
|
const metrics = measureText(text, font);
|
||||||
const textElement = newElementWith(
|
const textElement = newElementWith(
|
||||||
{
|
{
|
||||||
..._newElementBase<ExcalidrawTextElement>("text", opts),
|
..._newElementBase<ExcalidrawTextElement>("text", opts),
|
||||||
isDeleted: false,
|
|
||||||
text: text,
|
text: text,
|
||||||
font: font,
|
font: font,
|
||||||
// Center the text
|
// Center the text
|
||||||
@ -100,7 +100,7 @@ export function newLinearElement(
|
|||||||
type: ExcalidrawLinearElement["type"];
|
type: ExcalidrawLinearElement["type"];
|
||||||
lastCommittedPoint?: ExcalidrawLinearElement["lastCommittedPoint"];
|
lastCommittedPoint?: ExcalidrawLinearElement["lastCommittedPoint"];
|
||||||
} & ElementConstructorOpts,
|
} & ElementConstructorOpts,
|
||||||
): ExcalidrawLinearElement {
|
): NonDeleted<ExcalidrawLinearElement> {
|
||||||
return {
|
return {
|
||||||
..._newElementBase<ExcalidrawLinearElement>(opts.type, opts),
|
..._newElementBase<ExcalidrawLinearElement>(opts.type, opts),
|
||||||
points: [],
|
points: [],
|
||||||
|
@ -3,7 +3,11 @@ import { SHIFT_LOCKING_ANGLE } from "../constants";
|
|||||||
import { getSelectedElements, globalSceneState } from "../scene";
|
import { getSelectedElements, globalSceneState } from "../scene";
|
||||||
import { rescalePoints } from "../points";
|
import { rescalePoints } from "../points";
|
||||||
import { rotate, adjustXYWithRotation } from "../math";
|
import { rotate, adjustXYWithRotation } from "../math";
|
||||||
import { ExcalidrawElement, ExcalidrawLinearElement } from "./types";
|
import {
|
||||||
|
ExcalidrawLinearElement,
|
||||||
|
NonDeletedExcalidrawElement,
|
||||||
|
NonDeleted,
|
||||||
|
} from "./types";
|
||||||
import { getElementAbsoluteCoords, getCommonBounds } from "./bounds";
|
import { getElementAbsoluteCoords, getCommonBounds } from "./bounds";
|
||||||
import { isLinearElement } from "./typeChecks";
|
import { isLinearElement } from "./typeChecks";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
@ -17,7 +21,7 @@ import {
|
|||||||
type ResizeTestType = ReturnType<typeof resizeTest>;
|
type ResizeTestType = ReturnType<typeof resizeTest>;
|
||||||
|
|
||||||
export type ResizeArrowFnType = (
|
export type ResizeArrowFnType = (
|
||||||
element: ExcalidrawLinearElement,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
pointIndex: number,
|
pointIndex: number,
|
||||||
deltaX: number,
|
deltaX: number,
|
||||||
deltaY: number,
|
deltaY: number,
|
||||||
@ -27,13 +31,13 @@ export type ResizeArrowFnType = (
|
|||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
const arrowResizeOrigin: ResizeArrowFnType = (
|
const arrowResizeOrigin: ResizeArrowFnType = (
|
||||||
element: ExcalidrawLinearElement,
|
element,
|
||||||
pointIndex: number,
|
pointIndex,
|
||||||
deltaX: number,
|
deltaX,
|
||||||
deltaY: number,
|
deltaY,
|
||||||
pointerX: number,
|
pointerX,
|
||||||
pointerY: number,
|
pointerY,
|
||||||
perfect: boolean,
|
perfect,
|
||||||
) => {
|
) => {
|
||||||
const [px, py] = element.points[pointIndex];
|
const [px, py] = element.points[pointIndex];
|
||||||
let x = element.x + deltaX;
|
let x = element.x + deltaX;
|
||||||
@ -63,13 +67,13 @@ const arrowResizeOrigin: ResizeArrowFnType = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const arrowResizeEnd: ResizeArrowFnType = (
|
const arrowResizeEnd: ResizeArrowFnType = (
|
||||||
element: ExcalidrawLinearElement,
|
element,
|
||||||
pointIndex: number,
|
pointIndex,
|
||||||
deltaX: number,
|
deltaX,
|
||||||
deltaY: number,
|
deltaY,
|
||||||
pointerX: number,
|
pointerX,
|
||||||
pointerY: number,
|
pointerY,
|
||||||
perfect: boolean,
|
perfect,
|
||||||
) => {
|
) => {
|
||||||
const [px, py] = element.points[pointIndex];
|
const [px, py] = element.points[pointIndex];
|
||||||
if (perfect) {
|
if (perfect) {
|
||||||
@ -110,7 +114,7 @@ export function resizeElements(
|
|||||||
isRotating: resizeHandle === "rotation",
|
isRotating: resizeHandle === "rotation",
|
||||||
});
|
});
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = getSelectedElements(
|
||||||
globalSceneState.getAllElements(),
|
globalSceneState.getElements(),
|
||||||
appState,
|
appState,
|
||||||
);
|
);
|
||||||
if (selectedElements.length === 1) {
|
if (selectedElements.length === 1) {
|
||||||
@ -451,7 +455,7 @@ export function resizeElements(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function canResizeMutlipleElements(
|
export function canResizeMutlipleElements(
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
) {
|
) {
|
||||||
return elements.every((element) =>
|
return elements.every((element) =>
|
||||||
["rectangle", "diamond", "ellipse"].includes(element.type),
|
["rectangle", "diamond", "ellipse"].includes(element.type),
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
import { ExcalidrawElement, PointerType } from "./types";
|
import {
|
||||||
|
ExcalidrawElement,
|
||||||
|
PointerType,
|
||||||
|
NonDeletedExcalidrawElement,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
OMIT_SIDES_FOR_MULTIPLE_ELEMENTS,
|
OMIT_SIDES_FOR_MULTIPLE_ELEMENTS,
|
||||||
@ -24,7 +28,7 @@ function isInHandlerRect(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function resizeTest(
|
export function resizeTest(
|
||||||
element: ExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
@ -66,7 +70,7 @@ export function resizeTest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getElementWithResizeHandler(
|
export function getElementWithResizeHandler(
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
{ x, y }: { x: number; y: number },
|
{ x, y }: { x: number; y: number },
|
||||||
zoom: number,
|
zoom: number,
|
||||||
@ -78,7 +82,7 @@ export function getElementWithResizeHandler(
|
|||||||
}
|
}
|
||||||
const resizeHandle = resizeTest(element, appState, x, y, zoom, pointerType);
|
const resizeHandle = resizeTest(element, appState, x, y, zoom, pointerType);
|
||||||
return resizeHandle ? { element, resizeHandle } : null;
|
return resizeHandle ? { element, resizeHandle } : null;
|
||||||
}, null as { element: ExcalidrawElement; resizeHandle: ReturnType<typeof resizeTest> } | null);
|
}, null as { element: NonDeletedExcalidrawElement; resizeHandle: ReturnType<typeof resizeTest> } | null);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getResizeHandlerFromCoords(
|
export function getResizeHandlerFromCoords(
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { ExcalidrawElement } from "./types";
|
import { NonDeletedExcalidrawElement } from "./types";
|
||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
|
|
||||||
export const showSelectedShapeActions = (
|
export const showSelectedShapeActions = (
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
) =>
|
) =>
|
||||||
Boolean(
|
Boolean(
|
||||||
appState.editingElement ||
|
appState.editingElement ||
|
||||||
|
@ -33,6 +33,12 @@ export type ExcalidrawElement =
|
|||||||
| ExcalidrawTextElement
|
| ExcalidrawTextElement
|
||||||
| ExcalidrawLinearElement;
|
| ExcalidrawLinearElement;
|
||||||
|
|
||||||
|
export type NonDeleted<TElement extends ExcalidrawElement> = TElement & {
|
||||||
|
isDeleted: false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NonDeletedExcalidrawElement = NonDeleted<ExcalidrawElement>;
|
||||||
|
|
||||||
export type ExcalidrawTextElement = _ExcalidrawElementBase &
|
export type ExcalidrawTextElement = _ExcalidrawElementBase &
|
||||||
Readonly<{
|
Readonly<{
|
||||||
type: "text";
|
type: "text";
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
|
import {
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawTextElement,
|
||||||
|
NonDeletedExcalidrawElement,
|
||||||
|
} from "../element/types";
|
||||||
import { isTextElement } from "../element/typeChecks";
|
import { isTextElement } from "../element/typeChecks";
|
||||||
import {
|
import {
|
||||||
getDiamondPoints,
|
getDiamondPoints,
|
||||||
@ -24,7 +28,7 @@ export interface ExcalidrawElementWithCanvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function generateElementCanvas(
|
function generateElementCanvas(
|
||||||
element: ExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
zoom: number,
|
zoom: number,
|
||||||
): ExcalidrawElementWithCanvas {
|
): ExcalidrawElementWithCanvas {
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
@ -72,7 +76,7 @@ function generateElementCanvas(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function drawElementOnCanvas(
|
function drawElementOnCanvas(
|
||||||
element: ExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
rc: RoughCanvas,
|
rc: RoughCanvas,
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
) {
|
) {
|
||||||
@ -133,7 +137,7 @@ export function invalidateShapeForElement(element: ExcalidrawElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function generateElement(
|
function generateElement(
|
||||||
element: ExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
generator: RoughGenerator,
|
generator: RoughGenerator,
|
||||||
sceneState?: SceneState,
|
sceneState?: SceneState,
|
||||||
) {
|
) {
|
||||||
@ -285,7 +289,7 @@ function drawElementFromCanvas(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function renderElement(
|
export function renderElement(
|
||||||
element: ExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
rc: RoughCanvas,
|
rc: RoughCanvas,
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
renderOptimizations: boolean,
|
renderOptimizations: boolean,
|
||||||
@ -342,7 +346,7 @@ export function renderElement(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function renderElementToSvg(
|
export function renderElementToSvg(
|
||||||
element: ExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
rsvg: RoughSVG,
|
rsvg: RoughSVG,
|
||||||
svgRoot: SVGElement,
|
svgRoot: SVGElement,
|
||||||
offsetX?: number,
|
offsetX?: number,
|
||||||
|
@ -2,7 +2,10 @@ import { RoughCanvas } from "roughjs/bin/canvas";
|
|||||||
import { RoughSVG } from "roughjs/bin/svg";
|
import { RoughSVG } from "roughjs/bin/svg";
|
||||||
|
|
||||||
import { FlooredNumber, AppState } from "../types";
|
import { FlooredNumber, AppState } from "../types";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import {
|
||||||
|
ExcalidrawElement,
|
||||||
|
NonDeletedExcalidrawElement,
|
||||||
|
} from "../element/types";
|
||||||
import {
|
import {
|
||||||
getElementAbsoluteCoords,
|
getElementAbsoluteCoords,
|
||||||
OMIT_SIDES_FOR_MULTIPLE_ELEMENTS,
|
OMIT_SIDES_FOR_MULTIPLE_ELEMENTS,
|
||||||
@ -74,9 +77,9 @@ function strokeCircle(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function renderScene(
|
export function renderScene(
|
||||||
allElements: readonly ExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
selectionElement: ExcalidrawElement | null,
|
selectionElement: NonDeletedExcalidrawElement | null,
|
||||||
scale: number,
|
scale: number,
|
||||||
rc: RoughCanvas,
|
rc: RoughCanvas,
|
||||||
canvas: HTMLCanvasElement,
|
canvas: HTMLCanvasElement,
|
||||||
@ -99,8 +102,6 @@ export function renderScene(
|
|||||||
return { atLeastOneVisibleElement: false };
|
return { atLeastOneVisibleElement: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const elements = allElements.filter((element) => !element.isDeleted);
|
|
||||||
|
|
||||||
const context = canvas.getContext("2d")!;
|
const context = canvas.getContext("2d")!;
|
||||||
context.scale(scale, scale);
|
context.scale(scale, scale);
|
||||||
|
|
||||||
@ -493,7 +494,7 @@ function isVisibleElement(
|
|||||||
|
|
||||||
// This should be only called for exporting purposes
|
// This should be only called for exporting purposes
|
||||||
export function renderSceneToSvg(
|
export function renderSceneToSvg(
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
rsvg: RoughSVG,
|
rsvg: RoughSVG,
|
||||||
svgRoot: SVGElement,
|
svgRoot: SVGElement,
|
||||||
{
|
{
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import { ExcalidrawElement } from "../element/types";
|
import {
|
||||||
|
ExcalidrawElement,
|
||||||
|
NonDeletedExcalidrawElement,
|
||||||
|
} from "../element/types";
|
||||||
|
|
||||||
import { getElementAbsoluteCoords, hitTest } from "../element";
|
import { getElementAbsoluteCoords, hitTest } from "../element";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
@ -16,7 +19,7 @@ export const hasStroke = (type: string) =>
|
|||||||
export const hasText = (type: string) => type === "text";
|
export const hasText = (type: string) => type === "text";
|
||||||
|
|
||||||
export function getElementAtPosition(
|
export function getElementAtPosition(
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import rough from "roughjs/bin/rough";
|
import rough from "roughjs/bin/rough";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
import { getCommonBounds } from "../element/bounds";
|
import { getCommonBounds } from "../element/bounds";
|
||||||
import { renderScene, renderSceneToSvg } from "../renderer/renderScene";
|
import { renderScene, renderSceneToSvg } from "../renderer/renderScene";
|
||||||
import { distance, SVG_NS } from "../utils";
|
import { distance, SVG_NS } from "../utils";
|
||||||
@ -9,7 +9,7 @@ import { AppState } from "../types";
|
|||||||
export const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
|
export const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
|
||||||
|
|
||||||
export function exportToCanvas(
|
export function exportToCanvas(
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
{
|
{
|
||||||
exportBackground,
|
exportBackground,
|
||||||
@ -66,7 +66,7 @@ export function exportToCanvas(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function exportToSvg(
|
export function exportToSvg(
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
{
|
{
|
||||||
exportBackground,
|
exportBackground,
|
||||||
exportPadding = 10,
|
exportPadding = 10,
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
import { ExcalidrawElement } from "../element/types";
|
import {
|
||||||
|
ExcalidrawElement,
|
||||||
|
NonDeletedExcalidrawElement,
|
||||||
|
} from "../element/types";
|
||||||
|
import { getNonDeletedElements } from "../element";
|
||||||
|
|
||||||
export interface SceneStateCallback {
|
export interface SceneStateCallback {
|
||||||
(): void;
|
(): void;
|
||||||
@ -8,15 +12,19 @@ export interface SceneStateCallbackRemover {
|
|||||||
(): void;
|
(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
class SceneState {
|
class GlobalScene {
|
||||||
private callbacks: Set<SceneStateCallback> = new Set();
|
private callbacks: Set<SceneStateCallback> = new Set();
|
||||||
|
|
||||||
constructor(private _elements: readonly ExcalidrawElement[] = []) {}
|
constructor(private _elements: readonly ExcalidrawElement[] = []) {}
|
||||||
|
|
||||||
getAllElements() {
|
getElementsIncludingDeleted() {
|
||||||
return this._elements;
|
return this._elements;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getElements(): readonly NonDeletedExcalidrawElement[] {
|
||||||
|
return getNonDeletedElements(this._elements);
|
||||||
|
}
|
||||||
|
|
||||||
replaceAllElements(nextElements: readonly ExcalidrawElement[]) {
|
replaceAllElements(nextElements: readonly ExcalidrawElement[]) {
|
||||||
this._elements = nextElements;
|
this._elements = nextElements;
|
||||||
this.informMutation();
|
this.informMutation();
|
||||||
@ -44,4 +52,4 @@ class SceneState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const globalSceneState = new SceneState();
|
export const globalSceneState = new GlobalScene();
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
import { ExcalidrawElement } from "../element/types";
|
import {
|
||||||
|
ExcalidrawElement,
|
||||||
|
NonDeletedExcalidrawElement,
|
||||||
|
} from "../element/types";
|
||||||
import { getElementAbsoluteCoords, getElementBounds } from "../element";
|
import { getElementAbsoluteCoords, getElementBounds } from "../element";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { newElementWith } from "../element/mutateElement";
|
import { newElementWith } from "../element/mutateElement";
|
||||||
|
|
||||||
export function getElementsWithinSelection(
|
export function getElementsWithinSelection(
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
selection: ExcalidrawElement,
|
selection: NonDeletedExcalidrawElement,
|
||||||
) {
|
) {
|
||||||
const [
|
const [
|
||||||
selectionX1,
|
selectionX1,
|
||||||
@ -47,7 +50,7 @@ export function deleteSelectedElements(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isSomeElementSelected(
|
export function isSomeElementSelected(
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
): boolean {
|
): boolean {
|
||||||
return elements.some((element) => appState.selectedElementIds[element.id]);
|
return elements.some((element) => appState.selectedElementIds[element.id]);
|
||||||
@ -58,7 +61,7 @@ export function isSomeElementSelected(
|
|||||||
* elements. If elements don't share the same value, returns `null`.
|
* elements. If elements don't share the same value, returns `null`.
|
||||||
*/
|
*/
|
||||||
export function getCommonAttributeOfSelectedElements<T>(
|
export function getCommonAttributeOfSelectedElements<T>(
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
getAttribute: (element: ExcalidrawElement) => T,
|
getAttribute: (element: ExcalidrawElement) => T,
|
||||||
): T | null {
|
): T | null {
|
||||||
@ -73,14 +76,14 @@ export function getCommonAttributeOfSelectedElements<T>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getSelectedElements(
|
export function getSelectedElements(
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
): readonly ExcalidrawElement[] {
|
) {
|
||||||
return elements.filter((element) => appState.selectedElementIds[element.id]);
|
return elements.filter((element) => appState.selectedElementIds[element.id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTargetElement(
|
export function getTargetElement(
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
) {
|
) {
|
||||||
return appState.editingElement
|
return appState.editingElement
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
actionBringToFront,
|
actionBringToFront,
|
||||||
actionSendToBack,
|
actionSendToBack,
|
||||||
} from "../actions";
|
} from "../actions";
|
||||||
|
import { ExcalidrawElement } from "../element/types";
|
||||||
|
|
||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
@ -27,7 +28,7 @@ function populateElements(
|
|||||||
const selectedElementIds: any = {};
|
const selectedElementIds: any = {};
|
||||||
|
|
||||||
h.elements = elements.map(({ id, isDeleted = false, isSelected = false }) => {
|
h.elements = elements.map(({ id, isDeleted = false, isSelected = false }) => {
|
||||||
const element: Mutable<ReturnType<typeof newElement>> = newElement({
|
const element: Mutable<ExcalidrawElement> = newElement({
|
||||||
type: "rectangle",
|
type: "rectangle",
|
||||||
x: 100,
|
x: 100,
|
||||||
y: 100,
|
y: 100,
|
||||||
|
13
src/types.ts
13
src/types.ts
@ -1,7 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
ExcalidrawElement,
|
|
||||||
PointerType,
|
PointerType,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
|
NonDeletedExcalidrawElement,
|
||||||
|
NonDeleted,
|
||||||
} from "./element/types";
|
} from "./element/types";
|
||||||
import { SHAPES } from "./shapes";
|
import { SHAPES } from "./shapes";
|
||||||
import { Point as RoughPoint } from "roughjs/bin/geometry";
|
import { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||||
@ -12,13 +13,13 @@ export type Point = Readonly<RoughPoint>;
|
|||||||
export type AppState = {
|
export type AppState = {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
errorMessage: string | null;
|
errorMessage: string | null;
|
||||||
draggingElement: ExcalidrawElement | null;
|
draggingElement: NonDeletedExcalidrawElement | null;
|
||||||
resizingElement: ExcalidrawElement | null;
|
resizingElement: NonDeletedExcalidrawElement | null;
|
||||||
multiElement: ExcalidrawLinearElement | null;
|
multiElement: NonDeleted<ExcalidrawLinearElement> | null;
|
||||||
selectionElement: ExcalidrawElement | null;
|
selectionElement: NonDeletedExcalidrawElement | null;
|
||||||
// element being edited, but not necessarily added to elements array yet
|
// element being edited, but not necessarily added to elements array yet
|
||||||
// (e.g. text element when typing into the input)
|
// (e.g. text element when typing into the input)
|
||||||
editingElement: ExcalidrawElement | null;
|
editingElement: NonDeletedExcalidrawElement | null;
|
||||||
elementType: typeof SHAPES[number]["value"];
|
elementType: typeof SHAPES[number]["value"];
|
||||||
elementLocked: boolean;
|
elementLocked: boolean;
|
||||||
exportBackground: boolean;
|
exportBackground: boolean;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user