diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index b461d4d167..d405b7213c 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -714,10 +714,7 @@ class App extends React.Component { id: this.id, }; - this.fonts = new Fonts({ - scene: this.scene, - onSceneUpdated: this.onSceneUpdated, - }); + this.fonts = new Fonts({ scene: this.scene }); this.history = new History(); this.actionManager.registerAll(actions); @@ -940,7 +937,7 @@ class App extends React.Component { }); if (updated) { - this.scene.informMutation(); + this.scene.triggerUpdate(); } // GC @@ -1452,10 +1449,10 @@ class App extends React.Component { const selectedElements = this.scene.getSelectedElements(this.state); const { renderTopRightUI, renderCustomStats } = this.props; - const versionNonce = this.scene.getVersionNonce(); + const sceneNonce = this.scene.getSceneNonce(); const { elementsMap, visibleElements } = this.renderer.getRenderableElements({ - versionNonce, + sceneNonce, zoom: this.state.zoom, offsetLeft: this.state.offsetLeft, offsetTop: this.state.offsetTop, @@ -1673,7 +1670,7 @@ class App extends React.Component { elementsMap={elementsMap} allElementsMap={allElementsMap} visibleElements={visibleElements} - versionNonce={versionNonce} + sceneNonce={sceneNonce} selectionNonce={ this.state.selectionElement?.versionNonce } @@ -1695,7 +1692,7 @@ class App extends React.Component { elementsMap={elementsMap} visibleElements={visibleElements} selectedElements={selectedElements} - versionNonce={versionNonce} + sceneNonce={sceneNonce} selectionNonce={ this.state.selectionElement?.versionNonce } @@ -1819,7 +1816,7 @@ class App extends React.Component { ); } this.magicGenerations.set(frameElement.id, data); - this.onSceneUpdated(); + this.triggerRender(); }; private getTextFromElements(elements: readonly ExcalidrawElement[]) { @@ -2444,7 +2441,7 @@ class App extends React.Component { this.history.record(increment.elementsChange, increment.appStateChange); }); - this.scene.addCallback(this.onSceneUpdated); + this.scene.onUpdate(this.triggerRender); this.addEventListeners(); if (this.props.autoFocus && this.excalidrawContainerRef.current) { @@ -2489,6 +2486,7 @@ class App extends React.Component { public componentWillUnmount() { this.renderer.destroy(); this.scene = new Scene(); + this.fonts = new Fonts({ scene: this.scene }); this.renderer = new Renderer(this.scene); this.files = {}; this.imageCache.clear(); @@ -3670,7 +3668,7 @@ class App extends React.Component { ShapeCache.delete(element); } }); - this.scene.informMutation(); + this.scene.triggerUpdate(); this.addNewImagesToImageCache(); }, @@ -3730,7 +3728,7 @@ class App extends React.Component { }, ); - private onSceneUpdated = () => { + private triggerRender = () => { this.setState({}); }; @@ -5577,7 +5575,7 @@ class App extends React.Component { } this.elementsPendingErasure = new Set(this.elementsPendingErasure); - this.onSceneUpdated(); + this.triggerRender(); } }; @@ -8069,7 +8067,7 @@ class App extends React.Component { this.scene.getNonDeletedElementsMap(), ); - this.scene.informMutation(); + this.scene.triggerUpdate(); } } } @@ -8564,7 +8562,7 @@ class App extends React.Component { private restoreReadyToEraseElements = () => { this.elementsPendingErasure = new Set(); - this.onSceneUpdated(); + this.triggerRender(); }; private eraseElements = () => { @@ -8978,7 +8976,7 @@ class App extends React.Component { files, ); if (updatedFiles.size) { - this.scene.informMutation(); + this.scene.triggerUpdate(); } } }; diff --git a/packages/excalidraw/components/LayerUI.tsx b/packages/excalidraw/components/LayerUI.tsx index dd34f433bf..a623891c6b 100644 --- a/packages/excalidraw/components/LayerUI.tsx +++ b/packages/excalidraw/components/LayerUI.tsx @@ -444,7 +444,7 @@ const LayerUI = ({ ); ShapeCache.delete(element); } - Scene.getScene(selectedElements[0])?.informMutation(); + Scene.getScene(selectedElements[0])?.triggerUpdate(); } else if (colorPickerType === "elementBackground") { setAppState({ currentItemBackgroundColor: color, diff --git a/packages/excalidraw/components/canvases/InteractiveCanvas.tsx b/packages/excalidraw/components/canvases/InteractiveCanvas.tsx index c8eb799a5d..ceed879b73 100644 --- a/packages/excalidraw/components/canvases/InteractiveCanvas.tsx +++ b/packages/excalidraw/components/canvases/InteractiveCanvas.tsx @@ -19,7 +19,7 @@ type InteractiveCanvasProps = { elementsMap: RenderableElementsMap; visibleElements: readonly NonDeletedExcalidrawElement[]; selectedElements: readonly NonDeletedExcalidrawElement[]; - versionNonce: number | undefined; + sceneNonce: number | undefined; selectionNonce: number | undefined; scale: number; appState: InteractiveCanvasAppState; @@ -206,10 +206,10 @@ const areEqual = ( // This could be further optimised if needed, as we don't have to render interactive canvas on each scene mutation if ( prevProps.selectionNonce !== nextProps.selectionNonce || - prevProps.versionNonce !== nextProps.versionNonce || + prevProps.sceneNonce !== nextProps.sceneNonce || prevProps.scale !== nextProps.scale || // we need to memoize on elementsMap because they may have renewed - // even if versionNonce didn't change (e.g. we filter elements out based + // even if sceneNonce didn't change (e.g. we filter elements out based // on appState) prevProps.elementsMap !== nextProps.elementsMap || prevProps.visibleElements !== nextProps.visibleElements || diff --git a/packages/excalidraw/components/canvases/StaticCanvas.tsx b/packages/excalidraw/components/canvases/StaticCanvas.tsx index ef54bf33f3..5d73a57b1d 100644 --- a/packages/excalidraw/components/canvases/StaticCanvas.tsx +++ b/packages/excalidraw/components/canvases/StaticCanvas.tsx @@ -19,7 +19,7 @@ type StaticCanvasProps = { elementsMap: RenderableElementsMap; allElementsMap: NonDeletedSceneElementsMap; visibleElements: readonly NonDeletedExcalidrawElement[]; - versionNonce: number | undefined; + sceneNonce: number | undefined; selectionNonce: number | undefined; scale: number; appState: StaticCanvasAppState; @@ -112,10 +112,10 @@ const areEqual = ( nextProps: StaticCanvasProps, ) => { if ( - prevProps.versionNonce !== nextProps.versionNonce || + prevProps.sceneNonce !== nextProps.sceneNonce || prevProps.scale !== nextProps.scale || // we need to memoize on elementsMap because they may have renewed - // even if versionNonce didn't change (e.g. we filter elements out based + // even if sceneNonce didn't change (e.g. we filter elements out based // on appState) prevProps.elementsMap !== nextProps.elementsMap || prevProps.visibleElements !== nextProps.visibleElements diff --git a/packages/excalidraw/element/mutateElement.ts b/packages/excalidraw/element/mutateElement.ts index 6bff903846..de0adeeff4 100644 --- a/packages/excalidraw/element/mutateElement.ts +++ b/packages/excalidraw/element/mutateElement.ts @@ -98,7 +98,7 @@ export const mutateElement = >( element.updated = getUpdatedTimestamp(); if (informMutation) { - Scene.getScene(element)?.informMutation(); + Scene.getScene(element)?.triggerUpdate(); } return element; @@ -107,6 +107,8 @@ export const mutateElement = >( export const newElementWith = ( element: TElement, updates: ElementUpdate, + /** pass `true` to always regenerate */ + force = false, ): TElement => { let didChange = false; for (const key in updates) { @@ -123,7 +125,7 @@ export const newElementWith = ( } } - if (!didChange) { + if (!didChange && !force) { return element; } diff --git a/packages/excalidraw/element/resizeElements.ts b/packages/excalidraw/element/resizeElements.ts index db2f496251..3630fafd00 100644 --- a/packages/excalidraw/element/resizeElements.ts +++ b/packages/excalidraw/element/resizeElements.ts @@ -876,7 +876,7 @@ export const resizeMultipleElements = ( } } - Scene.getScene(elementsAndUpdates[0].element)?.informMutation(); + Scene.getScene(elementsAndUpdates[0].element)?.triggerUpdate(); }; const rotateMultipleElements = ( @@ -938,7 +938,7 @@ const rotateMultipleElements = ( } }); - Scene.getScene(elements[0])?.informMutation(); + Scene.getScene(elements[0])?.triggerUpdate(); }; export const getResizeOffsetXY = ( diff --git a/packages/excalidraw/element/textWysiwyg.tsx b/packages/excalidraw/element/textWysiwyg.tsx index bfcea23480..e738e27ee6 100644 --- a/packages/excalidraw/element/textWysiwyg.tsx +++ b/packages/excalidraw/element/textWysiwyg.tsx @@ -644,7 +644,7 @@ export const textWysiwyg = ({ }; // handle updates of textElement properties of editing element - const unbindUpdate = Scene.getScene(element)!.addCallback(() => { + const unbindUpdate = Scene.getScene(element)!.onUpdate(() => { updateWysiwygStyle(); const isColorPickerActive = !!document.activeElement?.closest( ".color-picker-content", diff --git a/packages/excalidraw/scene/Fonts.ts b/packages/excalidraw/scene/Fonts.ts index c2f0c38aed..ff241a40f1 100644 --- a/packages/excalidraw/scene/Fonts.ts +++ b/packages/excalidraw/scene/Fonts.ts @@ -1,7 +1,5 @@ -import { isTextElement, refreshTextDimensions } from "../element"; +import { isTextElement } from "../element"; import { newElementWith } from "../element/mutateElement"; -import { getContainerElement } from "../element/textElement"; -import { isBoundToContainer } from "../element/typeChecks"; import type { ExcalidrawElement, ExcalidrawTextElement, @@ -12,17 +10,9 @@ import { ShapeCache } from "./ShapeCache"; export class Fonts { private scene: Scene; - private onSceneUpdated: () => void; - constructor({ - scene, - onSceneUpdated, - }: { - scene: Scene; - onSceneUpdated: () => void; - }) { + constructor({ scene }: { scene: Scene }) { this.scene = scene; - this.onSceneUpdated = onSceneUpdated; } // it's ok to track fonts across multiple instances only once, so let's use @@ -57,22 +47,16 @@ export class Fonts { let didUpdate = false; this.scene.mapElements((element) => { - if (isTextElement(element) && !isBoundToContainer(element)) { - ShapeCache.delete(element); + if (isTextElement(element)) { didUpdate = true; - return newElementWith(element, { - ...refreshTextDimensions( - element, - getContainerElement(element, this.scene.getNonDeletedElementsMap()), - this.scene.getNonDeletedElementsMap(), - ), - }); + ShapeCache.delete(element); + return newElementWith(element, {}, true); } return element; }); if (didUpdate) { - this.onSceneUpdated(); + this.scene.triggerUpdate(); } }; diff --git a/packages/excalidraw/scene/Renderer.ts b/packages/excalidraw/scene/Renderer.ts index 754bb7d76a..63b7e7da7d 100644 --- a/packages/excalidraw/scene/Renderer.ts +++ b/packages/excalidraw/scene/Renderer.ts @@ -107,9 +107,8 @@ export class Renderer { width, editingElement, pendingImageElementId, - // unused but serves we cache on it to invalidate elements if they - // get mutated - versionNonce: _versionNonce, + // cache-invalidation nonce + sceneNonce: _sceneNonce, }: { zoom: AppState["zoom"]; offsetLeft: AppState["offsetLeft"]; @@ -120,7 +119,7 @@ export class Renderer { width: AppState["width"]; editingElement: AppState["editingElement"]; pendingImageElementId: AppState["pendingImageElementId"]; - versionNonce: ReturnType["getVersionNonce"]>; + sceneNonce: ReturnType["getSceneNonce"]>; }) => { const elements = this.scene.getNonDeletedElements(); diff --git a/packages/excalidraw/scene/Scene.ts b/packages/excalidraw/scene/Scene.ts index 2e46d77f55..105ef3d342 100644 --- a/packages/excalidraw/scene/Scene.ts +++ b/packages/excalidraw/scene/Scene.ts @@ -138,7 +138,17 @@ class Scene { elements: null, cache: new Map(), }; - private versionNonce: number | undefined; + /** + * Random integer regenerated each scene update. + * + * Does not relate to elements versions, it's only a renderer + * cache-invalidation nonce at the moment. + */ + private sceneNonce: number | undefined; + + getSceneNonce() { + return this.sceneNonce; + } getNonDeletedElementsMap() { return this.nonDeletedElementsMap; @@ -214,10 +224,6 @@ class Scene { return (this.elementsMap.get(id) as T | undefined) || null; } - getVersionNonce() { - return this.versionNonce; - } - getNonDeletedElement( id: ExcalidrawElement["id"], ): NonDeleted | null { @@ -286,18 +292,18 @@ class Scene { this.frames = nextFrameLikes; this.nonDeletedFramesLikes = getNonDeletedElements(this.frames).elements; - this.informMutation(); + this.triggerUpdate(); } - informMutation() { - this.versionNonce = randomInteger(); + triggerUpdate() { + this.sceneNonce = randomInteger(); for (const callback of Array.from(this.callbacks)) { callback(); } } - addCallback(cb: SceneStateCallback): SceneStateCallbackRemover { + onUpdate(cb: SceneStateCallback): SceneStateCallbackRemover { if (this.callbacks.has(cb)) { throw new Error(); }