sync remote selection (#1207)
* sync remote selection * skip deleted elements * remove unnecessary condition & change naming
This commit is contained in:
parent
adc099ed15
commit
23540eba4c
@ -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(
|
||||||
|
@ -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"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -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,29 +187,52 @@ 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;
|
||||||
strokeRectWithRotation(
|
|
||||||
context,
|
const count = selectionColors.length;
|
||||||
elementX1 - dashedLinePadding,
|
for (var i = 0; i < count; ++i) {
|
||||||
elementY1 - dashedLinePadding,
|
context.strokeStyle = selectionColors[i];
|
||||||
elementWidth + dashedLinePadding * 2,
|
context.setLineDash([
|
||||||
elementHeight + dashedLinePadding * 2,
|
dashWidth,
|
||||||
elementX1 + elementWidth / 2,
|
spaceWidth + (dashWidth + spaceWidth) * (count - 1),
|
||||||
elementY1 + elementHeight / 2,
|
]);
|
||||||
element.angle,
|
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.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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -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 = {
|
||||||
|
11
src/types.ts
11
src/types.ts
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user