feat: initial commit web-cad-neu with docker-compose, frontend and backend
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
export interface UseYjsBindingOptions {
|
||||
docName: string;
|
||||
wsUrl?: string;
|
||||
userId: string;
|
||||
userName: string;
|
||||
userColor: string;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface UseYjsBindingResult {
|
||||
yjsDoc: YjsDocument | null;
|
||||
provider: WebSocketProvider | null;
|
||||
awareness: AwarenessManager | null;
|
||||
status: ConnectionStatus;
|
||||
elements: CADElement[];
|
||||
layers: CADLayer[];
|
||||
blocks: BlockDefinition[];
|
||||
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;
|
||||
loadFromState: (state: { elements: CADElement[]; layers: CADLayer[]; blocks: BlockDefinition[] }) => void;
|
||||
setCursor: (x: number, y: number) => void;
|
||||
hideCursor: () => void;
|
||||
setSelection: (elementIds: string[]) => void;
|
||||
clearSelection: () => void;
|
||||
}
|
||||
|
||||
const DEFAULT_WS_URL = `ws://${window.location.hostname}:3001`;
|
||||
|
||||
export function useYjsBinding(opts: UseYjsBindingOptions): UseYjsBindingResult {
|
||||
const { docName, wsUrl = DEFAULT_WS_URL, userId, userName, userColor, enabled = true } = opts;
|
||||
|
||||
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[]>([]);
|
||||
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;
|
||||
|
||||
const provider = new WebSocketProvider({
|
||||
url: wsUrl,
|
||||
docName,
|
||||
yjsDoc: doc,
|
||||
onStatusChange: (s) => setStatus(s),
|
||||
});
|
||||
providerRef.current = provider;
|
||||
|
||||
const awareness = new AwarenessManager(doc, userId);
|
||||
awarenessRef.current = awareness;
|
||||
|
||||
// Sync local state from Y.Doc on changes
|
||||
const syncState = () => {
|
||||
setElements(doc.getElements());
|
||||
setLayers(doc.getLayers());
|
||||
setBlocks(doc.getBlocks());
|
||||
setCursors(awareness.getAllCursors());
|
||||
setSelections(awareness.getAllSelections());
|
||||
};
|
||||
|
||||
const unbindDoc = doc.onChange(syncState);
|
||||
const unbindAwareness = awareness.onChange(syncState);
|
||||
|
||||
// Forward local updates to server
|
||||
const unbindLocal = provider.bindLocalUpdates();
|
||||
|
||||
// Connect
|
||||
provider.connect();
|
||||
syncState();
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
unbindDoc();
|
||||
unbindAwareness();
|
||||
unbindLocal();
|
||||
awareness.removeSelf();
|
||||
provider.disconnect();
|
||||
doc.destroy();
|
||||
yjsDocRef.current = null;
|
||||
providerRef.current = null;
|
||||
awarenessRef.current = null;
|
||||
};
|
||||
}, [docName, wsUrl, userId, userName, userColor, enabled]);
|
||||
|
||||
// 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);
|
||||
}, []);
|
||||
|
||||
const loadFromState = useCallback((state: { elements: CADElement[]; layers: CADLayer[]; blocks: BlockDefinition[] }) => {
|
||||
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,
|
||||
cursors,
|
||||
selections,
|
||||
setElement,
|
||||
deleteElement,
|
||||
setLayer,
|
||||
deleteLayer,
|
||||
setBlock,
|
||||
deleteBlock,
|
||||
loadFromState,
|
||||
setCursor,
|
||||
hideCursor,
|
||||
setSelection,
|
||||
clearSelection,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user