feat: apply deltas API (#9869)

This commit is contained in:
Marcel Mraz
2025-08-15 15:25:56 +02:00
committed by GitHub
parent dda3affcb0
commit 2535d73054
8 changed files with 750 additions and 152 deletions

View File

@@ -233,6 +233,8 @@ import {
hitElementBoundingBox,
isLineElement,
isSimpleArrow,
StoreDelta,
type ApplyToOptions,
} from "@excalidraw/element";
import type { LocalPoint, Radians } from "@excalidraw/math";
@@ -259,6 +261,7 @@ import type {
MagicGenerationData,
ExcalidrawArrowElement,
ExcalidrawElbowArrowElement,
SceneElementsMap,
} from "@excalidraw/element/types";
import type { Mutable, ValueOf } from "@excalidraw/common/utility-types";
@@ -697,6 +700,7 @@ class App extends React.Component<AppProps, AppState> {
if (excalidrawAPI) {
const api: ExcalidrawImperativeAPI = {
updateScene: this.updateScene,
applyDeltas: this.applyDeltas,
mutateElement: this.mutateElement,
updateLibrary: this.library.updateLibrary,
addFiles: this.addFiles,
@@ -3938,6 +3942,27 @@ class App extends React.Component<AppProps, AppState> {
},
);
public applyDeltas = (
deltas: StoreDelta[],
options?: ApplyToOptions,
): [SceneElementsMap, AppState, boolean] => {
// squash all deltas together, starting with a fresh new delta instance
const aggregatedDelta = StoreDelta.squash(...deltas);
// create new instance of elements map & appState, so we don't accidentaly mutate existing ones
const nextAppState = { ...this.state };
const nextElements = new Map(
this.scene.getElementsMapIncludingDeleted(),
) as SceneElementsMap;
return StoreDelta.applyTo(
aggregatedDelta,
nextElements,
nextAppState,
options,
);
};
public mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
element: TElement,
updates: ElementUpdate<TElement>,

View File

@@ -175,7 +175,7 @@ export class History {
let nextAppState = appState;
let containsVisibleChange = false;
// iterate through the history entries in case ;they result in no visible changes
// iterate through the history entries in case they result in no visible changes
while (historyDelta) {
try {
[nextElements, nextAppState, containsVisibleChange] =

View File

@@ -137,7 +137,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 18,
"version": 13,
"width": 100,
"x": -100,
"y": -50,
@@ -258,7 +258,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 14,
"version": 10,
"width": 50,
"x": 100,
"y": 100,
@@ -305,11 +305,11 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"type": "arrow",
},
],
"version": 12,
"version": 9,
},
"inserted": {
"boundElements": [],
"version": 11,
"version": 8,
},
},
"id4": {
@@ -384,7 +384,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"id0": {
"deleted": {
"boundElements": [],
"version": 18,
"version": 13,
},
"inserted": {
"boundElements": [
@@ -393,7 +393,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"type": "arrow",
},
],
"version": 17,
"version": 12,
},
},
"id4": {
@@ -735,7 +735,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 19,
"version": 14,
"width": 100,
"x": 150,
"y": -50,
@@ -884,7 +884,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"id0": {
"deleted": {
"boundElements": [],
"version": 19,
"version": 14,
},
"inserted": {
"boundElements": [
@@ -893,7 +893,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"type": "arrow",
},
],
"version": 18,
"version": 13,
},
},
"id4": {
@@ -1242,7 +1242,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"strokeWidth": 2,
"type": "arrow",
"updated": 1,
"version": 11,
"version": 10,
"width": 98,
"x": 1,
"y": 0,
@@ -1421,12 +1421,12 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"focus": 0,
"gap": 1,
},
"version": 11,
"version": 10,
},
"inserted": {
"endBinding": null,
"startBinding": null,
"version": 8,
"version": 7,
},
},
},
@@ -1639,7 +1639,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 12,
"version": 8,
"width": 100,
"x": -100,
"y": -50,
@@ -1674,7 +1674,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 11,
"version": 7,
"width": 100,
"x": 100,
"y": -50,
@@ -1772,11 +1772,11 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"type": "arrow",
},
],
"version": 12,
"version": 8,
},
"inserted": {
"boundElements": [],
"version": 9,
"version": 7,
},
},
"id1": {
@@ -1787,11 +1787,11 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"type": "arrow",
},
],
"version": 11,
"version": 7,
},
"inserted": {
"boundElements": [],
"version": 8,
"version": 6,
},
},
},
@@ -2202,7 +2202,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 7,
"version": 5,
"width": 100,
"x": -100,
"y": -50,
@@ -2237,7 +2237,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 8,
"version": 6,
"width": 100,
"x": 500,
"y": -500,
@@ -2473,7 +2473,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"type": "arrow",
},
],
"version": 7,
"version": 5,
},
"inserted": {
"boundElements": [],
@@ -2488,7 +2488,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"type": "arrow",
},
],
"version": 8,
"version": 6,
},
"inserted": {
"boundElements": [],
@@ -2720,7 +2720,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"textAlign": "left",
"type": "text",
"updated": 1,
"version": 10,
"version": 7,
"verticalAlign": "top",
"width": 30,
"x": 15,
@@ -2780,11 +2780,11 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"id5": {
"deleted": {
"containerId": null,
"version": 10,
"version": 7,
},
"inserted": {
"containerId": "id0",
"version": 9,
"version": 6,
},
},
},
@@ -2937,7 +2937,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 11,
"version": 9,
"width": 100,
"x": 10,
"y": 10,
@@ -2975,7 +2975,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"textAlign": "left",
"type": "text",
"updated": 1,
"version": 9,
"version": 8,
"verticalAlign": "top",
"width": 100,
"x": 15,
@@ -3014,7 +3014,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"textAlign": "left",
"type": "text",
"updated": 1,
"version": 11,
"version": 7,
"verticalAlign": "top",
"width": 30,
"x": 15,
@@ -3041,7 +3041,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"deleted": {
"containerId": "id0",
"isDeleted": true,
"version": 9,
"version": 8,
},
"inserted": {
"angle": 0,
@@ -3071,7 +3071,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"text": "que pasa",
"textAlign": "left",
"type": "text",
"version": 8,
"version": 7,
"verticalAlign": "top",
"width": 100,
"x": 15,
@@ -3084,7 +3084,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"id0": {
"deleted": {
"boundElements": [],
"version": 11,
"version": 9,
},
"inserted": {
"boundElements": [
@@ -3093,7 +3093,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"type": "text",
},
],
"version": 10,
"version": 8,
},
},
},
@@ -3246,7 +3246,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 10,
"version": 9,
"width": 100,
"x": 10,
"y": 10,
@@ -3356,7 +3356,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"type": "text",
},
],
"version": 10,
"version": 9,
},
"inserted": {
"boundElements": [
@@ -3365,7 +3365,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"type": "text",
},
],
"version": 9,
"version": 8,
},
},
"id1": {
@@ -4093,7 +4093,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"textAlign": "left",
"type": "text",
"updated": 1,
"version": 12,
"version": 8,
"verticalAlign": "top",
"width": 80,
"x": 15,
@@ -4155,11 +4155,11 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"id1": {
"deleted": {
"containerId": "id0",
"version": 12,
"version": 8,
},
"inserted": {
"containerId": null,
"version": 9,
"version": 7,
},
},
},
@@ -4310,7 +4310,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 11,
"version": 7,
"width": 100,
"x": 10,
"y": 10,
@@ -4424,11 +4424,11 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"type": "text",
},
],
"version": 11,
"version": 7,
},
"inserted": {
"boundElements": [],
"version": 8,
"version": 6,
},
},
},
@@ -4617,7 +4617,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"textAlign": "left",
"type": "text",
"updated": 1,
"version": 7,
"version": 8,
"verticalAlign": "top",
"width": 80,
"x": 15,
@@ -5028,7 +5028,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 8,
"version": 7,
"width": 100,
"x": 10,
"y": 10,
@@ -5113,7 +5113,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"version": 8,
"version": 7,
"width": 100,
"x": 10,
"y": 10,
@@ -5126,7 +5126,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
},
],
"isDeleted": true,
"version": 7,
"version": 6,
},
},
},
@@ -5316,7 +5316,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"textAlign": "left",
"type": "text",
"updated": 1,
"version": 8,
"version": 7,
"verticalAlign": "top",
"width": 100,
"x": 15,
@@ -5371,7 +5371,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"text": "que pasa",
"textAlign": "left",
"type": "text",
"version": 8,
"version": 7,
"verticalAlign": "top",
"width": 100,
"x": 15,
@@ -5380,7 +5380,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"inserted": {
"containerId": "id0",
"isDeleted": true,
"version": 7,
"version": 6,
},
},
},
@@ -5527,7 +5527,7 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 10,
"version": 9,
"width": 100,
"x": 10,
"y": 10,
@@ -5784,7 +5784,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 4,
"version": 5,
"width": 100,
"x": 0,
"y": 0,
@@ -5816,7 +5816,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 3,
"version": 4,
"width": 100,
"x": 100,
"y": 100,
@@ -6072,7 +6072,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 7,
"version": 8,
"width": 10,
"x": 20,
"y": 0,
@@ -6102,7 +6102,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 7,
"version": 8,
"width": 10,
"x": 50,
"y": 50,
@@ -6205,11 +6205,11 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"id3": {
"deleted": {
"backgroundColor": "#ffc9c9",
"version": 7,
"version": 8,
},
"inserted": {
"backgroundColor": "transparent",
"version": 6,
"version": 7,
},
},
},
@@ -6251,12 +6251,12 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"updated": {
"id8": {
"deleted": {
"version": 7,
"version": 8,
"x": 50,
"y": 50,
},
"inserted": {
"version": 6,
"version": 7,
"x": 30,
"y": 30,
},
@@ -7104,7 +7104,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"strokeWidth": 2,
"type": "arrow",
"updated": 1,
"version": 7,
"version": 8,
"width": 10,
"x": 0,
"y": 0,
@@ -7344,7 +7344,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 7,
"version": 8,
"width": 10,
"x": 10,
"y": 0,
@@ -7393,11 +7393,11 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"id0": {
"deleted": {
"backgroundColor": "#ffec99",
"version": 7,
"version": 8,
},
"inserted": {
"backgroundColor": "transparent",
"version": 6,
"version": 7,
},
},
},
@@ -10326,7 +10326,7 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 7,
"version": 8,
"width": 10,
"x": 10,
"y": 0,
@@ -10378,14 +10378,14 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"version": 7,
"version": 8,
"width": 10,
"x": 10,
"y": 0,
},
"inserted": {
"isDeleted": true,
"version": 6,
"version": 7,
},
},
},
@@ -15584,7 +15584,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 8,
"version": 6,
"width": 100,
"x": -100,
"y": -50,
@@ -15622,7 +15622,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"textAlign": "center",
"type": "text",
"updated": 1,
"version": 6,
"version": 5,
"verticalAlign": "middle",
"width": 30,
"x": -65,
@@ -15658,7 +15658,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 7,
"version": 5,
"width": 100,
"x": 100,
"y": -50,
@@ -15768,7 +15768,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"type": "arrow",
},
],
"version": 8,
"version": 6,
},
"inserted": {
"boundElements": [],
@@ -15783,7 +15783,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"type": "arrow",
},
],
"version": 7,
"version": 5,
},
"inserted": {
"boundElements": [],
@@ -16279,7 +16279,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 8,
"version": 6,
"width": 100,
"x": -100,
"y": -50,
@@ -16317,7 +16317,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"textAlign": "center",
"type": "text",
"updated": 1,
"version": 8,
"version": 6,
"verticalAlign": "middle",
"width": 30,
"x": -65,
@@ -16353,7 +16353,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 7,
"version": 5,
"width": 100,
"x": 100,
"y": -50,
@@ -16729,7 +16729,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"type": "arrow",
},
],
"version": 8,
"version": 6,
},
"inserted": {
"boundElements": [],
@@ -16744,7 +16744,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"type": "arrow",
},
],
"version": 7,
"version": 5,
},
"inserted": {
"boundElements": [],
@@ -16904,7 +16904,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 12,
"version": 10,
"width": 100,
"x": -100,
"y": -50,
@@ -16942,7 +16942,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"textAlign": "center",
"type": "text",
"updated": 1,
"version": 12,
"version": 10,
"verticalAlign": "middle",
"width": 30,
"x": -65,
@@ -16978,7 +16978,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 9,
"version": 7,
"width": 100,
"x": 100,
"y": -50,
@@ -17119,7 +17119,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"text": "ola",
"textAlign": "left",
"type": "text",
"version": 9,
"version": 8,
"verticalAlign": "top",
"width": 100,
"x": -200,
@@ -17127,7 +17127,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
},
"inserted": {
"isDeleted": true,
"version": 8,
"version": 7,
},
},
"id2": {
@@ -17243,7 +17243,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"containerId": "id0",
"height": 25,
"textAlign": "center",
"version": 10,
"version": 9,
"verticalAlign": "middle",
"width": 30,
"x": -65,
@@ -17253,7 +17253,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"containerId": null,
"height": 100,
"textAlign": "left",
"version": 9,
"version": 8,
"verticalAlign": "top",
"width": 100,
"x": -200,
@@ -17354,7 +17354,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"type": "arrow",
},
],
"version": 12,
"version": 10,
},
"inserted": {
"boundElements": [],
@@ -17369,7 +17369,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"type": "arrow",
},
],
"version": 9,
"version": 7,
},
"inserted": {
"boundElements": [],
@@ -17527,7 +17527,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 8,
"version": 6,
"width": 100,
"x": -100,
"y": -50,
@@ -17565,7 +17565,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"textAlign": "center",
"type": "text",
"updated": 1,
"version": 8,
"version": 6,
"verticalAlign": "middle",
"width": 30,
"x": -65,
@@ -17601,7 +17601,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 5,
"version": 4,
"width": 100,
"x": 100,
"y": -50,
@@ -17689,7 +17689,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"id0": {
"deleted": {
"isDeleted": false,
"version": 8,
"version": 6,
},
"inserted": {
"isDeleted": true,
@@ -17699,7 +17699,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"id1": {
"deleted": {
"isDeleted": false,
"version": 8,
"version": 6,
},
"inserted": {
"isDeleted": true,
@@ -18239,7 +18239,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 8,
"version": 6,
"width": 100,
"x": -100,
"y": -50,
@@ -18277,7 +18277,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"textAlign": "center",
"type": "text",
"updated": 1,
"version": 8,
"version": 6,
"verticalAlign": "middle",
"width": 30,
"x": -65,
@@ -18402,7 +18402,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"id0": {
"deleted": {
"isDeleted": false,
"version": 8,
"version": 6,
},
"inserted": {
"isDeleted": true,
@@ -18412,7 +18412,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"id1": {
"deleted": {
"isDeleted": false,
"version": 8,
"version": 6,
},
"inserted": {
"isDeleted": true,

View File

@@ -801,6 +801,7 @@ export type UnsubscribeCallback = () => void;
export interface ExcalidrawImperativeAPI {
updateScene: InstanceType<typeof App>["updateScene"];
applyDeltas: InstanceType<typeof App>["applyDeltas"];
mutateElement: InstanceType<typeof App>["mutateElement"];
updateLibrary: InstanceType<typeof Library>["updateLibrary"];
resetScene: InstanceType<typeof App>["resetScene"];