diff options
Diffstat (limited to 'packages/excalidraw/scene/Renderer.ts')
| -rw-r--r-- | packages/excalidraw/scene/Renderer.ts | 167 |
1 files changed, 167 insertions, 0 deletions
diff --git a/packages/excalidraw/scene/Renderer.ts b/packages/excalidraw/scene/Renderer.ts new file mode 100644 index 0000000..8048d20 --- /dev/null +++ b/packages/excalidraw/scene/Renderer.ts @@ -0,0 +1,167 @@ +import { isElementInViewport } from "../element/sizeHelpers"; +import { isImageElement } from "../element/typeChecks"; +import type { + ExcalidrawElement, + NonDeletedElementsMap, + NonDeletedExcalidrawElement, +} from "../element/types"; +import { renderInteractiveSceneThrottled } from "../renderer/interactiveScene"; +import { renderStaticSceneThrottled } from "../renderer/staticScene"; + +import type { AppState } from "../types"; +import { memoize, toBrandedType } from "../utils"; +import type Scene from "./Scene"; +import type { RenderableElementsMap } from "./types"; + +export class Renderer { + private scene: Scene; + + constructor(scene: Scene) { + this.scene = scene; + } + + public getRenderableElements = (() => { + const getVisibleCanvasElements = ({ + elementsMap, + zoom, + offsetLeft, + offsetTop, + scrollX, + scrollY, + height, + width, + }: { + elementsMap: NonDeletedElementsMap; + zoom: AppState["zoom"]; + offsetLeft: AppState["offsetLeft"]; + offsetTop: AppState["offsetTop"]; + scrollX: AppState["scrollX"]; + scrollY: AppState["scrollY"]; + height: AppState["height"]; + width: AppState["width"]; + }): readonly NonDeletedExcalidrawElement[] => { + const visibleElements: NonDeletedExcalidrawElement[] = []; + for (const element of elementsMap.values()) { + if ( + isElementInViewport( + element, + width, + height, + { + zoom, + offsetLeft, + offsetTop, + scrollX, + scrollY, + }, + elementsMap, + ) + ) { + visibleElements.push(element); + } + } + return visibleElements; + }; + + const getRenderableElements = ({ + elements, + editingTextElement, + newElementId, + pendingImageElementId, + }: { + elements: readonly NonDeletedExcalidrawElement[]; + editingTextElement: AppState["editingTextElement"]; + newElementId: ExcalidrawElement["id"] | undefined; + pendingImageElementId: AppState["pendingImageElementId"]; + }) => { + const elementsMap = toBrandedType<RenderableElementsMap>(new Map()); + + for (const element of elements) { + if (isImageElement(element)) { + if ( + // => not placed on canvas yet (but in elements array) + pendingImageElementId === element.id + ) { + continue; + } + } + + if (newElementId === element.id) { + continue; + } + + // we don't want to render text element that's being currently edited + // (it's rendered on remote only) + if ( + !editingTextElement || + editingTextElement.type !== "text" || + element.id !== editingTextElement.id + ) { + elementsMap.set(element.id, element); + } + } + return elementsMap; + }; + + return memoize( + ({ + zoom, + offsetLeft, + offsetTop, + scrollX, + scrollY, + height, + width, + editingTextElement, + newElementId, + pendingImageElementId, + // cache-invalidation nonce + sceneNonce: _sceneNonce, + }: { + zoom: AppState["zoom"]; + offsetLeft: AppState["offsetLeft"]; + offsetTop: AppState["offsetTop"]; + scrollX: AppState["scrollX"]; + scrollY: AppState["scrollY"]; + height: AppState["height"]; + width: AppState["width"]; + editingTextElement: AppState["editingTextElement"]; + /** note: first render of newElement will always bust the cache + * (we'd have to prefilter elements outside of this function) */ + newElementId: ExcalidrawElement["id"] | undefined; + pendingImageElementId: AppState["pendingImageElementId"]; + sceneNonce: ReturnType<InstanceType<typeof Scene>["getSceneNonce"]>; + }) => { + const elements = this.scene.getNonDeletedElements(); + + const elementsMap = getRenderableElements({ + elements, + editingTextElement, + newElementId, + pendingImageElementId, + }); + + const visibleElements = getVisibleCanvasElements({ + elementsMap, + zoom, + offsetLeft, + offsetTop, + scrollX, + scrollY, + height, + width, + }); + + return { elementsMap, visibleElements }; + }, + ); + })(); + + // NOTE Doesn't destroy everything (scene, rc, etc.) because it may not be + // safe to break TS contract here (for upstream cases) + public destroy() { + renderInteractiveSceneThrottled.cancel(); + renderStaticSceneThrottled.cancel(); + this.getRenderableElements.clear(); + } +} |
