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:
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user