diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index de2882b..df87517 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -266,7 +266,14 @@ const CADEditor: React.FC = ({ projectId, token, onNavigateBack setDataLoaded(true); // Push initial data to Yjs CRDT after load if (collab.status === 'connected') { - collab.loadFromState({ elements: data.elements, layers: data.layers, blocks: data.blocks }); + const bgConfigs = bgConfig ? [bgConfig] : []; + collab.loadFromState({ + elements: data.elements, + layers: data.layers, + blocks: data.blocks, + groups, + bgConfigs, + }); } } catch (err) { console.error('Failed to load project:', err); @@ -316,8 +323,43 @@ const CADEditor: React.FC = ({ projectId, token, onNavigateBack collab.blocks.every((b, i) => b.id === blocks[i]?.id); if (!sameBlocks) setBlocks(collab.blocks); } + + // Sync groups from remote → local + if (collab.groups.length > 0) { + const sameGroups = collab.groups.length === groups.length && + collab.groups.every((g, i) => g.id === groups[i]?.id); + if (!sameGroups) setGroups(collab.groups); + } + + // Sync bgConfig from remote → local + if (collab.bgConfigs.length > 0) { + const remoteBg = collab.bgConfigs[0]; + if (!bgConfig || remoteBg.src !== bgConfig.src || remoteBg.scale !== bgConfig.scale || + remoteBg.offsetX !== bgConfig.offsetX || remoteBg.offsetY !== bgConfig.offsetY || + remoteBg.visible !== bgConfig.visible || remoteBg.opacity !== bgConfig.opacity) { + setBgConfig(remoteBg); + } + } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [collab.elements, collab.layers, collab.blocks, collab.status]); + }, [collab.elements, collab.layers, collab.blocks, collab.groups, collab.bgConfigs, collab.status]); + + // Sync local → remote: push local groups to Yjs when groups change + React.useEffect(() => { + if (collab.status !== 'connected') return; + for (const group of groups) { + collab.setGroup(group); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [groups, collab.status]); + + // Sync local → remote: push local bgConfig to Yjs when bgConfig changes + React.useEffect(() => { + if (collab.status !== 'connected') return; + if (bgConfig) { + collab.setBgConfig(bgConfig.name || 'default', bgConfig); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [bgConfig, collab.status]); const handleElementCreated = useCallback((el: CADElement) => { const elWithLayer = el.layerId ? el : { ...el, layerId: activeLayerId }; diff --git a/frontend/src/crdt/YjsDocument.ts b/frontend/src/crdt/YjsDocument.ts index 85f90b0..4ffbbdd 100644 --- a/frontend/src/crdt/YjsDocument.ts +++ b/frontend/src/crdt/YjsDocument.ts @@ -4,12 +4,16 @@ */ import * as Y from 'yjs'; import type { CADElement, CADLayer, BlockDefinition } from '../types/cad.types'; +import type { ElementGroup } from '../tools/modification/GroupTool'; +import type { BackgroundConfig } from '../services/backgroundService'; export interface YjsDocumentData { elements: Y.Map; layers: Y.Map; blocks: Y.Map; meta: Y.Map; + groups: Y.Map; + bgConfig: Y.Map; } export class YjsDocument { @@ -23,6 +27,8 @@ export class YjsDocument { layers: this.doc.getMap('layers'), blocks: this.doc.getMap('blocks'), meta: this.doc.getMap('meta'), + groups: this.doc.getMap('groups'), + bgConfig: this.doc.getMap('bgConfig'), }; } @@ -41,6 +47,16 @@ export class YjsDocument { return Array.from(this.data.blocks.values()); } + /** Get all groups as a plain array */ + getGroups(): ElementGroup[] { + return Array.from(this.data.groups.values()); + } + + /** Get all background configs as a plain array */ + getBgConfigs(): BackgroundConfig[] { + return Array.from(this.data.bgConfig.values()); + } + /** Add or update a single element */ setElement(el: CADElement): void { this.doc.transact(() => { @@ -73,11 +89,37 @@ export class YjsDocument { this.data.blocks.delete(id); } + /** Add or update a group */ + setGroup(group: ElementGroup): void { + this.doc.transact(() => { + this.data.groups.set(group.id, group); + }); + } + + /** Remove a group by id */ + deleteGroup(id: string): void { + this.data.groups.delete(id); + } + + /** Add or update a background config */ + setBgConfig(id: string, config: BackgroundConfig): void { + this.doc.transact(() => { + this.data.bgConfig.set(id, config); + }); + } + + /** Remove a background config by id */ + deleteBgConfig(id: string): void { + this.data.bgConfig.delete(id); + } + /** Bulk-load local state into the Y.Doc (replaces all content) */ loadFromState(state: { elements: CADElement[]; layers: CADLayer[]; blocks: BlockDefinition[]; + groups?: ElementGroup[]; + bgConfigs?: BackgroundConfig[]; }): void { this.doc.transact(() => { this.data.elements.clear(); @@ -92,6 +134,19 @@ export class YjsDocument { for (const block of state.blocks) { this.data.blocks.set(block.id, block); } + if (state.groups) { + this.data.groups.clear(); + for (const group of state.groups) { + this.data.groups.set(group.id, group); + } + } + if (state.bgConfigs) { + this.data.bgConfig.clear(); + for (const cfg of state.bgConfigs) { + // Use 'default' as the key since there is typically one background config + this.data.bgConfig.set(cfg.name || 'default', cfg); + } + } }); } @@ -100,11 +155,15 @@ export class YjsDocument { elements: CADElement[]; layers: CADLayer[]; blocks: BlockDefinition[]; + groups: ElementGroup[]; + bgConfigs: BackgroundConfig[]; } { return { elements: this.getElements(), layers: this.getLayers(), blocks: this.getBlocks(), + groups: this.getGroups(), + bgConfigs: this.getBgConfigs(), }; } diff --git a/frontend/src/crdt/useYjsBinding.ts b/frontend/src/crdt/useYjsBinding.ts index 928eb3d..cb5c2ad 100644 --- a/frontend/src/crdt/useYjsBinding.ts +++ b/frontend/src/crdt/useYjsBinding.ts @@ -8,6 +8,8 @@ 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'; +import type { ElementGroup } from '../tools/modification/GroupTool'; +import type { BackgroundConfig } from '../services/backgroundService'; export interface UseYjsBindingOptions { docName: string; @@ -27,6 +29,8 @@ export interface UseYjsBindingResult { elements: CADElement[]; layers: CADLayer[]; blocks: BlockDefinition[]; + groups: ElementGroup[]; + bgConfigs: BackgroundConfig[]; cursors: UserCursor[]; selections: UserSelection[]; setElement: (el: CADElement) => void; @@ -35,7 +39,17 @@ export interface UseYjsBindingResult { deleteLayer: (id: string) => void; setBlock: (block: BlockDefinition) => void; deleteBlock: (id: string) => void; - loadFromState: (state: { elements: CADElement[]; layers: CADLayer[]; blocks: BlockDefinition[] }) => void; + 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; setCursor: (x: number, y: number) => void; hideCursor: () => void; setSelection: (elementIds: string[]) => void; @@ -56,6 +70,8 @@ export function useYjsBinding(opts: UseYjsBindingOptions): UseYjsBindingResult { const [elements, setElements] = useState([]); const [layers, setLayers] = useState([]); const [blocks, setBlocks] = useState([]); + const [groups, setGroups] = useState([]); + const [bgConfigs, setBgConfigs] = useState([]); const [cursors, setCursors] = useState([]); const [selections, setSelections] = useState([]); @@ -82,6 +98,8 @@ export function useYjsBinding(opts: UseYjsBindingOptions): UseYjsBindingResult { setElements(doc.getElements()); setLayers(doc.getLayers()); setBlocks(doc.getBlocks()); + setGroups(doc.getGroups()); + setBgConfigs(doc.getBgConfigs()); setCursors(awareness.getAllCursors()); setSelections(awareness.getAllSelections()); }; @@ -89,6 +107,20 @@ export function useYjsBinding(opts: UseYjsBindingOptions): UseYjsBindingResult { const unbindDoc = doc.onChange(syncState); const unbindAwareness = awareness.onChange(syncState); + // 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); + // Forward local updates to server const unbindLocal = provider.bindLocalUpdates(); @@ -98,6 +130,8 @@ export function useYjsBinding(opts: UseYjsBindingOptions): UseYjsBindingResult { // Cleanup return () => { + groupsMap.unobserve(groupsObserver); + bgConfigMap.unobserve(bgConfigObserver); unbindDoc(); unbindAwareness(); unbindLocal(); @@ -135,7 +169,29 @@ export function useYjsBinding(opts: UseYjsBindingOptions): UseYjsBindingResult { yjsDocRef.current?.deleteBlock(id); }, []); - const loadFromState = useCallback((state: { elements: CADElement[]; layers: CADLayer[]; blocks: BlockDefinition[] }) => { + 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[]; + }) => { yjsDocRef.current?.loadFromState(state); }, []); @@ -163,6 +219,8 @@ export function useYjsBinding(opts: UseYjsBindingOptions): UseYjsBindingResult { elements, layers, blocks, + groups, + bgConfigs, cursors, selections, setElement, @@ -171,6 +229,10 @@ export function useYjsBinding(opts: UseYjsBindingOptions): UseYjsBindingResult { deleteLayer, setBlock, deleteBlock, + setGroup, + deleteGroup, + setBgConfig, + deleteBgConfig, loadFromState, setCursor, hideCursor,