feat: RefBoard v0.4.0 — collaborative reference board with layers, groups & polished UI

Full-featured PureRef-style collaborative canvas for game dev teams:
- Layer panel with visibility, lock, drag reorder, group/ungroup (Ctrl+G/Shift+G)
- Arrangement tools (grid, row, column) via right-click context menu
- Copy to system clipboard (Ctrl+C writes PNG for external paste in Paint etc.)
- Number shortcuts (1-5) for tool selection with visible shortcut badges
- Premium dark UI across all pages (Login, Collections, Boards, Editor)
- Socket.IO rooms for cursors, transforms, and presence notifications
- MinIO image storage with backend proxy, drag/drop and paste upload
This commit is contained in:
Hiren
2026-03-09 13:57:49 +05:30
commit e47777d237
43 changed files with 10276 additions and 0 deletions
+83
View File
@@ -0,0 +1,83 @@
import { Canvas } from 'fabric';
export class UndoManager {
private stack: string[] = [];
private pointer: number = -1;
private maxEntries: number = 50;
private locked: boolean = false;
private canvas: Canvas;
constructor(canvas: Canvas) {
this.canvas = canvas;
// Save initial state
this.saveState();
}
isLocked(): boolean {
return this.locked;
}
saveState(): void {
if (this.locked) return;
const json = JSON.stringify((this.canvas as any).toJSON(['id']));
// If we're not at the end, discard forward history
if (this.pointer < this.stack.length - 1) {
this.stack = this.stack.slice(0, this.pointer + 1);
}
// Don't save if identical to current state
if (this.stack.length > 0 && this.stack[this.pointer] === json) {
return;
}
this.stack.push(json);
// Enforce max entries
if (this.stack.length > this.maxEntries) {
this.stack.shift();
}
this.pointer = this.stack.length - 1;
}
undo(): void {
if (!this.canUndo()) return;
this.pointer--;
this.restoreState();
}
redo(): void {
if (!this.canRedo()) return;
this.pointer++;
this.restoreState();
}
canUndo(): boolean {
return this.pointer > 0;
}
canRedo(): boolean {
return this.pointer < this.stack.length - 1;
}
private restoreState(): void {
const state = this.stack[this.pointer];
if (!state) return;
this.locked = true;
this.canvas.loadFromJSON(JSON.parse(state)).then(() => {
this.canvas.requestRenderAll();
this.locked = false;
});
}
clear(): void {
this.stack = [];
this.pointer = -1;
this.saveState();
}
}