瀏覽代碼

sync remote selection (#1207)

* sync remote selection

* skip deleted elements

* remove unnecessary condition & change naming
David Luzar 5 年之前
父節點
當前提交
23540eba4c
共有 6 個文件被更改,包括 94 次插入26 次删除
  1. 19 5
      src/components/App.tsx
  2. 1 0
      src/data/index.ts
  3. 62 20
      src/renderer/renderScene.ts
  4. 1 0
      src/scene/export.ts
  5. 1 0
      src/scene/types.ts
  6. 10 1
      src/types.ts

+ 19 - 5
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<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(

+ 1 - 0
src/data/index.ts

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

+ 62 - 20
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
             );
           }

+ 1 - 0
src/scene/export.ts

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

+ 1 - 0
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 = {

+ 10 - 1
src/types.ts

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