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:
parent
adc099ed15
commit
23540eba4c
@ -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 = {
|
||||
|
11
src/types.ts
11
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;
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user