Add Yjs integration for collaborative drawing and update socket handling

This commit is contained in:
2025-06-27 10:06:45 +05:30
parent 3ab5c6ee6a
commit 1dc1ee3b3c
6 changed files with 283 additions and 32 deletions

View File

@@ -53,7 +53,7 @@ function MainScene() {
const { toggleThreeD } = useThreeDStore();
const { isPlaying } = usePlayButtonStore();
const { widgetSubOption } = useWidgetSubOption();
const { visualizationSocket } = useSocketStore();
const { socket, visualizationSocket } = useSocketStore();
const { selectedZone } = useSelectedZoneStore();
const { setFloatingWidget } = useFloatingWidget();
const { clearComparisonProduct } = useComparisonProduct();
@@ -68,6 +68,24 @@ function MainScene() {
const { selectedVersion, setSelectedVersion } = selectedVersionStore();
const { selectedComment, commentPositionState } = useSelectedComment();
useEffect(() => {
console.log('hi');
if (!projectId || !selectedVersion?.versionId || !socket) return;
socket.emit("joinRoom", { projectId, versionId: selectedVersion.versionId });
socket.on("v1:Line-collab:response:create", (data: any) => {
console.log("v1:Line-collab:response:create:", data);
});
return () => {
if (projectId && selectedVersion?.versionId && socket) {
socket.emit("leaveRoom", { projectId, versionId: selectedVersion.versionId });
socket.off("v1:Line-collab:response:create");
}
};
}, [projectId, selectedVersion?.versionId]);
useEffect(() => {
if (activeModule !== 'simulation') {
clearComparisonProduct();
@@ -188,7 +206,7 @@ function MainScene() {
{activeModule !== "market" && !selectedUser && <Footer />}
<VersionSaved />
{(commentPositionState !== null || selectedComment !== null) && <ThreadChat/>}
{(commentPositionState !== null || selectedComment !== null) && <ThreadChat />}
</>
);

View File

@@ -12,6 +12,8 @@ import arrayLineToObject from "./lineConvertions/arrayLineToObject";
// import { setLine } from '../../../../services/factoryBuilder/lines/setLineApi';
import { Socket } from "socket.io-client";
import { getUserData } from "../../../../functions/getUserData";
import * as Y from 'yjs';
import { generateUniqueId } from "../../../../functions/generateUniqueId";
async function drawWall(
raycaster: THREE.Raycaster,
@@ -37,6 +39,7 @@ async function drawWall(
socket: Socket<any>,
projectId?: string,
versionId?: string,
ydoc?: any
): Promise<void> {
const { userId, organization, email } = getUserData();
////////// Creating lines Based on the positions clicked //////////
@@ -142,7 +145,45 @@ async function drawWall(
};
console.log('input: ', input);
socket.emit("v1:Line:create", input);
socket.emit("v1:Line-collab:create", input);
// ✅ Add to Yjs map (will auto-sync to other clients)
const yLine = new Y.Map();
yLine.set("organization", organization);
yLine.set("layer", data.layer);
yLine.set("type", data.type);
yLine.set("line", data.line)
yLine.set("socketId", socket.id)
yLine.set("projectId", projectId);
yLine.set("versionId", versionId);
yLine.set("userId", userId);
const yLineArray = new Y.Array();
if (!data.line) return
for (const point of data.line) {
const yPoint = new Y.Map();
yPoint.set("uuid", point.uuid);
if (point.position) {
yPoint.set("position", new Y.Map([
["x", point.position.x],
["y", point.position.y],
["z", point.position.z],
]));
}
yLineArray.push([yPoint]);
}
yLine.set("line", yLineArray);
const lineMap = ydoc.getMap("lines");
lineMap.observe((event: any) => {
console.log('event: ', event);
console.log("Map changed!", event.changes.keys);
});
const lineId = crypto.randomUUID(); // Or use backend _id if available
lineMap.set(lineId, yLine); // 👈 triggers Yjs update event
setNewLines([newLines[0], newLines[1], line.current]);
lines.current.push(line.current as Types.Line);
@@ -156,6 +197,8 @@ async function drawWall(
let lastPoint = line.current[line.current.length - 1];
line.current = [lastPoint];
}
return;
}
}
@@ -203,15 +246,45 @@ async function drawWall(
CONSTANTS.lineConfig.wallName,
]);
// if (line.current.length >= 2 && line.current[0] && line.current[1]) {
// const data = arrayLineToObject(line.current as Types.Line);
// //REST
// // setLine(organization, data.layer!, data.line!, data.type!);
// //SOCKET
// const input = {
// organization,
// layer: data.layer,
// line: data.line,
// type: data.type,
// socketId: socket.id,
// versionId,
// projectId,
// userId,
// };
// console.log('input: ', input);
// socket.emit("v1:Line-collab:create", input);
// setNewLines([line.current]);
// lines.current.push(line.current as Types.Line);
// addLineToScene(
// line.current[0][0],
// line.current[1][0],
// CONSTANTS.lineConfig.wallColor,
// line.current,
// floorPlanGroupLine
// );
// let lastPoint = line.current[line.current.length - 1];
// line.current = [lastPoint];
// }
if (line.current.length >= 2 && line.current[0] && line.current[1]) {
const data = arrayLineToObject(line.current as Types.Line);
//REST
// setLine(organization, data.layer!, data.line!, data.type!);
//SOCKET
// ✅ Prepare input for socket.emit (your backend)
const input = {
organization,
layer: data.layer,
@@ -223,9 +296,46 @@ async function drawWall(
userId,
};
console.log('input: ', input);
socket.emit("v1:Line:create", input);
// ✅ Emit to backend
socket.emit("v1:Line-collab:create", input);
// ✅ Add to Yjs map (will auto-sync to other clients)
const yLine = new Y.Map();
yLine.set("organization", organization);
yLine.set("layer", data.layer);
yLine.set("type", data.type);
yLine.set("line", data.line)
yLine.set("socketId", socket.id)
yLine.set("projectId", projectId);
yLine.set("versionId", versionId);
yLine.set("userId", userId);
const yLineArray = new Y.Array();
if (!data.line) return
for (const point of data.line) {
const yPoint = new Y.Map();
yPoint.set("uuid", point.uuid);
if (point.position) {
yPoint.set("position", new Y.Map([
["x", point.position.x],
["y", point.position.y],
["z", point.position.z],
]));
}
yLineArray.push([yPoint]);
}
yLine.set("line", yLineArray);
const lineMap = ydoc.getMap("lines");
lineMap.observe((event: any) => {
console.log('event: ', event);
console.log("Map changed!", event.changes.keys);
});
const lineId = generateUniqueId(); // Or use backend _id if available
lineMap.set(lineId, yLine); // 👈 triggers Yjs update event
// ✅ Scene + local updates
setNewLines([line.current]);
lines.current.push(line.current as Types.Line);
addLineToScene(
@@ -235,7 +345,8 @@ async function drawWall(
line.current,
floorPlanGroupLine
);
let lastPoint = line.current[line.current.length - 1];
const lastPoint = line.current[line.current.length - 1];
line.current = [lastPoint];
}
if (isSnapped.current) {

View File

@@ -31,11 +31,13 @@ const FloorPlanGroup = ({ floorPlanGroup, floorPlanGroupLine, floorPlanGroupPoin
const { setNewLines } = useNewLines();
const { setDeletedLines } = useDeletedLines();
const { socket } = useSocketStore();
const { ydoc } = useSocketStore();
const { selectedVersionStore } = useVersionContext();
const { selectedVersion } = selectedVersionStore();
const { projectId } = useParams();
const { organization } = getUserData();
useEffect(() => {
if (toolMode === 'move') {
addDragControl(dragPointControls, currentLayerPoint, state, floorPlanGroupPoint, floorPlanGroupLine, lines, onlyFloorlines, socket, projectId, selectedVersion?.versionId || '',);
@@ -148,7 +150,7 @@ const FloorPlanGroup = ({ floorPlanGroup, floorPlanGroupLine, floorPlanGroupPoin
}
if (toolMode === "Wall") {
drawWall(raycaster, plane, floorPlanGroupPoint, snappedPoint, isSnapped, isSnappedUUID, line, ispreSnapped, anglesnappedPoint, isAngleSnapped, lines, floorPlanGroupLine, floorPlanGroup, ReferenceLineMesh, LineCreated, currentLayerPoint, dragPointControls, setNewLines, setDeletedLines, activeLayer, socket, projectId, selectedVersion?.versionId || '',);
drawWall(raycaster, plane, floorPlanGroupPoint, snappedPoint, isSnapped, isSnappedUUID, line, ispreSnapped, anglesnappedPoint, isAngleSnapped, lines, floorPlanGroupLine, floorPlanGroup, ReferenceLineMesh, LineCreated, currentLayerPoint, dragPointControls, setNewLines, setDeletedLines, activeLayer, socket, projectId, selectedVersion?.versionId || '', ydoc);
}
if (toolMode === "Floor") {

View File

@@ -1,24 +1,73 @@
import * as THREE from "three";
import { create } from "zustand";
import * as Y from 'yjs';
import { io } from "socket.io-client";
import * as CONSTANTS from "../../types/world/worldConstants";
export const useSocketStore = create<any>((set: any, get: any) => ({
socket: null,
ydoc: new Y.Doc(),
initializeSocket: (email?: string, organization?: string, token?: string) => {
const existingSocket = get().socket;
if (existingSocket) {
return;
}
if (get().socket) return;
// const socket = io(
// `http://${process.env.REACT_APP_SERVER_SOCKET_API_BASE_URL}/Builder_v1`,
// {
// reconnection: true,
// auth: { token },
// }
// );
const socket = io(
`http://${process.env.REACT_APP_SERVER_SOCKET_API_BASE_URL}/Builder_v1`,
`http://${process.env.REACT_APP_SERVER_SOCKET_API_BASE_URL}/BuilderYjs_v1`,
{
reconnection: true,
auth: { token },
}
);
const ydoc = get().ydoc;
socket.on('v1:Line-collab:response:create', (update) => {
try {
console.log('socket: ', update);
const base64Update = update?.data?.update;
// console.log('base64Update: ', base64Update);
if (!base64Update) return;
// Decode base64 to Uint8Array
const binaryString = atob(base64Update);
// console.log('binaryString: ', binaryString);
const binaryUpdate = new Uint8Array(binaryString.length);
console.log('binaryUpdate: ', binaryUpdate);
for (let i = 0; i < binaryString.length; i++) {
binaryUpdate[i] = binaryString.charCodeAt(i);
}
console.log('binaryUpdate:', binaryUpdate);
const isUintArray = binaryUpdate instanceof Uint8Array;
console.log('isUintArray:', isUintArray);
if (isUintArray) {
Y.applyUpdate(ydoc, binaryUpdate); // ✅ apply decoded update
}
} catch (err) {
console.error('Error applying update:', err);
}
});
socket.on("line:add", (data) => {
console.log("📥 received line:add", data); // <- Add this
// log( line:add\n${JSON.stringify(data, null, 2)});
});
// Broadcast local changes
ydoc.on('update', (update: any) => {
console.log('ydoc: ', update);
socket.emit('v1:Line-collab:create', update);
});
const visualizationSocket = io(
`http://${process.env.REACT_APP_SERVER_SOCKET_API_BASE_URL}/Visualization_v1`,
{
@@ -51,6 +100,7 @@ export const useSocketStore = create<any>((set: any, get: any) => ({
set({
socket,
ydoc,
visualizationSocket,
dashBoardSocket,
projectSocket,