UNPKG

@gravity-ui/graph

Version:

Modern graph editor component

170 lines (169 loc) 5.01 kB
import { cache } from "../../../../lib/utils"; class Path2DChunk { constructor() { this.items = new Set(); this.visibleItems = cache(() => { return Array.from(this.items).filter((item) => item.isPathVisible?.() ?? true); }); this.path = cache(() => { const path = new Path2D(); path.moveTo(0, 0); // Use already filtered visibleItems - no need for additional visibility checks for (const item of this.visibleItems.get()) { const subPath = item.getPath(); if (subPath) { path.addPath(subPath); } } return path; }); } applyStyles(ctx) { // Style comes from first visible item const first = this.visibleItems.get()[0]; return first?.style(ctx); } add(item) { this.items.add(item); this.reset(); } delete(item) { this.items.delete(item); this.reset(); } reset() { this.path.reset(); this.visibleItems.reset(); } render(ctx) { const vis = this.visibleItems.get(); if (!vis.length) return; ctx.save(); const style = this.applyStyles(ctx); if (style) { const p = this.path.get(); if (style.type === "fill" || style.type === "both") { ctx.fill(p, style.fillRule); } if (style.type === "stroke" || style.type === "both") { ctx.stroke(p); } } ctx.restore(); for (const item of vis) { item.afterRender?.(ctx); } } get size() { return this.items.size; } } class Path2DGroup { constructor(chunkSize) { this.chunkSize = chunkSize; this.chunks = []; this.itemToChunk = new Map(); this.chunks.push(new Path2DChunk()); } add(item) { let lastChunk = this.chunks[this.chunks.length - 1]; if (lastChunk.size >= this.chunkSize) { lastChunk = new Path2DChunk(); this.chunks.push(lastChunk); } lastChunk.add(item); this.itemToChunk.set(item, lastChunk); } delete(item) { const chunk = this.itemToChunk.get(item); if (chunk) { chunk.delete(item); this.itemToChunk.delete(item); if (chunk.size === 0 && this.chunks.length > 1) { const index = this.chunks.indexOf(chunk); if (index > -1) { this.chunks.splice(index, 1); } } } } resetItem(item) { const chunk = this.itemToChunk.get(item); chunk?.reset(); } render(ctx) { for (const chunk of this.chunks) { chunk.render(ctx); } } } export class BatchPath2DRenderer { constructor(onChange, chunkSize = 100) { this.onChange = onChange; this.chunkSize = chunkSize; this.indexes = new Map(); this.itemParams = new Map(); this.orderedPaths = cache(() => { return Array.from(this.indexes.entries()) .sort(([indexA], [indexB]) => indexA - indexB) .reduce((acc, [_, items]) => { acc.push(...Array.from(items.values())); return acc; }, []); }); this.requestRender = () => { this.onChange?.(); }; /* debounce( () => { this.onChange?.(); }, { priority: ESchedulerPriority.HIGHEST, } ); */ } getGroup(zIndex, group) { if (!this.indexes.has(zIndex)) { this.indexes.set(zIndex, new Map()); } const index = this.indexes.get(zIndex); if (!index.has(group)) { index.set(group, new Path2DGroup(this.chunkSize)); } return index.get(group); } add(item, params) { if (this.itemParams.has(item)) { this.update(item, params); } const bucket = this.getGroup(params.zIndex, params.group); bucket.add(item); this.itemParams.set(item, params); this.orderedPaths.reset(); this.requestRender(); } update(item, params) { this.delete(item); this.add(item, params); } delete(item) { if (!this.itemParams.has(item)) { return; } const params = this.itemParams.get(item); const bucket = this.getGroup(params.zIndex, params.group); bucket.delete(item); this.itemParams.delete(item); this.orderedPaths.reset(); this.requestRender(); } markDirty(item) { const params = this.itemParams.get(item); if (params) { const group = this.getGroup(params.zIndex, params.group); group.resetItem(item); this.requestRender(); } } }