mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
feat: partition main canvas vertically (#6759)
Co-authored-by: Marcel Mraz <marcel.mraz@adacta-fintech.com> Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
parent
3ea07076ad
commit
a376bd9495
69 changed files with 4348 additions and 2970 deletions
|
@ -2,9 +2,9 @@ import { isTextElement, refreshTextDimensions } from "../element";
|
|||
import { newElementWith } from "../element/mutateElement";
|
||||
import { isBoundToContainer } from "../element/typeChecks";
|
||||
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
|
||||
import { invalidateShapeForElement } from "../renderer/renderElement";
|
||||
import { getFontString } from "../utils";
|
||||
import type Scene from "./Scene";
|
||||
import { ShapeCache } from "./ShapeCache";
|
||||
|
||||
export class Fonts {
|
||||
private scene: Scene;
|
||||
|
@ -54,7 +54,7 @@ export class Fonts {
|
|||
|
||||
this.scene.mapElements((element) => {
|
||||
if (isTextElement(element) && !isBoundToContainer(element)) {
|
||||
invalidateShapeForElement(element);
|
||||
ShapeCache.delete(element);
|
||||
didUpdate = true;
|
||||
return newElementWith(element, {
|
||||
...refreshTextDimensions(element),
|
||||
|
|
131
src/scene/Renderer.ts
Normal file
131
src/scene/Renderer.ts
Normal file
|
@ -0,0 +1,131 @@
|
|||
import { isElementInViewport } from "../element/sizeHelpers";
|
||||
import { isImageElement } from "../element/typeChecks";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
import { cancelRender } from "../renderer/renderScene";
|
||||
import { AppState } from "../types";
|
||||
import { memoize } from "../utils";
|
||||
import Scene from "./Scene";
|
||||
|
||||
export class Renderer {
|
||||
private scene: Scene;
|
||||
|
||||
constructor(scene: Scene) {
|
||||
this.scene = scene;
|
||||
}
|
||||
|
||||
public getRenderableElements = (() => {
|
||||
const getVisibleCanvasElements = ({
|
||||
elements,
|
||||
zoom,
|
||||
offsetLeft,
|
||||
offsetTop,
|
||||
scrollX,
|
||||
scrollY,
|
||||
height,
|
||||
width,
|
||||
}: {
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
zoom: AppState["zoom"];
|
||||
offsetLeft: AppState["offsetLeft"];
|
||||
offsetTop: AppState["offsetTop"];
|
||||
scrollX: AppState["scrollX"];
|
||||
scrollY: AppState["scrollY"];
|
||||
height: AppState["height"];
|
||||
width: AppState["width"];
|
||||
}): readonly NonDeletedExcalidrawElement[] => {
|
||||
return elements.filter((element) =>
|
||||
isElementInViewport(element, width, height, {
|
||||
zoom,
|
||||
offsetLeft,
|
||||
offsetTop,
|
||||
scrollX,
|
||||
scrollY,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const getCanvasElements = ({
|
||||
editingElement,
|
||||
elements,
|
||||
pendingImageElementId,
|
||||
}: {
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
editingElement: AppState["editingElement"];
|
||||
pendingImageElementId: AppState["pendingImageElementId"];
|
||||
}) => {
|
||||
return elements.filter((element) => {
|
||||
if (isImageElement(element)) {
|
||||
if (
|
||||
// => not placed on canvas yet (but in elements array)
|
||||
pendingImageElementId === element.id
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// we don't want to render text element that's being currently edited
|
||||
// (it's rendered on remote only)
|
||||
return (
|
||||
!editingElement ||
|
||||
editingElement.type !== "text" ||
|
||||
element.id !== editingElement.id
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return memoize(
|
||||
({
|
||||
zoom,
|
||||
offsetLeft,
|
||||
offsetTop,
|
||||
scrollX,
|
||||
scrollY,
|
||||
height,
|
||||
width,
|
||||
editingElement,
|
||||
pendingImageElementId,
|
||||
// unused but serves we cache on it to invalidate elements if they
|
||||
// get mutated
|
||||
versionNonce: _versionNonce,
|
||||
}: {
|
||||
zoom: AppState["zoom"];
|
||||
offsetLeft: AppState["offsetLeft"];
|
||||
offsetTop: AppState["offsetTop"];
|
||||
scrollX: AppState["scrollX"];
|
||||
scrollY: AppState["scrollY"];
|
||||
height: AppState["height"];
|
||||
width: AppState["width"];
|
||||
editingElement: AppState["editingElement"];
|
||||
pendingImageElementId: AppState["pendingImageElementId"];
|
||||
versionNonce: ReturnType<InstanceType<typeof Scene>["getVersionNonce"]>;
|
||||
}) => {
|
||||
const elements = this.scene.getNonDeletedElements();
|
||||
|
||||
const canvasElements = getCanvasElements({
|
||||
elements,
|
||||
editingElement,
|
||||
pendingImageElementId,
|
||||
});
|
||||
|
||||
const visibleElements = getVisibleCanvasElements({
|
||||
elements: canvasElements,
|
||||
zoom,
|
||||
offsetLeft,
|
||||
offsetTop,
|
||||
scrollX,
|
||||
scrollY,
|
||||
height,
|
||||
width,
|
||||
});
|
||||
|
||||
return { canvasElements, 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() {
|
||||
cancelRender();
|
||||
this.getRenderableElements.clear();
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@ import { isFrameElement } from "../element/typeChecks";
|
|||
import { getSelectedElements } from "./selection";
|
||||
import { AppState } from "../types";
|
||||
import { Assert, SameType } from "../utility-types";
|
||||
import { randomInteger } from "../random";
|
||||
|
||||
type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"];
|
||||
type ElementKey = ExcalidrawElement | ElementIdKey;
|
||||
|
@ -105,6 +106,7 @@ class Scene {
|
|||
elements: null,
|
||||
cache: new Map(),
|
||||
};
|
||||
private versionNonce: number | undefined;
|
||||
|
||||
getElementsIncludingDeleted() {
|
||||
return this.elements;
|
||||
|
@ -172,6 +174,10 @@ class Scene {
|
|||
return (this.elementsMap.get(id) as T | undefined) || null;
|
||||
}
|
||||
|
||||
getVersionNonce() {
|
||||
return this.versionNonce;
|
||||
}
|
||||
|
||||
getNonDeletedElement(
|
||||
id: ExcalidrawElement["id"],
|
||||
): NonDeleted<ExcalidrawElement> | null {
|
||||
|
@ -230,6 +236,8 @@ class Scene {
|
|||
}
|
||||
|
||||
informMutation() {
|
||||
this.versionNonce = randomInteger();
|
||||
|
||||
for (const callback of Array.from(this.callbacks)) {
|
||||
callback();
|
||||
}
|
||||
|
|
61
src/scene/ShapeCache.ts
Normal file
61
src/scene/ShapeCache.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
import { Drawable } from "roughjs/bin/core";
|
||||
import { RoughGenerator } from "roughjs/bin/generator";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { generateElementShape } from "../renderer/renderElement";
|
||||
|
||||
type ElementShape = Drawable | Drawable[] | null;
|
||||
|
||||
type ElementShapes = {
|
||||
freedraw: Drawable | null;
|
||||
arrow: Drawable[];
|
||||
line: Drawable[];
|
||||
text: null;
|
||||
image: null;
|
||||
};
|
||||
|
||||
export class ShapeCache {
|
||||
private static rg = new RoughGenerator();
|
||||
private static cache = new WeakMap<ExcalidrawElement, ElementShape>();
|
||||
|
||||
public static get = <T extends ExcalidrawElement>(element: T) => {
|
||||
return ShapeCache.cache.get(
|
||||
element,
|
||||
) as T["type"] extends keyof ElementShapes
|
||||
? ElementShapes[T["type"]] | undefined
|
||||
: Drawable | null | undefined;
|
||||
};
|
||||
|
||||
public static set = <T extends ExcalidrawElement>(
|
||||
element: T,
|
||||
shape: T["type"] extends keyof ElementShapes
|
||||
? ElementShapes[T["type"]]
|
||||
: Drawable,
|
||||
) => ShapeCache.cache.set(element, shape);
|
||||
|
||||
public static delete = (element: ExcalidrawElement) =>
|
||||
ShapeCache.cache.delete(element);
|
||||
|
||||
public static destroy = () => {
|
||||
ShapeCache.cache = new WeakMap();
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates & caches shape for element if not already cached, otherwise
|
||||
* return cached shape.
|
||||
*/
|
||||
public static generateElementShape = <T extends ExcalidrawElement>(
|
||||
element: T,
|
||||
) => {
|
||||
const shape = generateElementShape(
|
||||
element,
|
||||
ShapeCache.rg,
|
||||
/* so it prefers cache */ false,
|
||||
) as T["type"] extends keyof ElementShapes
|
||||
? ElementShapes[T["type"]]
|
||||
: Drawable | null;
|
||||
|
||||
ShapeCache.cache.set(element, shape);
|
||||
|
||||
return shape;
|
||||
};
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import rough from "roughjs/bin/rough";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
import { getCommonBounds, getElementAbsoluteCoords } from "../element/bounds";
|
||||
import { renderScene, renderSceneToSvg } from "../renderer/renderScene";
|
||||
import { renderSceneToSvg, renderStaticScene } from "../renderer/renderScene";
|
||||
import { distance, isOnlyExportingSingleFrame } from "../utils";
|
||||
import { AppState, BinaryFiles } from "../types";
|
||||
import { DEFAULT_EXPORT_PADDING, SVG_NS, THEME_FILTER } from "../constants";
|
||||
|
@ -54,26 +54,23 @@ export const exportToCanvas = async (
|
|||
|
||||
const onlyExportingSingleFrame = isOnlyExportingSingleFrame(elements);
|
||||
|
||||
renderScene({
|
||||
elements,
|
||||
appState,
|
||||
scale,
|
||||
rc: rough.canvas(canvas),
|
||||
renderStaticScene({
|
||||
canvas,
|
||||
renderConfig: {
|
||||
rc: rough.canvas(canvas),
|
||||
elements,
|
||||
visibleElements: elements,
|
||||
scale,
|
||||
appState: {
|
||||
...appState,
|
||||
viewBackgroundColor: exportBackground ? viewBackgroundColor : null,
|
||||
scrollX: -minX + (onlyExportingSingleFrame ? 0 : exportPadding),
|
||||
scrollY: -minY + (onlyExportingSingleFrame ? 0 : exportPadding),
|
||||
zoom: defaultAppState.zoom,
|
||||
remotePointerViewportCoords: {},
|
||||
remoteSelectedElementIds: {},
|
||||
shouldCacheIgnoreZoom: false,
|
||||
remotePointerUsernames: {},
|
||||
remotePointerUserStates: {},
|
||||
theme: appState.exportWithDarkMode ? "dark" : "light",
|
||||
},
|
||||
renderConfig: {
|
||||
imageCache,
|
||||
renderScrollbars: false,
|
||||
renderSelection: false,
|
||||
renderGrid: false,
|
||||
isExporting: true,
|
||||
},
|
||||
|
|
|
@ -11,11 +11,7 @@ import {
|
|||
viewportCoordsToSceneCoords,
|
||||
} from "../utils";
|
||||
|
||||
const isOutsideViewPort = (
|
||||
appState: AppState,
|
||||
canvas: HTMLCanvasElement | null,
|
||||
cords: Array<number>,
|
||||
) => {
|
||||
const isOutsideViewPort = (appState: AppState, cords: Array<number>) => {
|
||||
const [x1, y1, x2, y2] = cords;
|
||||
const { x: viewportX1, y: viewportY1 } = sceneCoordsToViewportCoords(
|
||||
{ sceneX: x1, sceneY: y1 },
|
||||
|
@ -49,7 +45,6 @@ export const centerScrollOn = ({
|
|||
export const calculateScrollCenter = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
canvas: HTMLCanvasElement | null,
|
||||
): { scrollX: number; scrollY: number } => {
|
||||
elements = getVisibleElements(elements);
|
||||
|
||||
|
@ -61,7 +56,7 @@ export const calculateScrollCenter = (
|
|||
}
|
||||
let [x1, y1, x2, y2] = getCommonBounds(elements);
|
||||
|
||||
if (isOutsideViewPort(appState, canvas, [x1, y1, x2, y2])) {
|
||||
if (isOutsideViewPort(appState, [x1, y1, x2, y2])) {
|
||||
[x1, y1, x2, y2] = getClosestElementBounds(
|
||||
elements,
|
||||
viewportCoordsToSceneCoords(
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { ExcalidrawElement } from "../element/types";
|
||||
import { getCommonBounds } from "../element";
|
||||
import { Zoom } from "../types";
|
||||
import { InteractiveCanvasAppState } from "../types";
|
||||
import { ScrollBars } from "./types";
|
||||
import { getGlobalCSSVariable } from "../utils";
|
||||
import { getLanguage } from "../i18n";
|
||||
|
@ -13,15 +13,7 @@ export const getScrollBars = (
|
|||
elements: readonly ExcalidrawElement[],
|
||||
viewportWidth: number,
|
||||
viewportHeight: number,
|
||||
{
|
||||
scrollX,
|
||||
scrollY,
|
||||
zoom,
|
||||
}: {
|
||||
scrollX: number;
|
||||
scrollY: number;
|
||||
zoom: Zoom;
|
||||
},
|
||||
appState: InteractiveCanvasAppState,
|
||||
): ScrollBars => {
|
||||
if (elements.length === 0) {
|
||||
return {
|
||||
|
@ -34,8 +26,8 @@ export const getScrollBars = (
|
|||
getCommonBounds(elements);
|
||||
|
||||
// Apply zoom
|
||||
const viewportWidthWithZoom = viewportWidth / zoom.value;
|
||||
const viewportHeightWithZoom = viewportHeight / zoom.value;
|
||||
const viewportWidthWithZoom = viewportWidth / appState.zoom.value;
|
||||
const viewportHeightWithZoom = viewportHeight / appState.zoom.value;
|
||||
|
||||
const viewportWidthDiff = viewportWidth - viewportWidthWithZoom;
|
||||
const viewportHeightDiff = viewportHeight - viewportHeightWithZoom;
|
||||
|
@ -50,8 +42,10 @@ export const getScrollBars = (
|
|||
const isRTL = getLanguage().rtl;
|
||||
|
||||
// The viewport is the rectangle currently visible for the user
|
||||
const viewportMinX = -scrollX + viewportWidthDiff / 2 + safeArea.left;
|
||||
const viewportMinY = -scrollY + viewportHeightDiff / 2 + safeArea.top;
|
||||
const viewportMinX =
|
||||
-appState.scrollX + viewportWidthDiff / 2 + safeArea.left;
|
||||
const viewportMinY =
|
||||
-appState.scrollY + viewportHeightDiff / 2 + safeArea.top;
|
||||
const viewportMaxX = viewportMinX + viewportWidthWithZoom - safeArea.right;
|
||||
const viewportMaxY = viewportMinY + viewportHeightWithZoom - safeArea.bottom;
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import {
|
|||
NonDeletedExcalidrawElement,
|
||||
} from "../element/types";
|
||||
import { getElementAbsoluteCoords, getElementBounds } from "../element";
|
||||
import { AppState } from "../types";
|
||||
import { AppState, InteractiveCanvasAppState } from "../types";
|
||||
import { isBoundToContainer } from "../element/typeChecks";
|
||||
import {
|
||||
elementOverlapsWithFrame,
|
||||
|
@ -146,7 +146,7 @@ export const getCommonAttributeOfSelectedElements = <T>(
|
|||
|
||||
export const getSelectedElements = (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
appState: Pick<AppState, "selectedElementIds">,
|
||||
appState: Pick<InteractiveCanvasAppState, "selectedElementIds">,
|
||||
opts?: {
|
||||
includeBoundTextElement?: boolean;
|
||||
includeElementsInFrames?: boolean;
|
||||
|
|
|
@ -1,33 +1,63 @@
|
|||
import { ExcalidrawTextElement } from "../element/types";
|
||||
import { AppClassProperties, AppState } from "../types";
|
||||
import { RoughCanvas } from "roughjs/bin/canvas";
|
||||
import {
|
||||
ExcalidrawTextElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "../element/types";
|
||||
import {
|
||||
AppClassProperties,
|
||||
InteractiveCanvasAppState,
|
||||
StaticCanvasAppState,
|
||||
} from "../types";
|
||||
|
||||
export type RenderConfig = {
|
||||
// AppState values
|
||||
// ---------------------------------------------------------------------------
|
||||
scrollX: AppState["scrollX"];
|
||||
scrollY: AppState["scrollY"];
|
||||
/** null indicates transparent bg */
|
||||
viewBackgroundColor: AppState["viewBackgroundColor"] | null;
|
||||
zoom: AppState["zoom"];
|
||||
shouldCacheIgnoreZoom: AppState["shouldCacheIgnoreZoom"];
|
||||
theme: AppState["theme"];
|
||||
// collab-related state
|
||||
// ---------------------------------------------------------------------------
|
||||
remotePointerViewportCoords: { [id: string]: { x: number; y: number } };
|
||||
remotePointerButton?: { [id: string]: string | undefined };
|
||||
remoteSelectedElementIds: { [elementId: string]: string[] };
|
||||
remotePointerUsernames: { [id: string]: string };
|
||||
remotePointerUserStates: { [id: string]: string };
|
||||
export type StaticCanvasRenderConfig = {
|
||||
// extra options passed to the renderer
|
||||
// ---------------------------------------------------------------------------
|
||||
imageCache: AppClassProperties["imageCache"];
|
||||
renderScrollbars?: boolean;
|
||||
renderSelection?: boolean;
|
||||
renderGrid?: boolean;
|
||||
renderGrid: boolean;
|
||||
/** when exporting the behavior is slightly different (e.g. we can't use
|
||||
CSS filters), and we disable render optimizations for best output */
|
||||
CSS filters), and we disable render optimizations for best output */
|
||||
isExporting: boolean;
|
||||
};
|
||||
|
||||
export type InteractiveCanvasRenderConfig = {
|
||||
// collab-related state
|
||||
// ---------------------------------------------------------------------------
|
||||
remoteSelectedElementIds: { [elementId: string]: string[] };
|
||||
remotePointerViewportCoords: { [id: string]: { x: number; y: number } };
|
||||
remotePointerUserStates: { [id: string]: string };
|
||||
remotePointerUsernames: { [id: string]: string };
|
||||
remotePointerButton?: { [id: string]: string | undefined };
|
||||
selectionColor?: string;
|
||||
// extra options passed to the renderer
|
||||
// ---------------------------------------------------------------------------
|
||||
renderScrollbars?: boolean;
|
||||
};
|
||||
|
||||
export type RenderInteractiveSceneCallback = {
|
||||
atLeastOneVisibleElement: boolean;
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
scrollBars?: ScrollBars;
|
||||
};
|
||||
|
||||
export type StaticSceneRenderConfig = {
|
||||
canvas: HTMLCanvasElement;
|
||||
rc: RoughCanvas;
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
visibleElements: readonly NonDeletedExcalidrawElement[];
|
||||
scale: number;
|
||||
appState: StaticCanvasAppState;
|
||||
renderConfig: StaticCanvasRenderConfig;
|
||||
};
|
||||
|
||||
export type InteractiveSceneRenderConfig = {
|
||||
canvas: HTMLCanvasElement | null;
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
visibleElements: readonly NonDeletedExcalidrawElement[];
|
||||
selectedElements: readonly NonDeletedExcalidrawElement[];
|
||||
scale: number;
|
||||
appState: InteractiveCanvasAppState;
|
||||
renderConfig: InteractiveCanvasRenderConfig;
|
||||
callback: (data: RenderInteractiveSceneCallback) => void;
|
||||
};
|
||||
|
||||
export type SceneScroll = {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue