agent-ecosystem/packages/agent-graph/src/canvas/render-cache.ts
iliya 26fe42739b refactor(graph): fix SOLID/DRY violations from audit
DRY fixes:
- GraphOverlay.tsx: 539 LOC → 85 LOC minimal fallback (was dead code)
- COLUMN_LABELS: import from COLORS instead of hardcoded hex duplicates
- Alpha hex LUT: consolidated to single source in colors.ts
- TeamGraphTab: extract typed dispatchOpenTask/dispatchSendMessage helpers
  (was 8 repeated CustomEvent dispatches)

SOLID fixes:
- D-1: lucide-react added to peerDependencies in package.json
- S-1: GraphOverlay no longer has 8 responsibilities (now just 1 fallback)

Architecture:
- renderOverlay prop properly used by both Tab and Overlay
- Popover rendering lives in features/ layer only (no duplication with package)
2026-03-28 14:41:24 +02:00

139 lines
4.4 KiB
TypeScript

/**
* Pre-rendered sprite cache for Canvas 2D glow effects.
* Adapted from agent-flow (Apache 2.0).
*/
const glowCache = new Map<string, HTMLCanvasElement>();
const textCache = new Map<string, number>();
const TEXT_CACHE_LIMIT = 2000;
// ─── Color resolution: named colors → hex ───────────────────────────────────
let _resolverCtx: CanvasRenderingContext2D | null = null;
const _hexCache = new Map<string, string>();
/**
* Ensure a color string is in #rrggbb hex format.
* Resolves CSS named colors (purple → #800080), shorthand (#abc → #aabbcc).
*/
function ensureHex(color: string): string {
if (color.startsWith('#') && color.length === 7) return color;
let hex = _hexCache.get(color);
if (hex) return hex;
if (color.startsWith('#') && color.length === 4) {
hex = `#${color[1]}${color[1]}${color[2]}${color[2]}${color[3]}${color[3]}`;
} else {
// Resolve named/rgb/hsl colors via canvas
_resolverCtx ??= document.createElement('canvas').getContext('2d')!;
_resolverCtx.fillStyle = '#000000';
_resolverCtx.fillStyle = color;
hex = _resolverCtx.fillStyle; // always returns #rrggbb
}
_hexCache.set(color, hex);
return hex;
}
/** Build a hex color with alpha: "#rrggbbaa" — cached for repeated calls */
const _hexAlphaCache = new Map<string, string>();
function hexWithAlpha(color: string, alpha: number): string {
// Quantize alpha to 1/255 steps for cache hit rate
const a = Math.round(Math.max(0, Math.min(1, alpha)) * 255);
const key = `${color}|${a}`;
let result = _hexAlphaCache.get(key);
if (result) return result;
result = ensureHex(color) + alphaHex(a / 255);
_hexAlphaCache.set(key, result);
if (_hexAlphaCache.size > 500) _hexAlphaCache.clear(); // prevent unbounded growth
return result;
}
// Reuse alpha hex LUT from colors.ts (DRY — single source)
import { alphaHex } from '../constants/colors';
// ─── Glow Sprite Cache ──────────────────────────────────────────────────────
/**
* Get or create a pre-rendered radial gradient glow sprite.
*/
export function getGlowSprite(
color: string,
radius: number,
innerAlpha: number,
outerAlpha: number,
): HTMLCanvasElement {
const hex = ensureHex(color);
const key = `${hex}|${radius}|${innerAlpha}|${outerAlpha}`;
let canvas = glowCache.get(key);
if (canvas) return canvas;
const size = Math.ceil(radius * 2);
canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d')!;
const cx = size / 2;
const grad = ctx.createRadialGradient(cx, cx, 0, cx, cx, radius);
grad.addColorStop(0, hexWithAlpha(hex, innerAlpha));
grad.addColorStop(1, hexWithAlpha(hex, outerAlpha));
ctx.fillStyle = grad;
ctx.fillRect(0, 0, size, size);
glowCache.set(key, canvas);
return canvas;
}
/**
* Get or create a pre-rendered agent glow sprite (inner + outer radius).
*/
export function getAgentGlowSprite(
color: string,
innerRadius: number,
outerRadius: number,
): HTMLCanvasElement {
const hex = ensureHex(color);
const key = `agent|${hex}|${innerRadius}|${outerRadius}`;
let canvas = glowCache.get(key);
if (canvas) return canvas;
const size = Math.ceil(outerRadius * 2);
canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d')!;
const cx = size / 2;
const grad = ctx.createRadialGradient(cx, cx, innerRadius, cx, cx, outerRadius);
grad.addColorStop(0, hexWithAlpha(hex, 0.25));
grad.addColorStop(0.5, hexWithAlpha(hex, 0.08));
grad.addColorStop(1, hexWithAlpha(hex, 0));
ctx.fillStyle = grad;
ctx.fillRect(0, 0, size, size);
glowCache.set(key, canvas);
return canvas;
}
/**
* Cached text width measurement.
*/
export function measureTextCached(ctx: CanvasRenderingContext2D, font: string, text: string): number {
const key = `${font}|${text}`;
let w = textCache.get(key);
if (w !== undefined) return w;
if (textCache.size > TEXT_CACHE_LIMIT) textCache.clear();
const prevFont = ctx.font;
ctx.font = font;
w = ctx.measureText(text).width;
ctx.font = prevFont;
textCache.set(key, w);
return w;
}
/** Exported for use by draw functions that need hex+alpha colors */
export { ensureHex, hexWithAlpha };