mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Review fixes
This commit is contained in:
parent
d671730971
commit
1ed4590c69
6 changed files with 102 additions and 79 deletions
|
@ -735,6 +735,25 @@ export const arrayToList = <T>(array: readonly T[]): Node<T>[] =>
|
||||||
return acc;
|
return acc;
|
||||||
}, [] as Node<T>[]);
|
}, [] as Node<T>[]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a readonly array or map into an iterable.
|
||||||
|
* Useful for avoiding entry allocations when iterating object / map on each iteration.
|
||||||
|
*/
|
||||||
|
export const toIterable = <T>(
|
||||||
|
values: readonly T[] | ReadonlyMap<string, T>,
|
||||||
|
): Iterable<T> => {
|
||||||
|
return Array.isArray(values) ? values : values.values();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a readonly array or map into an array.
|
||||||
|
*/
|
||||||
|
export const toArray = <T>(
|
||||||
|
values: readonly T[] | ReadonlyMap<string, T>,
|
||||||
|
): T[] => {
|
||||||
|
return Array.from(toIterable(values));
|
||||||
|
};
|
||||||
|
|
||||||
export const isTestEnv = () => import.meta.env.MODE === ENV.TEST;
|
export const isTestEnv = () => import.meta.env.MODE === ENV.TEST;
|
||||||
|
|
||||||
export const isDevEnv = () => import.meta.env.MODE === ENV.DEVELOPMENT;
|
export const isDevEnv = () => import.meta.env.MODE === ENV.DEVELOPMENT;
|
||||||
|
|
|
@ -6,14 +6,13 @@ import {
|
||||||
toBrandedType,
|
toBrandedType,
|
||||||
isDevEnv,
|
isDevEnv,
|
||||||
isTestEnv,
|
isTestEnv,
|
||||||
isReadonlyArray,
|
toArray,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
import { isNonDeletedElement } from "@excalidraw/element";
|
import { isNonDeletedElement } from "@excalidraw/element";
|
||||||
import { isFrameLikeElement } from "@excalidraw/element/typeChecks";
|
import { isFrameLikeElement } from "@excalidraw/element/typeChecks";
|
||||||
import { getElementsInGroup } from "@excalidraw/element/groups";
|
import { getElementsInGroup } from "@excalidraw/element/groups";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
orderByFractionalIndex,
|
|
||||||
syncInvalidIndices,
|
syncInvalidIndices,
|
||||||
syncMovedIndices,
|
syncMovedIndices,
|
||||||
validateFractionalIndices,
|
validateFractionalIndices,
|
||||||
|
@ -268,19 +267,13 @@ class Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
replaceAllElements(nextElements: ElementsMapOrArray) {
|
replaceAllElements(nextElements: ElementsMapOrArray) {
|
||||||
// ts doesn't like `Array.isArray` of `instanceof Map`
|
// we do trust the insertion order on the map, though maybe we shouldn't and should prefer order defined by fractional indices
|
||||||
if (!isReadonlyArray(nextElements)) {
|
const _nextElements = toArray(nextElements);
|
||||||
// need to order by fractional indices to get the correct order
|
|
||||||
nextElements = orderByFractionalIndex(
|
|
||||||
Array.from(nextElements.values()) as OrderedExcalidrawElement[],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextFrameLikes: ExcalidrawFrameLikeElement[] = [];
|
const nextFrameLikes: ExcalidrawFrameLikeElement[] = [];
|
||||||
|
|
||||||
validateIndicesThrottled(nextElements);
|
validateIndicesThrottled(_nextElements);
|
||||||
|
|
||||||
this.elements = syncInvalidIndices(nextElements);
|
this.elements = syncInvalidIndices(_nextElements);
|
||||||
this.elementsMap.clear();
|
this.elementsMap.clear();
|
||||||
this.elements.forEach((element) => {
|
this.elements.forEach((element) => {
|
||||||
if (isFrameLikeElement(element)) {
|
if (isFrameLikeElement(element)) {
|
||||||
|
|
|
@ -316,7 +316,7 @@ export class Delta<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all the object1 keys that have distinct values.
|
* Returns sorted object1 keys that have distinct values.
|
||||||
*/
|
*/
|
||||||
public static getLeftDifferences<T extends {}>(
|
public static getLeftDifferences<T extends {}>(
|
||||||
object1: T,
|
object1: T,
|
||||||
|
@ -325,11 +325,11 @@ export class Delta<T> {
|
||||||
) {
|
) {
|
||||||
return Array.from(
|
return Array.from(
|
||||||
this.distinctKeysIterator("left", object1, object2, skipShallowCompare),
|
this.distinctKeysIterator("left", object1, object2, skipShallowCompare),
|
||||||
);
|
).sort();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all the object2 keys that have distinct values.
|
* Returns sorted object2 keys that have distinct values.
|
||||||
*/
|
*/
|
||||||
public static getRightDifferences<T extends {}>(
|
public static getRightDifferences<T extends {}>(
|
||||||
object1: T,
|
object1: T,
|
||||||
|
@ -338,7 +338,7 @@ export class Delta<T> {
|
||||||
) {
|
) {
|
||||||
return Array.from(
|
return Array.from(
|
||||||
this.distinctKeysIterator("right", object1, object2, skipShallowCompare),
|
this.distinctKeysIterator("right", object1, object2, skipShallowCompare),
|
||||||
);
|
).sort();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -430,7 +430,8 @@ export class AppStateDelta implements DeltaContainer<AppState> {
|
||||||
const delta = Delta.calculate(
|
const delta = Delta.calculate(
|
||||||
prevAppState,
|
prevAppState,
|
||||||
nextAppState,
|
nextAppState,
|
||||||
undefined,
|
// making the order of keys in deltas stable for hashing purposes
|
||||||
|
AppStateDelta.orderAppStateKeys,
|
||||||
AppStateDelta.postProcess,
|
AppStateDelta.postProcess,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -539,40 +540,6 @@ export class AppStateDelta implements DeltaContainer<AppState> {
|
||||||
return Delta.isEmpty(this.delta);
|
return Delta.isEmpty(this.delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* It is necessary to post process the partials in case of reference values,
|
|
||||||
* for which we need to calculate the real diff between `deleted` and `inserted`.
|
|
||||||
*/
|
|
||||||
private static postProcess<T extends ObservedAppState>(
|
|
||||||
deleted: Partial<T>,
|
|
||||||
inserted: Partial<T>,
|
|
||||||
): [Partial<T>, Partial<T>] {
|
|
||||||
try {
|
|
||||||
Delta.diffObjects(
|
|
||||||
deleted,
|
|
||||||
inserted,
|
|
||||||
"selectedElementIds",
|
|
||||||
// ts language server has a bit trouble resolving this, so we are giving it a little push
|
|
||||||
(_) => true as ValueOf<T["selectedElementIds"]>,
|
|
||||||
);
|
|
||||||
Delta.diffObjects(
|
|
||||||
deleted,
|
|
||||||
inserted,
|
|
||||||
"selectedGroupIds",
|
|
||||||
(prevValue) => (prevValue ?? false) as ValueOf<T["selectedGroupIds"]>,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
// if postprocessing fails it does not make sense to bubble up, but let's make sure we know about it
|
|
||||||
console.error(`Couldn't postprocess appstate change deltas.`);
|
|
||||||
|
|
||||||
if (isTestEnv() || isDevEnv()) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
return [deleted, inserted];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mutates `nextAppState` be filtering out state related to deleted elements.
|
* Mutates `nextAppState` be filtering out state related to deleted elements.
|
||||||
*
|
*
|
||||||
|
@ -807,6 +774,51 @@ export class AppStateDelta implements DeltaContainer<AppState> {
|
||||||
ObservedElementsAppState
|
ObservedElementsAppState
|
||||||
>;
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* It is necessary to post process the partials in case of reference values,
|
||||||
|
* for which we need to calculate the real diff between `deleted` and `inserted`.
|
||||||
|
*/
|
||||||
|
private static postProcess<T extends ObservedAppState>(
|
||||||
|
deleted: Partial<T>,
|
||||||
|
inserted: Partial<T>,
|
||||||
|
): [Partial<T>, Partial<T>] {
|
||||||
|
try {
|
||||||
|
Delta.diffObjects(
|
||||||
|
deleted,
|
||||||
|
inserted,
|
||||||
|
"selectedElementIds",
|
||||||
|
// ts language server has a bit trouble resolving this, so we are giving it a little push
|
||||||
|
(_) => true as ValueOf<T["selectedElementIds"]>,
|
||||||
|
);
|
||||||
|
Delta.diffObjects(
|
||||||
|
deleted,
|
||||||
|
inserted,
|
||||||
|
"selectedGroupIds",
|
||||||
|
(prevValue) => (prevValue ?? false) as ValueOf<T["selectedGroupIds"]>,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// if postprocessing fails it does not make sense to bubble up, but let's make sure we know about it
|
||||||
|
console.error(`Couldn't postprocess appstate change deltas.`);
|
||||||
|
|
||||||
|
if (isTestEnv() || isDevEnv()) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
return [deleted, inserted];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static orderAppStateKeys(partial: Partial<ObservedAppState>) {
|
||||||
|
const orderedPartial: { [key: string]: unknown } = {};
|
||||||
|
|
||||||
|
for (const key of Object.keys(partial).sort()) {
|
||||||
|
// relying on insertion order
|
||||||
|
orderedPartial[key] = partial[key as keyof ObservedAppState];
|
||||||
|
}
|
||||||
|
|
||||||
|
return orderedPartial as Partial<ObservedAppState>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ElementPartial<T extends ExcalidrawElement = ExcalidrawElement> = Omit<
|
type ElementPartial<T extends ExcalidrawElement = ExcalidrawElement> = Omit<
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { toIterable } from "@excalidraw/common";
|
||||||
|
|
||||||
import { isInvisiblySmallElement } from "./sizeHelpers";
|
import { isInvisiblySmallElement } from "./sizeHelpers";
|
||||||
import { isLinearElementType } from "./typeChecks";
|
import { isLinearElementType } from "./typeChecks";
|
||||||
|
|
||||||
|
@ -5,6 +7,7 @@ import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
|
ElementsMapOrArray,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -16,12 +19,10 @@ export const getSceneVersion = (elements: readonly ExcalidrawElement[]) =>
|
||||||
/**
|
/**
|
||||||
* Hashes elements' versionNonce (using djb2 algo). Order of elements matters.
|
* Hashes elements' versionNonce (using djb2 algo). Order of elements matters.
|
||||||
*/
|
*/
|
||||||
export const hashElementsVersion = (
|
export const hashElementsVersion = (elements: ElementsMapOrArray): number => {
|
||||||
elements: readonly ExcalidrawElement[],
|
|
||||||
): number => {
|
|
||||||
let hash = 5381;
|
let hash = 5381;
|
||||||
for (let i = 0; i < elements.length; i++) {
|
for (const element of toIterable(elements)) {
|
||||||
hash = (hash << 5) + hash + elements[i].versionNonce;
|
hash = (hash << 5) + hash + element.versionNonce;
|
||||||
}
|
}
|
||||||
return hash >>> 0; // Ensure unsigned 32-bit integer
|
return hash >>> 0; // Ensure unsigned 32-bit integer
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
isTestEnv,
|
isTestEnv,
|
||||||
randomId,
|
randomId,
|
||||||
Emitter,
|
Emitter,
|
||||||
|
toIterable,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import type { DTO, ValueOf } from "@excalidraw/common/utility-types";
|
import type { DTO, ValueOf } from "@excalidraw/common/utility-types";
|
||||||
|
@ -158,25 +159,25 @@ export class Store {
|
||||||
): SceneElementsMap {
|
): SceneElementsMap {
|
||||||
const movedElements = new Map<string, ExcalidrawElement>();
|
const movedElements = new Map<string, ExcalidrawElement>();
|
||||||
|
|
||||||
for (const [id, prevElement] of prevElements.entries()) {
|
for (const prevElement of toIterable(prevElements)) {
|
||||||
const nextElement = nextElements.get(id);
|
const nextElement = nextElements.get(prevElement.id);
|
||||||
|
|
||||||
if (!nextElement) {
|
if (!nextElement) {
|
||||||
// Nothing to care about here, element was forcefully deleted
|
// Nothing to care about here, element was forcefully deleted
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const elementSnapshot = this.snapshot.elements.get(id);
|
const elementSnapshot = this.snapshot.elements.get(prevElement.id);
|
||||||
|
|
||||||
// Checks for in progress async user action
|
// Checks for in progress async user action
|
||||||
if (!elementSnapshot) {
|
if (!elementSnapshot) {
|
||||||
// Detected yet uncomitted local element
|
// Detected yet uncomitted local element
|
||||||
nextElements.delete(id);
|
nextElements.delete(prevElement.id);
|
||||||
} else if (elementSnapshot.version < prevElement.version) {
|
} else if (elementSnapshot.version < prevElement.version) {
|
||||||
// Element was already commited, but the snapshot version is lower than current local version
|
// Element was already commited, but the snapshot version is lower than current local version
|
||||||
nextElements.set(id, elementSnapshot);
|
nextElements.set(prevElement.id, elementSnapshot);
|
||||||
// Mark the element as potentially moved, as it could have
|
// Mark the element as potentially moved, as it could have
|
||||||
movedElements.set(id, elementSnapshot);
|
movedElements.set(prevElement.id, elementSnapshot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -601,10 +602,10 @@ export class StoreSnapshot {
|
||||||
public getChangedElements(prevSnapshot: StoreSnapshot) {
|
public getChangedElements(prevSnapshot: StoreSnapshot) {
|
||||||
const changedElements: Record<string, OrderedExcalidrawElement> = {};
|
const changedElements: Record<string, OrderedExcalidrawElement> = {};
|
||||||
|
|
||||||
for (const [id, nextElement] of this.elements.entries()) {
|
for (const nextElement of toIterable(this.elements)) {
|
||||||
// Due to the structural clone inside `maybeClone`, we can perform just these reference checks
|
// Due to the structural clone inside `maybeClone`, we can perform just these reference checks
|
||||||
if (prevSnapshot.elements.get(id) !== nextElement) {
|
if (prevSnapshot.elements.get(nextElement.id) !== nextElement) {
|
||||||
changedElements[id] = nextElement;
|
changedElements[nextElement.id] = nextElement;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -794,26 +795,26 @@ export class StoreSnapshot {
|
||||||
|
|
||||||
const changedElements: SceneElementsMap = new Map() as SceneElementsMap;
|
const changedElements: SceneElementsMap = new Map() as SceneElementsMap;
|
||||||
|
|
||||||
for (const [id, prevElement] of this.elements) {
|
for (const prevElement of toIterable(this.elements)) {
|
||||||
const nextElement = nextElements.get(id);
|
const nextElement = nextElements.get(prevElement.id);
|
||||||
|
|
||||||
if (!nextElement) {
|
if (!nextElement) {
|
||||||
// element was deleted
|
// element was deleted
|
||||||
changedElements.set(
|
changedElements.set(
|
||||||
id,
|
prevElement.id,
|
||||||
newElementWith(prevElement, { isDeleted: true }),
|
newElementWith(prevElement, { isDeleted: true }),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [id, nextElement] of nextElements) {
|
for (const nextElement of toIterable(nextElements)) {
|
||||||
const prevElement = this.elements.get(id);
|
const prevElement = this.elements.get(nextElement.id);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!prevElement || // element was added
|
!prevElement || // element was added
|
||||||
prevElement.version < nextElement.version // element was updated
|
prevElement.version < nextElement.version // element was updated
|
||||||
) {
|
) {
|
||||||
changedElements.set(id, nextElement);
|
changedElements.set(nextElement.id, nextElement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -821,9 +822,7 @@ export class StoreSnapshot {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const changedElementsHash = hashElementsVersion(
|
const changedElementsHash = hashElementsVersion(changedElements);
|
||||||
Array.from(changedElements.values()),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
options.shouldCompareHashes &&
|
options.shouldCompareHashes &&
|
||||||
|
@ -843,15 +842,15 @@ export class StoreSnapshot {
|
||||||
private createElementsSnapshot(changedElements: SceneElementsMap) {
|
private createElementsSnapshot(changedElements: SceneElementsMap) {
|
||||||
const clonedElements = new Map() as SceneElementsMap;
|
const clonedElements = new Map() as SceneElementsMap;
|
||||||
|
|
||||||
for (const [id, prevElement] of this.elements) {
|
for (const prevElement of toIterable(this.elements)) {
|
||||||
// Clone previous elements, never delete, in case nextElements would be just a subset of previous elements
|
// Clone previous elements, never delete, in case nextElements would be just a subset of previous elements
|
||||||
// i.e. during collab, persist or whenenever isDeleted elements get cleared
|
// i.e. during collab, persist or whenenever isDeleted elements get cleared
|
||||||
clonedElements.set(id, prevElement);
|
clonedElements.set(prevElement.id, prevElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [id, changedElement] of changedElements) {
|
for (const changedElement of toIterable(changedElements)) {
|
||||||
// TODO: consider just creating new instance, once we can ensure that all reference properties on every element are immutable
|
// TODO: consider just creating new instance, once we can ensure that all reference properties on every element are immutable
|
||||||
clonedElements.set(id, deepCopyElement(changedElement));
|
clonedElements.set(changedElement.id, deepCopyElement(changedElement));
|
||||||
}
|
}
|
||||||
|
|
||||||
return clonedElements;
|
return clonedElements;
|
||||||
|
|
|
@ -307,7 +307,6 @@ import Scene from "@excalidraw/element/Scene";
|
||||||
import {
|
import {
|
||||||
Store,
|
Store,
|
||||||
CaptureUpdateAction,
|
CaptureUpdateAction,
|
||||||
StoreIncrement,
|
|
||||||
} from "@excalidraw/element/store";
|
} from "@excalidraw/element/store";
|
||||||
|
|
||||||
import type { ElementUpdate } from "@excalidraw/element/mutateElement";
|
import type { ElementUpdate } from "@excalidraw/element/mutateElement";
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue