Merge pull request #898 from idlewinn/master

enable version bumping for collaboration
This commit is contained in:
Edwin Lin 2020-03-09 23:41:46 -07:00 committed by GitHub
commit c95dcf2682
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 191 additions and 85 deletions

View File

@ -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);

View File

@ -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;
}

View File

@ -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,35 @@ export class App extends React.Component<any, AppState> {
sceneAppState || getDefaultAppState(),
{ scrollToContent: true },
);
elements = restoredState.elements;
// Perform reconciliation - in collaboration, if we encounter
// elements with more staler versions than ours, ignore them
// and keep ours.
if (elements == null || elements.length === 0) {
elements = restoredState.elements;
} else {
// create a map of ids so we don't have to iterate
// over the array more than once.
const elementMap = elements.reduce(
(
acc: { [key: string]: ExcalidrawElement },
element: ExcalidrawElement,
) => {
acc[element.id] = element;
return acc;
},
{},
);
// Reconcile
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 +803,10 @@ export class App extends React.Component<any, AppState> {
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 +814,10 @@ export class App extends React.Component<any, AppState> {
);
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 +1369,17 @@ export class App extends React.Component<any, AppState> {
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 +1489,17 @@ export class App extends React.Component<any, AppState> {
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 +1522,16 @@ export class App extends React.Component<any, AppState> {
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 +1554,15 @@ export class App extends React.Component<any, AppState> {
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 +1585,22 @@ export class App extends React.Component<any, AppState> {
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 +1615,10 @@ export class App extends React.Component<any, AppState> {
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 +1632,37 @@ export class App extends React.Component<any, AppState> {
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 +1676,10 @@ export class App extends React.Component<any, AppState> {
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 +1702,10 @@ export class App extends React.Component<any, AppState> {
);
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 +1767,13 @@ export class App extends React.Component<any, AppState> {
}
}
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);

View File

@ -0,0 +1,26 @@
import {
MutableExcalidrawElement,
MutableExcalidrawTextElement,
} from "./types";
// This function tracks updates of text elements for the purposes for collaboration.
// The version is used to compare updates when more than one user is working in
// the same drawing.
export function mutateElement(
element: MutableExcalidrawElement,
callback: (mutatableElement: MutableExcalidrawElement) => void,
): void {
element.version++;
callback(element);
}
// This function tracks updates of text elements for the purposes for collaboration.
// The version is used to compare updates when more than one user is working in
// the same document.
export function mutateTextElement(
element: MutableExcalidrawTextElement,
callback: (mutatableElement: MutableExcalidrawTextElement) => void,
): void {
element.version++;
callback(element);
}

View File

@ -33,6 +33,7 @@ export function newElement(
opacity,
seed: randomSeed(),
points: [] as Point[],
version: 1,
};
return element;
}

View File

@ -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);

View File

@ -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;

View File

@ -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<typeof newElement>;
export type ExcalidrawTextElement = ExcalidrawElement & {
export type ExcalidrawElement = Readonly<ReturnType<typeof newElement>>;
export type MutableExcalidrawElement = ReturnType<typeof newElement>;
export type MutableExcalidrawTextElement = MutableExcalidrawElement & {
type: "text";
font: string;
text: string;
@ -15,4 +17,6 @@ export type ExcalidrawTextElement = ExcalidrawElement & {
baseline: number;
};
export type ExcalidrawTextElement = Readonly<MutableExcalidrawTextElement>;
export type PointerType = "mouse" | "pen" | "touch";