From 1419f17175604c2266d1881e326d665f47755e23 Mon Sep 17 00:00:00 2001 From: idlewinn Date: Mon, 9 Mar 2020 22:34:50 -0700 Subject: [PATCH] enable version bumping for collaboration --- src/actions/actionFinalize.tsx | 11 +- src/actions/actionStyles.ts | 7 +- src/components/App.tsx | 194 +++++++++++++++++++++------------ src/element/mutateElement.ts | 20 ++++ src/element/newElement.ts | 1 + src/element/sizeHelpers.ts | 17 ++- src/element/textElement.ts | 6 +- src/element/types.ts | 8 +- 8 files changed, 179 insertions(+), 85 deletions(-) create mode 100644 src/element/mutateElement.ts diff --git a/src/actions/actionFinalize.tsx b/src/actions/actionFinalize.tsx index 526bc4e4..74a8a8cc 100644 --- a/src/actions/actionFinalize.tsx +++ b/src/actions/actionFinalize.tsx @@ -7,6 +7,7 @@ import { done } from "../components/icons"; import { t } from "../i18n"; import { register } from "./register"; import { invalidateShapeForElement } from "../renderer/renderElement"; +import { mutateElement } from "../element/mutateElement"; export const actionFinalize = register({ name: "finalize", @@ -18,10 +19,12 @@ export const actionFinalize = register({ if (appState.multiElement) { // pen and mouse have hover if (appState.lastPointerDownWith !== "touch") { - appState.multiElement.points = appState.multiElement.points.slice( - 0, - appState.multiElement.points.length - 1, - ); + mutateElement(appState.multiElement, multiElement => { + multiElement.points = multiElement.points.slice( + 0, + multiElement.points.length - 1, + ); + }); } if (isInvisiblySmallElement(appState.multiElement)) { newElements = newElements.slice(0, -1); diff --git a/src/actions/actionStyles.ts b/src/actions/actionStyles.ts index ee5aa4ed..fa3f280c 100644 --- a/src/actions/actionStyles.ts +++ b/src/actions/actionStyles.ts @@ -6,6 +6,7 @@ import { import { KEYS } from "../keys"; import { DEFAULT_FONT } from "../appState"; import { register } from "./register"; +import { mutateTextElement } from "../element/mutateElement"; let copiedStyles: string = "{}"; @@ -44,8 +45,10 @@ export const actionPasteStyles = register({ roughness: pastedElement?.roughness, }; if (isTextElement(newElement)) { - newElement.font = pastedElement?.font || DEFAULT_FONT; - redrawTextBoundingBox(newElement); + mutateTextElement(newElement, newElement => { + newElement.font = pastedElement?.font || DEFAULT_FONT; + redrawTextBoundingBox(newElement); + }); } return newElement; } diff --git a/src/components/App.tsx b/src/components/App.tsx index 95ab228d..ba4e29d8 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -88,6 +88,7 @@ import { LayerUI } from "./LayerUI"; import { ScrollBars } from "../scene/types"; import { invalidateShapeForElement } from "../renderer/renderElement"; import { generateCollaborationLink, getCollaborationLinkData } from "../data"; +import { mutateElement } from "../element/mutateElement"; // ----------------------------------------------------------------------------- // TEST HOOKS @@ -262,7 +263,29 @@ export class App extends React.Component { sceneAppState || getDefaultAppState(), { scrollToContent: true }, ); - elements = restoredState.elements; + if (elements == null || elements.length === 0) { + elements = restoredState.elements; + } else { + const elementMap = elements.reduce( + ( + acc: { [key: string]: ExcalidrawElement }, + element: ExcalidrawElement, + ) => { + acc[element.id] = element; + return acc; + }, + {}, + ); + elements = restoredState.elements.map(element => { + if ( + elementMap.hasOwnProperty(element.id) && + elementMap[element.id].version > element.version + ) { + return elementMap[element.id]; + } + return element; + }); + } this.setState({}); if (this.socketInitialized === false) { this.socketInitialized = true; @@ -774,8 +797,10 @@ export class App extends React.Component { textY = centerElementYInViewport; // x and y will change after calling newTextElement function - element.x = centerElementX; - element.y = centerElementY; + mutateElement(element, element => { + element.x = centerElementX; + element.y = centerElementY; + }); } else if (!event.altKey) { const snappedToCenterPosition = this.getTextWysiwygSnappedToCenterPosition( x, @@ -783,8 +808,10 @@ export class App extends React.Component { ); if (snappedToCenterPosition) { - element.x = snappedToCenterPosition.elementCenterX; - element.y = snappedToCenterPosition.elementCenterY; + mutateElement(element, element => { + element.x = snappedToCenterPosition.elementCenterX; + element.y = snappedToCenterPosition.elementCenterY; + }); textX = snappedToCenterPosition.wysiwygX; textY = snappedToCenterPosition.wysiwygY; } @@ -1336,13 +1363,17 @@ export class App extends React.Component { const dx = element.x + width + p1[0]; const dy = element.y + height + p1[1]; - element.x = dx; - element.y = dy; + mutateElement(element, element => { + element.x = dx; + element.y = dy; + }); p1[0] = absPx - element.x; p1[1] = absPy - element.y; } else { - element.x += deltaX; - element.y += deltaY; + mutateElement(element, element => { + element.x += deltaX; + element.y += deltaY; + }); p1[0] -= deltaX; p1[1] -= deltaY; } @@ -1452,16 +1483,17 @@ export class App extends React.Component { event.shiftKey, ); } else { - element.width -= deltaX; - element.x += deltaX; - - if (event.shiftKey) { - element.y += element.height - element.width; - element.height = element.width; - } else { - element.height -= deltaY; - element.y += deltaY; - } + mutateElement(element, element => { + element.width -= deltaX; + element.x += deltaX; + if (event.shiftKey) { + element.y += element.height - element.width; + element.height = element.width; + } else { + element.height -= deltaY; + element.y += deltaY; + } + }); } break; case "ne": @@ -1484,14 +1516,16 @@ export class App extends React.Component { event.shiftKey, ); } else { - element.width += deltaX; - if (event.shiftKey) { - element.y += element.height - element.width; - element.height = element.width; - } else { - element.height -= deltaY; - element.y += deltaY; - } + mutateElement(element, element => { + element.width += deltaX; + if (event.shiftKey) { + element.y += element.height - element.width; + element.height = element.width; + } else { + element.height -= deltaY; + element.y += deltaY; + } + }); } break; case "sw": @@ -1514,13 +1548,15 @@ export class App extends React.Component { event.shiftKey, ); } else { - element.width -= deltaX; - element.x += deltaX; - if (event.shiftKey) { - element.height = element.width; - } else { - element.height += deltaY; - } + mutateElement(element, element => { + element.width -= deltaX; + element.x += deltaX; + if (event.shiftKey) { + element.height = element.width; + } else { + element.height += deltaY; + } + }); } break; case "se": @@ -1543,18 +1579,22 @@ export class App extends React.Component { event.shiftKey, ); } else { - if (event.shiftKey) { - element.width += deltaX; - element.height = element.width; - } else { - element.width += deltaX; - element.height += deltaY; - } + mutateElement(element, element => { + if (event.shiftKey) { + element.width += deltaX; + element.height = element.width; + } else { + element.width += deltaX; + element.height += deltaY; + } + }); } break; case "n": { - element.height -= deltaY; - element.y += deltaY; + mutateElement(element, element => { + element.height -= deltaY; + element.y += deltaY; + }); if (element.points.length > 0) { const len = element.points.length; @@ -1569,8 +1609,10 @@ export class App extends React.Component { break; } case "w": { - element.width -= deltaX; - element.x += deltaX; + mutateElement(element, element => { + element.width -= deltaX; + element.x += deltaX; + }); if (element.points.length > 0) { const len = element.points.length; @@ -1584,29 +1626,37 @@ export class App extends React.Component { break; } case "s": { - element.height += deltaY; - if (element.points.length > 0) { - const len = element.points.length; - const points = [...element.points].sort((a, b) => a[1] - b[1]); + mutateElement(element, element => { + element.height += deltaY; + if (element.points.length > 0) { + const len = element.points.length; + const points = [...element.points].sort( + (a, b) => a[1] - b[1], + ); - for (let i = 1; i < points.length; ++i) { - const pnt = points[i]; - pnt[1] += deltaY / (len - i); + for (let i = 1; i < points.length; ++i) { + const pnt = points[i]; + pnt[1] += deltaY / (len - i); + } } - } + }); break; } case "e": { - element.width += deltaX; - if (element.points.length > 0) { - const len = element.points.length; - const points = [...element.points].sort((a, b) => a[0] - b[0]); + mutateElement(element, element => { + element.width += deltaX; + if (element.points.length > 0) { + const len = element.points.length; + const points = [...element.points].sort( + (a, b) => a[0] - b[0], + ); - for (let i = 1; i < points.length; ++i) { - const pnt = points[i]; - pnt[0] += deltaX / (len - i); + for (let i = 1; i < points.length; ++i) { + const pnt = points[i]; + pnt[0] += deltaX / (len - i); + } } - } + }); break; } } @@ -1620,8 +1670,10 @@ export class App extends React.Component { element, resizeHandle, }); - el.x = element.x; - el.y = element.y; + mutateElement(el, el => { + el.x = element.x; + el.y = element.y; + }); invalidateShapeForElement(el); lastX = x; @@ -1644,8 +1696,10 @@ export class App extends React.Component { ); selectedElements.forEach(element => { - element.x += x - lastX; - element.y += y - lastY; + mutateElement(element, element => { + element.x += x - lastX; + element.y += y - lastY; + }); }); lastX = x; lastY = y; @@ -1707,11 +1761,13 @@ export class App extends React.Component { } } - draggingElement.x = x < originX ? originX - width : originX; - draggingElement.y = y < originY ? originY - height : originY; + mutateElement(draggingElement, draggingElement => { + draggingElement.x = x < originX ? originX - width : originX; + draggingElement.y = y < originY ? originY - height : originY; - draggingElement.width = width; - draggingElement.height = height; + draggingElement.width = width; + draggingElement.height = height; + }); } invalidateShapeForElement(draggingElement); diff --git a/src/element/mutateElement.ts b/src/element/mutateElement.ts new file mode 100644 index 00000000..72823322 --- /dev/null +++ b/src/element/mutateElement.ts @@ -0,0 +1,20 @@ +import { + MutableExcalidrawElement, + MutableExcalidrawTextElement, +} from "./types"; + +export function mutateElement( + element: MutableExcalidrawElement, + callback: (mutatableElement: MutableExcalidrawElement) => void, +): void { + element.version++; + callback(element); +} + +export function mutateTextElement( + element: MutableExcalidrawTextElement, + callback: (mutatableElement: MutableExcalidrawTextElement) => void, +): void { + element.version++; + callback(element); +} diff --git a/src/element/newElement.ts b/src/element/newElement.ts index aceb4ee4..bddd6816 100644 --- a/src/element/newElement.ts +++ b/src/element/newElement.ts @@ -33,6 +33,7 @@ export function newElement( opacity, seed: randomSeed(), points: [] as Point[], + version: 1, }; return element; } diff --git a/src/element/sizeHelpers.ts b/src/element/sizeHelpers.ts index 48c7fb82..2ecd2da2 100644 --- a/src/element/sizeHelpers.ts +++ b/src/element/sizeHelpers.ts @@ -1,5 +1,6 @@ -import { ExcalidrawElement } from "./types"; +import { ExcalidrawElement, MutableExcalidrawElement } from "./types"; import { invalidateShapeForElement } from "../renderer/renderElement"; +import { mutateElement } from "./mutateElement"; export function isInvisiblySmallElement(element: ExcalidrawElement): boolean { if (element.type === "arrow" || element.type === "line") { @@ -35,7 +36,7 @@ export function getPerfectElementSize( } export function resizePerfectLineForNWHandler( - element: ExcalidrawElement, + element: MutableExcalidrawElement, x: number, y: number, ) { @@ -78,13 +79,17 @@ export function normalizeDimensions( } if (element.width < 0) { - element.width = Math.abs(element.width); - element.x -= element.width; + mutateElement(element, element => { + element.width = Math.abs(element.width); + element.x -= element.width; + }); } if (element.height < 0) { - element.height = Math.abs(element.height); - element.y -= element.height; + mutateElement(element, element => { + element.height = Math.abs(element.height); + element.y -= element.height; + }); } invalidateShapeForElement(element); diff --git a/src/element/textElement.ts b/src/element/textElement.ts index 2908c859..44c952de 100644 --- a/src/element/textElement.ts +++ b/src/element/textElement.ts @@ -1,7 +1,9 @@ import { measureText } from "../utils"; -import { ExcalidrawTextElement } from "./types"; +import { MutableExcalidrawTextElement } from "./types"; -export const redrawTextBoundingBox = (element: ExcalidrawTextElement) => { +export const redrawTextBoundingBox = ( + element: MutableExcalidrawTextElement, +) => { const metrics = measureText(element.text, element.font); element.width = metrics.width; element.height = metrics.height; diff --git a/src/element/types.ts b/src/element/types.ts index fec6c8cd..fed98493 100644 --- a/src/element/types.ts +++ b/src/element/types.ts @@ -5,8 +5,10 @@ import { newElement } from "./newElement"; * no computed data. The list of all ExcalidrawElements should be shareable * between peers and contain no state local to the peer. */ -export type ExcalidrawElement = ReturnType; -export type ExcalidrawTextElement = ExcalidrawElement & { +export type ExcalidrawElement = Readonly>; +export type MutableExcalidrawElement = ReturnType; + +export type MutableExcalidrawTextElement = MutableExcalidrawElement & { type: "text"; font: string; text: string; @@ -15,4 +17,6 @@ export type ExcalidrawTextElement = ExcalidrawElement & { baseline: number; }; +export type ExcalidrawTextElement = Readonly; + export type PointerType = "mouse" | "pen" | "touch";