1
0
mirror of https://github.com/excalidraw/excalidraw.git synced 2025-02-18 13:29:36 +01:00

sync remote selection (#1207)

* sync remote selection

* skip deleted elements

* remove unnecessary condition & change naming
This commit is contained in:
David Luzar 2020-04-04 16:02:16 +02:00 committed by GitHub
parent adc099ed15
commit 23540eba4c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 94 additions and 26 deletions

@ -103,7 +103,7 @@ import {
SHIFT_LOCKING_ANGLE,
} from "../constants";
import { LayerUI } from "./LayerUI";
import { ScrollBars } from "../scene/types";
import { ScrollBars, SceneState } from "../scene/types";
import { generateCollaborationLink, getCollaborationLinkData } from "../data";
import { mutateElement, newElementWith } from "../element/mutateElement";
import { invalidateShapeForElement } from "../renderer/renderElement";
@ -441,10 +441,17 @@ export class App extends React.Component<any, AppState> {
if (this.state.isCollaborating && !this.socket) {
this.initializeSocketClient({ showLoadingState: true });
}
const pointerViewportCoords: {
[id: string]: { x: number; y: number };
} = {};
const pointerViewportCoords: SceneState["remotePointerViewportCoords"] = {};
const remoteSelectedElementIds: SceneState["remoteSelectedElementIds"] = {};
this.state.collaborators.forEach((user, socketID) => {
if (user.selectedElementIds) {
for (const id of Object.keys(user.selectedElementIds)) {
if (!(id in remoteSelectedElementIds)) {
remoteSelectedElementIds[id] = [];
}
remoteSelectedElementIds[id].push(socketID);
}
}
if (!user.pointer) {
return;
}
@ -479,6 +486,7 @@ export class App extends React.Component<any, AppState> {
viewBackgroundColor: this.state.viewBackgroundColor,
zoom: this.state.zoom,
remotePointerViewportCoords: pointerViewportCoords,
remoteSelectedElementIds: remoteSelectedElementIds,
shouldCacheIgnoreZoom: this.state.shouldCacheIgnoreZoom,
},
{
@ -860,13 +868,18 @@ export class App extends React.Component<any, AppState> {
updateScene(decryptedData);
break;
case "MOUSE_LOCATION": {
const { socketID, pointerCoords } = decryptedData.payload;
const {
socketID,
pointerCoords,
selectedElementIds,
} = decryptedData.payload;
this.setState((state) => {
if (!state.collaborators.has(socketID)) {
state.collaborators.set(socketID, {});
}
const user = state.collaborators.get(socketID)!;
user.pointer = pointerCoords;
user.selectedElementIds = selectedElementIds;
state.collaborators.set(socketID, user);
return state;
});
@ -917,6 +930,7 @@ export class App extends React.Component<any, AppState> {
payload: {
socketID: this.socket.id,
pointerCoords: payload.pointerCoords,
selectedElementIds: this.state.selectedElementIds,
},
};
return this._broadcastSocketData(

@ -49,6 +49,7 @@ export type SocketUpdateDataSource = {
payload: {
socketID: string;
pointerCoords: { x: number; y: number };
selectedElementIds: AppState["selectedElementIds"];
};
};
};

@ -150,13 +150,32 @@ export function renderScene(
);
}
// Pain selected elements
// Paint selected elements
if (renderSelection) {
const selectedElements = getSelectedElements(elements, appState);
const dashedLinePadding = 4 / sceneState.zoom;
context.translate(sceneState.scrollX, sceneState.scrollY);
selectedElements.forEach((element) => {
const selections = elements.reduce((acc, element) => {
const selectionColors = [];
// local user
if (appState.selectedElementIds[element.id]) {
selectionColors.push("#000000");
}
// remote users
if (sceneState.remoteSelectedElementIds[element.id]) {
selectionColors.push(
...sceneState.remoteSelectedElementIds[element.id].map((socketId) => {
const { background } = colorsForClientId(socketId);
return background;
}),
);
}
if (selectionColors.length) {
acc.push({ element, selectionColors });
}
return acc;
}, [] as { element: ExcalidrawElement; selectionColors: string[] }[]);
selections.forEach(({ element, selectionColors }) => {
const [
elementX1,
elementY1,
@ -168,29 +187,52 @@ export function renderScene(
const elementHeight = elementY2 - elementY1;
const initialLineDash = context.getLineDash();
context.setLineDash([8 / sceneState.zoom, 4 / sceneState.zoom]);
const lineWidth = context.lineWidth;
const lineDashOffset = context.lineDashOffset;
const strokeStyle = context.strokeStyle;
const dashedLinePadding = 4 / sceneState.zoom;
const dashWidth = 8 / sceneState.zoom;
const spaceWidth = 4 / sceneState.zoom;
context.lineWidth = 1 / sceneState.zoom;
strokeRectWithRotation(
context,
elementX1 - dashedLinePadding,
elementY1 - dashedLinePadding,
elementWidth + dashedLinePadding * 2,
elementHeight + dashedLinePadding * 2,
elementX1 + elementWidth / 2,
elementY1 + elementHeight / 2,
element.angle,
);
const count = selectionColors.length;
for (var i = 0; i < count; ++i) {
context.strokeStyle = selectionColors[i];
context.setLineDash([
dashWidth,
spaceWidth + (dashWidth + spaceWidth) * (count - 1),
]);
context.lineDashOffset = (dashWidth + spaceWidth) * i;
strokeRectWithRotation(
context,
elementX1 - dashedLinePadding,
elementY1 - dashedLinePadding,
elementWidth + dashedLinePadding * 2,
elementHeight + dashedLinePadding * 2,
elementX1 + elementWidth / 2,
elementY1 + elementHeight / 2,
element.angle,
);
}
context.lineDashOffset = lineDashOffset;
context.strokeStyle = strokeStyle;
context.lineWidth = lineWidth;
context.setLineDash(initialLineDash);
});
context.translate(-sceneState.scrollX, -sceneState.scrollY);
const locallySelectedElements = getSelectedElements(elements, appState);
// Paint resize handlers
if (selectedElements.length === 1) {
if (locallySelectedElements.length === 1) {
context.translate(sceneState.scrollX, sceneState.scrollY);
context.fillStyle = "#fff";
const handlers = handlerRectangles(selectedElements[0], sceneState.zoom);
const handlers = handlerRectangles(
locallySelectedElements[0],
sceneState.zoom,
);
Object.keys(handlers).forEach((key) => {
const handler = handlers[key as HandlerRectanglesRet];
if (handler !== undefined) {
@ -204,7 +246,7 @@ export function renderScene(
handler[2],
handler[3],
);
} else if (selectedElements[0].type !== "text") {
} else if (locallySelectedElements[0].type !== "text") {
strokeRectWithRotation(
context,
handler[0],
@ -213,7 +255,7 @@ export function renderScene(
handler[3],
handler[0] + handler[2] / 2,
handler[1] + handler[3] / 2,
selectedElements[0].angle,
locallySelectedElements[0].angle,
true, // fill before stroke
);
}

@ -50,6 +50,7 @@ export function exportToCanvas(
scrollY: normalizeScroll(-minY + exportPadding),
zoom: 1,
remotePointerViewportCoords: {},
remoteSelectedElementIds: {},
shouldCacheIgnoreZoom: false,
},
{

@ -9,6 +9,7 @@ export type SceneState = {
zoom: number;
shouldCacheIgnoreZoom: boolean;
remotePointerViewportCoords: { [id: string]: { x: number; y: number } };
remoteSelectedElementIds: { [elementId: string]: string[] };
};
export type SceneScroll = {

@ -43,7 +43,16 @@ export type AppState = {
openMenu: "canvas" | "shape" | null;
lastPointerDownWith: PointerType;
selectedElementIds: { [id: string]: boolean };
collaborators: Map<string, { pointer?: { x: number; y: number } }>;
collaborators: Map<
string,
{
pointer?: {
x: number;
y: number;
};
selectedElementIds?: AppState["selectedElementIds"];
}
>;
shouldCacheIgnoreZoom: boolean;
};