From 245d681b7d786e298f33046c1bda123d460a2f80 Mon Sep 17 00:00:00 2001 From: Marcel Mraz Date: Thu, 21 Nov 2024 22:03:55 +0100 Subject: [PATCH] Expose store, a bit --- excalidraw-app/App.tsx | 13 +++ packages/excalidraw/change.ts | 111 ++++++++++++++----------- packages/excalidraw/components/App.tsx | 2 + packages/excalidraw/index.tsx | 2 + packages/excalidraw/store.ts | 3 +- packages/excalidraw/types.ts | 6 +- 6 files changed, 86 insertions(+), 51 deletions(-) diff --git a/excalidraw-app/App.tsx b/excalidraw-app/App.tsx index f7842505a..6c1394a49 100644 --- a/excalidraw-app/App.tsx +++ b/excalidraw-app/App.tsx @@ -107,6 +107,7 @@ import Trans from "../packages/excalidraw/components/Trans"; import { ShareDialog, shareDialogStateAtom } from "./share/ShareDialog"; import CollabError, { collabErrorIndicatorAtom } from "./collab/CollabError"; import type { RemoteExcalidrawElement } from "../packages/excalidraw/data/reconcile"; +import type { StoreIncrementEvent } from "../packages/excalidraw/store"; import { CommandPalette, DEFAULT_CATEGORIES, @@ -663,6 +664,17 @@ const ExcalidrawWrapper = () => { } }; + const onIncrement = (increment: StoreIncrementEvent) => { + // ephemerals are not part of this (which is alright) + // - wysiwyg, dragging elements / points, mouse movements, etc. + const { elementsChange } = increment; + + // some appState like selections should also be transfered (we could even persist it) + if (!elementsChange.isEmpty()) { + console.log(elementsChange) + } + }; + const [latestShareableLink, setLatestShareableLink] = useState( null, ); @@ -795,6 +807,7 @@ const ExcalidrawWrapper = () => { = Omit< */ export class ElementsChange implements Change { private constructor( - private readonly added: Map>, - private readonly removed: Map>, - private readonly updated: Map>, + private readonly added: Record>, + private readonly removed: Record>, + private readonly updated: Record>, ) {} public static create( - added: Map>, - removed: Map>, - updated: Map>, + added: Record>, + removed: Record>, + updated: Record>, options = { shouldRedistribute: false }, ) { let change: ElementsChange; if (options.shouldRedistribute) { - const nextAdded = new Map>(); - const nextRemoved = new Map>(); - const nextUpdated = new Map>(); + const nextAdded: Record> = {}; + const nextRemoved: Record> = {}; + const nextUpdated: Record> = {}; - const deltas = [...added, ...removed, ...updated]; + const deltas = [ + ...Object.entries(added), + ...Object.entries(removed), + ...Object.entries(updated), + ]; for (const [id, delta] of deltas) { if (this.satisfiesAddition(delta)) { - nextAdded.set(id, delta); + nextAdded[id] = delta; } else if (this.satisfiesRemoval(delta)) { - nextRemoved.set(id, delta); + nextRemoved[id] = delta; } else { - nextUpdated.set(id, delta); + nextUpdated[id] = delta; } } @@ -873,7 +877,7 @@ export class ElementsChange implements Change { type: "added" | "removed" | "updated", satifies: (delta: Delta) => boolean, ) { - for (const [id, delta] of change[type].entries()) { + for (const [id, delta] of Object.entries(change[type])) { if (!satifies(delta)) { console.error( `Broken invariant for "${type}" delta, element "${id}", delta:`, @@ -900,9 +904,9 @@ export class ElementsChange implements Change { return ElementsChange.empty(); } - const added = new Map>(); - const removed = new Map>(); - const updated = new Map>(); + const added: Record> = {}; + const removed: Record> = {}; + const updated: Record> = {}; // this might be needed only in same edge cases, like during collab, when `isDeleted` elements get removed or when we (un)intentionally remove the elements for (const prevElement of prevElements.values()) { @@ -918,7 +922,7 @@ export class ElementsChange implements Change { ElementsChange.stripIrrelevantProps, ); - removed.set(prevElement.id, delta); + removed[prevElement.id] = delta; } } @@ -938,7 +942,7 @@ export class ElementsChange implements Change { ElementsChange.stripIrrelevantProps, ); - added.set(nextElement.id, delta); + added[nextElement.id] = delta; continue; } @@ -959,9 +963,9 @@ export class ElementsChange implements Change { ) { // notice that other props could have been updated as well if (prevElement.isDeleted && !nextElement.isDeleted) { - added.set(nextElement.id, delta); + added[nextElement.id] = delta; } else { - removed.set(nextElement.id, delta); + removed[nextElement.id] = delta; } continue; @@ -969,7 +973,7 @@ export class ElementsChange implements Change { // making sure there are at least some changes if (!Delta.isEmpty(delta)) { - updated.set(nextElement.id, delta); + updated[nextElement.id] = delta; } } } @@ -978,15 +982,23 @@ export class ElementsChange implements Change { } public static empty() { - return ElementsChange.create(new Map(), new Map(), new Map()); + return ElementsChange.create({}, {}, {}); + } + + public static load(data: { + added: Record>; + removed: Record>; + updated: Record>; + }) { + return ElementsChange.create(data.added, data.removed, data.updated); } public inverse(): ElementsChange { - const inverseInternal = (deltas: Map>) => { - const inversedDeltas = new Map>(); + const inverseInternal = (deltas: Record>) => { + const inversedDeltas: Record> = {}; - for (const [id, delta] of deltas.entries()) { - inversedDeltas.set(id, Delta.create(delta.inserted, delta.deleted)); + for (const [id, delta] of Object.entries(deltas)) { + inversedDeltas[id] = Delta.create(delta.inserted, delta.deleted); } return inversedDeltas; @@ -1002,9 +1014,9 @@ export class ElementsChange implements Change { public isEmpty(): boolean { return ( - this.added.size === 0 && - this.removed.size === 0 && - this.updated.size === 0 + Object.keys(this.added).length === 0 && + Object.keys(this.removed).length === 0 && + Object.keys(this.updated).length === 0 ); } @@ -1036,11 +1048,11 @@ export class ElementsChange implements Change { }; const applyLatestChangesInternal = ( - deltas: Map>, + deltas: Record>, ) => { - const modifiedDeltas = new Map>(); + const modifiedDeltas: Record> = {}; - for (const [id, delta] of deltas.entries()) { + for (const [id, delta] of Object.entries(deltas)) { const existingElement = elements.get(id); if (existingElement) { @@ -1051,9 +1063,9 @@ export class ElementsChange implements Change { "inserted", ); - modifiedDeltas.set(id, modifiedDelta); + modifiedDeltas[id] = modifiedDelta; } else { - modifiedDeltas.set(id, delta); + modifiedDeltas[id] = delta; } } @@ -1158,8 +1170,8 @@ export class ElementsChange implements Change { flags, ); - return (deltas: Map>) => - Array.from(deltas.entries()).reduce((acc, [id, delta]) => { + return (deltas: Record>) => + Object.entries(deltas).reduce((acc, [id, delta]) => { const element = getElement(id, delta.inserted); if (element) { @@ -1331,20 +1343,21 @@ export class ElementsChange implements Change { }; // removed delta is affecting the bindings always, as all the affected elements of the removed elements need to be unbound - for (const [id] of this.removed) { + for (const id of Object.keys(this.removed)) { ElementsChange.unbindAffected(prevElements, nextElements, id, updater); } // added delta is affecting the bindings always, all the affected elements of the added elements need to be rebound - for (const [id] of this.added) { + for (const id of Object.keys(this.added)) { ElementsChange.rebindAffected(prevElements, nextElements, id, updater); } // updated delta is affecting the binding only in case it contains changed binding or bindable property - for (const [id] of Array.from(this.updated).filter(([_, delta]) => - Object.keys({ ...delta.deleted, ...delta.inserted }).find((prop) => - bindingProperties.has(prop as BindingProp | BindableProp), - ), + for (const [id] of Array.from(Object.entries(this.updated)).filter( + ([_, delta]) => + Object.keys({ ...delta.deleted, ...delta.inserted }).find((prop) => + bindingProperties.has(prop as BindingProp | BindableProp), + ), )) { const updatedElement = nextElements.get(id); if (!updatedElement || updatedElement.isDeleted) { @@ -1367,16 +1380,16 @@ export class ElementsChange implements Change { nextAffectedElements, ); - for (const [id, delta] of added) { - this.added.set(id, delta); + for (const [id, delta] of Object.entries(added)) { + this.added[id] = delta; } - for (const [id, delta] of removed) { - this.removed.set(id, delta); + for (const [id, delta] of Object.entries(removed)) { + this.removed[id] = delta; } - for (const [id, delta] of updated) { - this.updated.set(id, delta); + for (const [id, delta] of Object.entries(updated)) { + this.updated[id] = delta; } return nextAffectedElements; diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index f16f88657..debc84f50 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -739,6 +739,7 @@ class App extends React.Component { updateFrameRendering: this.updateFrameRendering, toggleSidebar: this.toggleSidebar, onChange: (cb) => this.onChangeEmitter.on(cb), + onIncrement: (cb) => this.store.onStoreIncrementEmitter.on(cb), onPointerDown: (cb) => this.onPointerDownEmitter.on(cb), onPointerUp: (cb) => this.onPointerUpEmitter.on(cb), onScrollChange: (cb) => this.onScrollChangeEmitter.on(cb), @@ -2462,6 +2463,7 @@ class App extends React.Component { this.store.onStoreIncrementEmitter.on((increment) => { this.history.record(increment.elementsChange, increment.appStateChange); + this.props.onIncrement?.(increment); }); this.scene.onUpdate(this.triggerRender); diff --git a/packages/excalidraw/index.tsx b/packages/excalidraw/index.tsx index 0af660f19..7014fab26 100644 --- a/packages/excalidraw/index.tsx +++ b/packages/excalidraw/index.tsx @@ -22,6 +22,7 @@ polyfill(); const ExcalidrawBase = (props: ExcalidrawProps) => { const { onChange, + onIncrement, initialData, excalidrawAPI, isCollaborating = false, @@ -111,6 +112,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => { ; /** * Represent an increment to the Store. */ -class StoreIncrementEvent { +export class StoreIncrementEvent { constructor( public readonly elementsChange: ElementsChange, public readonly appStateChange: AppStateChange, @@ -395,6 +395,7 @@ export class Snapshot { !prev || !next || prev.id !== next.id || + prev.version !== next.version || prev.versionNonce !== next.versionNonce ) { return true; diff --git a/packages/excalidraw/types.ts b/packages/excalidraw/types.ts index d1d1824f0..a0e10f8ed 100644 --- a/packages/excalidraw/types.ts +++ b/packages/excalidraw/types.ts @@ -40,7 +40,7 @@ import type { IMAGE_MIME_TYPES, MIME_TYPES } from "./constants"; import type { ContextMenuItems } from "./components/ContextMenu"; import type { SnapLine } from "./snapping"; import type { Merge, MaybePromise, ValueOf, MakeBrand } from "./utility-types"; -import type { StoreActionType } from "./store"; +import type { StoreActionType, StoreIncrementEvent } from "./store"; export type SocketId = string & { _brand: "SocketId" }; @@ -498,6 +498,7 @@ export interface ExcalidrawProps { appState: AppState, files: BinaryFiles, ) => void; + onIncrement?: (event: StoreIncrementEvent) => void; initialData?: | (() => MaybePromise) | MaybePromise; @@ -782,6 +783,9 @@ export interface ExcalidrawImperativeAPI { files: BinaryFiles, ) => void, ) => UnsubscribeCallback; + onIncrement: ( + callback: (event: StoreIncrementEvent) => void, + ) => UnsubscribeCallback; onPointerDown: ( callback: ( activeTool: AppState["activeTool"],