From 5c722e17f530425c381aef9f6f4964fd47dab9cf Mon Sep 17 00:00:00 2001 From: Mike Kucera Date: Tue, 22 Oct 2024 09:06:43 -0400 Subject: [PATCH] support node background image loading --- debug/webgl/network-images.json | 64 +++++++++++++++++++ ...{network-tiny.json => network-styles.json} | 17 +++-- debug/webgl/networks.js | 11 +++- src/extensions/renderer/canvas/webgl/atlas.js | 53 ++++++++++----- .../canvas/webgl/drawing-edges-webgl.js | 4 +- .../canvas/webgl/drawing-nodes-webgl.js | 7 +- .../canvas/webgl/drawing-redraw-webgl.js | 7 +- 7 files changed, 135 insertions(+), 28 deletions(-) create mode 100644 debug/webgl/network-images.json rename debug/webgl/{network-tiny.json => network-styles.json} (86%) diff --git a/debug/webgl/network-images.json b/debug/webgl/network-images.json new file mode 100644 index 000000000..b987aac48 --- /dev/null +++ b/debug/webgl/network-images.json @@ -0,0 +1,64 @@ +{ + "elements": { + "nodes": [ + { "data": { "id": "n1", "weight": 1 }, "position": { "x": -29, "y": 26 } }, + { "data": { "id": "n2", "weight": 2 }, "position": { "x": 77, "y": -15 } }, + { "data": { "id": "n3", "weight": 3 }, "position": { "x": 178, "y": 36 } }, + { "data": { "id": "n4", "weight": 4 }, "position": { "x": -17, "y": 146 } }, + { "data": { "id": "n5", "weight": 5 }, "position": { "x": 134, "y": 157 } } + ], + "edges": [ + { "data": { "id":"n1-n2", "source": "n1", "target": "n2", "directed": "false" } }, + { "data": { "id":"n1-n3", "source": "n1", "target": "n3", "directed": "false" } }, + { "data": { "id":"n1-n4", "source": "n1", "target": "n4", "directed": "false" } }, + { "data": { "id":"n1-n5", "source": "n1", "target": "n5", "directed": "false" } }, + { "data": { "id":"n2-n3", "source": "n2", "target": "n3", "directed": "false" } }, + { "data": { "id":"n2-n4", "source": "n2", "target": "n4", "directed": "false" } }, + { "data": { "id":"n2-n5", "source": "n2", "target": "n5", "directed": "false" } }, + { "data": { "id":"n3-n4", "source": "n3", "target": "n4", "directed": "false" } }, + { "data": { "id":"n3-n5", "source": "n3", "target": "n5", "directed": "false" } }, + { "data": { "id":"n4-n5", "source": "n4", "target": "n5", "directed": "false" } } + ] + }, + "style": [ + { + "selector": "node", + "style": { + "label": "data(id)", + "text-valign": "top", + "color": "#000000", + "background-color": "#3a7ecf", + "font-family": "Helvetica", + "background-fit": "cover", + "font-size": "8px", + "border-color": "darkblue", + "border-width": 2 + } + }, { + "selector": "#n1", + "style": { + "background-image": "https://picsum.photos/601" + } + }, { + "selector": "#n2", + "style": { + "background-image": "https://picsum.photos/602" + } + }, { + "selector": "#n3", + "style": { + "background-image": "https://picsum.photos/603" + } + }, { + "selector": "#n4", + "style": { + "background-image": "https://picsum.photos/604" + } + }, { + "selector": "#n5", + "style": { + "background-image": "https://picsum.photos/605" + } + } + ] +} \ No newline at end of file diff --git a/debug/webgl/network-tiny.json b/debug/webgl/network-styles.json similarity index 86% rename from debug/webgl/network-tiny.json rename to debug/webgl/network-styles.json index 10efaeee4..cd0dd3218 100644 --- a/debug/webgl/network-tiny.json +++ b/debug/webgl/network-styles.json @@ -1,11 +1,11 @@ { "elements": { "nodes": [ - { "data": { "id": "n1", "weight": 1 } }, - { "data": { "id": "n2", "weight": 2 } }, - { "data": { "id": "n3", "weight": 3 } }, - { "data": { "id": "n4", "weight": 4 } }, - { "data": { "id": "n5", "weight": 5 } } + { "data": { "id": "n1", "weight": 1 }, "position": { "x": -29, "y": 26 } }, + { "data": { "id": "n2", "weight": 2 }, "position": { "x": 77, "y": -15 } }, + { "data": { "id": "n3", "weight": 3 }, "position": { "x": 178, "y": 36 } }, + { "data": { "id": "n4", "weight": 4 }, "position": { "x": -17, "y": 146 } }, + { "data": { "id": "n5", "weight": 5 }, "position": { "x": 134, "y": 157 } } ], "edges": [ { "data": { "id":"n1-n2", "source": "n1", "target": "n2", "directed": "false" } }, @@ -45,7 +45,12 @@ "selector": "#n2", "style": { "border-color": "black", - "border-width": 2 + "border-width": 2, + "background-image": "https://picsum.photos/600", + "background-fit": "cover", + "text-valign": "top", + "label": "n2 (random image)", + "font-size": "8px" } }, { diff --git a/debug/webgl/networks.js b/debug/webgl/networks.js index b5fd65f7c..f0325c145 100644 --- a/debug/webgl/networks.js +++ b/debug/webgl/networks.js @@ -6,8 +6,8 @@ var networks = { desc: 'Style Test', nodes: 5, edges: 10, - url: 'network-tiny.json', - layout: { name: 'cose' } + url: 'network-styles.json', + layout: { name: 'preset' } }, 'compound': { desc: 'Compound nodes', @@ -16,6 +16,13 @@ var networks = { url: 'network-compound-nodes.json', layout: { name: 'preset' } }, + 'images': { + desc: 'Image Load Test', + nodes: 5, + edges: 10, + url: 'network-images.json', + layout: { name: 'preset' } + }, 'em-web': { desc: 'EM web', nodes: 569, diff --git a/src/extensions/renderer/canvas/webgl/atlas.js b/src/extensions/renderer/canvas/webgl/atlas.js index 496b0f627..27fb3254e 100644 --- a/src/extensions/renderer/canvas/webgl/atlas.js +++ b/src/extensions/renderer/canvas/webgl/atlas.js @@ -232,6 +232,9 @@ export class AtlasCollection { this.atlases = []; this.styleKeyToAtlas = new Map(); + this.styleKeyNeedsRedraw = new Set(); + + this.forceGC = false; } getKeys() { @@ -263,9 +266,21 @@ export class AtlasCollection { } draw(id, key, bb, doDrawing) { + if(this.styleKeyNeedsRedraw.delete(key)) { + this.deleteKey(id, key); + // We need to mark the atlas as needing GC because the key will be mapped to + // this atlas or a new atlas, so the key itself won't be marked for GC. + const atlas = this.styleKeyToAtlas.get(key); + if(atlas) { + atlas.forceGC = true; + } + this.styleKeyToAtlas.delete(key); + } + let atlas = this.styleKeyToAtlas.get(key); if(!atlas) { - // this is an overly simplistic way of finding an atlas, needs to be rewritten + // This is a simplistic way of finding an atlas. + // May waste space at the end of the atalas if the element doesn't fit. atlas = this.atlases[this.atlases.length - 1]; if(!atlas || !atlas.canFit(bb)) { atlas = this._createAtlas(); @@ -289,14 +304,17 @@ export class AtlasCollection { return this.styleKeyToAtlas.has(key); } + deleteKey(id, key) { + this.idToKey.delete(id); + this.getIdsFor(key).delete(id); + } + checkKey(id, newKey) { if(!this.idToKey.has(id)) return; - const oldKey = this.idToKey.get(id); if(oldKey != newKey) { - this.idToKey.delete(id); - this.getIdsFor(oldKey).delete(id); + this.deleteKey(id, oldKey); } } @@ -314,8 +332,10 @@ export class AtlasCollection { * TODO dispose of the old atlas and texture */ gc() { + const forceGC = this.atlases.some(atlas => atlas.forceGC); const markedKeys = this._getKeysToCollect(); - if(markedKeys.size === 0) { + + if(markedKeys.size === 0 && !forceGC) { console.log("nothing to garbage collect"); return; } @@ -330,7 +350,7 @@ export class AtlasCollection { const keysToCollect = intersection(markedKeys, keys); - if(keysToCollect.size === 0) { + if(keysToCollect.size === 0 && !atlas.forceGC) { newAtlases.push(atlas); keys.forEach(k => newStyleKeyToAtlas.set(k, atlas)); continue; @@ -352,13 +372,10 @@ export class AtlasCollection { newStyleKeyToAtlas.set(key, newAtlas); } } - } this.atlases = newAtlases; this.styleKeyToAtlas = newStyleKeyToAtlas; - // TODO, I might not clean up every key - this.markedKeys = new Set(); } @@ -453,14 +470,20 @@ export class AtlasManager { } /** Marks textues associated with the element for garbage collection. */ - invalidate(eles, testEle) { - const renderTypes = this.getRenderTypes(); + invalidate(eles, { testEle, testType, forceRedraw } = {}) { for(const ele of eles) { - if(testEle(ele)) { + if(!testEle || testEle(ele)) { const id = ele.id(); - for(const opts of renderTypes) { - const styleKey = opts.getKey(ele); - opts.atlasCollection.checkKey(id, styleKey); + for(const opts of this.getRenderTypes()) { + if(!testType || testType(opts.type)) { + const styleKey = opts.getKey(ele); + if(forceRedraw) { + opts.atlasCollection.deleteKey(id, styleKey); + opts.atlasCollection.styleKeyNeedsRedraw.add(styleKey); + } else { + opts.atlasCollection.checkKey(id, styleKey); + } + } } } } diff --git a/src/extensions/renderer/canvas/webgl/drawing-edges-webgl.js b/src/extensions/renderer/canvas/webgl/drawing-edges-webgl.js index 6ea6188a7..e2351e7ba 100644 --- a/src/extensions/renderer/canvas/webgl/drawing-edges-webgl.js +++ b/src/extensions/renderer/canvas/webgl/drawing-edges-webgl.js @@ -47,11 +47,11 @@ export class EdgeDrawing { } invalidate(eles) { - this.atlasManager.invalidate(eles, ele => ele.isNode()); + this.atlasManager.invalidate(eles, { testEle: ele => ele.isEdge() }); } gc() { - this.atlasCollection.gc(); + this.atlasManager.gc(); } createShaderProgram(renderTarget) { diff --git a/src/extensions/renderer/canvas/webgl/drawing-nodes-webgl.js b/src/extensions/renderer/canvas/webgl/drawing-nodes-webgl.js index e8fe47496..15f077eda 100644 --- a/src/extensions/renderer/canvas/webgl/drawing-nodes-webgl.js +++ b/src/extensions/renderer/canvas/webgl/drawing-nodes-webgl.js @@ -28,8 +28,11 @@ export class NodeDrawing { this.atlasManager.addRenderType(type, opts); } - invalidate(eles) { - this.atlasManager.invalidate(eles, ele => ele.isNode()); + invalidate(eles, { type } = {}) { + const testEle = ele => ele.isNode(); + const testType = type ? t => t === type : null; + const forceRedraw = type ? true : false; + this.atlasManager.invalidate(eles, { testEle, testType, forceRedraw }); } gc() { diff --git a/src/extensions/renderer/canvas/webgl/drawing-redraw-webgl.js b/src/extensions/renderer/canvas/webgl/drawing-redraw-webgl.js index 19e1962d1..f4dc97bd7 100644 --- a/src/extensions/renderer/canvas/webgl/drawing-redraw-webgl.js +++ b/src/extensions/renderer/canvas/webgl/drawing-redraw-webgl.js @@ -111,7 +111,10 @@ CRp.initWebgl = function(opts, fns) { // TODO not called when deleting elements r.onUpdateEleCalcs((willDraw, eles) => { - r.nodeDrawing.invalidate(eles); + if(eles && eles.length > 0) { + r.nodeDrawing.invalidate(eles); + r.edgeDrawing.invalidate(eles); + } }); // "Override" certain functions in canvas and base renderer @@ -176,6 +179,8 @@ function overrideCanvasRendererFunctions(r) { baseFunc.call(r, eventName, eles); if(eventName === 'viewport' || eventName === 'bounds') { r.pickingFrameBuffer.needsDraw = true; + } else if(eventName === 'background') { // background image finished loading, need to redraw + r.nodeDrawing.invalidate(eles, { type: 'node-body' }); } }; }