Graduation
/
ui
/node_modules
/cytoscape
/src
/extensions
/renderer
/canvas
/layered-texture-cache.js
import * as util from '../../../util'; | |
import * as math from '../../../math'; | |
import Heap from '../../../heap'; | |
import * as is from '../../../is'; | |
import defs from './texture-cache-defs'; | |
var defNumLayers = 1; // default number of layers to use | |
var minLvl = -4; // when scaling smaller than that we don't need to re-render | |
var maxLvl = 2; // when larger than this scale just render directly (caching is not helpful) | |
var maxZoom = 3.99; // beyond this zoom level, layered textures are not used | |
var deqRedrawThreshold = 50; // time to batch redraws together from dequeueing to allow more dequeueing calcs to happen in the meanwhile | |
var refineEleDebounceTime = 50; // time to debounce sharper ele texture updates | |
var disableEleImgSmoothing = true; // when drawing eles on layers from an ele cache ; crisper and more performant when true | |
var deqCost = 0.15; // % of add'l rendering cost allowed for dequeuing ele caches each frame | |
var deqAvgCost = 0.1; // % of add'l rendering cost compared to average overall redraw time | |
var deqNoDrawCost = 0.9; // % of avg frame time that can be used for dequeueing when not drawing | |
var deqFastCost = 0.9; // % of frame time to be used when >60fps | |
var maxDeqSize = 1; // number of eles to dequeue and render at higher texture in each batch | |
var invalidThreshold = 250; // time threshold for disabling b/c of invalidations | |
var maxLayerArea = 4000 * 4000; // layers can't be bigger than this | |
var alwaysQueue = true; // never draw all the layers in a level on a frame; draw directly until all dequeued | |
var useHighQualityEleTxrReqs = true; // whether to use high quality ele txr requests (generally faster and cheaper in the longterm) | |
var useEleTxrCaching = true; // whether to use individual ele texture caching underneath this cache | |
// var log = function(){ console.log.apply( console, arguments ); }; | |
var LayeredTextureCache = function( renderer ){ | |
var self = this; | |
var r = self.renderer = renderer; | |
var cy = r.cy; | |
self.layersByLevel = {}; // e.g. 2 => [ layer1, layer2, ..., layerN ] | |
self.firstGet = true; | |
self.lastInvalidationTime = util.performanceNow() - 2*invalidThreshold; | |
self.skipping = false; | |
self.eleTxrDeqs = cy.collection(); | |
self.scheduleElementRefinement = util.debounce( function(){ | |
self.refineElementTextures( self.eleTxrDeqs ); | |
self.eleTxrDeqs.unmerge( self.eleTxrDeqs ); | |
}, refineEleDebounceTime ); | |
r.beforeRender(function( willDraw, now ){ | |
if( now - self.lastInvalidationTime <= invalidThreshold ){ | |
self.skipping = true; | |
} else { | |
self.skipping = false; | |
} | |
}, r.beforeRenderPriorities.lyrTxrSkip); | |
var qSort = function(a, b){ | |
return b.reqs - a.reqs; | |
}; | |
self.layersQueue = new Heap( qSort ); | |
self.setupDequeueing(); | |
}; | |
var LTCp = LayeredTextureCache.prototype; | |
var layerIdPool = 0; | |
var MAX_INT = Math.pow(2, 53) - 1; | |
LTCp.makeLayer = function( bb, lvl ){ | |
var scale = Math.pow( 2, lvl ); | |
var w = Math.ceil( bb.w * scale ); | |
var h = Math.ceil( bb.h * scale ); | |
var canvas = this.renderer.makeOffscreenCanvas(w, h); | |
var layer = { | |
id: (layerIdPool = ++layerIdPool % MAX_INT ), | |
bb: bb, | |
level: lvl, | |
width: w, | |
height: h, | |
canvas: canvas, | |
context: canvas.getContext('2d'), | |
eles: [], | |
elesQueue: [], | |
reqs: 0 | |
}; | |
// log('make layer %s with w %s and h %s and lvl %s', layer.id, layer.width, layer.height, layer.level); | |
var cxt = layer.context; | |
var dx = -layer.bb.x1; | |
var dy = -layer.bb.y1; | |
// do the transform on creation to save cycles (it's the same for all eles) | |
cxt.scale( scale, scale ); | |
cxt.translate( dx, dy ); | |
return layer; | |
}; | |
LTCp.getLayers = function( eles, pxRatio, lvl ){ | |
var self = this; | |
var r = self.renderer; | |
var cy = r.cy; | |
var zoom = cy.zoom(); | |
var firstGet = self.firstGet; | |
self.firstGet = false; | |
// log('--\nget layers with %s eles', eles.length); | |
//log eles.map(function(ele){ return ele.id() }) ); | |
if( lvl == null ){ | |
lvl = Math.ceil( math.log2( zoom * pxRatio ) ); | |
if( lvl < minLvl ){ | |
lvl = minLvl; | |
} else if( zoom >= maxZoom || lvl > maxLvl ){ | |
return null; | |
} | |
} | |
self.validateLayersElesOrdering( lvl, eles ); | |
var layersByLvl = self.layersByLevel; | |
var scale = Math.pow( 2, lvl ); | |
var layers = layersByLvl[ lvl ] = layersByLvl[ lvl ] || []; | |
var bb; | |
var lvlComplete = self.levelIsComplete( lvl, eles ); | |
var tmpLayers; | |
var checkTempLevels = function(){ | |
var canUseAsTmpLvl = function( l ){ | |
self.validateLayersElesOrdering( l, eles ); | |
if( self.levelIsComplete( l, eles ) ){ | |
tmpLayers = layersByLvl[l]; | |
return true; | |
} | |
}; | |
var checkLvls = function( dir ){ | |
if( tmpLayers ){ return; } | |
for( var l = lvl + dir; minLvl <= l && l <= maxLvl; l += dir ){ | |
if( canUseAsTmpLvl(l) ){ break; } | |
} | |
}; | |
checkLvls( +1 ); | |
checkLvls( -1 ); | |
// remove the invalid layers; they will be replaced as needed later in this function | |
for( var i = layers.length - 1; i >= 0; i-- ){ | |
var layer = layers[i]; | |
if( layer.invalid ){ | |
util.removeFromArray( layers, layer ); | |
} | |
} | |
}; | |
if( !lvlComplete ){ | |
// if the current level is incomplete, then use the closest, best quality layerset temporarily | |
// and later queue the current layerset so we can get the proper quality level soon | |
checkTempLevels(); | |
} else { | |
// log('level complete, using existing layers\n--'); | |
return layers; | |
} | |
var getBb = function(){ | |
if( !bb ){ | |
bb = math.makeBoundingBox(); | |
for( var i = 0; i < eles.length; i++ ){ | |
math.updateBoundingBox( bb, eles[i].boundingBox() ); | |
} | |
} | |
return bb; | |
}; | |
var makeLayer = function( opts ){ | |
opts = opts || {}; | |
var after = opts.after; | |
getBb(); | |
var area = ( bb.w * scale ) * ( bb.h * scale ); | |
if( area > maxLayerArea ){ | |
return null; | |
} | |
var layer = self.makeLayer( bb, lvl ); | |
if( after != null ){ | |
var index = layers.indexOf( after ) + 1; | |
layers.splice( index, 0, layer ); | |
} else if( opts.insert === undefined || opts.insert ){ | |
// no after specified => first layer made so put at start | |
layers.unshift( layer ); | |
} | |
// if( tmpLayers ){ | |
//self.queueLayer( layer ); | |
// } | |
return layer; | |
}; | |
if( self.skipping && !firstGet ){ | |
// log('skip layers'); | |
return null; | |
} | |
// log('do layers'); | |
var layer = null; | |
var maxElesPerLayer = eles.length / defNumLayers; | |
var allowLazyQueueing = alwaysQueue && !firstGet; | |
for( var i = 0; i < eles.length; i++ ){ | |
var ele = eles[i]; | |
var rs = ele._private.rscratch; | |
var caches = rs.imgLayerCaches = rs.imgLayerCaches || {}; | |
// log('look at ele', ele.id()); | |
var existingLayer = caches[ lvl ]; | |
if( existingLayer ){ | |
// reuse layer for later eles | |
// log('reuse layer for', ele.id()); | |
layer = existingLayer; | |
continue; | |
} | |
if( | |
!layer | |
|| layer.eles.length >= maxElesPerLayer | |
|| !math.boundingBoxInBoundingBox( layer.bb, ele.boundingBox() ) | |
){ | |
// log('make new layer for ele %s', ele.id()); | |
layer = makeLayer({ insert: true, after: layer }); | |
// if now layer can be built then we can't use layers at this level | |
if( !layer ){ return null; } | |
// log('new layer with id %s', layer.id); | |
} | |
if( tmpLayers || allowLazyQueueing ){ | |
// log('queue ele %s in layer %s', ele.id(), layer.id); | |
self.queueLayer( layer, ele ); | |
} else { | |
// log('draw ele %s in layer %s', ele.id(), layer.id); | |
self.drawEleInLayer( layer, ele, lvl, pxRatio ); | |
} | |
layer.eles.push( ele ); | |
caches[ lvl ] = layer; | |
} | |
// log('--'); | |
if( tmpLayers ){ // then we only queued the current layerset and can't draw it yet | |
return tmpLayers; | |
} | |
if( allowLazyQueueing ){ | |
// log('lazy queue level', lvl); | |
return null; | |
} | |
return layers; | |
}; | |
// a layer may want to use an ele cache of a higher level to avoid blurriness | |
// so the layer level might not equal the ele level | |
LTCp.getEleLevelForLayerLevel = function( lvl, pxRatio ){ | |
return lvl; | |
}; | |
LTCp.drawEleInLayer = function( layer, ele, lvl, pxRatio ){ | |
var self = this; | |
var r = this.renderer; | |
var context = layer.context; | |
var bb = ele.boundingBox(); | |
if( bb.w === 0 || bb.h === 0 || !ele.visible() ){ return; } | |
lvl = self.getEleLevelForLayerLevel( lvl, pxRatio ); | |
if( disableEleImgSmoothing ){ r.setImgSmoothing( context, false ); } | |
if( useEleTxrCaching ){ | |
r.drawCachedElement( context, ele, null, null, lvl, useHighQualityEleTxrReqs ); | |
} else { // if the element is not cacheable, then draw directly | |
r.drawElement( context, ele ); | |
} | |
if( disableEleImgSmoothing ){ r.setImgSmoothing( context, true ); } | |
}; | |
LTCp.levelIsComplete = function( lvl, eles ){ | |
var self = this; | |
var layers = self.layersByLevel[ lvl ]; | |
if( !layers || layers.length === 0 ){ return false; } | |
var numElesInLayers = 0; | |
for( var i = 0; i < layers.length; i++ ){ | |
var layer = layers[i]; | |
// if there are any eles needed to be drawn yet, the level is not complete | |
if( layer.reqs > 0 ){ return false; } | |
// if the layer is invalid, the level is not complete | |
if( layer.invalid ){ return false; } | |
numElesInLayers += layer.eles.length; | |
} | |
// we should have exactly the number of eles passed in to be complete | |
if( numElesInLayers !== eles.length ){ return false; } | |
return true; | |
}; | |
LTCp.validateLayersElesOrdering = function( lvl, eles ){ | |
var layers = this.layersByLevel[ lvl ]; | |
if( !layers ){ return; } | |
// if in a layer the eles are not in the same order, then the layer is invalid | |
// (i.e. there is an ele in between the eles in the layer) | |
for( var i = 0; i < layers.length; i++ ){ | |
var layer = layers[i]; | |
var offset = -1; | |
// find the offset | |
for( var j = 0; j < eles.length; j++ ){ | |
if( layer.eles[0] === eles[j] ){ | |
offset = j; | |
break; | |
} | |
} | |
if( offset < 0 ){ | |
// then the layer has nonexistent elements and is invalid | |
this.invalidateLayer( layer ); | |
continue; | |
} | |
// the eles in the layer must be in the same continuous order, else the layer is invalid | |
var o = offset; | |
for( var j = 0; j < layer.eles.length; j++ ){ | |
if( layer.eles[j] !== eles[o+j] ){ | |
// log('invalidate based on ordering', layer.id); | |
this.invalidateLayer( layer ); | |
break; | |
} | |
} | |
} | |
}; | |
LTCp.updateElementsInLayers = function( eles, update ){ | |
var self = this; | |
var isEles = is.element( eles[0] ); | |
// collect udpated elements (cascaded from the layers) and update each | |
// layer itself along the way | |
for( var i = 0; i < eles.length; i++ ){ | |
var req = isEles ? null : eles[i]; | |
var ele = isEles ? eles[i] : eles[i].ele; | |
var rs = ele._private.rscratch; | |
var caches = rs.imgLayerCaches = rs.imgLayerCaches || {}; | |
for( var l = minLvl; l <= maxLvl; l++ ){ | |
var layer = caches[l]; | |
if( !layer ){ continue; } | |
// if update is a request from the ele cache, then it affects only | |
// the matching level | |
if( req && self.getEleLevelForLayerLevel( layer.level ) !== req.level ){ | |
continue; | |
} | |
update( layer, ele, req ); | |
} | |
} | |
}; | |
LTCp.haveLayers = function(){ | |
var self = this; | |
var haveLayers = false; | |
for( var l = minLvl; l <= maxLvl; l++ ){ | |
var layers = self.layersByLevel[l]; | |
if( layers && layers.length > 0 ){ | |
haveLayers = true; | |
break; | |
} | |
} | |
return haveLayers; | |
}; | |
LTCp.invalidateElements = function( eles ){ | |
var self = this; | |
if( eles.length === 0 ){ return; } | |
self.lastInvalidationTime = util.performanceNow(); | |
// log('update invalidate layer time from eles'); | |
if( eles.length === 0 || !self.haveLayers() ){ return; } | |
self.updateElementsInLayers( eles, function invalAssocLayers( layer, ele, req ){ | |
self.invalidateLayer( layer ); | |
} ); | |
}; | |
LTCp.invalidateLayer = function( layer ){ | |
// log('update invalidate layer time'); | |
this.lastInvalidationTime = util.performanceNow(); | |
if( layer.invalid ){ return; } // save cycles | |
var lvl = layer.level; | |
var eles = layer.eles; | |
var layers = this.layersByLevel[ lvl ]; | |
// log('invalidate layer', layer.id ); | |
util.removeFromArray( layers, layer ); | |
// layer.eles = []; | |
layer.elesQueue = []; | |
layer.invalid = true; | |
if( layer.replacement ){ | |
layer.replacement.invalid = true; | |
} | |
for( var i = 0; i < eles.length; i++ ){ | |
var caches = eles[i]._private.rscratch.imgLayerCaches; | |
if( caches ){ | |
caches[ lvl ] = null; | |
} | |
} | |
}; | |
LTCp.refineElementTextures = function( eles ){ | |
var self = this; | |
// log('refine', eles.length); | |
self.updateElementsInLayers( eles, function refineEachEle( layer, ele, req ){ | |
var rLyr = layer.replacement; | |
if( !rLyr ){ | |
rLyr = layer.replacement = self.makeLayer( layer.bb, layer.level ); | |
rLyr.replaces = layer; | |
rLyr.eles = layer.eles; | |
// log('make replacement layer %s for %s with level %s', rLyr.id, layer.id, rLyr.level); | |
} | |
if( !rLyr.reqs ){ | |
for( var i = 0; i < rLyr.eles.length; i++ ){ | |
self.queueLayer( rLyr, rLyr.eles[i] ); | |
} | |
// log('queue replacement layer refinement', rLyr.id); | |
} | |
} ); | |
}; | |
LTCp.enqueueElementRefinement = function( ele ){ | |
if( !useEleTxrCaching ){ return; } | |
this.eleTxrDeqs.merge( ele ); | |
this.scheduleElementRefinement(); | |
}; | |
LTCp.queueLayer = function( layer, ele ){ | |
var self = this; | |
var q = self.layersQueue; | |
var elesQ = layer.elesQueue; | |
var hasId = elesQ.hasId = elesQ.hasId || {}; | |
// if a layer is going to be replaced, queuing is a waste of time | |
if( layer.replacement ){ return; } | |
if( ele ){ | |
if( hasId[ ele.id() ] ){ | |
return; | |
} | |
elesQ.push( ele ); | |
hasId[ ele.id() ] = true; | |
} | |
if( layer.reqs ){ | |
layer.reqs++; | |
q.updateItem( layer ); | |
} else { | |
layer.reqs = 1; | |
q.push( layer ); | |
} | |
}; | |
LTCp.dequeue = function( pxRatio ){ | |
var self = this; | |
var q = self.layersQueue; | |
var deqd = []; | |
var eleDeqs = 0; | |
while( eleDeqs < maxDeqSize ){ | |
if( q.size() === 0 ){ break; } | |
var layer = q.peek(); | |
// if a layer has been or will be replaced, then don't waste time with it | |
if( layer.replacement ){ | |
// log('layer %s in queue skipped b/c it already has a replacement', layer.id); | |
q.pop(); | |
continue; | |
} | |
// if this is a replacement layer that has been superceded, then forget it | |
if( layer.replaces && layer !== layer.replaces.replacement ){ | |
// log('layer is no longer the most uptodate replacement; dequeued', layer.id) | |
q.pop(); | |
continue; | |
} | |
if( layer.invalid ){ | |
// log('replacement layer %s is invalid; dequeued', layer.id); | |
q.pop(); | |
continue; | |
} | |
var ele = layer.elesQueue.shift(); | |
if( ele ){ | |
// log('dequeue layer %s', layer.id); | |
self.drawEleInLayer( layer, ele, layer.level, pxRatio ); | |
eleDeqs++; | |
} | |
if( deqd.length === 0 ){ | |
// we need only one entry in deqd to queue redrawing etc | |
deqd.push( true ); | |
} | |
// if the layer has all its eles done, then remove from the queue | |
if( layer.elesQueue.length === 0 ){ | |
q.pop(); | |
layer.reqs = 0; | |
// log('dequeue of layer %s complete', layer.id); | |
// when a replacement layer is dequeued, it replaces the old layer in the level | |
if( layer.replaces ){ | |
self.applyLayerReplacement( layer ); | |
} | |
self.requestRedraw(); | |
} | |
} | |
return deqd; | |
}; | |
LTCp.applyLayerReplacement = function( layer ){ | |
var self = this; | |
var layersInLevel = self.layersByLevel[ layer.level ]; | |
var replaced = layer.replaces; | |
var index = layersInLevel.indexOf( replaced ); | |
// if the replaced layer is not in the active list for the level, then replacing | |
// refs would be a mistake (i.e. overwriting the true active layer) | |
if( index < 0 || replaced.invalid ){ | |
// log('replacement layer would have no effect', layer.id); | |
return; | |
} | |
layersInLevel[ index ] = layer; // replace level ref | |
// replace refs in eles | |
for( var i = 0; i < layer.eles.length; i++ ){ | |
var _p = layer.eles[i]._private; | |
var cache = _p.imgLayerCaches = _p.imgLayerCaches || {}; | |
if( cache ){ | |
cache[ layer.level ] = layer; | |
} | |
} | |
// log('apply replacement layer %s over %s', layer.id, replaced.id); | |
self.requestRedraw(); | |
}; | |
LTCp.requestRedraw = util.debounce( function(){ | |
var r = this.renderer; | |
r.redrawHint( 'eles', true ); | |
r.redrawHint( 'drag', true ); | |
r.redraw(); | |
}, 100 ); | |
LTCp.setupDequeueing = defs.setupDequeueing({ | |
deqRedrawThreshold: deqRedrawThreshold, | |
deqCost: deqCost, | |
deqAvgCost: deqAvgCost, | |
deqNoDrawCost: deqNoDrawCost, | |
deqFastCost: deqFastCost, | |
deq: function( self, pxRatio ){ | |
return self.dequeue( pxRatio ); | |
}, | |
onDeqd: util.noop, | |
shouldRedraw: util.trueify, | |
priority: function( self ){ | |
return self.renderer.beforeRenderPriorities.lyrTxrDeq; | |
} | |
}); | |
export default LayeredTextureCache; | |