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

View File

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

View File

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

View File

@ -150,13 +150,32 @@ export function renderScene(
); );
} }
// Pain selected elements // Paint selected elements
if (renderSelection) { if (renderSelection) {
const selectedElements = getSelectedElements(elements, appState);
const dashedLinePadding = 4 / sceneState.zoom;
context.translate(sceneState.scrollX, sceneState.scrollY); 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 [ const [
elementX1, elementX1,
elementY1, elementY1,
@ -168,9 +187,24 @@ export function renderScene(
const elementHeight = elementY2 - elementY1; const elementHeight = elementY2 - elementY1;
const initialLineDash = context.getLineDash(); const initialLineDash = context.getLineDash();
context.setLineDash([8 / sceneState.zoom, 4 / sceneState.zoom]);
const lineWidth = context.lineWidth; 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; context.lineWidth = 1 / sceneState.zoom;
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( strokeRectWithRotation(
context, context,
elementX1 - dashedLinePadding, elementX1 - dashedLinePadding,
@ -181,16 +215,24 @@ export function renderScene(
elementY1 + elementHeight / 2, elementY1 + elementHeight / 2,
element.angle, element.angle,
); );
}
context.lineDashOffset = lineDashOffset;
context.strokeStyle = strokeStyle;
context.lineWidth = lineWidth; context.lineWidth = lineWidth;
context.setLineDash(initialLineDash); context.setLineDash(initialLineDash);
}); });
context.translate(-sceneState.scrollX, -sceneState.scrollY); context.translate(-sceneState.scrollX, -sceneState.scrollY);
const locallySelectedElements = getSelectedElements(elements, appState);
// Paint resize handlers // Paint resize handlers
if (selectedElements.length === 1) { if (locallySelectedElements.length === 1) {
context.translate(sceneState.scrollX, sceneState.scrollY); context.translate(sceneState.scrollX, sceneState.scrollY);
context.fillStyle = "#fff"; context.fillStyle = "#fff";
const handlers = handlerRectangles(selectedElements[0], sceneState.zoom); const handlers = handlerRectangles(
locallySelectedElements[0],
sceneState.zoom,
);
Object.keys(handlers).forEach((key) => { Object.keys(handlers).forEach((key) => {
const handler = handlers[key as HandlerRectanglesRet]; const handler = handlers[key as HandlerRectanglesRet];
if (handler !== undefined) { if (handler !== undefined) {
@ -204,7 +246,7 @@ export function renderScene(
handler[2], handler[2],
handler[3], handler[3],
); );
} else if (selectedElements[0].type !== "text") { } else if (locallySelectedElements[0].type !== "text") {
strokeRectWithRotation( strokeRectWithRotation(
context, context,
handler[0], handler[0],
@ -213,7 +255,7 @@ export function renderScene(
handler[3], handler[3],
handler[0] + handler[2] / 2, handler[0] + handler[2] / 2,
handler[1] + handler[3] / 2, handler[1] + handler[3] / 2,
selectedElements[0].angle, locallySelectedElements[0].angle,
true, // fill before stroke true, // fill before stroke
); );
} }

View File

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

View File

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

View File

@ -43,7 +43,16 @@ export type AppState = {
openMenu: "canvas" | "shape" | null; openMenu: "canvas" | "shape" | null;
lastPointerDownWith: PointerType; lastPointerDownWith: PointerType;
selectedElementIds: { [id: string]: boolean }; 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; shouldCacheIgnoreZoom: boolean;
}; };