2026-06-26 10:50:24 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* 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';
|
2026-07-04 16:43:55 +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 YjsDocumentData {
|
|
|
|
|
|
elements: Y.Map<CADElement>;
|
|
|
|
|
|
layers: Y.Map<CADLayer>;
|
|
|
|
|
|
blocks: Y.Map<BlockDefinition>;
|
|
|
|
|
|
meta: Y.Map<unknown>;
|
2026-07-04 16:43:55 +02:00
|
|
|
|
groups: Y.Map<ElementGroup>;
|
|
|
|
|
|
bgConfig: Y.Map<BackgroundConfig>;
|
2026-06-26 10:50:24 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export class YjsDocument {
|
|
|
|
|
|
readonly doc: Y.Doc;
|
|
|
|
|
|
readonly data: YjsDocumentData;
|
|
|
|
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
|
this.doc = new Y.Doc();
|
|
|
|
|
|
this.data = {
|
|
|
|
|
|
elements: this.doc.getMap<CADElement>('elements'),
|
|
|
|
|
|
layers: this.doc.getMap<CADLayer>('layers'),
|
|
|
|
|
|
blocks: this.doc.getMap<BlockDefinition>('blocks'),
|
|
|
|
|
|
meta: this.doc.getMap<unknown>('meta'),
|
2026-07-04 16:43:55 +02:00
|
|
|
|
groups: this.doc.getMap<ElementGroup>('groups'),
|
|
|
|
|
|
bgConfig: this.doc.getMap<BackgroundConfig>('bgConfig'),
|
2026-06-26 10:50:24 +02:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 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());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-04 16:43:55 +02:00
|
|
|
|
/** 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());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-26 10:50:24 +02:00
|
|
|
|
/** 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-04 16:43:55 +02:00
|
|
|
|
/** 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-26 10:50:24 +02:00
|
|
|
|
/** Bulk-load local state into the Y.Doc (replaces all content) */
|
|
|
|
|
|
loadFromState(state: {
|
|
|
|
|
|
elements: CADElement[];
|
|
|
|
|
|
layers: CADLayer[];
|
|
|
|
|
|
blocks: BlockDefinition[];
|
2026-07-04 16:43:55 +02:00
|
|
|
|
groups?: ElementGroup[];
|
|
|
|
|
|
bgConfigs?: BackgroundConfig[];
|
2026-06-26 10:50:24 +02:00
|
|
|
|
}): void {
|
|
|
|
|
|
this.doc.transact(() => {
|
2026-07-04 16:43:55 +02:00
|
|
|
|
// 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);
|
|
|
|
|
|
}
|
2026-06-26 10:50:24 +02:00
|
|
|
|
}
|
2026-07-04 16:43:55 +02:00
|
|
|
|
if (state.groups && state.groups.length > 0) {
|
|
|
|
|
|
this.data.groups.clear();
|
|
|
|
|
|
for (const group of state.groups) {
|
|
|
|
|
|
this.data.groups.set(group.id, group);
|
|
|
|
|
|
}
|
2026-06-26 10:50:24 +02:00
|
|
|
|
}
|
2026-07-04 16:43:55 +02:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-06-26 10:50:24 +02:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Export current state as a plain object */
|
|
|
|
|
|
toState(): {
|
|
|
|
|
|
elements: CADElement[];
|
|
|
|
|
|
layers: CADLayer[];
|
|
|
|
|
|
blocks: BlockDefinition[];
|
2026-07-04 16:43:55 +02:00
|
|
|
|
groups: ElementGroup[];
|
|
|
|
|
|
bgConfigs: BackgroundConfig[];
|
2026-06-26 10:50:24 +02:00
|
|
|
|
} {
|
|
|
|
|
|
return {
|
|
|
|
|
|
elements: this.getElements(),
|
|
|
|
|
|
layers: this.getLayers(),
|
|
|
|
|
|
blocks: this.getBlocks(),
|
2026-07-04 16:43:55 +02:00
|
|
|
|
groups: this.getGroups(),
|
|
|
|
|
|
bgConfigs: this.getBgConfigs(),
|
2026-06-26 10:50:24 +02:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 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();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|