/** * YjsDocument – Manages a Y.Doc with shared maps for CAD elements, layers, and blocks. * Provides helpers to sync local state with the CRDT document. */ 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'; import type { LayoutDefinition } from '../types/cad.types'; export interface YjsDocumentData { elements: Y.Map; layers: Y.Map; blocks: Y.Map; meta: Y.Map; groups: Y.Map; bgConfig: Y.Map; layouts: Y.Map; // Task G1 } export class YjsDocument { readonly doc: Y.Doc; readonly data: YjsDocumentData; constructor() { this.doc = new Y.Doc(); this.data = { elements: this.doc.getMap('elements'), 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'), layouts: this.doc.getMap('layouts'), }; } /** Get all elements as a plain array */ getElements(): CADElement[] { return Array.from(this.data.elements.values()); } /** Get all layers as a plain array */ getLayers(): CADLayer[] { return Array.from(this.data.layers.values()); } /** Get all blocks as a plain array */ getBlocks(): BlockDefinition[] { 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()); } /** Get all layouts as a plain array (Task G1) */ getLayouts(): LayoutDefinition[] { return Array.from(this.data.layouts.values()); } /** Add or update a single element */ setElement(el: CADElement): void { this.doc.transact(() => { this.data.elements.set(el.id, el); }); } /** Remove an element by id */ deleteElement(id: string): void { this.data.elements.delete(id); } /** Add or update a layer */ setLayer(layer: CADLayer): void { this.data.layers.set(layer.id, layer); } /** Remove a layer by id */ deleteLayer(id: string): void { this.data.layers.delete(id); } /** Add or update a block definition */ setBlock(block: BlockDefinition): void { this.data.blocks.set(block.id, block); } /** Remove a block by id */ deleteBlock(id: string): void { 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(() => { // Issue #7: Only clear and replace a map if the incoming array is non-empty. // Clearing maps when data is empty wipes CRDT data from other connected clients. if (state.elements.length > 0) { this.data.elements.clear(); for (const el of state.elements) { this.data.elements.set(el.id, el); } } if (state.layers.length > 0) { this.data.layers.clear(); for (const layer of state.layers) { this.data.layers.set(layer.id, layer); } } if (state.blocks.length > 0) { this.data.blocks.clear(); for (const block of state.blocks) { this.data.blocks.set(block.id, block); } } if (state.groups && state.groups.length > 0) { this.data.groups.clear(); for (const group of state.groups) { this.data.groups.set(group.id, group); } } if (state.bgConfigs && state.bgConfigs.length > 0) { 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); } } }); } /** Export current state as a plain object */ toState(): { 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(), }; } /** Encode full document state as update binary */ encodeState(): Uint8Array { return Y.encodeStateAsUpdate(this.doc); } /** Apply a remote update binary */ applyUpdate(update: Uint8Array): void { Y.applyUpdate(this.doc, update); } /** Register a callback for document changes */ onChange(callback: () => void): () => void { this.doc.on('update', callback); return () => this.doc.off('update', callback); } /** Destroy the document */ destroy(): void { this.doc.destroy(); } }