feat(refboard): canvas polish — transform box sync, dot grid, snap tuning, perf fixes

- Fix transform box not updating after alignment/arrangement/normalize/flip operations
  (shortcuts, context menu, and selection toolbar all fixed with DRY _opUpdate helper)
- Replace 100ms polling loop with event-driven viewport updates (moved + wheel-scroll)
- Add adaptive dot grid background that responds to zoom/pan
- Reduce snap guide threshold from 8px to 4px for subtler snapping
- Remove PresenceOverlay (remote selection highlighting) — too heavy for minimal benefit
- Offset multiple dropped images so they don't overlap
- Add new canvas modules: SnapGuides, clipboard, grouping, FrameSprite, DrawingSprite,
  LaserPointer, context-menu-items, SelectionToolbar, Minimap
- Extract Editor hooks into dedicated files (useBoardLoader, useCanvasSetup,
  useShortcutHandler, useLayerPanel, useSaveManager, useFollowMode)
- Sync improvements: real-time transform broadcast, board rooms, viewport sync
This commit is contained in:
Hiren Kangad
2026-03-10 03:58:53 +05:30
parent cfd7adf73d
commit e9509ef7b0
41 changed files with 4171 additions and 705 deletions
+20 -47
View File
@@ -1,67 +1,42 @@
import { Texture, Assets } from "pixi.js";
export type LODTier = "thumb" | "medium" | "full";
interface TextureEntry {
texture: Texture;
tier: LODTier;
lastUsed: number;
memoryEstimate: number;
}
/**
* TextureManager — GPU texture cache with LRU eviction and memory budget.
* No LOD tiers — PixiJS GPU handles scaling natively.
* Just loads the full-res image and caches it.
*/
export class TextureManager {
private cache = new Map<string, TextureEntry>();
private budget = 512 * 1024 * 1024; // 512 MB
private currentUsage = 0;
/** Map zoom level to the appropriate LOD tier. */
tierForZoom(zoom: number): LODTier {
if (zoom < 0.3) return "thumb";
if (zoom > 1.5) return "full";
return "medium";
}
/**
* Detect whether an asset key is a new LOD-format key (no extension)
* or an old pre-migration path (has file extension).
*/
private _isLegacyAsset(assetKey: string): boolean {
// New LOD keys look like: boards/{boardId}/{imageId} (no extension)
// Old keys look like: boards/{boardId}/{imageId}.png or full URLs
return /\.\w{2,5}$/.test(assetKey) || assetKey.startsWith('http') || assetKey.startsWith('/api/');
}
/** Build the URL for a given asset + tier. */
urlForAsset(assetKey: string, tier: LODTier): string {
if (this._isLegacyAsset(assetKey)) {
// Legacy: load original file directly (GPU handles scaling)
if (assetKey.startsWith('http') || assetKey.startsWith('/api/')) {
return assetKey;
}
return `/api/images/${assetKey}`;
/** Build the URL for a given asset. */
urlForAsset(assetKey: string): string {
if (assetKey.startsWith('http') || assetKey.startsWith('/api/')) {
return assetKey;
}
// New LOD format: boards/{boardId}/{imageId}/thumb.webp
return `/api/images/${assetKey}/${tier}.webp`;
return `/api/images/${assetKey}`;
}
/**
* Load a texture for the given asset and LOD tier.
* For legacy assets (pre-migration), loads the original file directly —
* the GPU handles downscaling naturally.
* For new assets, loads the appropriate LOD tier with fallback.
* Load a texture for the given asset.
* Returns cached texture if available, otherwise fetches and caches.
* GPU handles all scaling — no LOD tiers needed.
*/
async load(assetKey: string, tier: LODTier): Promise<Texture> {
// For legacy assets, all tiers resolve to the same URL
const effectiveTier = this._isLegacyAsset(assetKey) ? 'full' : tier;
const key = `${assetKey}:${effectiveTier}`;
const existing = this.cache.get(key);
async load(assetKey: string): Promise<Texture> {
const existing = this.cache.get(assetKey);
if (existing) {
existing.lastUsed = performance.now();
return existing.texture;
}
const url = this.urlForAsset(assetKey, effectiveTier);
const url = this.urlForAsset(assetKey);
let texture: Texture;
try {
texture = await Assets.load(url);
@@ -76,9 +51,8 @@ export class TextureManager {
this.currentUsage += memoryEstimate;
this.cache.set(key, {
this.cache.set(assetKey, {
texture,
tier,
lastUsed: performance.now(),
memoryEstimate,
});
@@ -91,14 +65,13 @@ export class TextureManager {
}
/** Remove a specific texture from the cache and destroy it. */
unload(assetKey: string, tier: LODTier): void {
const key = `${assetKey}:${tier}`;
const entry = this.cache.get(key);
unload(assetKey: string): void {
const entry = this.cache.get(assetKey);
if (!entry) return;
this.currentUsage -= entry.memoryEstimate;
entry.texture.destroy(true);
this.cache.delete(key);
this.cache.delete(assetKey);
}
/** Evict the least-recently-used cache entry. */