feat: initial commit web-cad-neu with docker-compose, frontend and backend

This commit is contained in:
2026-06-26 10:50:24 +02:00
commit 4ec76fe406
102 changed files with 25722 additions and 0 deletions
+121
View File
@@ -0,0 +1,121 @@
/**
* AwarenessManager Tracks user presence, cursor position, and selection.
* Uses a Y.Map inside the shared doc so awareness state syncs via the same
* raw-update WebSocket protocol as the rest of the document.
*/
import * as Y from 'yjs';
import type { YjsDocument } from './YjsDocument';
export interface UserCursor {
userId: string;
userName: string;
color: string;
x: number;
y: number;
visible: boolean;
}
export interface UserSelection {
userId: string;
elementIds: string[];
}
export interface AwarenessState {
cursors: Y.Map<UserCursor>;
selections: Y.Map<UserSelection>;
}
export class AwarenessManager {
readonly state: AwarenessState;
private yjsDoc: YjsDocument;
private userId: string;
private listeners: Set<() => void> = new Set();
constructor(yjsDoc: YjsDocument, userId: string) {
this.yjsDoc = yjsDoc;
this.userId = userId;
this.state = {
cursors: this.yjsDoc.doc.getMap<UserCursor>('awareness-cursors'),
selections: this.yjsDoc.doc.getMap<UserSelection>('awareness-selections'),
};
}
/** Set the local user's cursor position */
setCursor(x: number, y: number, userName: string, color: string): void {
this.state.cursors.set(this.userId, {
userId: this.userId,
userName,
color,
x,
y,
visible: true,
});
}
/** Hide the local user's cursor */
hideCursor(): void {
const cur = this.state.cursors.get(this.userId);
if (cur) {
this.state.cursors.set(this.userId, { ...cur, visible: false });
}
}
/** Set the local user's selection */
setSelection(elementIds: string[]): void {
this.state.selections.set(this.userId, {
userId: this.userId,
elementIds,
});
}
/** Clear the local user's selection */
clearSelection(): void {
this.state.selections.delete(this.userId);
}
/** Remove the local user from awareness (on disconnect) */
removeSelf(): void {
this.state.cursors.delete(this.userId);
this.state.selections.delete(this.userId);
}
/** Get all active cursors */
getAllCursors(): UserCursor[] {
return Array.from(this.state.cursors.values()).filter((c) => c.visible);
}
/** Get all selections */
getAllSelections(): UserSelection[] {
return Array.from(this.state.selections.values());
}
/** Get a specific user's cursor */
getCursor(userId: string): UserCursor | undefined {
return this.state.cursors.get(userId);
}
/** Get a specific user's selection */
getSelection(userId: string): UserSelection | undefined {
return this.state.selections.get(userId);
}
/** Register a callback for awareness changes */
onChange(callback: () => void): () => void {
this.listeners.add(callback);
const cursorObserver = () => this.notifyListeners();
const selectionObserver = () => this.notifyListeners();
this.state.cursors.observe(cursorObserver);
this.state.selections.observe(selectionObserver);
return () => {
this.listeners.delete(callback);
this.state.cursors.unobserve(cursorObserver);
this.state.selections.unobserve(selectionObserver);
};
}
private notifyListeners(): void {
for (const cb of this.listeners) {
cb();
}
}
}