feat: initial commit web-cad-neu with docker-compose, frontend and backend
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user