From 23540eba4cad324537ff2bd689bc6aec68986042 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Sat, 4 Apr 2020 16:02:16 +0200 Subject: [PATCH] sync remote selection (#1207) * sync remote selection * skip deleted elements * remove unnecessary condition & change naming --- src/components/App.tsx | 24 ++++++++--- src/data/index.ts | 1 + src/renderer/renderScene.ts | 82 ++++++++++++++++++++++++++++--------- src/scene/export.ts | 1 + src/scene/types.ts | 1 + src/types.ts | 11 ++++- 6 files changed, 94 insertions(+), 26 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index 961d83b7..173e49b4 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -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 { 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 { 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 { 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 { payload: { socketID: this.socket.id, pointerCoords: payload.pointerCoords, + selectedElementIds: this.state.selectedElementIds, }, }; return this._broadcastSocketData( diff --git a/src/data/index.ts b/src/data/index.ts index 21679a12..13818b93 100644 --- a/src/data/index.ts +++ b/src/data/index.ts @@ -49,6 +49,7 @@ export type SocketUpdateDataSource = { payload: { socketID: string; pointerCoords: { x: number; y: number }; + selectedElementIds: AppState["selectedElementIds"]; }; }; }; diff --git a/src/renderer/renderScene.ts b/src/renderer/renderScene.ts index 9f794faf..ca42a238 100644 --- a/src/renderer/renderScene.ts +++ b/src/renderer/renderScene.ts @@ -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 ); } diff --git a/src/scene/export.ts b/src/scene/export.ts index 26e27c29..95033dfe 100644 --- a/src/scene/export.ts +++ b/src/scene/export.ts @@ -50,6 +50,7 @@ export function exportToCanvas( scrollY: normalizeScroll(-minY + exportPadding), zoom: 1, remotePointerViewportCoords: {}, + remoteSelectedElementIds: {}, shouldCacheIgnoreZoom: false, }, { diff --git a/src/scene/types.ts b/src/scene/types.ts index 96c93fb1..7855eb4e 100644 --- a/src/scene/types.ts +++ b/src/scene/types.ts @@ -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 = { diff --git a/src/types.ts b/src/types.ts index ec321f82..dbe4e2fb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -43,7 +43,16 @@ export type AppState = { openMenu: "canvas" | "shape" | null; lastPointerDownWith: PointerType; selectedElementIds: { [id: string]: boolean }; - collaborators: Map; + collaborators: Map< + string, + { + pointer?: { + x: number; + y: number; + }; + selectedElementIds?: AppState["selectedElementIds"]; + } + >; shouldCacheIgnoreZoom: boolean; };