restore.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. import {
  2. ExcalidrawElement,
  3. FontFamily,
  4. ExcalidrawSelectionElement,
  5. } from "../element/types";
  6. import { AppState, NormalizedZoomValue } from "../types";
  7. import { DataState, ImportedDataState } from "./types";
  8. import { isInvisiblySmallElement, getNormalizedDimensions } from "../element";
  9. import { isLinearElementType } from "../element/typeChecks";
  10. import { randomId } from "../random";
  11. import {
  12. FONT_FAMILY,
  13. DEFAULT_FONT_FAMILY,
  14. DEFAULT_TEXT_ALIGN,
  15. DEFAULT_VERTICAL_ALIGN,
  16. } from "../constants";
  17. import { getDefaultAppState } from "../appState";
  18. const getFontFamilyByName = (fontFamilyName: string): FontFamily => {
  19. for (const [id, fontFamilyString] of Object.entries(FONT_FAMILY)) {
  20. if (fontFamilyString.includes(fontFamilyName)) {
  21. return parseInt(id) as FontFamily;
  22. }
  23. }
  24. return DEFAULT_FONT_FAMILY;
  25. };
  26. const restoreElementWithProperties = <T extends ExcalidrawElement>(
  27. element: Required<T>,
  28. extra: Omit<Required<T>, keyof ExcalidrawElement>,
  29. ): T => {
  30. const base: Pick<T, keyof ExcalidrawElement> = {
  31. type: element.type,
  32. // all elements must have version > 0 so getSceneVersion() will pick up
  33. // newly added elements
  34. version: element.version || 1,
  35. versionNonce: element.versionNonce ?? 0,
  36. isDeleted: element.isDeleted ?? false,
  37. id: element.id || randomId(),
  38. fillStyle: element.fillStyle || "hachure",
  39. strokeWidth: element.strokeWidth || 1,
  40. strokeStyle: element.strokeStyle ?? "solid",
  41. roughness: element.roughness ?? 1,
  42. opacity: element.opacity == null ? 100 : element.opacity,
  43. angle: element.angle || 0,
  44. x: element.x || 0,
  45. y: element.y || 0,
  46. strokeColor: element.strokeColor,
  47. backgroundColor: element.backgroundColor,
  48. width: element.width || 0,
  49. height: element.height || 0,
  50. seed: element.seed ?? 1,
  51. groupIds: element.groupIds ?? [],
  52. strokeSharpness:
  53. element.strokeSharpness ??
  54. (isLinearElementType(element.type) ? "round" : "sharp"),
  55. boundElementIds: element.boundElementIds ?? [],
  56. };
  57. return ({
  58. ...base,
  59. ...getNormalizedDimensions(base),
  60. ...extra,
  61. } as unknown) as T;
  62. };
  63. const restoreElement = (
  64. element: Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
  65. ): typeof element => {
  66. switch (element.type) {
  67. case "text":
  68. let fontSize = element.fontSize;
  69. let fontFamily = element.fontFamily;
  70. if ("font" in element) {
  71. const [fontPx, _fontFamily]: [
  72. string,
  73. string,
  74. ] = (element as any).font.split(" ");
  75. fontSize = parseInt(fontPx, 10);
  76. fontFamily = getFontFamilyByName(_fontFamily);
  77. }
  78. return restoreElementWithProperties(element, {
  79. fontSize,
  80. fontFamily,
  81. text: element.text ?? "",
  82. baseline: element.baseline,
  83. textAlign: element.textAlign || DEFAULT_TEXT_ALIGN,
  84. verticalAlign: element.verticalAlign || DEFAULT_VERTICAL_ALIGN,
  85. });
  86. case "draw":
  87. case "line":
  88. case "arrow": {
  89. const {
  90. startArrowhead = null,
  91. endArrowhead = element.type === "arrow" ? "arrow" : null,
  92. } = element;
  93. return restoreElementWithProperties(element, {
  94. startBinding: element.startBinding,
  95. endBinding: element.endBinding,
  96. points:
  97. // migrate old arrow model to new one
  98. !Array.isArray(element.points) || element.points.length < 2
  99. ? [
  100. [0, 0],
  101. [element.width, element.height],
  102. ]
  103. : element.points,
  104. lastCommittedPoint: null,
  105. startArrowhead,
  106. endArrowhead,
  107. });
  108. }
  109. // generic elements
  110. case "ellipse":
  111. return restoreElementWithProperties(element, {});
  112. case "rectangle":
  113. return restoreElementWithProperties(element, {});
  114. case "diamond":
  115. return restoreElementWithProperties(element, {});
  116. // Don't use default case so as to catch a missing an element type case.
  117. // We also don't want to throw, but instead return void so we filter
  118. // out these unsupported elements from the restored array.
  119. }
  120. };
  121. export const restoreElements = (
  122. elements: ImportedDataState["elements"],
  123. ): ExcalidrawElement[] => {
  124. return (elements || []).reduce((elements, element) => {
  125. // filtering out selection, which is legacy, no longer kept in elements,
  126. // and causing issues if retained
  127. if (element.type !== "selection" && !isInvisiblySmallElement(element)) {
  128. const migratedElement = restoreElement(element);
  129. if (migratedElement) {
  130. elements.push(migratedElement);
  131. }
  132. }
  133. return elements;
  134. }, [] as ExcalidrawElement[]);
  135. };
  136. export const restoreAppState = (
  137. appState: ImportedDataState["appState"],
  138. localAppState: Partial<AppState> | null,
  139. ): AppState => {
  140. appState = appState || {};
  141. const defaultAppState = getDefaultAppState();
  142. const nextAppState = {} as typeof defaultAppState;
  143. for (const [key, val] of Object.entries(defaultAppState) as [
  144. keyof typeof defaultAppState,
  145. any,
  146. ][]) {
  147. const restoredValue = appState[key];
  148. const localValue = localAppState ? localAppState[key] : undefined;
  149. (nextAppState as any)[key] =
  150. restoredValue !== undefined
  151. ? restoredValue
  152. : localValue !== undefined
  153. ? localValue
  154. : val;
  155. }
  156. return {
  157. ...nextAppState,
  158. offsetLeft: appState.offsetLeft || 0,
  159. offsetTop: appState.offsetTop || 0,
  160. // Migrates from previous version where appState.zoom was a number
  161. zoom:
  162. typeof appState.zoom === "number"
  163. ? {
  164. value: appState.zoom as NormalizedZoomValue,
  165. translation: defaultAppState.zoom.translation,
  166. }
  167. : appState.zoom || defaultAppState.zoom,
  168. };
  169. };
  170. export const restore = (
  171. data: ImportedDataState | null,
  172. /**
  173. * Local AppState (`this.state` or initial state from localStorage) so that we
  174. * don't overwrite local state with default values (when values not
  175. * explicitly specified).
  176. * Supply `null` if you can't get access to it.
  177. */
  178. localAppState: Partial<AppState> | null | undefined,
  179. ): DataState => {
  180. return {
  181. elements: restoreElements(data?.elements),
  182. appState: restoreAppState(data?.appState, localAppState || null),
  183. };
  184. };