- 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
106 lines
2.7 KiB
TypeScript
106 lines
2.7 KiB
TypeScript
import { Texture, Assets } from "pixi.js";
|
|
|
|
interface TextureEntry {
|
|
texture: Texture;
|
|
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;
|
|
|
|
/** Build the URL for a given asset. */
|
|
urlForAsset(assetKey: string): string {
|
|
if (assetKey.startsWith('http') || assetKey.startsWith('/api/')) {
|
|
return assetKey;
|
|
}
|
|
return `/api/images/${assetKey}`;
|
|
}
|
|
|
|
/**
|
|
* 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): Promise<Texture> {
|
|
const existing = this.cache.get(assetKey);
|
|
if (existing) {
|
|
existing.lastUsed = performance.now();
|
|
return existing.texture;
|
|
}
|
|
|
|
const url = this.urlForAsset(assetKey);
|
|
let texture: Texture;
|
|
try {
|
|
texture = await Assets.load(url);
|
|
} catch {
|
|
console.warn(`[TextureManager] Failed to load ${url}`);
|
|
return Texture.EMPTY;
|
|
}
|
|
|
|
const w = texture.source.width ?? 256;
|
|
const h = texture.source.height ?? 256;
|
|
const memoryEstimate = w * h * 4; // RGBA
|
|
|
|
this.currentUsage += memoryEstimate;
|
|
|
|
this.cache.set(assetKey, {
|
|
texture,
|
|
lastUsed: performance.now(),
|
|
memoryEstimate,
|
|
});
|
|
|
|
while (this.currentUsage > this.budget && this.cache.size > 1) {
|
|
this.evictLRU();
|
|
}
|
|
|
|
return texture;
|
|
}
|
|
|
|
/** Remove a specific texture from the cache and destroy it. */
|
|
unload(assetKey: string): void {
|
|
const entry = this.cache.get(assetKey);
|
|
if (!entry) return;
|
|
|
|
this.currentUsage -= entry.memoryEstimate;
|
|
entry.texture.destroy(true);
|
|
this.cache.delete(assetKey);
|
|
}
|
|
|
|
/** Evict the least-recently-used cache entry. */
|
|
private evictLRU(): void {
|
|
let oldestKey: string | null = null;
|
|
let oldestTime = Infinity;
|
|
|
|
for (const [key, entry] of this.cache) {
|
|
if (entry.lastUsed < oldestTime) {
|
|
oldestTime = entry.lastUsed;
|
|
oldestKey = key;
|
|
}
|
|
}
|
|
|
|
if (oldestKey) {
|
|
const entry = this.cache.get(oldestKey)!;
|
|
this.currentUsage -= entry.memoryEstimate;
|
|
entry.texture.destroy(true);
|
|
this.cache.delete(oldestKey);
|
|
}
|
|
}
|
|
|
|
/** Destroy all cached textures and reset memory tracking. */
|
|
clear(): void {
|
|
for (const entry of this.cache.values()) {
|
|
entry.texture.destroy(true);
|
|
}
|
|
this.cache.clear();
|
|
this.currentUsage = 0;
|
|
}
|
|
}
|