Rewrite restore to guard against missing migrations (#1664)

This commit is contained in:
David Luzar 2020-05-28 11:41:34 +02:00 committed by GitHub
parent 56f8bc092d
commit 44a88d2d58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 131 additions and 117 deletions

View File

@ -16,7 +16,7 @@ import {
getCommonBounds, getCommonBounds,
getCursorForResizingElement, getCursorForResizingElement,
getPerfectElementSize, getPerfectElementSize,
normalizeDimensions, getNormalizedDimensions,
getElementMap, getElementMap,
getDrawingVersion, getDrawingVersion,
getSyncableElements, getSyncableElements,
@ -2527,7 +2527,12 @@ class App extends React.Component<any, AppState> {
return; return;
} }
normalizeDimensions(draggingElement); if (draggingElement) {
mutateElement(
draggingElement,
getNormalizedDimensions(draggingElement),
);
}
if (resizingElement) { if (resizingElement) {
history.resumeRecording(); history.resumeRecording();

View File

@ -1,17 +1,11 @@
import { Point } from "../types";
import { import {
ExcalidrawElement, ExcalidrawElement,
ExcalidrawTextElement,
FontFamily, FontFamily,
ExcalidrawSelectionElement,
} from "../element/types"; } from "../element/types";
import { AppState } from "../types"; import { AppState } from "../types";
import { DataState } from "./types"; import { DataState } from "./types";
import { import { isInvisiblySmallElement, getNormalizedDimensions } from "../element";
isInvisiblySmallElement,
normalizeDimensions,
isTextElement,
} from "../element";
import { calculateScrollCenter } from "../scene"; import { calculateScrollCenter } from "../scene";
import { randomId } from "../random"; import { randomId } from "../random";
import { DEFAULT_TEXT_ALIGN, DEFAULT_FONT_FAMILY } from "../appState"; import { DEFAULT_TEXT_ALIGN, DEFAULT_FONT_FAMILY } from "../appState";
@ -26,96 +20,105 @@ const getFontFamilyByName = (fontFamilyName: string): FontFamily => {
return DEFAULT_FONT_FAMILY; return DEFAULT_FONT_FAMILY;
}; };
export const restore = ( function migrateElementWithProperties<T extends ExcalidrawElement>(
// we're making the elements mutable for this API because we want to element: T,
// efficiently remove/tweak properties on them (to migrate old scenes) extra: Omit<T, keyof ExcalidrawElement>,
savedElements: readonly Mutable<ExcalidrawElement>[], ): T {
savedState: AppState | null, const base: Pick<T, keyof ExcalidrawElement> = {
opts?: { scrollToContent: boolean }, type: element.type,
): DataState => { // all elements must have version > 0 so getDrawingVersion() will pick up
const elements = savedElements // newly added elements
.filter((el) => {
// filtering out selection, which is legacy, no longer kept in elements,
// and causing issues if retained
return el.type !== "selection" && !isInvisiblySmallElement(el);
})
.map((element) => {
let points: Point[] = [];
if (element.type === "arrow") {
if (Array.isArray(element.points)) {
// if point array is empty, add one point to the arrow
// this is used as fail safe to convert incoming data to a valid
// arrow. In the new arrow, width and height are not being usde
points = element.points.length > 0 ? element.points : [[0, 0]];
} else {
// convert old arrow type to a new one
// old arrow spec used width and height
// to determine the endpoints
points = [
[0, 0],
[element.width, element.height],
];
}
element.points = points;
} else if (element.type === "line" || element.type === "draw") {
// old spec, pre-arrows
// old spec, post-arrows
if (!Array.isArray(element.points) || element.points.length === 0) {
points = [
[0, 0],
[element.width, element.height],
];
} else {
points = element.points;
}
element.points = points;
} else {
if (isTextElement(element)) {
if ("font" in element) {
const [fontPx, fontFamily]: [
string,
string,
] = (element as any).font.split(" ");
(element as Mutable<ExcalidrawTextElement>).fontSize = parseInt(
fontPx,
10,
);
(element as Mutable<
ExcalidrawTextElement
>).fontFamily = getFontFamilyByName(fontFamily);
delete (element as any).font;
}
if (!element.textAlign) {
element.textAlign = DEFAULT_TEXT_ALIGN;
}
}
normalizeDimensions(element);
// old spec, where non-linear elements used to have empty points arrays
if ("points" in element) {
delete element.points;
}
}
return {
...element,
// all elements must have version > 0 so getDrawingVersion() will pick
// up newly added elements
version: element.version || 1, version: element.version || 1,
id: element.id || randomId(), versionNonce: element.versionNonce ?? 0,
isDeleted: false, isDeleted: false,
id: element.id || randomId(),
fillStyle: element.fillStyle || "hachure", fillStyle: element.fillStyle || "hachure",
strokeWidth: element.strokeWidth || 1, strokeWidth: element.strokeWidth || 1,
strokeStyle: element.strokeStyle ?? "solid", strokeStyle: element.strokeStyle ?? "solid",
roughness: element.roughness ?? 1, roughness: element.roughness ?? 1,
opacity: opacity: element.opacity == null ? 100 : element.opacity,
element.opacity === null || element.opacity === undefined angle: element.angle || 0,
? 100 x: element.x || 0,
: element.opacity, y: element.y || 0,
angle: element.angle ?? 0, strokeColor: element.strokeColor,
backgroundColor: element.backgroundColor,
width: element.width || 0,
height: element.height || 0,
seed: element.seed ?? 1,
groupIds: element.groupIds || [], groupIds: element.groupIds || [],
}; };
return {
...base,
...getNormalizedDimensions(base),
...extra,
} as T;
}
const migrateElement = (
element: Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
): typeof element => {
switch (element.type) {
case "text":
let fontSize = element.fontSize;
let fontFamily = element.fontFamily;
if ("font" in element) {
const [fontPx, _fontFamily]: [
string,
string,
] = (element as any).font.split(" ");
fontSize = parseInt(fontPx, 10);
fontFamily = getFontFamilyByName(_fontFamily);
}
return migrateElementWithProperties(element, {
fontSize,
fontFamily,
text: element.text ?? "",
baseline: element.baseline,
textAlign: element.textAlign ?? DEFAULT_TEXT_ALIGN,
}); });
case "draw":
case "line":
case "arrow": {
return migrateElementWithProperties(element, {
points:
// migrate old arrow model to new one
!Array.isArray(element.points) || element.points.length < 2
? [
[0, 0],
[element.width, element.height],
]
: element.points,
});
}
// generic elements
case "ellipse":
case "rectangle":
case "diamond":
return migrateElementWithProperties(element, {});
// don't use default case so as to catch a missing an element type case
// (we also don't want to throw, but instead return void so we
// filter out these unsupported elements from the restored array)
}
};
export const restore = (
savedElements: readonly ExcalidrawElement[],
savedState: AppState | null,
opts?: { scrollToContent: boolean },
): DataState => {
const elements = savedElements.reduce((elements, element) => {
// filtering out selection, which is legacy, no longer kept in elements,
// and causing issues if retained
if (element.type !== "selection" && !isInvisiblySmallElement(element)) {
const migratedElement = migrateElement(element);
if (migratedElement) {
elements.push(migratedElement);
}
}
return elements;
}, [] as ExcalidrawElement[]);
if (opts?.scrollToContent && savedState) { if (opts?.scrollToContent && savedState) {
savedState = { ...savedState, ...calculateScrollCenter(elements) }; savedState = { ...savedState, ...calculateScrollCenter(elements) };

View File

@ -45,7 +45,7 @@ export {
getPerfectElementSize, getPerfectElementSize,
isInvisiblySmallElement, isInvisiblySmallElement,
resizePerfectLineForNWHandler, resizePerfectLineForNWHandler,
normalizeDimensions, getNormalizedDimensions,
} from "./sizeHelpers"; } from "./sizeHelpers";
export { showSelectedShapeActions } from "./showSelectedShapeActions"; export { showSelectedShapeActions } from "./showSelectedShapeActions";

View File

@ -81,31 +81,32 @@ export const resizePerfectLineForNWHandler = (
} }
}; };
/** export const getNormalizedDimensions = (
* @returns {boolean} whether element was normalized element: Pick<ExcalidrawElement, "width" | "height" | "x" | "y">,
*/ ): {
export const normalizeDimensions = ( width: ExcalidrawElement["width"];
element: ExcalidrawElement | null, height: ExcalidrawElement["height"];
): element is ExcalidrawElement => { x: ExcalidrawElement["x"];
if (!element || (element.width >= 0 && element.height >= 0)) { y: ExcalidrawElement["y"];
return false; } => {
} const ret = {
width: element.width,
height: element.height,
x: element.x,
y: element.y,
};
if (element.width < 0) { if (element.width < 0) {
const nextWidth = Math.abs(element.width); const nextWidth = Math.abs(element.width);
mutateElement(element, { ret.width = nextWidth;
width: nextWidth, ret.x = element.x - nextWidth;
x: element.x - nextWidth,
});
} }
if (element.height < 0) { if (element.height < 0) {
const nextHeight = Math.abs(element.height); const nextHeight = Math.abs(element.height);
mutateElement(element, { ret.height = nextHeight;
height: nextHeight, ret.y = element.y - nextHeight;
y: element.y - nextHeight,
});
} }
return true; return ret;
}; };

View File

@ -24,12 +24,17 @@ type _ExcalidrawElementBase = Readonly<{
groupIds: GroupId[]; groupIds: GroupId[];
}>; }>;
export type ExcalidrawSelectionElement = _ExcalidrawElementBase & {
type: "selection";
};
/** /**
* These are elements that don't have any additional properties. * These are elements that don't have any additional properties.
*/ */
export type ExcalidrawGenericElement = _ExcalidrawElementBase & { export type ExcalidrawGenericElement =
type: "selection" | "rectangle" | "diamond" | "ellipse"; | ExcalidrawSelectionElement
}; | (_ExcalidrawElementBase & {
type: "rectangle" | "diamond" | "ellipse";
});
/** /**
* ExcalidrawElement should be JSON serializable and (eventually) contain * ExcalidrawElement should be JSON serializable and (eventually) contain