@gravity-ui/graph
Version:
Modern graph editor component
170 lines (169 loc) • 5.01 kB
JavaScript
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();
}
}
}