2026-06-26 10:50:24 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* useYjsBinding – React hook that ties YjsDocument, WebSocketProvider,
|
|
|
|
|
|
* and AwarenessManager together for real-time collaboration.
|
|
|
|
|
|
*/
|
|
|
|
|
|
import { useEffect, useRef, useState, useCallback } from 'react';
|
|
|
|
|
|
import * as Y from 'yjs';
|
|
|
|
|
|
import { YjsDocument } from './YjsDocument';
|
|
|
|
|
|
import { WebSocketProvider, type ConnectionStatus } from './WebSocketProvider';
|
|
|
|
|
|
import { AwarenessManager, type UserCursor, type UserSelection } from './AwarenessManager';
|
|
|
|
|
|
import type { CADElement, CADLayer, BlockDefinition } from '../types/cad.types';
|
2026-06-29 23:42:03 +02:00
|
|
|
|
import type { ElementGroup } from '../tools/modification/GroupTool';
|
|
|
|
|
|
import type { BackgroundConfig } from '../services/backgroundService';
|
2026-06-26 10:50:24 +02:00
|
|
|
|
|
|
|
|
|
|
export interface UseYjsBindingOptions {
|
|
|
|
|
|
docName: string;
|
|
|
|
|
|
wsUrl?: string;
|
|
|
|
|
|
userId: string;
|
|
|
|
|
|
userName: string;
|
|
|
|
|
|
userColor: string;
|
|
|
|
|
|
enabled?: boolean;
|
2026-06-29 23:33:58 +02:00
|
|
|
|
token?: string;
|
2026-06-26 10:50:24 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface UseYjsBindingResult {
|
|
|
|
|
|
yjsDoc: YjsDocument | null;
|
|
|
|
|
|
provider: WebSocketProvider | null;
|
|
|
|
|
|
awareness: AwarenessManager | null;
|
|
|
|
|
|
status: ConnectionStatus;
|
|
|
|
|
|
elements: CADElement[];
|
|
|
|
|
|
layers: CADLayer[];
|
|
|
|
|
|
blocks: BlockDefinition[];
|
2026-06-29 23:42:03 +02:00
|
|
|
|
groups: ElementGroup[];
|
|
|
|
|
|
bgConfigs: BackgroundConfig[];
|
2026-06-26 10:50:24 +02:00
|
|
|
|
cursors: UserCursor[];
|
|
|
|
|
|
selections: UserSelection[];
|
|
|
|
|
|
setElement: (el: CADElement) => void;
|
|
|
|
|
|
deleteElement: (id: string) => void;
|
|
|
|
|
|
setLayer: (layer: CADLayer) => void;
|
|
|
|
|
|
deleteLayer: (id: string) => void;
|
|
|
|
|
|
setBlock: (block: BlockDefinition) => void;
|
|
|
|
|
|
deleteBlock: (id: string) => void;
|
2026-06-29 23:42:03 +02:00
|
|
|
|
setGroup: (group: ElementGroup) => void;
|
|
|
|
|
|
deleteGroup: (id: string) => void;
|
|
|
|
|
|
setBgConfig: (id: string, config: BackgroundConfig) => void;
|
|
|
|
|
|
deleteBgConfig: (id: string) => void;
|
|
|
|
|
|
loadFromState: (state: {
|
|
|
|
|
|
elements: CADElement[];
|
|
|
|
|
|
layers: CADLayer[];
|
|
|
|
|
|
blocks: BlockDefinition[];
|
|
|
|
|
|
groups?: ElementGroup[];
|
|
|
|
|
|
bgConfigs?: BackgroundConfig[];
|
|
|
|
|
|
}) => void;
|
2026-06-26 10:50:24 +02:00
|
|
|
|
setCursor: (x: number, y: number) => void;
|
|
|
|
|
|
hideCursor: () => void;
|
|
|
|
|
|
setSelection: (elementIds: string[]) => void;
|
|
|
|
|
|
clearSelection: () => void;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-26 17:58:06 +02:00
|
|
|
|
const DEFAULT_WS_URL = `wss://${window.location.host}`;
|
2026-06-26 10:50:24 +02:00
|
|
|
|
|
|
|
|
|
|
export function useYjsBinding(opts: UseYjsBindingOptions): UseYjsBindingResult {
|
2026-06-29 23:33:58 +02:00
|
|
|
|
const { docName, wsUrl = DEFAULT_WS_URL, userId, userName, userColor, enabled = true, token } = opts;
|
2026-06-26 10:50:24 +02:00
|
|
|
|
|
|
|
|
|
|
const yjsDocRef = useRef<YjsDocument | null>(null);
|
|
|
|
|
|
const providerRef = useRef<WebSocketProvider | null>(null);
|
|
|
|
|
|
const awarenessRef = useRef<AwarenessManager | null>(null);
|
|
|
|
|
|
const unbindRef = useRef<(() => void) | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
const [status, setStatus] = useState<ConnectionStatus>('disconnected');
|
|
|
|
|
|
const [elements, setElements] = useState<CADElement[]>([]);
|
|
|
|
|
|
const [layers, setLayers] = useState<CADLayer[]>([]);
|
|
|
|
|
|
const [blocks, setBlocks] = useState<BlockDefinition[]>([]);
|
2026-06-29 23:42:03 +02:00
|
|
|
|
const [groups, setGroups] = useState<ElementGroup[]>([]);
|
|
|
|
|
|
const [bgConfigs, setBgConfigs] = useState<BackgroundConfig[]>([]);
|
2026-06-26 10:50:24 +02:00
|
|
|
|
const [cursors, setCursors] = useState<UserCursor[]>([]);
|
|
|
|
|
|
const [selections, setSelections] = useState<UserSelection[]>([]);
|
|
|
|
|
|
|
|
|
|
|
|
// Initialize Yjs document, provider, and awareness
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!enabled || !docName) return;
|
|
|
|
|
|
|
|
|
|
|
|
const doc = new YjsDocument();
|
|
|
|
|
|
yjsDocRef.current = doc;
|
|
|
|
|
|
|
2026-06-30 12:42:49 +02:00
|
|
|
|
const awareness = new AwarenessManager(doc, userId);
|
|
|
|
|
|
awarenessRef.current = awareness;
|
|
|
|
|
|
|
2026-06-26 10:50:24 +02:00
|
|
|
|
const provider = new WebSocketProvider({
|
|
|
|
|
|
url: wsUrl,
|
|
|
|
|
|
docName,
|
|
|
|
|
|
yjsDoc: doc,
|
2026-06-30 12:42:49 +02:00
|
|
|
|
awareness,
|
2026-06-26 10:50:24 +02:00
|
|
|
|
onStatusChange: (s) => setStatus(s),
|
|
|
|
|
|
});
|
|
|
|
|
|
providerRef.current = provider;
|
|
|
|
|
|
|
|
|
|
|
|
// Sync local state from Y.Doc on changes
|
|
|
|
|
|
const syncState = () => {
|
|
|
|
|
|
setElements(doc.getElements());
|
|
|
|
|
|
setLayers(doc.getLayers());
|
|
|
|
|
|
setBlocks(doc.getBlocks());
|
2026-06-29 23:42:03 +02:00
|
|
|
|
setGroups(doc.getGroups());
|
|
|
|
|
|
setBgConfigs(doc.getBgConfigs());
|
2026-06-26 10:50:24 +02:00
|
|
|
|
setCursors(awareness.getAllCursors());
|
|
|
|
|
|
setSelections(awareness.getAllSelections());
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const unbindDoc = doc.onChange(syncState);
|
|
|
|
|
|
const unbindAwareness = awareness.onChange(syncState);
|
|
|
|
|
|
|
2026-06-29 23:42:03 +02:00
|
|
|
|
// Dedicated observers for groups and bgConfig maps
|
|
|
|
|
|
const groupsMap = doc.data.groups;
|
|
|
|
|
|
const bgConfigMap = doc.data.bgConfig;
|
|
|
|
|
|
|
|
|
|
|
|
const groupsObserver = () => {
|
|
|
|
|
|
setGroups(Array.from(groupsMap.values()));
|
|
|
|
|
|
};
|
|
|
|
|
|
const bgConfigObserver = () => {
|
|
|
|
|
|
setBgConfigs(Array.from(bgConfigMap.values()));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
groupsMap.observe(groupsObserver);
|
|
|
|
|
|
bgConfigMap.observe(bgConfigObserver);
|
|
|
|
|
|
|
2026-06-26 10:50:24 +02:00
|
|
|
|
// Forward local updates to server
|
|
|
|
|
|
const unbindLocal = provider.bindLocalUpdates();
|
2026-06-30 12:42:49 +02:00
|
|
|
|
// Forward local awareness updates to server
|
|
|
|
|
|
const unbindAwarenessUpdates = provider.bindAwarenessUpdates();
|
2026-06-26 10:50:24 +02:00
|
|
|
|
|
|
|
|
|
|
// Connect
|
2026-06-29 23:33:58 +02:00
|
|
|
|
provider.connect(token);
|
2026-06-26 10:50:24 +02:00
|
|
|
|
syncState();
|
|
|
|
|
|
|
|
|
|
|
|
// Cleanup
|
|
|
|
|
|
return () => {
|
2026-06-29 23:42:03 +02:00
|
|
|
|
groupsMap.unobserve(groupsObserver);
|
|
|
|
|
|
bgConfigMap.unobserve(bgConfigObserver);
|
2026-06-26 10:50:24 +02:00
|
|
|
|
unbindDoc();
|
|
|
|
|
|
unbindAwareness();
|
|
|
|
|
|
unbindLocal();
|
2026-06-30 12:42:49 +02:00
|
|
|
|
unbindAwarenessUpdates();
|
2026-06-26 10:50:24 +02:00
|
|
|
|
awareness.removeSelf();
|
2026-06-30 12:42:49 +02:00
|
|
|
|
awareness.destroy();
|
2026-06-26 10:50:24 +02:00
|
|
|
|
provider.disconnect();
|
|
|
|
|
|
doc.destroy();
|
|
|
|
|
|
yjsDocRef.current = null;
|
|
|
|
|
|
providerRef.current = null;
|
|
|
|
|
|
awarenessRef.current = null;
|
|
|
|
|
|
};
|
2026-06-29 23:33:58 +02:00
|
|
|
|
}, [docName, wsUrl, userId, userName, userColor, enabled, token]);
|
2026-06-26 10:50:24 +02:00
|
|
|
|
|
|
|
|
|
|
// Mutators
|
|
|
|
|
|
const setElement = useCallback((el: CADElement) => {
|
|
|
|
|
|
yjsDocRef.current?.setElement(el);
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
const deleteElement = useCallback((id: string) => {
|
|
|
|
|
|
yjsDocRef.current?.deleteElement(id);
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
const setLayer = useCallback((layer: CADLayer) => {
|
|
|
|
|
|
yjsDocRef.current?.setLayer(layer);
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
const deleteLayer = useCallback((id: string) => {
|
|
|
|
|
|
yjsDocRef.current?.deleteLayer(id);
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
const setBlock = useCallback((block: BlockDefinition) => {
|
|
|
|
|
|
yjsDocRef.current?.setBlock(block);
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
const deleteBlock = useCallback((id: string) => {
|
|
|
|
|
|
yjsDocRef.current?.deleteBlock(id);
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
2026-06-29 23:42:03 +02:00
|
|
|
|
const setGroup = useCallback((group: ElementGroup) => {
|
|
|
|
|
|
yjsDocRef.current?.setGroup(group);
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
const deleteGroup = useCallback((id: string) => {
|
|
|
|
|
|
yjsDocRef.current?.deleteGroup(id);
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
const setBgConfig = useCallback((id: string, config: BackgroundConfig) => {
|
|
|
|
|
|
yjsDocRef.current?.setBgConfig(id, config);
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
const deleteBgConfig = useCallback((id: string) => {
|
|
|
|
|
|
yjsDocRef.current?.deleteBgConfig(id);
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
const loadFromState = useCallback((state: {
|
|
|
|
|
|
elements: CADElement[];
|
|
|
|
|
|
layers: CADLayer[];
|
|
|
|
|
|
blocks: BlockDefinition[];
|
|
|
|
|
|
groups?: ElementGroup[];
|
|
|
|
|
|
bgConfigs?: BackgroundConfig[];
|
|
|
|
|
|
}) => {
|
2026-06-26 10:50:24 +02:00
|
|
|
|
yjsDocRef.current?.loadFromState(state);
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
const setCursor = useCallback((x: number, y: number) => {
|
|
|
|
|
|
awarenessRef.current?.setCursor(x, y, userName, userColor);
|
|
|
|
|
|
}, [userName, userColor]);
|
|
|
|
|
|
|
|
|
|
|
|
const hideCursor = useCallback(() => {
|
|
|
|
|
|
awarenessRef.current?.hideCursor();
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
const setSelection = useCallback((elementIds: string[]) => {
|
|
|
|
|
|
awarenessRef.current?.setSelection(elementIds);
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
const clearSelection = useCallback(() => {
|
|
|
|
|
|
awarenessRef.current?.clearSelection();
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
yjsDoc: yjsDocRef.current,
|
|
|
|
|
|
provider: providerRef.current,
|
|
|
|
|
|
awareness: awarenessRef.current,
|
|
|
|
|
|
status,
|
|
|
|
|
|
elements,
|
|
|
|
|
|
layers,
|
|
|
|
|
|
blocks,
|
2026-06-29 23:42:03 +02:00
|
|
|
|
groups,
|
|
|
|
|
|
bgConfigs,
|
2026-06-26 10:50:24 +02:00
|
|
|
|
cursors,
|
|
|
|
|
|
selections,
|
|
|
|
|
|
setElement,
|
|
|
|
|
|
deleteElement,
|
|
|
|
|
|
setLayer,
|
|
|
|
|
|
deleteLayer,
|
|
|
|
|
|
setBlock,
|
|
|
|
|
|
deleteBlock,
|
2026-06-29 23:42:03 +02:00
|
|
|
|
setGroup,
|
|
|
|
|
|
deleteGroup,
|
|
|
|
|
|
setBgConfig,
|
|
|
|
|
|
deleteBgConfig,
|
2026-06-26 10:50:24 +02:00
|
|
|
|
loadFromState,
|
|
|
|
|
|
setCursor,
|
|
|
|
|
|
hideCursor,
|
|
|
|
|
|
setSelection,
|
|
|
|
|
|
clearSelection,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|