Merge branch 'master' into frame-group-perf
This commit is contained in:
commit
d636abff79
@ -19,4 +19,4 @@ Frames should be ordered where frame children come first, followed by the frame
|
|||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
If not oredered correctly, the editor will still function, but the elements may not be rendered and clipped correctly. Further, the renderer relies on this ordering for performance optimizations.
|
If not ordered correctly, the editor will still function, but the elements may not be rendered and clipped correctly. Further, the renderer relies on this ordering for performance optimizations.
|
||||||
|
@ -691,7 +691,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Excalidraw
|
<Excalidraw
|
||||||
ref={excalidrawRefCallback}
|
excalidrawAPI={excalidrawRefCallback}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
initialData={initialStatePromiseRef.current.promise}
|
initialData={initialStatePromiseRef.current.promise}
|
||||||
isCollaborating={isCollaborating}
|
isCollaborating={isCollaborating}
|
||||||
|
@ -20,9 +20,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@braintree/sanitize-url": "6.0.2",
|
"@braintree/sanitize-url": "6.0.2",
|
||||||
"@excalidraw/mermaid-to-excalidraw": "0.1.2",
|
|
||||||
"@excalidraw/laser-pointer": "1.2.0",
|
"@excalidraw/laser-pointer": "1.2.0",
|
||||||
"@excalidraw/random-username": "1.0.0",
|
"@excalidraw/mermaid-to-excalidraw": "0.1.2",
|
||||||
|
"@excalidraw/random-username": "1.1.0",
|
||||||
"@radix-ui/react-popover": "1.0.3",
|
"@radix-ui/react-popover": "1.0.3",
|
||||||
"@radix-ui/react-tabs": "1.0.2",
|
"@radix-ui/react-tabs": "1.0.2",
|
||||||
"@sentry/browser": "6.2.5",
|
"@sentry/browser": "6.2.5",
|
||||||
|
@ -438,5 +438,6 @@ export const actionToggleHandTool = register({
|
|||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
keyTest: (event) => event.key === KEYS.H,
|
keyTest: (event) =>
|
||||||
|
!event.altKey && !event[KEYS.CTRL_OR_CMD] && event.key === KEYS.H,
|
||||||
});
|
});
|
||||||
|
@ -17,15 +17,12 @@ import {
|
|||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import { randomId } from "../random";
|
import { randomId } from "../random";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import {
|
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
|
||||||
ExcalidrawElement,
|
|
||||||
ExcalidrawFrameElement,
|
|
||||||
ExcalidrawTextElement,
|
|
||||||
} from "../element/types";
|
|
||||||
import { AppClassProperties, AppState } from "../types";
|
import { AppClassProperties, AppState } from "../types";
|
||||||
import { isBoundToContainer } from "../element/typeChecks";
|
import { isBoundToContainer } from "../element/typeChecks";
|
||||||
import {
|
import {
|
||||||
getElementsInResizingFrame,
|
getElementsInResizingFrame,
|
||||||
|
getFrameElements,
|
||||||
groupByFrames,
|
groupByFrames,
|
||||||
removeElementsFromFrame,
|
removeElementsFromFrame,
|
||||||
replaceAllElementsInFrame,
|
replaceAllElementsInFrame,
|
||||||
@ -190,19 +187,6 @@ export const actionUngroup = register({
|
|||||||
|
|
||||||
let nextElements = [...elements];
|
let nextElements = [...elements];
|
||||||
|
|
||||||
const frameIds: {
|
|
||||||
[id: string]: true;
|
|
||||||
} = {};
|
|
||||||
const selectedElements = app.scene.getSelectedElements(appState);
|
|
||||||
for (const element of selectedElements) {
|
|
||||||
if (element.frameId) {
|
|
||||||
frameIds[element.frameId] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const frames = Object.keys(frameIds)
|
|
||||||
.map((eid) => app.scene.getElement(eid))
|
|
||||||
.filter((element) => element != null) as ExcalidrawFrameElement[];
|
|
||||||
|
|
||||||
const boundTextElementIds: ExcalidrawTextElement["id"][] = [];
|
const boundTextElementIds: ExcalidrawTextElement["id"][] = [];
|
||||||
nextElements = nextElements.map((element) => {
|
nextElements = nextElements.map((element) => {
|
||||||
if (isBoundToContainer(element)) {
|
if (isBoundToContainer(element)) {
|
||||||
@ -227,7 +211,19 @@ export const actionUngroup = register({
|
|||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
frames.forEach((frame) => {
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
|
|
||||||
|
const selectedElementFrameIds = new Set(
|
||||||
|
selectedElements
|
||||||
|
.filter((element) => element.frameId)
|
||||||
|
.map((element) => element.frameId!),
|
||||||
|
);
|
||||||
|
|
||||||
|
const targetFrames = getFrameElements(elements).filter((frame) =>
|
||||||
|
selectedElementFrameIds.has(frame.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
targetFrames.forEach((frame) => {
|
||||||
if (frame) {
|
if (frame) {
|
||||||
nextElements = replaceAllElementsInFrame(
|
nextElements = replaceAllElementsInFrame(
|
||||||
nextElements,
|
nextElements,
|
||||||
|
@ -3,7 +3,6 @@ import { ToolButton } from "../components/ToolButton";
|
|||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { showSelectedShapeActions, getNonDeletedElements } from "../element";
|
import { showSelectedShapeActions, getNonDeletedElements } from "../element";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { allowFullScreen, exitFullScreen, isFullScreen } from "../utils";
|
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
|
|
||||||
export const actionToggleCanvasMenu = register({
|
export const actionToggleCanvasMenu = register({
|
||||||
@ -52,23 +51,6 @@ export const actionToggleEditMenu = register({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionFullScreen = register({
|
|
||||||
name: "toggleFullScreen",
|
|
||||||
viewMode: true,
|
|
||||||
trackEvent: { category: "canvas", predicate: (appState) => !isFullScreen() },
|
|
||||||
perform: () => {
|
|
||||||
if (!isFullScreen()) {
|
|
||||||
allowFullScreen();
|
|
||||||
}
|
|
||||||
if (isFullScreen()) {
|
|
||||||
exitFullScreen();
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
commitToHistory: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const actionShortcuts = register({
|
export const actionShortcuts = register({
|
||||||
name: "toggleShortcuts",
|
name: "toggleShortcuts",
|
||||||
viewMode: true,
|
viewMode: true,
|
||||||
|
@ -44,7 +44,6 @@ export { actionCopyStyles, actionPasteStyles } from "./actionStyles";
|
|||||||
export {
|
export {
|
||||||
actionToggleCanvasMenu,
|
actionToggleCanvasMenu,
|
||||||
actionToggleEditMenu,
|
actionToggleEditMenu,
|
||||||
actionFullScreen,
|
|
||||||
actionShortcuts,
|
actionShortcuts,
|
||||||
} from "./actionMenu";
|
} from "./actionMenu";
|
||||||
|
|
||||||
|
@ -240,7 +240,6 @@ import {
|
|||||||
isInputLike,
|
isInputLike,
|
||||||
isToolIcon,
|
isToolIcon,
|
||||||
isWritableElement,
|
isWritableElement,
|
||||||
resolvablePromise,
|
|
||||||
sceneCoordsToViewportCoords,
|
sceneCoordsToViewportCoords,
|
||||||
tupleToCoors,
|
tupleToCoors,
|
||||||
viewportCoordsToSceneCoords,
|
viewportCoordsToSceneCoords,
|
||||||
@ -540,7 +539,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
super(props);
|
super(props);
|
||||||
const defaultAppState = getDefaultAppState();
|
const defaultAppState = getDefaultAppState();
|
||||||
const {
|
const {
|
||||||
excalidrawRef,
|
excalidrawAPI,
|
||||||
viewModeEnabled = false,
|
viewModeEnabled = false,
|
||||||
zenModeEnabled = false,
|
zenModeEnabled = false,
|
||||||
gridModeEnabled = false,
|
gridModeEnabled = false,
|
||||||
@ -571,14 +570,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.rc = rough.canvas(this.canvas);
|
this.rc = rough.canvas(this.canvas);
|
||||||
this.renderer = new Renderer(this.scene);
|
this.renderer = new Renderer(this.scene);
|
||||||
|
|
||||||
if (excalidrawRef) {
|
if (excalidrawAPI) {
|
||||||
const readyPromise =
|
|
||||||
("current" in excalidrawRef && excalidrawRef.current?.readyPromise) ||
|
|
||||||
resolvablePromise<ExcalidrawImperativeAPI>();
|
|
||||||
|
|
||||||
const api: ExcalidrawImperativeAPI = {
|
const api: ExcalidrawImperativeAPI = {
|
||||||
ready: true,
|
|
||||||
readyPromise,
|
|
||||||
updateScene: this.updateScene,
|
updateScene: this.updateScene,
|
||||||
updateLibrary: this.library.updateLibrary,
|
updateLibrary: this.library.updateLibrary,
|
||||||
addFiles: this.addFiles,
|
addFiles: this.addFiles,
|
||||||
@ -603,12 +596,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
onPointerDown: (cb) => this.onPointerDownEmitter.on(cb),
|
onPointerDown: (cb) => this.onPointerDownEmitter.on(cb),
|
||||||
onPointerUp: (cb) => this.onPointerUpEmitter.on(cb),
|
onPointerUp: (cb) => this.onPointerUpEmitter.on(cb),
|
||||||
} as const;
|
} as const;
|
||||||
if (typeof excalidrawRef === "function") {
|
if (typeof excalidrawAPI === "function") {
|
||||||
excalidrawRef(api);
|
excalidrawAPI(api);
|
||||||
} else {
|
} else {
|
||||||
excalidrawRef.current = api;
|
console.error("excalidrawAPI should be a function!");
|
||||||
}
|
}
|
||||||
readyPromise.resolve(api);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.excalidrawContainerValue = {
|
this.excalidrawContainerValue = {
|
||||||
@ -1181,6 +1173,19 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
pendingImageElementId: this.state.pendingImageElementId,
|
pendingImageElementId: this.state.pendingImageElementId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const shouldBlockPointerEvents =
|
||||||
|
!(
|
||||||
|
this.state.editingElement && isLinearElement(this.state.editingElement)
|
||||||
|
) &&
|
||||||
|
(this.state.selectionElement ||
|
||||||
|
this.state.draggingElement ||
|
||||||
|
this.state.resizingElement ||
|
||||||
|
(this.state.activeTool.type === "laser" &&
|
||||||
|
// technically we can just test on this once we make it more safe
|
||||||
|
this.state.cursorButton === "down") ||
|
||||||
|
(this.state.editingElement &&
|
||||||
|
!isTextElement(this.state.editingElement)));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx("excalidraw excalidraw-container", {
|
className={clsx("excalidraw excalidraw-container", {
|
||||||
@ -1188,15 +1193,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
"excalidraw--mobile": this.device.editor.isMobile,
|
"excalidraw--mobile": this.device.editor.isMobile,
|
||||||
})}
|
})}
|
||||||
style={{
|
style={{
|
||||||
["--ui-pointerEvents" as any]:
|
["--ui-pointerEvents" as any]: shouldBlockPointerEvents
|
||||||
this.state.selectionElement ||
|
|
||||||
this.state.draggingElement ||
|
|
||||||
this.state.resizingElement ||
|
|
||||||
(this.state.activeTool.type === "laser" &&
|
|
||||||
// technically we can just test on this once we make it more safe
|
|
||||||
this.state.cursorButton === "down") ||
|
|
||||||
(this.state.editingElement &&
|
|
||||||
!isTextElement(this.state.editingElement))
|
|
||||||
? POINTER_EVENTS.disabled
|
? POINTER_EVENTS.disabled
|
||||||
: POINTER_EVENTS.enabled,
|
: POINTER_EVENTS.enabled,
|
||||||
}}
|
}}
|
||||||
@ -2719,7 +2716,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
togglePenMode = (force?: boolean) => {
|
togglePenMode = (force: boolean | null) => {
|
||||||
this.setState((prevState) => {
|
this.setState((prevState) => {
|
||||||
return {
|
return {
|
||||||
penMode: force ?? !prevState.penMode,
|
penMode: force ?? !prevState.penMode,
|
||||||
|
@ -254,7 +254,6 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
label={t("helpDialog.movePageLeftRight")}
|
label={t("helpDialog.movePageLeftRight")}
|
||||||
shortcuts={["Shift+PgUp/PgDn"]}
|
shortcuts={["Shift+PgUp/PgDn"]}
|
||||||
/>
|
/>
|
||||||
<Shortcut label={t("buttons.fullScreen")} shortcuts={["F"]} />
|
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("buttons.zenMode")}
|
label={t("buttons.zenMode")}
|
||||||
shortcuts={[getShortcutKey("Alt+Z")]}
|
shortcuts={[getShortcutKey("Alt+Z")]}
|
||||||
|
@ -66,7 +66,7 @@ interface LayerUIProps {
|
|||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
onLockToggle: () => void;
|
onLockToggle: () => void;
|
||||||
onHandToolToggle: () => void;
|
onHandToolToggle: () => void;
|
||||||
onPenModeToggle: () => void;
|
onPenModeToggle: AppClassProperties["togglePenMode"];
|
||||||
showExitZenModeBtn: boolean;
|
showExitZenModeBtn: boolean;
|
||||||
langCode: Language["code"];
|
langCode: Language["code"];
|
||||||
renderTopRightUI?: ExcalidrawProps["renderTopRightUI"];
|
renderTopRightUI?: ExcalidrawProps["renderTopRightUI"];
|
||||||
@ -258,7 +258,7 @@ const LayerUI = ({
|
|||||||
<PenModeButton
|
<PenModeButton
|
||||||
zenModeEnabled={appState.zenModeEnabled}
|
zenModeEnabled={appState.zenModeEnabled}
|
||||||
checked={appState.penMode}
|
checked={appState.penMode}
|
||||||
onChange={onPenModeToggle}
|
onChange={() => onPenModeToggle(null)}
|
||||||
title={t("toolBar.penMode")}
|
title={t("toolBar.penMode")}
|
||||||
penDetected={appState.penDetected}
|
penDetected={appState.penDetected}
|
||||||
/>
|
/>
|
||||||
|
@ -35,7 +35,7 @@ type MobileMenuProps = {
|
|||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
onLockToggle: () => void;
|
onLockToggle: () => void;
|
||||||
onHandToolToggle: () => void;
|
onHandToolToggle: () => void;
|
||||||
onPenModeToggle: () => void;
|
onPenModeToggle: AppClassProperties["togglePenMode"];
|
||||||
|
|
||||||
renderTopRightUI?: (
|
renderTopRightUI?: (
|
||||||
isMobile: boolean,
|
isMobile: boolean,
|
||||||
@ -94,7 +94,7 @@ export const MobileMenu = ({
|
|||||||
)}
|
)}
|
||||||
<PenModeButton
|
<PenModeButton
|
||||||
checked={appState.penMode}
|
checked={appState.penMode}
|
||||||
onChange={onPenModeToggle}
|
onChange={() => onPenModeToggle(null)}
|
||||||
title={t("toolBar.penMode")}
|
title={t("toolBar.penMode")}
|
||||||
isMobile
|
isMobile
|
||||||
penDetected={appState.penDetected}
|
penDetected={appState.penDetected}
|
||||||
|
@ -64,6 +64,7 @@ const ALLOWED_DOMAINS = new Set([
|
|||||||
"stackblitz.com",
|
"stackblitz.com",
|
||||||
"val.town",
|
"val.town",
|
||||||
"giphy.com",
|
"giphy.com",
|
||||||
|
"dddice.com",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const createSrcDoc = (body: string) => {
|
const createSrcDoc = (body: string) => {
|
||||||
|
@ -15,6 +15,14 @@ Please add the latest change on the top under the correct section.
|
|||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
|
- Support `excalidrawAPI` prop for accessing the Excalidraw API [#7251](https://github.com/excalidraw/excalidraw/pull/7251).
|
||||||
|
|
||||||
|
#### BREAKING CHANGE
|
||||||
|
|
||||||
|
- The `Ref` support has been removed in v0.17.0 so if you are using refs, please update the integration to use the [`excalidrawAPI`](http://localhost:3003/docs/@excalidraw/excalidraw/api/props/excalidraw-api)
|
||||||
|
|
||||||
|
- Additionally `ready` and `readyPromise` from the API have been discontinued. These APIs were found to be superfluous, and as part of the effort to streamline the APIs and maintain simplicity, they were removed in version v0.17.0.
|
||||||
|
|
||||||
- Export [`getCommonBounds`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils#getcommonbounds) helper from the package [#7247](https://github.com/excalidraw/excalidraw/pull/7247).
|
- Export [`getCommonBounds`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils#getcommonbounds) helper from the package [#7247](https://github.com/excalidraw/excalidraw/pull/7247).
|
||||||
|
|
||||||
- Support frames via programmatic API [#7205](https://github.com/excalidraw/excalidraw/pull/7205).
|
- Support frames via programmatic API [#7205](https://github.com/excalidraw/excalidraw/pull/7205).
|
||||||
|
@ -665,7 +665,9 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="excalidraw-wrapper">
|
<div className="excalidraw-wrapper">
|
||||||
<Excalidraw
|
<Excalidraw
|
||||||
ref={(api: ExcalidrawImperativeAPI) => setExcalidrawAPI(api)}
|
excalidrawAPI={(api: ExcalidrawImperativeAPI) =>
|
||||||
|
setExcalidrawAPI(api)
|
||||||
|
}
|
||||||
initialData={initialStatePromiseRef.current.promise}
|
initialData={initialStatePromiseRef.current.promise}
|
||||||
onChange={(elements, state) => {
|
onChange={(elements, state) => {
|
||||||
console.info("Elements :", elements, "State : ", state);
|
console.info("Elements :", elements, "State : ", state);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, forwardRef } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { InitializeApp } from "../../components/InitializeApp";
|
import { InitializeApp } from "../../components/InitializeApp";
|
||||||
import App from "../../components/App";
|
import App from "../../components/App";
|
||||||
import { isShallowEqual } from "../../utils";
|
import { isShallowEqual } from "../../utils";
|
||||||
@ -6,7 +6,7 @@ import { isShallowEqual } from "../../utils";
|
|||||||
import "../../css/app.scss";
|
import "../../css/app.scss";
|
||||||
import "../../css/styles.scss";
|
import "../../css/styles.scss";
|
||||||
|
|
||||||
import { AppProps, ExcalidrawAPIRefValue, ExcalidrawProps } from "../../types";
|
import { AppProps, ExcalidrawProps } from "../../types";
|
||||||
import { defaultLang } from "../../i18n";
|
import { defaultLang } from "../../i18n";
|
||||||
import { DEFAULT_UI_OPTIONS } from "../../constants";
|
import { DEFAULT_UI_OPTIONS } from "../../constants";
|
||||||
import { Provider } from "jotai";
|
import { Provider } from "jotai";
|
||||||
@ -20,7 +20,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
|||||||
const {
|
const {
|
||||||
onChange,
|
onChange,
|
||||||
initialData,
|
initialData,
|
||||||
excalidrawRef,
|
excalidrawAPI,
|
||||||
isCollaborating = false,
|
isCollaborating = false,
|
||||||
onPointerUpdate,
|
onPointerUpdate,
|
||||||
renderTopRightUI,
|
renderTopRightUI,
|
||||||
@ -95,7 +95,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
|||||||
<App
|
<App
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
initialData={initialData}
|
initialData={initialData}
|
||||||
excalidrawRef={excalidrawRef}
|
excalidrawAPI={excalidrawAPI}
|
||||||
isCollaborating={isCollaborating}
|
isCollaborating={isCollaborating}
|
||||||
onPointerUpdate={onPointerUpdate}
|
onPointerUpdate={onPointerUpdate}
|
||||||
renderTopRightUI={renderTopRightUI}
|
renderTopRightUI={renderTopRightUI}
|
||||||
@ -127,12 +127,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
type PublicExcalidrawProps = Omit<ExcalidrawProps, "forwardedRef">;
|
const areEqual = (prevProps: ExcalidrawProps, nextProps: ExcalidrawProps) => {
|
||||||
|
|
||||||
const areEqual = (
|
|
||||||
prevProps: PublicExcalidrawProps,
|
|
||||||
nextProps: PublicExcalidrawProps,
|
|
||||||
) => {
|
|
||||||
// short-circuit early
|
// short-circuit early
|
||||||
if (prevProps.children !== nextProps.children) {
|
if (prevProps.children !== nextProps.children) {
|
||||||
return false;
|
return false;
|
||||||
@ -189,12 +184,7 @@ const areEqual = (
|
|||||||
return isUIOptionsSame && isShallowEqual(prev, next);
|
return isUIOptionsSame && isShallowEqual(prev, next);
|
||||||
};
|
};
|
||||||
|
|
||||||
const forwardedRefComp = forwardRef<
|
export const Excalidraw = React.memo(ExcalidrawBase, areEqual);
|
||||||
ExcalidrawAPIRefValue,
|
|
||||||
PublicExcalidrawProps
|
|
||||||
>((props, ref) => <ExcalidrawBase {...props} excalidrawRef={ref} />);
|
|
||||||
|
|
||||||
export const Excalidraw = React.memo(forwardedRefComp, areEqual);
|
|
||||||
Excalidraw.displayName = "Excalidraw";
|
Excalidraw.displayName = "Excalidraw";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -14,18 +14,34 @@ import { generateFreeDrawShape } from "../renderer/renderElement";
|
|||||||
import { isTransparent, assertNever } from "../utils";
|
import { isTransparent, assertNever } from "../utils";
|
||||||
import { simplify } from "points-on-curve";
|
import { simplify } from "points-on-curve";
|
||||||
import { ROUGHNESS } from "../constants";
|
import { ROUGHNESS } from "../constants";
|
||||||
|
import { isLinearElement } from "../element/typeChecks";
|
||||||
|
import { canChangeRoundness } from "./comparisons";
|
||||||
|
|
||||||
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
|
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
|
||||||
|
|
||||||
const getDashArrayDotted = (strokeWidth: number) => [1.5, 6 + strokeWidth];
|
const getDashArrayDotted = (strokeWidth: number) => [1.5, 6 + strokeWidth];
|
||||||
|
|
||||||
function adjustRoughness(size: number, roughness: number): number {
|
function adjustRoughness(element: ExcalidrawElement): number {
|
||||||
if (size >= 50) {
|
const roughness = element.roughness;
|
||||||
|
|
||||||
|
const maxSize = Math.max(element.width, element.height);
|
||||||
|
const minSize = Math.min(element.width, element.height);
|
||||||
|
|
||||||
|
// don't reduce roughness if
|
||||||
|
if (
|
||||||
|
// both sides relatively big
|
||||||
|
(minSize >= 20 && maxSize >= 50) ||
|
||||||
|
// is round & both sides above 15px
|
||||||
|
(minSize >= 15 &&
|
||||||
|
!!element.roundness &&
|
||||||
|
canChangeRoundness(element.type)) ||
|
||||||
|
// relatively long linear element
|
||||||
|
(isLinearElement(element) && maxSize >= 50)
|
||||||
|
) {
|
||||||
return roughness;
|
return roughness;
|
||||||
}
|
}
|
||||||
const factor = 2 + (50 - size) / 10;
|
|
||||||
|
|
||||||
return roughness / factor;
|
return Math.min(roughness / (maxSize < 10 ? 3 : 2), 2.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const generateRoughOptions = (
|
export const generateRoughOptions = (
|
||||||
@ -54,10 +70,7 @@ export const generateRoughOptions = (
|
|||||||
// calculate them (and we don't want the fills to be modified)
|
// calculate them (and we don't want the fills to be modified)
|
||||||
fillWeight: element.strokeWidth / 2,
|
fillWeight: element.strokeWidth / 2,
|
||||||
hachureGap: element.strokeWidth * 4,
|
hachureGap: element.strokeWidth * 4,
|
||||||
roughness: adjustRoughness(
|
roughness: adjustRoughness(element),
|
||||||
Math.min(element.width, element.height),
|
|
||||||
element.roughness,
|
|
||||||
),
|
|
||||||
stroke: element.strokeColor,
|
stroke: element.strokeColor,
|
||||||
preserveVertices:
|
preserveVertices:
|
||||||
continuousPath || element.roughness < ROUGHNESS.cartoonist,
|
continuousPath || element.roughness < ROUGHNESS.cartoonist,
|
||||||
|
@ -11,10 +11,11 @@ import {
|
|||||||
getElementAbsoluteCoords,
|
getElementAbsoluteCoords,
|
||||||
} from "../element/bounds";
|
} from "../element/bounds";
|
||||||
import { renderSceneToSvg, renderStaticScene } from "../renderer/renderScene";
|
import { renderSceneToSvg, renderStaticScene } from "../renderer/renderScene";
|
||||||
import { distance, getFontString } from "../utils";
|
import { cloneJSON, distance, getFontString } from "../utils";
|
||||||
import { AppState, BinaryFiles } from "../types";
|
import { AppState, BinaryFiles } from "../types";
|
||||||
import {
|
import {
|
||||||
DEFAULT_EXPORT_PADDING,
|
DEFAULT_EXPORT_PADDING,
|
||||||
|
FONT_FAMILY,
|
||||||
FRAME_STYLE,
|
FRAME_STYLE,
|
||||||
SVG_NS,
|
SVG_NS,
|
||||||
THEME_FILTER,
|
THEME_FILTER,
|
||||||
@ -51,8 +52,9 @@ const __createSceneForElementsHack__ = (
|
|||||||
// we can't duplicate elements to regenerate ids because we need the
|
// we can't duplicate elements to regenerate ids because we need the
|
||||||
// orig ids when embedding. So we do another hack of not mapping element
|
// orig ids when embedding. So we do another hack of not mapping element
|
||||||
// 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.
|
||||||
scene.replaceAllElements(elements, false);
|
// We still need to clone the objects themselves to regen references.
|
||||||
|
scene.replaceAllElements(cloneJSON(elements), false);
|
||||||
return scene;
|
return scene;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -105,7 +107,7 @@ const addFrameLabelsAsTextElements = (
|
|||||||
let textElement: Mutable<ExcalidrawTextElement> = newTextElement({
|
let textElement: Mutable<ExcalidrawTextElement> = newTextElement({
|
||||||
x: element.x,
|
x: element.x,
|
||||||
y: element.y - FRAME_STYLE.nameOffsetY,
|
y: element.y - FRAME_STYLE.nameOffsetY,
|
||||||
fontFamily: 4,
|
fontFamily: FONT_FAMILY.Assistant,
|
||||||
fontSize: FRAME_STYLE.nameFontSize,
|
fontSize: FRAME_STYLE.nameFontSize,
|
||||||
lineHeight:
|
lineHeight:
|
||||||
FRAME_STYLE.nameLineHeight as ExcalidrawTextElement["lineHeight"],
|
FRAME_STYLE.nameLineHeight as ExcalidrawTextElement["lineHeight"],
|
||||||
@ -139,6 +141,36 @@ const getFrameRenderingConfig = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const prepareElementsForRender = ({
|
||||||
|
elements,
|
||||||
|
exportingFrame,
|
||||||
|
frameRendering,
|
||||||
|
exportWithDarkMode,
|
||||||
|
}: {
|
||||||
|
elements: readonly ExcalidrawElement[];
|
||||||
|
exportingFrame: ExcalidrawFrameElement | null | undefined;
|
||||||
|
frameRendering: AppState["frameRendering"];
|
||||||
|
exportWithDarkMode: AppState["exportWithDarkMode"];
|
||||||
|
}) => {
|
||||||
|
let nextElements: readonly ExcalidrawElement[];
|
||||||
|
|
||||||
|
if (exportingFrame) {
|
||||||
|
nextElements = elementsOverlappingBBox({
|
||||||
|
elements,
|
||||||
|
bounds: exportingFrame,
|
||||||
|
type: "overlap",
|
||||||
|
});
|
||||||
|
} else if (frameRendering.enabled && frameRendering.name) {
|
||||||
|
nextElements = addFrameLabelsAsTextElements(elements, {
|
||||||
|
exportWithDarkMode,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
nextElements = elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextElements;
|
||||||
|
};
|
||||||
|
|
||||||
export const exportToCanvas = async (
|
export const exportToCanvas = async (
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
@ -167,21 +199,24 @@ export const exportToCanvas = async (
|
|||||||
const tempScene = __createSceneForElementsHack__(elements);
|
const tempScene = __createSceneForElementsHack__(elements);
|
||||||
elements = tempScene.getNonDeletedElements();
|
elements = tempScene.getNonDeletedElements();
|
||||||
|
|
||||||
let nextElements: ExcalidrawElement[];
|
const frameRendering = getFrameRenderingConfig(
|
||||||
|
exportingFrame ?? null,
|
||||||
|
appState.frameRendering ?? null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const elementsForRender = prepareElementsForRender({
|
||||||
|
elements,
|
||||||
|
exportingFrame,
|
||||||
|
exportWithDarkMode: appState.exportWithDarkMode,
|
||||||
|
frameRendering,
|
||||||
|
});
|
||||||
|
|
||||||
if (exportingFrame) {
|
if (exportingFrame) {
|
||||||
exportPadding = 0;
|
exportPadding = 0;
|
||||||
nextElements = elementsOverlappingBBox({
|
|
||||||
elements,
|
|
||||||
bounds: exportingFrame,
|
|
||||||
type: "overlap",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
nextElements = addFrameLabelsAsTextElements(elements, appState);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const [minX, minY, width, height] = getCanvasSize(
|
const [minX, minY, width, height] = getCanvasSize(
|
||||||
exportingFrame ? [exportingFrame] : getRootElements(nextElements),
|
exportingFrame ? [exportingFrame] : getRootElements(elementsForRender),
|
||||||
exportPadding,
|
exportPadding,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -191,7 +226,7 @@ export const exportToCanvas = async (
|
|||||||
|
|
||||||
const { imageCache } = await updateImageCache({
|
const { imageCache } = await updateImageCache({
|
||||||
imageCache: new Map(),
|
imageCache: new Map(),
|
||||||
fileIds: getInitializedImageElements(nextElements).map(
|
fileIds: getInitializedImageElements(elementsForRender).map(
|
||||||
(element) => element.fileId,
|
(element) => element.fileId,
|
||||||
),
|
),
|
||||||
files,
|
files,
|
||||||
@ -200,15 +235,12 @@ export const exportToCanvas = async (
|
|||||||
renderStaticScene({
|
renderStaticScene({
|
||||||
canvas,
|
canvas,
|
||||||
rc: rough.canvas(canvas),
|
rc: rough.canvas(canvas),
|
||||||
elements: nextElements,
|
elements: elementsForRender,
|
||||||
visibleElements: nextElements,
|
visibleElements: elementsForRender,
|
||||||
scale,
|
scale,
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
frameRendering: getFrameRenderingConfig(
|
frameRendering,
|
||||||
exportingFrame ?? null,
|
|
||||||
appState.frameRendering ?? null,
|
|
||||||
),
|
|
||||||
viewBackgroundColor: exportBackground ? viewBackgroundColor : null,
|
viewBackgroundColor: exportBackground ? viewBackgroundColor : null,
|
||||||
scrollX: -minX + exportPadding,
|
scrollX: -minX + exportPadding,
|
||||||
scrollY: -minY + exportPadding,
|
scrollY: -minY + exportPadding,
|
||||||
@ -248,8 +280,14 @@ export const exportToSvg = async (
|
|||||||
const tempScene = __createSceneForElementsHack__(elements);
|
const tempScene = __createSceneForElementsHack__(elements);
|
||||||
elements = tempScene.getNonDeletedElements();
|
elements = tempScene.getNonDeletedElements();
|
||||||
|
|
||||||
|
const frameRendering = getFrameRenderingConfig(
|
||||||
|
opts?.exportingFrame ?? null,
|
||||||
|
appState.frameRendering ?? null,
|
||||||
|
);
|
||||||
|
|
||||||
let {
|
let {
|
||||||
exportPadding = DEFAULT_EXPORT_PADDING,
|
exportPadding = DEFAULT_EXPORT_PADDING,
|
||||||
|
exportWithDarkMode = false,
|
||||||
viewBackgroundColor,
|
viewBackgroundColor,
|
||||||
exportScale = 1,
|
exportScale = 1,
|
||||||
exportEmbedScene,
|
exportEmbedScene,
|
||||||
@ -257,19 +295,15 @@ export const exportToSvg = async (
|
|||||||
|
|
||||||
const { exportingFrame = null } = opts || {};
|
const { exportingFrame = null } = opts || {};
|
||||||
|
|
||||||
let nextElements: ExcalidrawElement[] = [];
|
const elementsForRender = prepareElementsForRender({
|
||||||
|
elements,
|
||||||
|
exportingFrame,
|
||||||
|
exportWithDarkMode,
|
||||||
|
frameRendering,
|
||||||
|
});
|
||||||
|
|
||||||
if (exportingFrame) {
|
if (exportingFrame) {
|
||||||
exportPadding = 0;
|
exportPadding = 0;
|
||||||
nextElements = elementsOverlappingBBox({
|
|
||||||
elements,
|
|
||||||
bounds: exportingFrame,
|
|
||||||
type: "overlap",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
nextElements = addFrameLabelsAsTextElements(elements, {
|
|
||||||
exportWithDarkMode: appState.exportWithDarkMode ?? false,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let metadata = "";
|
let metadata = "";
|
||||||
@ -293,7 +327,7 @@ export const exportToSvg = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [minX, minY, width, height] = getCanvasSize(
|
const [minX, minY, width, height] = getCanvasSize(
|
||||||
exportingFrame ? [exportingFrame] : getRootElements(nextElements),
|
exportingFrame ? [exportingFrame] : getRootElements(elementsForRender),
|
||||||
exportPadding,
|
exportPadding,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -304,7 +338,7 @@ export const exportToSvg = async (
|
|||||||
svgRoot.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
svgRoot.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
||||||
svgRoot.setAttribute("width", `${width * exportScale}`);
|
svgRoot.setAttribute("width", `${width * exportScale}`);
|
||||||
svgRoot.setAttribute("height", `${height * exportScale}`);
|
svgRoot.setAttribute("height", `${height * exportScale}`);
|
||||||
if (appState.exportWithDarkMode) {
|
if (exportWithDarkMode) {
|
||||||
svgRoot.setAttribute("filter", THEME_FILTER);
|
svgRoot.setAttribute("filter", THEME_FILTER);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -379,15 +413,12 @@ export const exportToSvg = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const rsvg = rough.svg(svgRoot);
|
const rsvg = rough.svg(svgRoot);
|
||||||
renderSceneToSvg(nextElements, rsvg, svgRoot, files || {}, {
|
renderSceneToSvg(elementsForRender, rsvg, svgRoot, files || {}, {
|
||||||
offsetX,
|
offsetX,
|
||||||
offsetY,
|
offsetY,
|
||||||
exportWithDarkMode: appState.exportWithDarkMode ?? false,
|
exportWithDarkMode,
|
||||||
renderEmbeddables: opts?.renderEmbeddables ?? false,
|
renderEmbeddables: opts?.renderEmbeddables ?? false,
|
||||||
frameRendering: getFrameRenderingConfig(
|
frameRendering,
|
||||||
exportingFrame ?? null,
|
|
||||||
appState.frameRendering ?? null,
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
tempScene.destroy();
|
tempScene.destroy();
|
||||||
|
@ -15,7 +15,9 @@ describe("event callbacks", () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const excalidrawAPIPromise = resolvablePromise<ExcalidrawImperativeAPI>();
|
const excalidrawAPIPromise = resolvablePromise<ExcalidrawImperativeAPI>();
|
||||||
await render(
|
await render(
|
||||||
<Excalidraw ref={(api) => excalidrawAPIPromise.resolve(api as any)} />,
|
<Excalidraw
|
||||||
|
excalidrawAPI={(api) => excalidrawAPIPromise.resolve(api as any)}
|
||||||
|
/>,
|
||||||
);
|
);
|
||||||
excalidrawAPI = await excalidrawAPIPromise;
|
excalidrawAPI = await excalidrawAPIPromise;
|
||||||
});
|
});
|
||||||
|
@ -14,7 +14,9 @@ describe("setActiveTool()", () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const excalidrawAPIPromise = resolvablePromise<ExcalidrawImperativeAPI>();
|
const excalidrawAPIPromise = resolvablePromise<ExcalidrawImperativeAPI>();
|
||||||
await render(
|
await render(
|
||||||
<Excalidraw ref={(api) => excalidrawAPIPromise.resolve(api as any)} />,
|
<Excalidraw
|
||||||
|
excalidrawAPI={(api) => excalidrawAPIPromise.resolve(api as any)}
|
||||||
|
/>,
|
||||||
);
|
);
|
||||||
excalidrawAPI = await excalidrawAPIPromise;
|
excalidrawAPI = await excalidrawAPIPromise;
|
||||||
});
|
});
|
||||||
|
17
src/types.ts
17
src/types.ts
@ -23,7 +23,7 @@ import { LinearElementEditor } from "./element/linearElementEditor";
|
|||||||
import { SuggestedBinding } from "./element/binding";
|
import { SuggestedBinding } from "./element/binding";
|
||||||
import { ImportedDataState } from "./data/types";
|
import { ImportedDataState } from "./data/types";
|
||||||
import type App from "./components/App";
|
import type App from "./components/App";
|
||||||
import type { ResolvablePromise, throttleRAF } from "./utils";
|
import type { throttleRAF } from "./utils";
|
||||||
import { Spreadsheet } from "./charts";
|
import { Spreadsheet } from "./charts";
|
||||||
import { Language } from "./i18n";
|
import { Language } from "./i18n";
|
||||||
import { ClipboardData } from "./clipboard";
|
import { ClipboardData } from "./clipboard";
|
||||||
@ -34,7 +34,7 @@ import type { FileSystemHandle } from "./data/filesystem";
|
|||||||
import type { IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
|
import type { IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
|
||||||
import { ContextMenuItems } from "./components/ContextMenu";
|
import { ContextMenuItems } from "./components/ContextMenu";
|
||||||
import { SnapLine } from "./snapping";
|
import { SnapLine } from "./snapping";
|
||||||
import { Merge, ForwardRef, ValueOf } from "./utility-types";
|
import { Merge, ValueOf } from "./utility-types";
|
||||||
|
|
||||||
export type Point = Readonly<RoughPoint>;
|
export type Point = Readonly<RoughPoint>;
|
||||||
|
|
||||||
@ -362,15 +362,6 @@ export type LibraryItemsSource =
|
|||||||
| Promise<LibraryItems_anyVersion | Blob>;
|
| Promise<LibraryItems_anyVersion | Blob>;
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
// NOTE ready/readyPromise props are optional for host apps' sake (our own
|
|
||||||
// implem guarantees existence)
|
|
||||||
export type ExcalidrawAPIRefValue =
|
|
||||||
| ExcalidrawImperativeAPI
|
|
||||||
| {
|
|
||||||
readyPromise?: ResolvablePromise<ExcalidrawImperativeAPI>;
|
|
||||||
ready?: false;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ExcalidrawInitialDataState = Merge<
|
export type ExcalidrawInitialDataState = Merge<
|
||||||
ImportedDataState,
|
ImportedDataState,
|
||||||
{
|
{
|
||||||
@ -390,7 +381,7 @@ export interface ExcalidrawProps {
|
|||||||
| ExcalidrawInitialDataState
|
| ExcalidrawInitialDataState
|
||||||
| null
|
| null
|
||||||
| Promise<ExcalidrawInitialDataState | null>;
|
| Promise<ExcalidrawInitialDataState | null>;
|
||||||
excalidrawRef?: ForwardRef<ExcalidrawAPIRefValue>;
|
excalidrawAPI?: (api: ExcalidrawImperativeAPI) => void;
|
||||||
isCollaborating?: boolean;
|
isCollaborating?: boolean;
|
||||||
onPointerUpdate?: (payload: {
|
onPointerUpdate?: (payload: {
|
||||||
pointer: { x: number; y: number; tool: "pointer" | "laser" };
|
pointer: { x: number; y: number; tool: "pointer" | "laser" };
|
||||||
@ -630,8 +621,6 @@ export type ExcalidrawImperativeAPI = {
|
|||||||
refresh: InstanceType<typeof App>["refresh"];
|
refresh: InstanceType<typeof App>["refresh"];
|
||||||
setToast: InstanceType<typeof App>["setToast"];
|
setToast: InstanceType<typeof App>["setToast"];
|
||||||
addFiles: (data: BinaryFileData[]) => void;
|
addFiles: (data: BinaryFileData[]) => void;
|
||||||
readyPromise: ResolvablePromise<ExcalidrawImperativeAPI>;
|
|
||||||
ready: true;
|
|
||||||
id: string;
|
id: string;
|
||||||
setActiveTool: InstanceType<typeof App>["setActiveTool"];
|
setActiveTool: InstanceType<typeof App>["setActiveTool"];
|
||||||
setCursor: InstanceType<typeof App>["setCursor"];
|
setCursor: InstanceType<typeof App>["setCursor"];
|
||||||
|
@ -1602,10 +1602,10 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@excalidraw/prettier-config/-/prettier-config-1.0.2.tgz#b7c061c99cee2f78b9ca470ea1fbd602683bba65"
|
resolved "https://registry.yarnpkg.com/@excalidraw/prettier-config/-/prettier-config-1.0.2.tgz#b7c061c99cee2f78b9ca470ea1fbd602683bba65"
|
||||||
integrity sha512-rFIq8+A8WvkEzBsF++Rw6gzxE+hU3ZNkdg8foI+Upz2y/rOC/gUpWJaggPbCkoH3nlREVU59axQjZ1+F6ePRGg==
|
integrity sha512-rFIq8+A8WvkEzBsF++Rw6gzxE+hU3ZNkdg8foI+Upz2y/rOC/gUpWJaggPbCkoH3nlREVU59axQjZ1+F6ePRGg==
|
||||||
|
|
||||||
"@excalidraw/random-username@1.0.0":
|
"@excalidraw/random-username@1.1.0":
|
||||||
version "1.0.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@excalidraw/random-username/-/random-username-1.0.0.tgz#6d5293148aee6cd08dcdfcadc0c91276572f4499"
|
resolved "https://registry.yarnpkg.com/@excalidraw/random-username/-/random-username-1.1.0.tgz#6f388d6a9708cf655b8c9c6aa3fa569ee71ecf0f"
|
||||||
integrity sha512-pd4VapWahQ7PIyThGq32+C+JUS73mf3RSdC7BmQiXzhQsCTU4RHc8y9jBi+pb1CFV0iJXvjJRXnVdLCbTj3+HA==
|
integrity sha512-nULYsQxkWHnbmHvcs+efMkJ4/9TtvNyFeLyHdeGxW0zHs6P+jYVqcRff9A6Vq9w9JXeDRnRh2VKvTtS19GW2qA==
|
||||||
|
|
||||||
"@firebase/analytics-types@0.4.0":
|
"@firebase/analytics-types@0.4.0":
|
||||||
version "0.4.0"
|
version "0.4.0"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user