完成世界书、骰子、apiconfig页面处理

This commit is contained in:
2026-04-30 01:35:10 +08:00
parent a3e3711b2b
commit ba9b925c32
4602 changed files with 785225 additions and 23 deletions

View File

@@ -0,0 +1,329 @@
// Implemented by Zoe Xi @zoexi for GSOC 2016
// https://github.com/cytoscape/cytoscape.js-affinity-propagation
// Implemented from the reference library: https://github.com/juhis/affinity-propagation
// Additional reference: http://www.psi.toronto.edu/affinitypropagation/faq.html
import * as util from '../../util/index.mjs';
import * as math from '../../math.mjs';
import * as is from '../../is.mjs';
import clusteringDistance from './clustering-distances.mjs';
let defaults = util.defaults({
distance: 'euclidean', // distance metric to compare attributes between two nodes
preference: 'median', // suitability of a data point to serve as an exemplar
damping: 0.8, // damping factor between [0.5, 1)
maxIterations: 1000, // max number of iterations to run
minIterations: 100, // min number of iterations to run in order for clustering to stop
attributes: [ // functions to quantify the similarity between any two points
// e.g. node => node.data('weight')
]
});
let setOptions = function( options ) {
let dmp = options.damping;
let pref = options.preference;
if( !(0.5 <= dmp && dmp < 1) ){
util.error(`Damping must range on [0.5, 1). Got: ${dmp}`);
}
let validPrefs = ['median', 'mean', 'min', 'max'];
if( !( validPrefs.some(v => v === pref) || is.number(pref) ) ){
util.error(`Preference must be one of [${validPrefs.map( p => `'${p}'` ).join(', ')}] or a number. Got: ${pref}`);
}
return defaults( options );
};
if( process.env.NODE_ENV !== 'production' ){ /* eslint-disable no-console, no-unused-vars */
var printMatrix = function( M ) { // used for debugging purposes only
let str = '';
let log = s => str = str + s + '\n';
let n = Math.sqrt(M.length);
for ( let i = 0; i < n; i++ ) {
let row = '';
for ( let j = 0; j < n; j++ ) {
row += M[i*n+j] + ' ';
}
log(row);
}
console.log(str);
};
} /* eslint-enable */
let getSimilarity = function( type, n1, n2, attributes ) {
let attr = (n, i) => attributes[i](n);
// nb negative because similarity should have an inverse relationship to distance
return -clusteringDistance( type, attributes.length, i => attr(n1, i), i => attr(n2, i), n1, n2 );
};
let getPreference = function( S, preference ) { // larger preference = greater # of clusters
let p = null;
if( preference === 'median' ){
p = math.median( S );
} else if( preference === 'mean' ){
p = math.mean( S );
} else if ( preference === 'min' ){
p = math.min( S );
} else if ( preference === 'max' ){
p = math.max( S );
} else { // Custom preference number, as set by user
p = preference;
}
return p;
};
let findExemplars = function( n, R, A ) {
let indices = [];
for ( let i = 0; i < n; i++ ) {
if ( R[i * n + i] + A[i * n + i] > 0 ) {
indices.push(i);
}
}
return indices;
};
let assignClusters = function( n, S, exemplars ) {
let clusters = [];
for ( let i = 0; i < n; i++ ) {
let index = -1;
let max = -Infinity;
for ( let ei = 0; ei < exemplars.length; ei++ ) {
let e = exemplars[ei];
if ( S[i * n + e] > max ) {
index = e;
max = S[i * n + e];
}
}
if( index > 0 ){
clusters.push(index);
}
}
for (let ei = 0; ei < exemplars.length; ei++) {
clusters[ exemplars[ei] ] = exemplars[ei];
}
return clusters;
};
let assign = function( n, S, exemplars ) {
let clusters = assignClusters( n, S, exemplars );
for ( let ei = 0; ei < exemplars.length; ei++ ) {
let ii = [];
for ( let c = 0; c < clusters.length; c++ ) {
if (clusters[c] === exemplars[ei]) {
ii.push(c);
}
}
let maxI = -1;
let maxSum = -Infinity;
for ( let i = 0; i < ii.length; i++ ) {
let sum = 0;
for ( let j = 0; j < ii.length; j++ ) {
sum += S[ii[j] * n + ii[i]];
}
if ( sum > maxSum ) {
maxI = i;
maxSum = sum;
}
}
exemplars[ei] = ii[maxI];
}
clusters = assignClusters( n, S, exemplars );
return clusters;
};
let affinityPropagation = function( options ) {
let cy = this.cy();
let nodes = this.nodes();
let opts = setOptions( options );
// Map each node to its position in node array
let id2position = {};
for( let i = 0; i < nodes.length; i++ ){
id2position[ nodes[i].id() ] = i;
}
// Begin affinity propagation algorithm
let n; // number of data points
let n2; // size of matrices
let S; // similarity matrix (1D array)
let p; // preference/suitability of a data point to serve as an exemplar
let R; // responsibility matrix (1D array)
let A; // availability matrix (1D array)
n = nodes.length;
n2 = n * n;
// Initialize and build S similarity matrix
S = new Array(n2);
for ( let i = 0; i < n2; i++ ) {
S[i] = -Infinity; // for cases where two data points shouldn't be linked together
}
for ( let i = 0; i < n; i++ ) {
for ( let j = 0; j < n; j++ ) {
if ( i !== j ) {
S[i * n + j] = getSimilarity( opts.distance, nodes[i], nodes[j], opts.attributes );
}
}
}
// Place preferences on the diagonal of S
p = getPreference( S, opts.preference );
for ( let i = 0; i < n; i++ ) {
S[i * n + i] = p;
}
// Initialize R responsibility matrix
R = new Array(n2);
for ( let i = 0; i < n2; i++ ) {
R[i] = 0.0;
}
// Initialize A availability matrix
A = new Array(n2);
for ( let i = 0; i < n2; i++ ) {
A[i] = 0.0;
}
let old = new Array(n);
let Rp = new Array(n);
let se = new Array(n);
for ( let i = 0; i < n; i ++ ) {
old[i] = 0.0;
Rp[i] = 0.0;
se[i] = 0;
}
let e = new Array(n * opts.minIterations);
for ( let i = 0; i < e.length; i++ ) {
e[i] = 0;
}
let iter;
for ( iter = 0; iter < opts.maxIterations; iter++ ) { // main algorithmic loop
// Update R responsibility matrix
for ( let i = 0; i < n; i++ ) {
let max = -Infinity,
max2 = -Infinity,
maxI = -1,
AS = 0.0;
for ( let j = 0; j < n; j++ ) {
old[j] = R[i * n + j];
AS = A[i * n + j] + S[i * n + j];
if ( AS >= max ) {
max2 = max;
max = AS;
maxI = j;
}
else if ( AS > max2 ) {
max2 = AS;
}
}
for ( let j = 0; j < n; j++ ) {
R[i * n + j] = (1 - opts.damping) * (S[i * n + j] - max) + opts.damping * old[j];
}
R[i * n + maxI] = (1 - opts.damping) * (S[i * n + maxI] - max2) + opts.damping * old[maxI];
}
// Update A availability matrix
for ( let i = 0; i < n; i++ ) {
let sum = 0;
for ( let j = 0; j < n; j++ ) {
old[j] = A[j * n + i];
Rp[j] = Math.max(0, R[j * n + i]);
sum += Rp[j];
}
sum -= Rp[i];
Rp[i] = R[i * n + i];
sum += Rp[i];
for ( let j = 0; j < n; j++ ) {
A[j * n + i] = (1 - opts.damping) * Math.min(0, sum - Rp[j]) + opts.damping * old[j];
}
A[i * n + i] = (1 - opts.damping) * (sum - Rp[i]) + opts.damping * old[i];
}
// Check for convergence
let K = 0;
for ( let i = 0; i < n; i++ ) {
let E = A[i * n + i] + R[i * n + i] > 0 ? 1 : 0;
e[(iter % opts.minIterations) * n + i] = E;
K += E;
}
if ( K > 0 && (iter >= opts.minIterations - 1 || iter == opts.maxIterations - 1) ) {
let sum = 0;
for ( let i = 0; i < n; i++ ) {
se[i] = 0;
for ( let j = 0; j < opts.minIterations; j++ ) {
se[i] += e[j * n + i];
}
if ( se[i] === 0 || se[i] === opts.minIterations ) {
sum++;
}
}
if ( sum === n ) { // then we have convergence
break;
}
}
}
// Identify exemplars (cluster centers)
let exemplarsIndices = findExemplars( n, R, A );
// Assign nodes to clusters
let clusterIndices = assign( n, S, exemplarsIndices, nodes, id2position );
let clusters = {};
for ( let c = 0; c < exemplarsIndices.length; c++ ) {
clusters[ exemplarsIndices[c] ] = [];
}
for (let i = 0; i < nodes.length; i++) {
let pos = id2position[ nodes[i].id() ];
let clusterIndex = clusterIndices[pos];
if( clusterIndex != null ){ // the node may have not been assigned a cluster if no valid attributes were specified
clusters[ clusterIndex ].push( nodes[i] );
}
}
let retClusters = new Array(exemplarsIndices.length);
for ( let c = 0; c < exemplarsIndices.length; c++ ) {
retClusters[c] = cy.collection( clusters[ exemplarsIndices[c] ] );
}
return retClusters;
};
export default { affinityPropagation, ap: affinityPropagation };

View File

@@ -0,0 +1,174 @@
import Heap from '../../heap.mjs';
import * as util from '../../util/index.mjs';
const defaults = util.defaults({
weight: null,
directed: false
});
let elesfn = ({
// Implemented from the algorithm in the paper "On Variants of Shortest-Path Betweenness Centrality and their Generic Computation" by Ulrik Brandes
betweennessCentrality: function( options ){
let { directed, weight } = defaults(options);
let weighted = weight != null;
let cy = this.cy();
// starting
let V = this.nodes();
let A = {};
let _C = {};
let max = 0;
let C = {
set: function( key, val ){
_C[ key ] = val;
if( val > max ){ max = val; }
},
get: function( key ){ return _C[ key ]; }
};
// A contains the neighborhoods of every node
for( let i = 0; i < V.length; i++ ){
let v = V[ i ];
let vid = v.id();
if( directed ){
A[ vid ] = v.outgoers().nodes(); // get outgoers of every node
} else {
A[ vid ] = v.openNeighborhood().nodes(); // get neighbors of every node
}
C.set( vid, 0 );
}
for( let s = 0; s < V.length; s++ ){
let sid = V[s].id();
let S = []; // stack
let P = {};
let g = {};
let d = {};
let Q = new Heap(function( a, b ){
return d[a] - d[b];
}); // queue
// init dictionaries
for( let i = 0; i < V.length; i++ ){
let vid = V[ i ].id();
P[ vid ] = [];
g[ vid ] = 0;
d[ vid ] = Infinity;
}
g[ sid ] = 1; // sigma
d[ sid ] = 0; // distance to s
Q.push( sid );
while( !Q.empty() ){
let v = Q.pop();
S.push( v );
if( weighted ){
for( let j = 0; j < A[v].length; j++ ){
let w = A[v][j];
let vEle = cy.getElementById( v );
let edge;
if( vEle.edgesTo( w ).length > 0 ){
edge = vEle.edgesTo( w )[0];
} else {
edge = w.edgesTo( vEle )[0];
}
let edgeWeight = weight( edge );
w = w.id();
if( d[w] > d[v] + edgeWeight ){
d[w] = d[v] + edgeWeight;
if( Q.nodes.indexOf( w ) < 0 ){ //if w is not in Q
Q.push( w );
} else { // update position if w is in Q
Q.updateItem( w );
}
g[w] = 0;
P[w] = [];
}
if( d[w] == d[v] + edgeWeight ){
g[w] = g[w] + g[v];
P[w].push( v );
}
}
} else {
for( let j = 0; j < A[v].length; j++ ){
let w = A[v][j].id();
if( d[w] == Infinity ){
Q.push( w );
d[w] = d[v] + 1;
}
if( d[w] == d[v] + 1 ){
g[w] = g[w] + g[v];
P[w].push( v );
}
}
}
}
let e = {};
for( let i = 0; i < V.length; i++ ){
e[ V[ i ].id() ] = 0;
}
while( S.length > 0 ){
let w = S.pop();
for( let j = 0; j < P[w].length; j++ ){
let v = P[w][j];
e[v] = e[v] + (g[v] / g[w]) * (1 + e[w]);
}
if( w != V[s].id() ){
C.set( w, C.get( w ) + e[w] );
}
}
}
let ret = {
betweenness: function( node ){
let id = cy.collection(node).id();
return C.get( id );
},
betweennessNormalized: function( node ){
if ( max == 0 ){ return 0; }
let id = cy.collection(node).id();
return C.get( id ) / max;
}
};
// alias
ret.betweennessNormalised = ret.betweennessNormalized;
return ret;
} // betweennessCentrality
}); // elesfn
// nice, short mathematical alias
elesfn.bc = elesfn.betweennessCentrality;
export default elesfn;

View File

@@ -0,0 +1,101 @@
import * as is from '../../is.mjs';
import * as util from '../../util/index.mjs';
const defaults = util.defaults({
harmonic: true,
weight: () => 1,
directed: false,
root: null
});
const elesfn = ({
closenessCentralityNormalized: function( options ){
let { harmonic, weight, directed } = defaults(options);
let cy = this.cy();
let closenesses = {};
let maxCloseness = 0;
let nodes = this.nodes();
let fw = this.floydWarshall({ weight, directed });
// Compute closeness for every node and find the maximum closeness
for( let i = 0; i < nodes.length; i++ ){
let currCloseness = 0;
let node_i = nodes[i];
for( let j = 0; j < nodes.length; j++ ){
if( i !== j ){
let d = fw.distance( node_i, nodes[j] );
if( harmonic ){
currCloseness += 1 / d;
} else {
currCloseness += d;
}
}
}
if( !harmonic ){
currCloseness = 1 / currCloseness;
}
if( maxCloseness < currCloseness ){
maxCloseness = currCloseness;
}
closenesses[ node_i.id() ] = currCloseness;
}
return {
closeness: function( node ){
if( maxCloseness == 0 ){ return 0; }
if( is.string( node ) ){
// from is a selector string
node = (cy.filter( node )[0]).id();
} else {
// from is a node
node = node.id();
}
return closenesses[ node ] / maxCloseness;
}
};
},
// Implemented from pseudocode from wikipedia
closenessCentrality: function( options ){
let { root, weight, directed, harmonic } = defaults(options);
root = this.filter(root)[0];
// we need distance from this node to every other node
let dijkstra = this.dijkstra({ root, weight, directed });
let totalDistance = 0;
let nodes = this.nodes();
for( let i = 0; i < nodes.length; i++ ){
let n = nodes[i];
if( !n.same(root) ){
let d = dijkstra.distanceTo(n);
if( harmonic ){
totalDistance += 1 / d;
} else {
totalDistance += d;
}
}
}
return harmonic ? totalDistance : 1 / totalDistance;
} // closenessCentrality
}); // elesfn
// nice, short mathematical alias
elesfn.cc = elesfn.closenessCentrality;
elesfn.ccn = elesfn.closenessCentralityNormalised = elesfn.closenessCentralityNormalized;
export default elesfn;

View File

@@ -0,0 +1,133 @@
import * as is from '../../is.mjs';
import Heap from '../../heap.mjs';
import { defaults } from '../../util/index.mjs';
const dijkstraDefaults = defaults({
root: null,
weight: edge => 1,
directed: false
});
let elesfn = ({
dijkstra: function( options ){
if( !is.plainObject(options) ){
let args = arguments;
options = { root: args[0], weight: args[1], directed: args[2] };
}
let { root, weight, directed } = dijkstraDefaults(options);
let eles = this;
let weightFn = weight;
let source = is.string( root ) ? this.filter( root )[0] : root[0];
let dist = {};
let prev = {};
let knownDist = {};
let { nodes, edges } = this.byGroup();
edges.unmergeBy( ele => ele.isLoop() );
let getDist = node => dist[ node.id() ];
let setDist = ( node, d ) => {
dist[ node.id() ] = d;
Q.updateItem( node );
};
let Q = new Heap( (a, b) => getDist(a) - getDist(b) );
for( let i = 0; i < nodes.length; i++ ){
let node = nodes[ i ];
dist[ node.id() ] = node.same( source ) ? 0 : Infinity;
Q.push( node );
}
let distBetween = ( u, v ) => {
let uvs = ( directed ? u.edgesTo(v) : u.edgesWith(v) ).intersect( edges );
let smallestDistance = Infinity;
let smallestEdge;
for( let i = 0; i < uvs.length; i++ ){
let edge = uvs[ i ];
let weight = weightFn( edge );
if( weight < smallestDistance || !smallestEdge ){
smallestDistance = weight;
smallestEdge = edge;
}
}
return {
edge: smallestEdge,
dist: smallestDistance
};
};
while( Q.size() > 0 ){
let u = Q.pop();
let smalletsDist = getDist( u );
let uid = u.id();
knownDist[ uid ] = smalletsDist;
if( smalletsDist === Infinity ){
continue;
}
let neighbors = u.neighborhood().intersect( nodes );
for( let i = 0; i < neighbors.length; i++ ){
let v = neighbors[ i ];
let vid = v.id();
let vDist = distBetween( u, v );
let alt = smalletsDist + vDist.dist;
if( alt < getDist( v ) ){
setDist( v, alt );
prev[ vid ] = {
node: u,
edge: vDist.edge
};
}
} // for
} // while
return {
distanceTo: function( node ){
let target = is.string( node ) ? nodes.filter( node )[0] : node[0];
return knownDist[ target.id() ];
},
pathTo: function( node ){
let target = is.string( node ) ? nodes.filter( node )[0] : node[0];
let S = [];
let u = target;
let uid = u.id();
if( target.length > 0 ){
S.unshift( target );
while( prev[ uid ] ){
let p = prev[ uid ];
S.unshift( p.edge );
S.unshift( p.node );
u = p.node;
uid = u.id();
}
}
return eles.spawn( S );
}
};
}
});
export default elesfn;

View File

@@ -0,0 +1,118 @@
let hopcroftTarjanBiconnected = function() {
let eles = this;
let nodes = {};
let id = 0;
let edgeCount = 0;
let components = [];
let stack = [];
let visitedEdges = {};
const buildComponent = (x, y) => {
let i = stack.length-1;
let cutset = [];
let component = eles.spawn();
while (stack[i].x != x || stack[i].y != y) {
cutset.push(stack.pop().edge);
i--;
}
cutset.push(stack.pop().edge);
cutset.forEach(edge => {
let connectedNodes = edge.connectedNodes()
.intersection(eles);
component.merge(edge);
connectedNodes.forEach(node => {
const nodeId = node.id();
const connectedEdges = node.connectedEdges()
.intersection(eles);
component.merge(node);
if (!nodes[nodeId].cutVertex) {
component.merge(connectedEdges);
} else {
component.merge(connectedEdges.filter(edge => edge.isLoop()));
}
});
});
components.push(component);
};
const biconnectedSearch = (root, currentNode, parent) => {
if (root === parent) edgeCount += 1;
nodes[currentNode] = {
id : id,
low : id++,
cutVertex : false
};
let edges = eles.getElementById(currentNode)
.connectedEdges()
.intersection(eles);
if (edges.size() === 0) {
components.push(eles.spawn(eles.getElementById(currentNode)));
} else {
let sourceId, targetId, otherNodeId, edgeId;
edges.forEach(edge => {
sourceId = edge.source().id();
targetId = edge.target().id();
otherNodeId = (sourceId === currentNode) ? targetId : sourceId;
if (otherNodeId !== parent) {
edgeId = edge.id();
if (!visitedEdges[edgeId]) {
visitedEdges[edgeId] = true;
stack.push({
x : currentNode,
y : otherNodeId,
edge
});
}
if (!(otherNodeId in nodes)) {
biconnectedSearch(root, otherNodeId, currentNode);
nodes[currentNode].low = Math.min(nodes[currentNode].low,
nodes[otherNodeId].low);
if (nodes[currentNode].id <= nodes[otherNodeId].low) {
nodes[currentNode].cutVertex = true;
buildComponent(currentNode, otherNodeId);
}
} else {
nodes[currentNode].low = Math.min(nodes[currentNode].low,
nodes[otherNodeId].id);
}
}
});
}
};
eles.forEach(ele => {
if (ele.isNode()) {
let nodeId = ele.id();
if (!(nodeId in nodes)) {
edgeCount = 0;
biconnectedSearch(nodeId, nodeId);
nodes[nodeId].cutVertex = (edgeCount > 1);
}
}
});
let cutVertices = Object.keys(nodes)
.filter(id => nodes[id].cutVertex)
.map(id => eles.getElementById(id));
return {
cut: eles.spawn(cutVertices),
components
};
};
export default {
hopcroftTarjanBiconnected,
htbc: hopcroftTarjanBiconnected,
htb: hopcroftTarjanBiconnected,
hopcroftTarjanBiconnectedComponents: hopcroftTarjanBiconnected
};

View File

@@ -0,0 +1,34 @@
import * as is from '../is.mjs';
import * as util from '../util/index.mjs';
let cache = function( fn, name ){
return function traversalCache( arg1, arg2, arg3, arg4 ){
let selectorOrEles = arg1;
let eles = this;
let key;
if( selectorOrEles == null ){
key = '';
} else if( is.elementOrCollection( selectorOrEles ) && selectorOrEles.length === 1 ){
key = selectorOrEles.id();
}
if( eles.length === 1 && key ){
let _p = eles[0]._private;
let tch = _p.traversalCache = _p.traversalCache || {};
let ch = tch[ name ] = tch[ name ] || [];
let hash = util.hashString( key );
let cacheHit = ch[ hash ];
if( cacheHit ){
return cacheHit;
} else {
return ( ch[ hash ] = fn.call( eles, arg1, arg2, arg3, arg4 ) );
}
} else {
return fn.call( eles, arg1, arg2, arg3, arg4 );
}
};
};
export default cache;

View File

@@ -0,0 +1,86 @@
import define from '../define/index.mjs';
let fn, elesfn;
fn = elesfn = ({
data: define.data( {
field: 'data',
bindingEvent: 'data',
allowBinding: true,
allowSetting: true,
settingEvent: 'data',
settingTriggersEvent: true,
triggerFnName: 'trigger',
allowGetting: true,
immutableKeys: {
'id': true,
'source': true,
'target': true,
'parent': true
},
updateStyle: true
} ),
removeData: define.removeData( {
field: 'data',
event: 'data',
triggerFnName: 'trigger',
triggerEvent: true,
immutableKeys: {
'id': true,
'source': true,
'target': true,
'parent': true
},
updateStyle: true
} ),
scratch: define.data( {
field: 'scratch',
bindingEvent: 'scratch',
allowBinding: true,
allowSetting: true,
settingEvent: 'scratch',
settingTriggersEvent: true,
triggerFnName: 'trigger',
allowGetting: true,
updateStyle: true
} ),
removeScratch: define.removeData( {
field: 'scratch',
event: 'scratch',
triggerFnName: 'trigger',
triggerEvent: true,
updateStyle: true
} ),
rscratch: define.data( {
field: 'rscratch',
allowBinding: false,
allowSetting: true,
settingTriggersEvent: false,
allowGetting: true
} ),
removeRscratch: define.removeData( {
field: 'rscratch',
triggerEvent: false
} ),
id: function(){
let ele = this[0];
if( ele ){
return ele._private.data.id;
}
}
});
// aliases
fn.attr = fn.data;
fn.removeAttr = fn.removeData;
export default elesfn;

View File

@@ -0,0 +1,56 @@
import * as math from '../../math.mjs';
const ifEdge = (ele, getValue) => {
if( ele.isEdge() && ele.takesUpSpace() ){
return getValue( ele );
}
};
const ifEdgeRenderedPosition = (ele, getPoint) => {
if( ele.isEdge() && ele.takesUpSpace() ){
let cy = ele.cy();
return math.modelToRenderedPosition( getPoint( ele ), cy.zoom(), cy.pan() );
}
};
const ifEdgeRenderedPositions = (ele, getPoints) => {
if( ele.isEdge() && ele.takesUpSpace() ){
let cy = ele.cy();
let pan = cy.pan();
let zoom = cy.zoom();
return getPoints( ele ).map( p => math.modelToRenderedPosition( p, zoom, pan ) );
}
};
const controlPoints = ele => ele.renderer().getControlPoints( ele );
const segmentPoints = ele => ele.renderer().getSegmentPoints( ele );
const sourceEndpoint = ele => ele.renderer().getSourceEndpoint( ele );
const targetEndpoint = ele => ele.renderer().getTargetEndpoint( ele );
const midpoint = ele => ele.renderer().getEdgeMidpoint( ele );
const pts = {
controlPoints: { get: controlPoints, mult: true },
segmentPoints: { get: segmentPoints, mult: true },
sourceEndpoint: { get: sourceEndpoint },
targetEndpoint: { get: targetEndpoint },
midpoint: { get: midpoint }
};
const renderedName = name => 'rendered' + name[0].toUpperCase() + name.substr(1);
export default Object.keys( pts ).reduce( ( obj, name ) => {
let spec = pts[ name ];
let rName = renderedName( name );
obj[ name ] = function(){ return ifEdge( this, spec.get ); };
if( spec.mult ){
obj[ rName ] = function(){ return ifEdgeRenderedPositions( this, spec.get ); };
} else {
obj[ rName ] = function(){ return ifEdgeRenderedPosition( this, spec.get ); };
}
return obj;
}, {} );

View File

@@ -0,0 +1,130 @@
import * as util from '../util/index.mjs';
import * as is from '../is.mjs';
import Set from '../set.mjs';
// represents a node or an edge
let Element = function( cy, params, restore = true ){
if( cy === undefined || params === undefined || !is.core( cy ) ){
util.error( 'An element must have a core reference and parameters set' );
return;
}
let group = params.group;
// try to automatically infer the group if unspecified
if( group == null ){
if( params.data && params.data.source != null && params.data.target != null ){
group = 'edges';
} else {
group = 'nodes';
}
}
// validate group
if( group !== 'nodes' && group !== 'edges' ){
util.error( 'An element must be of type `nodes` or `edges`; you specified `' + group + '`' );
return;
}
// make the element array-like, just like a collection
this.length = 1;
this[0] = this;
// NOTE: when something is added here, add also to ele.json()
let _p = this._private = {
cy: cy,
single: true, // indicates this is an element
data: params.data || {}, // data object
position: params.position || { x: 0, y: 0 }, // (x, y) position pair
autoWidth: undefined, // width and height of nodes calculated by the renderer when set to special 'auto' value
autoHeight: undefined,
autoPadding: undefined,
compoundBoundsClean: false, // whether the compound dimensions need to be recalculated the next time dimensions are read
listeners: [], // array of bound listeners
group: group, // string; 'nodes' or 'edges'
style: {}, // properties as set by the style
rstyle: {}, // properties for style sent from the renderer to the core
styleCxts: [], // applied style contexts from the styler
styleKeys: {}, // per-group keys of style property values
removed: true, // whether it's inside the vis; true if removed (set true here since we call restore)
selected: params.selected ? true : false, // whether it's selected
selectable: params.selectable === undefined ? true : ( params.selectable ? true : false ), // whether it's selectable
locked: params.locked ? true : false, // whether the element is locked (cannot be moved)
grabbed: false, // whether the element is grabbed by the mouse; renderer sets this privately
grabbable: params.grabbable === undefined ? true : ( params.grabbable ? true : false ), // whether the element can be grabbed
pannable: params.pannable === undefined ? (group === 'edges' ? true : false) : ( params.pannable ? true : false ), // whether the element has passthrough panning enabled
active: false, // whether the element is active from user interaction
classes: new Set(), // map ( className => true )
animation: { // object for currently-running animations
current: [],
queue: []
},
rscratch: {}, // object in which the renderer can store information
scratch: params.scratch || {}, // scratch objects
edges: [], // array of connected edges
children: [], // array of children
parent: params.parent && params.parent.isNode() ? params.parent : null, // parent ref
traversalCache: {}, // cache of output of traversal functions
backgrounding: false, // whether background images are loading
bbCache: null, // cache of the current bounding box
bbCacheShift: { x: 0, y: 0 }, // shift applied to cached bb to be applied on next get
bodyBounds: null, // bounds cache of element body, w/o overlay
overlayBounds: null, // bounds cache of element body, including overlay
labelBounds: { // bounds cache of labels
all: null,
source: null,
target: null,
main: null
},
arrowBounds: { // bounds cache of edge arrows
source: null,
target: null,
'mid-source': null,
'mid-target': null
}
};
if( _p.position.x == null ){ _p.position.x = 0; }
if( _p.position.y == null ){ _p.position.y = 0; }
// renderedPosition overrides if specified
if( params.renderedPosition ){
let rpos = params.renderedPosition;
let pan = cy.pan();
let zoom = cy.zoom();
_p.position = {
x: (rpos.x - pan.x) / zoom,
y: (rpos.y - pan.y) / zoom
};
}
let classes = [];
if( is.array( params.classes ) ){
classes = params.classes;
} else if( is.string( params.classes ) ){
classes = params.classes.split( /\s+/ );
}
for( let i = 0, l = classes.length; i < l; i++ ){
let cls = classes[ i ];
if( !cls || cls === '' ){ continue; }
_p.classes.add(cls);
}
this.createEmitter();
if( restore === undefined || restore ){
this.restore();
}
let bypass = params.style || params.css;
if( bypass ){
util.warn('Setting a `style` bypass at element creation should be done only when absolutely necessary. Try to use the stylesheet instead.');
this.style(bypass);
}
};
export default Element;

View File

@@ -0,0 +1,196 @@
import * as util from '../util/index.mjs';
import Promise from '../promise.mjs';
import * as math from '../math.mjs';
const getLayoutDimensionOptions = util.defaults({
nodeDimensionsIncludeLabels: false
});
let elesfn = ({
// Calculates and returns node dimensions { x, y } based on options given
layoutDimensions: function( options ){
options = getLayoutDimensionOptions( options );
let dims;
if( !this.takesUpSpace() ){
dims = { w: 0, h: 0 };
} else if( options.nodeDimensionsIncludeLabels ){
let bbDim = this.boundingBox();
dims = {
w: bbDim.w,
h: bbDim.h
};
} else {
dims = {
w: this.outerWidth(),
h: this.outerHeight()
};
}
// sanitise the dimensions for external layouts (avoid division by zero)
if( dims.w === 0 || dims.h === 0 ){
dims.w = dims.h = 1;
}
return dims;
},
// using standard layout options, apply position function (w/ or w/o animation)
layoutPositions: function( layout, options, fn ){
let nodes = this.nodes().filter(n => !n.isParent());
let cy = this.cy();
let layoutEles = options.eles; // nodes & edges
let getMemoizeKey = node => node.id();
let fnMem = util.memoize( fn, getMemoizeKey ); // memoized version of position function
layout.emit( { type: 'layoutstart', layout: layout } );
layout.animations = [];
let calculateSpacing = function( spacing, nodesBb, pos ){
let center = {
x: nodesBb.x1 + nodesBb.w / 2,
y: nodesBb.y1 + nodesBb.h / 2
};
let spacingVector = { // scale from center of bounding box (not necessarily 0,0)
x: (pos.x - center.x) * spacing,
y: (pos.y - center.y) * spacing
};
return {
x: center.x + spacingVector.x,
y: center.y + spacingVector.y
};
};
let useSpacingFactor = options.spacingFactor && options.spacingFactor !== 1;
let spacingBb = function(){
if( !useSpacingFactor ){ return null; }
let bb = math.makeBoundingBox();
for( let i = 0; i < nodes.length; i++ ){
let node = nodes[i];
let pos = fnMem( node, i );
math.expandBoundingBoxByPoint( bb, pos.x, pos.y );
}
return bb;
};
let bb = spacingBb();
let getFinalPos = util.memoize( function( node, i ){
let newPos = fnMem( node, i );
if( useSpacingFactor ){
let spacing = Math.abs( options.spacingFactor );
newPos = calculateSpacing( spacing, bb, newPos );
}
if( options.transform != null ){
newPos = options.transform( node, newPos );
}
return newPos;
}, getMemoizeKey );
if( options.animate ){
for( let i = 0; i < nodes.length; i++ ){
let node = nodes[ i ];
let newPos = getFinalPos( node, i );
let animateNode = options.animateFilter == null || options.animateFilter( node, i );
if( animateNode ){
let ani = node.animation( {
position: newPos,
duration: options.animationDuration,
easing: options.animationEasing
} );
layout.animations.push( ani );
} else {
node.position( newPos );
}
}
if( options.fit ){
let fitAni = cy.animation({
fit: {
boundingBox: layoutEles.boundingBoxAt( getFinalPos ),
padding: options.padding
},
duration: options.animationDuration,
easing: options.animationEasing
});
layout.animations.push( fitAni );
} else if( options.zoom !== undefined && options.pan !== undefined ){
let zoomPanAni = cy.animation({
zoom: options.zoom,
pan: options.pan,
duration: options.animationDuration,
easing: options.animationEasing
});
layout.animations.push( zoomPanAni );
}
layout.animations.forEach(ani => ani.play());
layout.one( 'layoutready', options.ready );
layout.emit( { type: 'layoutready', layout: layout } );
Promise.all( layout.animations.map(function( ani ){
return ani.promise();
}) ).then(function(){
layout.one( 'layoutstop', options.stop );
layout.emit( { type: 'layoutstop', layout: layout } );
});
} else {
nodes.positions( getFinalPos );
if( options.fit ){
cy.fit( options.eles, options.padding );
}
if( options.zoom != null ){
cy.zoom( options.zoom );
}
if( options.pan ){
cy.pan( options.pan );
}
layout.one( 'layoutready', options.ready );
layout.emit( { type: 'layoutready', layout: layout } );
layout.one( 'layoutstop', options.stop );
layout.emit( { type: 'layoutstop', layout: layout } );
}
return this; // chaining
},
layout: function( options ){
let cy = this.cy();
return cy.makeLayout( util.extend( {}, options, {
eles: this
} ) );
}
});
// aliases:
elesfn.createLayout = elesfn.makeLayout = elesfn.layout;
export default elesfn;

View File

@@ -0,0 +1,457 @@
import * as is from '../is.mjs';
import * as util from '../util/index.mjs';
function styleCache( key, fn, ele ){
var _p = ele._private;
var cache = _p.styleCache = _p.styleCache || [];
var val;
if( (val = cache[key]) != null ){
return val;
} else {
val = cache[key] = fn( ele );
return val;
}
}
function cacheStyleFunction( key, fn ){
key = util.hashString( key );
return function cachedStyleFunction( ele ){
return styleCache( key, fn, ele );
};
}
function cachePrototypeStyleFunction( key, fn ){
key = util.hashString( key );
let selfFn = ele => fn.call( ele );
return function cachedPrototypeStyleFunction(){
var ele = this[0];
if( ele ){
return styleCache( key, selfFn, ele );
}
};
}
let elesfn = ({
recalculateRenderedStyle: function( useCache ){
let cy = this.cy();
let renderer = cy.renderer();
let styleEnabled = cy.styleEnabled();
if( renderer && styleEnabled ){
renderer.recalculateRenderedStyle( this, useCache );
}
return this;
},
dirtyStyleCache: function(){
let cy = this.cy();
let dirty = ele => ele._private.styleCache = null;
if( cy.hasCompoundNodes() ){
let eles;
eles = this.spawnSelf()
.merge( this.descendants() )
.merge( this.parents() )
;
eles.merge( eles.connectedEdges() );
eles.forEach( dirty );
} else {
this.forEach( ele => {
dirty( ele );
ele.connectedEdges().forEach( dirty );
} );
}
return this;
},
// fully updates (recalculates) the style for the elements
updateStyle: function( notifyRenderer ){
let cy = this._private.cy;
if( !cy.styleEnabled() ){ return this; }
if( cy.batching() ){
let bEles = cy._private.batchStyleEles;
bEles.merge( this );
return this; // chaining and exit early when batching
}
let hasCompounds = cy.hasCompoundNodes();
let updatedEles = this;
notifyRenderer = notifyRenderer || notifyRenderer === undefined ? true : false;
if( hasCompounds ){ // then add everything up and down for compound selector checks
updatedEles = this.spawnSelf().merge( this.descendants() ).merge( this.parents() );
}
// let changedEles = style.apply( updatedEles );
let changedEles = updatedEles;
if( notifyRenderer ){
changedEles.emitAndNotify( 'style' ); // let renderer know we changed style
} else {
changedEles.emit( 'style' ); // just fire the event
}
updatedEles.forEach(ele => ele._private.styleDirty = true);
return this; // chaining
},
// private: clears dirty flag and recalculates style
cleanStyle: function(){
let cy = this.cy();
if( !cy.styleEnabled() ){ return; }
for( let i = 0; i < this.length; i++ ){
let ele = this[i];
if( ele._private.styleDirty ){
// n.b. this flag should be set before apply() to avoid potential infinite recursion
ele._private.styleDirty = false;
cy.style().apply(ele);
}
}
},
// get the internal parsed style object for the specified property
parsedStyle: function( property, includeNonDefault = true ){
let ele = this[0];
let cy = ele.cy();
if( !cy.styleEnabled() ){ return; }
if( ele ){
// this.cleanStyle();
// Inline the important part of cleanStyle(), for raw performance
if( ele._private.styleDirty ){
// n.b. this flag should be set before apply() to avoid potential infinite recursion
ele._private.styleDirty = false;
cy.style().apply(ele);
}
let overriddenStyle = ele._private.style[ property ];
if( overriddenStyle != null ){
return overriddenStyle;
} else if( includeNonDefault ){
return cy.style().getDefaultProperty( property );
} else {
return null;
}
}
},
numericStyle: function( property ){
let ele = this[0];
if( !ele.cy().styleEnabled() ){ return; }
if( ele ){
let pstyle = ele.pstyle( property );
return pstyle.pfValue !== undefined ? pstyle.pfValue : pstyle.value;
}
},
numericStyleUnits: function( property ){
let ele = this[0];
if( !ele.cy().styleEnabled() ){ return; }
if( ele ){
return ele.pstyle( property ).units;
}
},
// get the specified css property as a rendered value (i.e. on-screen value)
// or get the whole rendered style if no property specified (NB doesn't allow setting)
renderedStyle: function( property ){
let cy = this.cy();
if( !cy.styleEnabled() ){ return this; }
let ele = this[0];
if( ele ){
return cy.style().getRenderedStyle( ele, property );
}
},
// read the calculated css style of the element or override the style (via a bypass)
style: function( name, value ){
let cy = this.cy();
if( !cy.styleEnabled() ){ return this; }
let updateTransitions = false;
let style = cy.style();
if( is.plainObject( name ) ){ // then extend the bypass
let props = name;
style.applyBypass( this, props, updateTransitions );
this.emitAndNotify( 'style' ); // let the renderer know we've updated style
} else if( is.string( name ) ){
if( value === undefined ){ // then get the property from the style
let ele = this[0];
if( ele ){
return style.getStylePropertyValue( ele, name );
} else { // empty collection => can't get any value
return;
}
} else { // then set the bypass with the property value
style.applyBypass( this, name, value, updateTransitions );
this.emitAndNotify( 'style' ); // let the renderer know we've updated style
}
} else if( name === undefined ){
let ele = this[0];
if( ele ){
return style.getRawStyle( ele );
} else { // empty collection => can't get any value
return;
}
}
return this; // chaining
},
removeStyle: function( names ){
let cy = this.cy();
if( !cy.styleEnabled() ){ return this; }
let updateTransitions = false;
let style = cy.style();
let eles = this;
if( names === undefined ){
for( let i = 0; i < eles.length; i++ ){
let ele = eles[ i ];
style.removeAllBypasses( ele, updateTransitions );
}
} else {
names = names.split( /\s+/ );
for( let i = 0; i < eles.length; i++ ){
let ele = eles[ i ];
style.removeBypasses( ele, names, updateTransitions );
}
}
this.emitAndNotify( 'style' ); // let the renderer know we've updated style
return this; // chaining
},
show: function(){
this.css( 'display', 'element' );
return this; // chaining
},
hide: function(){
this.css( 'display', 'none' );
return this; // chaining
},
effectiveOpacity: function(){
let cy = this.cy();
if( !cy.styleEnabled() ){ return 1; }
let hasCompoundNodes = cy.hasCompoundNodes();
let ele = this[0];
if( ele ){
let _p = ele._private;
let parentOpacity = ele.pstyle( 'opacity' ).value;
if( !hasCompoundNodes ){ return parentOpacity; }
let parents = !_p.data.parent ? null : ele.parents();
if( parents ){
for( let i = 0; i < parents.length; i++ ){
let parent = parents[ i ];
let opacity = parent.pstyle( 'opacity' ).value;
parentOpacity = opacity * parentOpacity;
}
}
return parentOpacity;
}
},
transparent: function(){
let cy = this.cy();
if( !cy.styleEnabled() ){ return false; }
let ele = this[0];
let hasCompoundNodes = ele.cy().hasCompoundNodes();
if( ele ){
if( !hasCompoundNodes ){
return ele.pstyle( 'opacity' ).value === 0;
} else {
return ele.effectiveOpacity() === 0;
}
}
},
backgrounding: function(){
let cy = this.cy();
if( !cy.styleEnabled() ){ return false; }
let ele = this[0];
return ele._private.backgrounding ? true : false;
}
});
function checkCompound( ele, parentOk ){
let _p = ele._private;
let parents = _p.data.parent ? ele.parents() : null;
if( parents ){ for( let i = 0; i < parents.length; i++ ){
let parent = parents[ i ];
if( !parentOk( parent ) ){ return false; }
} }
return true;
}
function defineDerivedStateFunction( specs ){
let ok = specs.ok;
let edgeOkViaNode = specs.edgeOkViaNode || specs.ok;
let parentOk = specs.parentOk || specs.ok;
return function(){
let cy = this.cy();
if( !cy.styleEnabled() ){ return true; }
let ele = this[0];
let hasCompoundNodes = cy.hasCompoundNodes();
if( ele ){
let _p = ele._private;
if( !ok( ele ) ){ return false; }
if( ele.isNode() ){
return !hasCompoundNodes || checkCompound( ele, parentOk );
} else {
let src = _p.source;
let tgt = _p.target;
return ( edgeOkViaNode(src) && (!hasCompoundNodes || checkCompound(src, edgeOkViaNode)) ) &&
( src === tgt || ( edgeOkViaNode(tgt) && (!hasCompoundNodes || checkCompound(tgt, edgeOkViaNode)) ) );
}
}
};
}
let eleTakesUpSpace = cacheStyleFunction( 'eleTakesUpSpace', function( ele ){
return (
ele.pstyle( 'display' ).value === 'element'
&& ele.width() !== 0
&& ( ele.isNode() ? ele.height() !== 0 : true )
);
} );
elesfn.takesUpSpace = cachePrototypeStyleFunction( 'takesUpSpace', defineDerivedStateFunction({
ok: eleTakesUpSpace
}) );
let eleInteractive = cacheStyleFunction( 'eleInteractive', function( ele ){
return (
ele.pstyle('events').value === 'yes'
&& ele.pstyle('visibility').value === 'visible'
&& eleTakesUpSpace( ele )
);
} );
let parentInteractive = cacheStyleFunction( 'parentInteractive', function( parent ){
return (
parent.pstyle('visibility').value === 'visible'
&& eleTakesUpSpace( parent )
);
} );
elesfn.interactive = cachePrototypeStyleFunction( 'interactive', defineDerivedStateFunction({
ok: eleInteractive,
parentOk: parentInteractive,
edgeOkViaNode: eleTakesUpSpace
}) );
elesfn.noninteractive = function(){
let ele = this[0];
if( ele ){
return !ele.interactive();
}
};
let eleVisible = cacheStyleFunction( 'eleVisible', function( ele ){
return (
ele.pstyle( 'visibility' ).value === 'visible'
&& ele.pstyle( 'opacity' ).pfValue !== 0
&& eleTakesUpSpace( ele )
);
} );
let edgeVisibleViaNode = eleTakesUpSpace;
elesfn.visible = cachePrototypeStyleFunction( 'visible', defineDerivedStateFunction({
ok: eleVisible,
edgeOkViaNode: edgeVisibleViaNode
}) );
elesfn.hidden = function(){
let ele = this[0];
if( ele ){
return !ele.visible();
}
};
elesfn.isBundledBezier = cachePrototypeStyleFunction('isBundledBezier', function(){
if( !this.cy().styleEnabled() ){ return false; }
return !this.removed() && this.pstyle('curve-style').value === 'bezier' && this.takesUpSpace();
});
elesfn.bypass = elesfn.css = elesfn.style;
elesfn.renderedCss = elesfn.renderedStyle;
elesfn.removeBypass = elesfn.removeCss = elesfn.removeStyle;
elesfn.pstyle = elesfn.parsedStyle;
export default elesfn;

View File

@@ -0,0 +1,82 @@
import * as is from '../is.mjs';
import * as util from '../util/index.mjs';
import Collection from '../collection/index.mjs';
import Element from '../collection/element.mjs';
let corefn = {
add: function( opts ){
let elements;
let cy = this;
// add the elements
if( is.elementOrCollection( opts ) ){
let eles = opts;
if( eles._private.cy === cy ){ // same instance => just restore
elements = eles.restore();
} else { // otherwise, copy from json
let jsons = [];
for( let i = 0; i < eles.length; i++ ){
let ele = eles[ i ];
jsons.push( ele.json() );
}
elements = new Collection( cy, jsons );
}
}
// specify an array of options
else if( is.array( opts ) ){
let jsons = opts;
elements = new Collection( cy, jsons );
}
// specify via opts.nodes and opts.edges
else if( is.plainObject( opts ) && (is.array( opts.nodes ) || is.array( opts.edges )) ){
let elesByGroup = opts;
let jsons = [];
let grs = [ 'nodes', 'edges' ];
for( let i = 0, il = grs.length; i < il; i++ ){
let group = grs[ i ];
let elesArray = elesByGroup[ group ];
if( is.array( elesArray ) ){
for( let j = 0, jl = elesArray.length; j < jl; j++ ){
let json = util.extend( { group: group }, elesArray[ j ] );
jsons.push( json );
}
}
}
elements = new Collection( cy, jsons );
}
// specify options for one element
else {
let json = opts;
elements = (new Element( cy, json )).collection();
}
return elements;
},
remove: function( collection ){
if( is.elementOrCollection( collection ) ){
// already have right ref
} else if( is.string( collection ) ){
let selector = collection;
collection = this.$( selector );
}
return collection.remove();
}
};
export default corefn;

View File

@@ -0,0 +1,82 @@
import * as is from '../../is.mjs';
function getEasedValue( type, start, end, percent, easingFn ){
if( percent === 1 ){
return end;
}
if( start === end ){
return end;
}
let val = easingFn( start, end, percent );
if( type == null ){
return val;
}
if( type.roundValue || type.color ){
val = Math.round( val );
}
if( type.min !== undefined ){
val = Math.max( val, type.min );
}
if( type.max !== undefined ){
val = Math.min( val, type.max );
}
return val;
}
function getValue( prop, spec ){
if( prop.pfValue != null || prop.value != null ){
if( prop.pfValue != null && (spec == null || spec.type.units !== '%') ){
return prop.pfValue;
} else {
return prop.value;
}
} else {
return prop;
}
}
function ease( startProp, endProp, percent, easingFn, propSpec ){
let type = propSpec != null ? propSpec.type : null;
if( percent < 0 ){
percent = 0;
} else if( percent > 1 ){
percent = 1;
}
let start = getValue( startProp, propSpec );
let end = getValue( endProp, propSpec );
if( is.number( start ) && is.number( end ) ){
return getEasedValue( type, start, end, percent, easingFn );
} else if( is.array( start ) && is.array( end ) ){
let easedArr = [];
for( let i = 0; i < end.length; i++ ){
let si = start[ i ];
let ei = end[ i ];
if( si != null && ei != null ){
let val = getEasedValue( type, si, ei, percent, easingFn );
easedArr.push( val );
} else {
easedArr.push( ei );
}
}
return easedArr;
}
return undefined;
}
export default ease;

View File

@@ -0,0 +1,76 @@
import generateCubicBezier from './cubic-bezier.mjs';
import generateSpringRK4 from './spring.mjs';
let cubicBezier = function( t1, p1, t2, p2 ){
let bezier = generateCubicBezier( t1, p1, t2, p2 );
return function( start, end, percent ){
return start + ( end - start ) * bezier( percent );
};
};
let easings = {
'linear': function( start, end, percent ){
return start + (end - start) * percent;
},
// default easings
'ease': cubicBezier( 0.25, 0.1, 0.25, 1 ),
'ease-in': cubicBezier( 0.42, 0, 1, 1 ),
'ease-out': cubicBezier( 0, 0, 0.58, 1 ),
'ease-in-out': cubicBezier( 0.42, 0, 0.58, 1 ),
// sine
'ease-in-sine': cubicBezier( 0.47, 0, 0.745, 0.715 ),
'ease-out-sine': cubicBezier( 0.39, 0.575, 0.565, 1 ),
'ease-in-out-sine': cubicBezier( 0.445, 0.05, 0.55, 0.95 ),
// quad
'ease-in-quad': cubicBezier( 0.55, 0.085, 0.68, 0.53 ),
'ease-out-quad': cubicBezier( 0.25, 0.46, 0.45, 0.94 ),
'ease-in-out-quad': cubicBezier( 0.455, 0.03, 0.515, 0.955 ),
// cubic
'ease-in-cubic': cubicBezier( 0.55, 0.055, 0.675, 0.19 ),
'ease-out-cubic': cubicBezier( 0.215, 0.61, 0.355, 1 ),
'ease-in-out-cubic': cubicBezier( 0.645, 0.045, 0.355, 1 ),
// quart
'ease-in-quart': cubicBezier( 0.895, 0.03, 0.685, 0.22 ),
'ease-out-quart': cubicBezier( 0.165, 0.84, 0.44, 1 ),
'ease-in-out-quart': cubicBezier( 0.77, 0, 0.175, 1 ),
// quint
'ease-in-quint': cubicBezier( 0.755, 0.05, 0.855, 0.06 ),
'ease-out-quint': cubicBezier( 0.23, 1, 0.32, 1 ),
'ease-in-out-quint': cubicBezier( 0.86, 0, 0.07, 1 ),
// expo
'ease-in-expo': cubicBezier( 0.95, 0.05, 0.795, 0.035 ),
'ease-out-expo': cubicBezier( 0.19, 1, 0.22, 1 ),
'ease-in-out-expo': cubicBezier( 1, 0, 0, 1 ),
// circ
'ease-in-circ': cubicBezier( 0.6, 0.04, 0.98, 0.335 ),
'ease-out-circ': cubicBezier( 0.075, 0.82, 0.165, 1 ),
'ease-in-out-circ': cubicBezier( 0.785, 0.135, 0.15, 0.86 ),
// user param easings...
'spring': function( tension, friction, duration ){
if( duration === 0 ){ // can't get a spring w/ duration 0
return easings.linear; // duration 0 => jump to end so impl doesn't matter
}
let spring = generateSpringRK4( tension, friction, duration );
return function( start, end, percent ){
return start + (end - start) * spring( percent );
};
},
'cubic-bezier': cubicBezier
};
export default easings;

522
frontend/node_modules/cytoscape/src/core/index.mjs generated vendored Normal file
View File

@@ -0,0 +1,522 @@
import window from '../window.mjs';
import * as util from '../util/index.mjs';
import Collection from '../collection/index.mjs';
import * as is from '../is.mjs';
import Promise from '../promise.mjs';
import addRemove from './add-remove.mjs';
import animation from './animation/index.mjs';
import events from './events.mjs';
import exportFormat from './export.mjs';
import layout from './layout.mjs';
import notification from './notification.mjs';
import renderer from './renderer.mjs';
import search from './search.mjs';
import style from './style.mjs';
import viewport from './viewport.mjs';
import data from './data.mjs';
let Core = function( opts ){
let cy = this;
opts = util.extend( {}, opts );
let container = opts.container;
// allow for passing a wrapped jquery object
// e.g. cytoscape({ container: $('#cy') })
if( container && !is.htmlElement( container ) && is.htmlElement( container[0] ) ){
container = container[0];
}
let reg = container ? container._cyreg : null; // e.g. already registered some info (e.g. readies) via jquery
reg = reg || {};
if( reg && reg.cy ){
reg.cy.destroy();
reg = {}; // old instance => replace reg completely
}
let readies = reg.readies = reg.readies || [];
if( container ){ container._cyreg = reg; } // make sure container assoc'd reg points to this cy
reg.cy = cy;
let head = window !== undefined && container !== undefined && !opts.headless;
let options = opts;
options.layout = util.extend( { name: head ? 'grid' : 'null' }, options.layout );
options.renderer = util.extend( { name: head ? 'canvas' : 'null' }, options.renderer );
let defVal = function( def, val, altVal ){
if( val !== undefined ){
return val;
} else if( altVal !== undefined ){
return altVal;
} else {
return def;
}
};
let _p = this._private = {
container: container, // html dom ele container
ready: false, // whether ready has been triggered
options: options, // cached options
elements: new Collection( this ), // elements in the graph
listeners: [], // list of listeners
aniEles: new Collection( this ), // elements being animated
data: options.data || {}, // data for the core
scratch: {}, // scratch object for core
layout: null,
renderer: null,
destroyed: false, // whether destroy was called
notificationsEnabled: true, // whether notifications are sent to the renderer
minZoom: 1e-50,
maxZoom: 1e50,
zoomingEnabled: defVal( true, options.zoomingEnabled ),
userZoomingEnabled: defVal( true, options.userZoomingEnabled ),
panningEnabled: defVal( true, options.panningEnabled ),
userPanningEnabled: defVal( true, options.userPanningEnabled ),
boxSelectionEnabled: defVal( true, options.boxSelectionEnabled ),
autolock: defVal( false, options.autolock, options.autolockNodes ),
autoungrabify: defVal( false, options.autoungrabify, options.autoungrabifyNodes ),
autounselectify: defVal( false, options.autounselectify ),
styleEnabled: options.styleEnabled === undefined ? head : options.styleEnabled,
zoom: is.number( options.zoom ) ? options.zoom : 1,
pan: {
x: is.plainObject( options.pan ) && is.number( options.pan.x ) ? options.pan.x : 0,
y: is.plainObject( options.pan ) && is.number( options.pan.y ) ? options.pan.y : 0
},
animation: { // object for currently-running animations
current: [],
queue: []
},
hasCompoundNodes: false,
multiClickDebounceTime: defVal(250, options.multiClickDebounceTime)
};
this.createEmitter();
// set selection type
this.selectionType( options.selectionType );
// init zoom bounds
this.zoomRange({ min: options.minZoom, max: options.maxZoom });
let loadExtData = function( extData, next ){
let anyIsPromise = extData.some( is.promise );
if( anyIsPromise ){
return Promise.all( extData ).then( next ); // load all data asynchronously, then exec rest of init
} else {
next( extData ); // exec synchronously for convenience
}
};
// start with the default stylesheet so we have something before loading an external stylesheet
if( _p.styleEnabled ){
cy.setStyle([]);
}
// create the renderer
let rendererOptions = util.assign({}, options, options.renderer); // allow rendering hints in top level options
cy.initRenderer( rendererOptions );
let setElesAndLayout = function( elements, onload, ondone ){
cy.notifications( false );
// remove old elements
let oldEles = cy.mutableElements();
if( oldEles.length > 0 ){
oldEles.remove();
}
if( elements != null ){
if( is.plainObject( elements ) || is.array( elements ) ){
cy.add( elements );
}
}
cy.one( 'layoutready', function( e ){
cy.notifications( true );
cy.emit( e ); // we missed this event by turning notifications off, so pass it on
cy.one( 'load', onload );
cy.emitAndNotify( 'load' );
} ).one( 'layoutstop', function(){
cy.one( 'done', ondone );
cy.emit( 'done' );
} );
let layoutOpts = util.extend( {}, cy._private.options.layout );
layoutOpts.eles = cy.elements();
cy.layout( layoutOpts ).run();
};
loadExtData([ options.style, options.elements ], function( thens ){
let initStyle = thens[0];
let initEles = thens[1];
// init style
if( _p.styleEnabled ){
cy.style().append( initStyle );
}
// initial load
setElesAndLayout( initEles, function(){ // onready
cy.startAnimationLoop();
_p.ready = true;
// if a ready callback is specified as an option, the bind it
if( is.fn( options.ready ) ){
cy.on( 'ready', options.ready );
}
// bind all the ready handlers registered before creating this instance
for( let i = 0; i < readies.length; i++ ){
let fn = readies[ i ];
cy.on( 'ready', fn );
}
if( reg ){ reg.readies = []; } // clear b/c we've bound them all and don't want to keep it around in case a new core uses the same div etc
cy.emit( 'ready' );
}, options.done );
} );
};
let corefn = Core.prototype; // short alias
util.extend( corefn, {
instanceString: function(){
return 'core';
},
isReady: function(){
return this._private.ready;
},
destroyed: function(){
return this._private.destroyed;
},
ready: function( fn ){
if( this.isReady() ){
this.emitter().emit( 'ready', [], fn ); // just calls fn as though triggered via ready event
} else {
this.on( 'ready', fn );
}
return this;
},
destroy: function(){
let cy = this;
if( cy.destroyed() ) return;
cy.stopAnimationLoop();
cy.destroyRenderer();
this.emit( 'destroy' );
cy._private.destroyed = true;
return cy;
},
hasElementWithId: function( id ){
return this._private.elements.hasElementWithId( id );
},
getElementById: function( id ){
return this._private.elements.getElementById( id );
},
hasCompoundNodes: function(){
return this._private.hasCompoundNodes;
},
headless: function(){
return this._private.renderer.isHeadless();
},
styleEnabled: function(){
return this._private.styleEnabled;
},
addToPool: function( eles ){
this._private.elements.merge( eles );
return this; // chaining
},
removeFromPool: function( eles ){
this._private.elements.unmerge( eles );
return this;
},
container: function(){
return this._private.container || null;
},
window: function() {
let container = this._private.container;
if (container == null) return window;
let ownerDocument = this._private.container.ownerDocument;
if (ownerDocument === undefined || ownerDocument == null) {
return window;
}
return ownerDocument.defaultView || window;
},
mount: function( container ){
if( container == null ){ return; }
let cy = this;
let _p = cy._private;
let options = _p.options;
if( !is.htmlElement( container ) && is.htmlElement( container[0] ) ){
container = container[0];
}
cy.stopAnimationLoop();
cy.destroyRenderer();
_p.container = container;
_p.styleEnabled = true;
cy.invalidateSize();
cy.initRenderer( util.assign({}, options, options.renderer, {
// allow custom renderer name to be re-used, otherwise use canvas
name: options.renderer.name === 'null' ? 'canvas' : options.renderer.name
}) );
cy.startAnimationLoop();
cy.style( options.style );
cy.emit( 'mount' );
return cy;
},
unmount: function(){
let cy = this;
cy.stopAnimationLoop();
cy.destroyRenderer();
cy.initRenderer( { name: 'null' } );
cy.emit( 'unmount' );
return cy;
},
options: function(){
return util.copy( this._private.options );
},
json: function( obj ){
let cy = this;
let _p = cy._private;
let eles = cy.mutableElements();
let getFreshRef = ele => cy.getElementById(ele.id());
if( is.plainObject( obj ) ){ // set
cy.startBatch();
if( obj.elements ){
let idInJson = {};
let updateEles = function( jsons, gr ){
let toAdd = [];
let toMod = [];
for( let i = 0; i < jsons.length; i++ ){
let json = jsons[ i ];
if( !json.data.id ){
util.warn( 'cy.json() cannot handle elements without an ID attribute' );
continue;
}
let id = '' + json.data.id; // id must be string
let ele = cy.getElementById( id );
idInJson[ id ] = true;
if( ele.length !== 0 ){ // existing element should be updated
toMod.push({ ele, json });
} else { // otherwise should be added
if( gr ){
json.group = gr;
toAdd.push( json );
} else {
toAdd.push( json );
}
}
}
cy.add( toAdd );
for( let i = 0; i < toMod.length; i++ ){
let { ele, json } = toMod[i];
ele.json(json);
}
};
if( is.array( obj.elements ) ){ // elements: []
updateEles( obj.elements );
} else { // elements: { nodes: [], edges: [] }
let grs = [ 'nodes', 'edges' ];
for( let i = 0; i < grs.length; i++ ){
let gr = grs[ i ];
let elements = obj.elements[ gr ];
if( is.array( elements ) ){
updateEles( elements, gr );
}
}
}
let parentsToRemove = cy.collection();
(eles
.filter(ele => !idInJson[ ele.id() ])
.forEach(ele => {
if ( ele.isParent() ) {
parentsToRemove.merge(ele);
} else {
ele.remove();
}
})
);
// so that children are not removed w/parent
parentsToRemove.forEach(ele => ele.children().move({ parent: null }));
// intermediate parents may be moved by prior line, so make sure we remove by fresh refs
parentsToRemove.forEach(ele => getFreshRef(ele).remove());
}
if( obj.style ){
cy.style( obj.style );
}
if( obj.zoom != null && obj.zoom !== _p.zoom ){
cy.zoom( obj.zoom );
}
if( obj.pan ){
if( obj.pan.x !== _p.pan.x || obj.pan.y !== _p.pan.y ){
cy.pan( obj.pan );
}
}
if( obj.data ){
cy.data( obj.data );
}
let fields = [
'minZoom', 'maxZoom', 'zoomingEnabled', 'userZoomingEnabled',
'panningEnabled', 'userPanningEnabled',
'boxSelectionEnabled',
'autolock', 'autoungrabify', 'autounselectify',
'multiClickDebounceTime'
];
for( let i = 0; i < fields.length; i++ ){
let f = fields[ i ];
if( obj[ f ] != null ){
cy[ f ]( obj[ f ] );
}
}
cy.endBatch();
return this; // chaining
} else { // get
let flat = !!obj;
let json = {};
if( flat ){
json.elements = this.elements().map( ele => ele.json() );
} else {
json.elements = {};
eles.forEach( function( ele ){
let group = ele.group();
if( !json.elements[ group ] ){
json.elements[ group ] = [];
}
json.elements[ group ].push( ele.json() );
} );
}
if( this._private.styleEnabled ){
json.style = cy.style().json();
}
json.data = util.copy( cy.data() );
let options = _p.options;
json.zoomingEnabled = _p.zoomingEnabled;
json.userZoomingEnabled = _p.userZoomingEnabled;
json.zoom = _p.zoom;
json.minZoom = _p.minZoom;
json.maxZoom = _p.maxZoom;
json.panningEnabled = _p.panningEnabled;
json.userPanningEnabled = _p.userPanningEnabled;
json.pan = util.copy( _p.pan );
json.boxSelectionEnabled = _p.boxSelectionEnabled;
json.renderer = util.copy( options.renderer );
json.hideEdgesOnViewport = options.hideEdgesOnViewport;
json.textureOnViewport = options.textureOnViewport;
json.wheelSensitivity = options.wheelSensitivity;
json.motionBlur = options.motionBlur;
json.multiClickDebounceTime = options.multiClickDebounceTime;
return json;
}
}
} );
corefn.$id = corefn.getElementById;
[
addRemove,
animation,
events,
exportFormat,
layout,
notification,
renderer,
search,
style,
viewport,
data
].forEach( function( props ){
util.extend( corefn, props );
} );
export default Core;

208
frontend/node_modules/cytoscape/src/define/data.mjs generated vendored Normal file
View File

@@ -0,0 +1,208 @@
import * as util from '../util/index.mjs';
import * as is from '../is.mjs';
import get from 'lodash/get.js';
import set from 'lodash/set.js';
import toPath from 'lodash/toPath.js';
let define = {
// access data field
data: function( params ){
let defaults = {
field: 'data',
bindingEvent: 'data',
allowBinding: false,
allowSetting: false,
allowGetting: false,
settingEvent: 'data',
settingTriggersEvent: false,
triggerFnName: 'trigger',
immutableKeys: {}, // key => true if immutable
updateStyle: false,
beforeGet: function( self ){},
beforeSet: function( self, obj ){},
onSet: function( self ){},
canSet: function( self ){ return true; }
};
params = util.extend( {}, defaults, params );
return function dataImpl( name, value ){
let p = params;
let self = this;
let selfIsArrayLike = self.length !== undefined;
let all = selfIsArrayLike ? self : [ self ]; // put in array if not array-like
let single = selfIsArrayLike ? self[0] : self;
// .data('foo', ...)
if (is.string(name)) { // set or get property
let isPathLike = name.indexOf('.') !== -1; // there might be a normal field with a dot
let path = isPathLike && toPath(name);
// .data('foo')
if( p.allowGetting && value === undefined ){ // get
let ret;
if( single ){
p.beforeGet( single );
// check if it's path and a field with the same name doesn't exist
if (path && single._private[ p.field ][ name ] === undefined) {
ret = get(single._private[ p.field ], path);
} else {
ret = single._private[ p.field ][ name ];
}
}
return ret;
// .data('foo', 'bar')
} else if( p.allowSetting && value !== undefined ){ // set
let valid = !p.immutableKeys[ name ];
if( valid ){
let change = { [name]: value };
p.beforeSet( self, change );
for( let i = 0, l = all.length; i < l; i++ ){
let ele = all[i];
if( p.canSet( ele ) ){
if (path && single._private[ p.field ][ name ] === undefined) {
set(ele._private[ p.field ], path, value);
} else {
ele._private[ p.field ][ name ] = value;
}
}
}
// update mappers if asked
if( p.updateStyle ){ self.updateStyle(); }
// call onSet callback
p.onSet( self );
if( p.settingTriggersEvent ){
self[ p.triggerFnName ]( p.settingEvent );
}
}
}
// .data({ 'foo': 'bar' })
} else if( p.allowSetting && is.plainObject( name ) ){ // extend
let obj = name;
let k, v;
let keys = Object.keys( obj );
p.beforeSet( self, obj );
for( let i = 0; i < keys.length; i++ ){
k = keys[ i ];
v = obj[ k ];
let valid = !p.immutableKeys[ k ];
if( valid ){
for( let j = 0; j < all.length; j++ ){
let ele = all[j];
if( p.canSet( ele ) ){
ele._private[ p.field ][ k ] = v;
}
}
}
}
// update mappers if asked
if( p.updateStyle ){ self.updateStyle(); }
// call onSet callback
p.onSet( self );
if( p.settingTriggersEvent ){
self[ p.triggerFnName ]( p.settingEvent );
}
// .data(function(){ ... })
} else if( p.allowBinding && is.fn( name ) ){ // bind to event
let fn = name;
self.on( p.bindingEvent, fn );
// .data()
} else if( p.allowGetting && name === undefined ){ // get whole object
let ret;
if( single ){
p.beforeGet( single );
ret = single._private[ p.field ];
}
return ret;
}
return self; // maintain chainability
}; // function
}, // data
// remove data field
removeData: function( params ){
let defaults = {
field: 'data',
event: 'data',
triggerFnName: 'trigger',
triggerEvent: false,
immutableKeys: {} // key => true if immutable
};
params = util.extend( {}, defaults, params );
return function removeDataImpl( names ){
let p = params;
let self = this;
let selfIsArrayLike = self.length !== undefined;
let all = selfIsArrayLike ? self : [ self ]; // put in array if not array-like
// .removeData('foo bar')
if( is.string( names ) ){ // then get the list of keys, and delete them
let keys = names.split( /\s+/ );
let l = keys.length;
for( let i = 0; i < l; i++ ){ // delete each non-empty key
let key = keys[ i ];
if( is.emptyString( key ) ){ continue; }
let valid = !p.immutableKeys[ key ]; // not valid if immutable
if( valid ){
for( let i_a = 0, l_a = all.length; i_a < l_a; i_a++ ){
all[ i_a ]._private[ p.field ][ key ] = undefined;
}
}
}
if( p.triggerEvent ){
self[ p.triggerFnName ]( p.event );
}
// .removeData()
} else if( names === undefined ){ // then delete all keys
for( let i_a = 0, l_a = all.length; i_a < l_a; i_a++ ){
let _privateFields = all[ i_a ]._private[ p.field ];
let keys = Object.keys( _privateFields );
for( let i = 0; i < keys.length; i++ ){
let key = keys[i];
let validKeyToDelete = !p.immutableKeys[ key ];
if( validKeyToDelete ){
_privateFields[ key ] = undefined;
}
}
}
if( p.triggerEvent ){
self[ p.triggerFnName ]( p.event );
}
}
return self; // maintain chaining
}; // function
}, // removeData
}; // define
export default define;

111
frontend/node_modules/cytoscape/src/event.mjs generated vendored Normal file
View File

@@ -0,0 +1,111 @@
/*!
Event object based on jQuery events, MIT license
https://jquery.org/license/
https://tldrlegal.com/license/mit-license
https://github.com/jquery/jquery/blob/master/src/event.js
*/
let Event = function( src, props ){
this.recycle( src, props );
};
function returnFalse(){
return false;
}
function returnTrue(){
return true;
}
// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
Event.prototype = {
instanceString: function(){
return 'event';
},
recycle: function( src, props ){
this.isImmediatePropagationStopped = this.isPropagationStopped = this.isDefaultPrevented = returnFalse;
if( src != null && src.preventDefault ){ // Browser Event object
this.type = src.type;
// Events bubbling up the document may have been marked as prevented
// by a handler lower down the tree; reflect the correct value.
this.isDefaultPrevented = ( src.defaultPrevented ) ? returnTrue : returnFalse;
} else if( src != null && src.type ){ // Plain object containing all event details
props = src;
} else { // Event string
this.type = src;
}
// Put explicitly provided properties onto the event object
if( props != null ){
// more efficient to manually copy fields we use
this.originalEvent = props.originalEvent;
this.type = props.type != null ? props.type : this.type;
this.cy = props.cy;
this.target = props.target;
this.position = props.position;
this.renderedPosition = props.renderedPosition;
this.namespace = props.namespace;
this.layout = props.layout;
}
if( this.cy != null && this.position != null && this.renderedPosition == null ){
// create a rendered position based on the passed position
let pos = this.position;
let zoom = this.cy.zoom();
let pan = this.cy.pan();
this.renderedPosition = {
x: pos.x * zoom + pan.x,
y: pos.y * zoom + pan.y
};
}
// Create a timestamp if incoming event doesn't have one
this.timeStamp = src && src.timeStamp || Date.now();
},
preventDefault: function(){
this.isDefaultPrevented = returnTrue;
let e = this.originalEvent;
if( !e ){
return;
}
// if preventDefault exists run it on the original event
if( e.preventDefault ){
e.preventDefault();
}
},
stopPropagation: function(){
this.isPropagationStopped = returnTrue;
let e = this.originalEvent;
if( !e ){
return;
}
// if stopPropagation exists run it on the original event
if( e.stopPropagation ){
e.stopPropagation();
}
},
stopImmediatePropagation: function(){
this.isImmediatePropagationStopped = returnTrue;
this.stopPropagation();
},
isDefaultPrevented: returnFalse,
isPropagationStopped: returnFalse,
isImmediatePropagationStopped: returnFalse
};
export default Event;

246
frontend/node_modules/cytoscape/src/extension.mjs generated vendored Normal file
View File

@@ -0,0 +1,246 @@
import * as util from'./util/index.mjs';
import define from './define/index.mjs';
import Collection from './collection/index.mjs';
import Core from './core/index.mjs';
import incExts from './extensions/index.mjs';
import * as is from './is.mjs';
import Emitter from './emitter.mjs';
// registered extensions to cytoscape, indexed by name
let extensions = {};
// registered modules for extensions, indexed by name
let modules = {};
function setExtension( type, name, registrant ){
let ext = registrant;
let overrideErr = function( field ){
util.warn( 'Can not register `' + name + '` for `' + type + '` since `' + field + '` already exists in the prototype and can not be overridden' );
};
if( type === 'core' ){
if( Core.prototype[ name ] ){
return overrideErr( name );
} else {
Core.prototype[ name ] = registrant;
}
} else if( type === 'collection' ){
if( Collection.prototype[ name ] ){
return overrideErr( name );
} else {
Collection.prototype[ name ] = registrant;
}
} else if( type === 'layout' ){
// fill in missing layout functions in the prototype
let Layout = function( options ){
this.options = options;
registrant.call( this, options );
// make sure layout has _private for use w/ std apis like .on()
if( !is.plainObject( this._private ) ){
this._private = {};
}
this._private.cy = options.cy;
this._private.listeners = [];
this.createEmitter();
};
let layoutProto = Layout.prototype = Object.create( registrant.prototype );
let optLayoutFns = [];
for( let i = 0; i < optLayoutFns.length; i++ ){
let fnName = optLayoutFns[ i ];
layoutProto[ fnName ] = layoutProto[ fnName ] || function(){ return this; };
}
// either .start() or .run() is defined, so autogen the other
if( layoutProto.start && !layoutProto.run ){
layoutProto.run = function(){ this.start(); return this; };
} else if( !layoutProto.start && layoutProto.run ){
layoutProto.start = function(){ this.run(); return this; };
}
let regStop = registrant.prototype.stop;
layoutProto.stop = function(){
let opts = this.options;
if( opts && opts.animate ){
let anis = this.animations;
if( anis ){
for( let i = 0; i < anis.length; i++ ){
anis[ i ].stop();
}
}
}
if( regStop ){
regStop.call( this );
} else {
this.emit( 'layoutstop' );
}
return this;
};
if( !layoutProto.destroy ){
layoutProto.destroy = function(){
return this;
};
}
layoutProto.cy = function(){
return this._private.cy;
};
let getCy = layout => layout._private.cy;
let emitterOpts = {
addEventFields: function( layout, evt ){
evt.layout = layout;
evt.cy = getCy(layout);
evt.target = layout;
},
bubble: function(){ return true; },
parent: function( layout ){ return getCy(layout); }
};
util.assign( layoutProto, {
createEmitter: function(){
this._private.emitter = new Emitter( emitterOpts, this );
return this;
},
emitter: function(){ return this._private.emitter; },
on: function( evt, cb ){ this.emitter().on( evt, cb ); return this; },
one: function( evt, cb ){ this.emitter().one( evt, cb ); return this; },
once: function( evt, cb ){ this.emitter().one( evt, cb ); return this; },
removeListener: function( evt, cb ){ this.emitter().removeListener( evt, cb ); return this; },
removeAllListeners: function(){ this.emitter().removeAllListeners(); return this; },
emit: function( evt, params ){ this.emitter().emit( evt, params ); return this; }
} );
define.eventAliasesOn( layoutProto );
ext = Layout; // replace with our wrapped layout
} else if( type === 'renderer' && name !== 'null' && name !== 'base' ){
// user registered renderers inherit from base
let BaseRenderer = getExtension( 'renderer', 'base' );
let bProto = BaseRenderer.prototype;
let RegistrantRenderer = registrant;
let rProto = registrant.prototype;
let Renderer = function(){
BaseRenderer.apply( this, arguments );
RegistrantRenderer.apply( this, arguments );
};
let proto = Renderer.prototype;
for( let pName in bProto ){
let pVal = bProto[ pName ];
let existsInR = rProto[ pName ] != null;
if( existsInR ){
return overrideErr( pName );
}
proto[ pName ] = pVal; // take impl from base
}
for( let pName in rProto ){
proto[ pName ] = rProto[ pName ]; // take impl from registrant
}
bProto.clientFunctions.forEach( function( name ){
proto[ name ] = proto[ name ] || function(){
util.error( 'Renderer does not implement `renderer.' + name + '()` on its prototype' );
};
} );
ext = Renderer;
} else if (type === '__proto__' || type === 'constructor' || type === 'prototype'){
// to avoid potential prototype pollution
return util.error( type + ' is an illegal type to be registered, possibly lead to prototype pollutions' );
}
return util.setMap( {
map: extensions,
keys: [ type, name ],
value: ext
} );
}
function getExtension( type, name ){
return util.getMap( {
map: extensions,
keys: [ type, name ]
} );
}
function setModule( type, name, moduleType, moduleName, registrant ){
return util.setMap( {
map: modules,
keys: [ type, name, moduleType, moduleName ],
value: registrant
} );
}
function getModule( type, name, moduleType, moduleName ){
return util.getMap( {
map: modules,
keys: [ type, name, moduleType, moduleName ]
} );
}
let extension = function(){
// e.g. extension('renderer', 'svg')
if( arguments.length === 2 ){
return getExtension.apply( null, arguments );
}
// e.g. extension('renderer', 'svg', { ... })
else if( arguments.length === 3 ){
return setExtension.apply( null, arguments );
}
// e.g. extension('renderer', 'svg', 'nodeShape', 'ellipse')
else if( arguments.length === 4 ){
return getModule.apply( null, arguments );
}
// e.g. extension('renderer', 'svg', 'nodeShape', 'ellipse', { ... })
else if( arguments.length === 5 ){
return setModule.apply( null, arguments );
}
else {
util.error( 'Invalid extension access syntax' );
}
};
// allows a core instance to access extensions internally
Core.prototype.extension = extension;
// included extensions
incExts.forEach( function( group ){
group.extensions.forEach( function( ext ){
setExtension( group.type, ext.name, ext.impl );
} );
} );
export default extension;

View File

@@ -0,0 +1,105 @@
import * as util from '../../util/index.mjs';
import * as math from '../../math.mjs';
import * as is from '../../is.mjs';
let defaults = {
fit: true, // whether to fit the viewport to the graph
padding: 30, // the padding on fit
boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
avoidOverlap: true, // prevents node overlap, may overflow boundingBox and radius if not enough space
nodeDimensionsIncludeLabels: false, // Excludes the label when calculating node bounding boxes for the layout algorithm
spacingFactor: undefined, // Applies a multiplicative factor (>0) to expand or compress the overall area that the nodes take up
radius: undefined, // the radius of the circle
startAngle: 3 / 2 * Math.PI, // where nodes start in radians
sweep: undefined, // how many radians should be between the first and last node (defaults to full circle)
clockwise: true, // whether the layout should go clockwise (true) or counterclockwise/anticlockwise (false)
sort: undefined, // a sorting function to order the nodes; e.g. function(a, b){ return a.data('weight') - b.data('weight') }
animate: false, // whether to transition the node positions
animationDuration: 500, // duration of animation in ms if enabled
animationEasing: undefined, // easing of animation if enabled
animateFilter: function ( node, i ){ return true; }, // a function that determines whether the node should be animated. All nodes animated by default on animate enabled. Non-animated nodes are positioned immediately when the layout starts
ready: undefined, // callback on layoutready
stop: undefined, // callback on layoutstop
transform: function (node, position ){ return position; } // transform a given node position. Useful for changing flow direction in discrete layouts
};
function CircleLayout( options ){
this.options = util.extend( {}, defaults, options );
}
CircleLayout.prototype.run = function(){
let params = this.options;
let options = params;
let cy = params.cy;
let eles = options.eles;
let clockwise = options.counterclockwise !== undefined ? !options.counterclockwise : options.clockwise;
let nodes = eles.nodes().not( ':parent' );
if( options.sort ){
nodes = nodes.sort( options.sort );
}
let bb = math.makeBoundingBox( options.boundingBox ? options.boundingBox : {
x1: 0, y1: 0, w: cy.width(), h: cy.height()
} );
let center = {
x: bb.x1 + bb.w / 2,
y: bb.y1 + bb.h / 2
};
let sweep = options.sweep === undefined ? 2 * Math.PI - 2 * Math.PI / nodes.length : options.sweep;
let dTheta = sweep / ( Math.max( 1, nodes.length - 1 ) );
let r;
let minDistance = 0;
for( let i = 0; i < nodes.length; i++ ){
let n = nodes[ i ];
let nbb = n.layoutDimensions( options );
let w = nbb.w;
let h = nbb.h;
minDistance = Math.max( minDistance, w, h );
}
if( is.number( options.radius ) ){
r = options.radius;
} else if( nodes.length <= 1 ){
r = 0;
} else {
r = Math.min( bb.h, bb.w ) / 2 - minDistance;
}
// calculate the radius
if( nodes.length > 1 && options.avoidOverlap ){ // but only if more than one node (can't overlap)
minDistance *= 1.75; // just to have some nice spacing
let dcos = Math.cos( dTheta ) - Math.cos( 0 );
let dsin = Math.sin( dTheta ) - Math.sin( 0 );
let rMin = Math.sqrt( minDistance * minDistance / ( dcos * dcos + dsin * dsin ) ); // s.t. no nodes overlapping
r = Math.max( rMin, r );
}
let getPos = function( ele, i ){
let theta = options.startAngle + i * dTheta * ( clockwise ? 1 : -1 );
let rx = r * Math.cos( theta );
let ry = r * Math.sin( theta );
let pos = {
x: center.x + rx,
y: center.y + ry
};
return pos;
};
eles.nodes().layoutPositions( this, options, getPos );
return this; // chaining
};
export default CircleLayout;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,43 @@
import * as util from '../../util/index.mjs';
import * as math from '../../math.mjs';
let defaults = {
fit: true, // whether to fit to viewport
padding: 30, // fit padding
boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
animate: false, // whether to transition the node positions
animationDuration: 500, // duration of animation in ms if enabled
animationEasing: undefined, // easing of animation if enabled
animateFilter: function ( node, i ){ return true; }, // a function that determines whether the node should be animated. All nodes animated by default on animate enabled. Non-animated nodes are positioned immediately when the layout starts
ready: undefined, // callback on layoutready
stop: undefined, // callback on layoutstop
transform: function (node, position ){ return position; } // transform a given node position. Useful for changing flow direction in discrete layouts
};
function RandomLayout( options ){
this.options = util.extend( {}, defaults, options );
}
RandomLayout.prototype.run = function(){
let options = this.options;
let cy = options.cy;
let eles = options.eles;
let bb = math.makeBoundingBox( options.boundingBox ? options.boundingBox : {
x1: 0, y1: 0, w: cy.width(), h: cy.height()
} );
let getPos = function( node, i ){
return {
x: bb.x1 + Math.round( Math.random() * bb.w ),
y: bb.y1 + Math.round( Math.random() * bb.h )
};
};
eles.nodes().layoutPositions( this, options, getPos );
return this; // chaining
};
export default RandomLayout;

View File

@@ -0,0 +1,29 @@
import * as util from '../../../../util/index.mjs';
import coords from './coords.mjs';
import edgeArrows from './edge-arrows.mjs';
import edgeControlPoints from './edge-control-points.mjs';
import edgeEndpoints from './edge-endpoints.mjs';
import edgeProjection from './edge-projection.mjs';
import labels from './labels.mjs';
import nodes from './nodes.mjs';
import renderedStyle from './rendered-style.mjs';
import zOrdering from './z-ordering.mjs';
var BRp = {};
[
coords,
edgeArrows,
edgeControlPoints,
edgeEndpoints,
edgeProjection,
labels,
nodes,
renderedStyle,
zOrdering
].forEach(function( props ){
util.extend( BRp, props );
});
export default BRp;

View File

@@ -0,0 +1,38 @@
var BRp = {};
BRp.getCachedImage = function( url, crossOrigin, onLoad ){
var r = this;
var imageCache = r.imageCache = r.imageCache || {};
var cache = imageCache[ url ];
if( cache ){
if( !cache.image.complete ){
cache.image.addEventListener('load', onLoad);
}
return cache.image;
} else {
cache = imageCache[ url ] = imageCache[ url ] || {};
var image = cache.image = new Image(); // eslint-disable-line no-undef
image.addEventListener('load', onLoad);
image.addEventListener('error', function(){ image.error = true; });
// #1582 safari doesn't load data uris with crossOrigin properly
// https://bugs.webkit.org/show_bug.cgi?id=123978
var dataUriPrefix = 'data:';
var isDataUri = url.substring( 0, dataUriPrefix.length ).toLowerCase() === dataUriPrefix;
if( !isDataUri ){
// if crossorigin is 'null'(stringified), then manually set it to null
crossOrigin = crossOrigin === 'null' ? null : crossOrigin;
image.crossOrigin = crossOrigin; // prevent tainted canvas
}
image.src = url;
return image;
}
};
export default BRp;

View File

@@ -0,0 +1,224 @@
import * as util from '../../../util/index.mjs';
import * as is from '../../../is.mjs';
import arrowShapes from './arrow-shapes.mjs';
import coordEleMath from './coord-ele-math/index.mjs';
import images from './images.mjs';
import loadListeners from './load-listeners.mjs';
import nodeShapes from './node-shapes.mjs';
import redraw from './redraw.mjs';
var BaseRenderer = function( options ){ this.init( options ); };
var BR = BaseRenderer;
var BRp = BR.prototype;
BRp.clientFunctions = [ 'redrawHint', 'render', 'renderTo', 'matchCanvasSize', 'nodeShapeImpl', 'arrowShapeImpl' ];
BRp.init = function( options ){
var r = this;
r.options = options;
r.cy = options.cy;
var ctr = r.container = options.cy.container();
var containerWindow = r.cy.window();
// prepend a stylesheet in the head such that
if( containerWindow ){
var document = containerWindow.document;
var head = document.head;
var stylesheetId = '__________cytoscape_stylesheet';
var className = '__________cytoscape_container';
var stylesheetAlreadyExists = document.getElementById( stylesheetId ) != null;
if( ctr.className.indexOf( className ) < 0 ){
ctr.className = ( ctr.className || '' ) + ' ' + className;
}
if( !stylesheetAlreadyExists ){
var stylesheet = document.createElement('style');
stylesheet.id = stylesheetId;
stylesheet.textContent = '.'+className+' { position: relative; }';
head.insertBefore( stylesheet, head.children[0] ); // first so lowest priority
}
var computedStyle = containerWindow.getComputedStyle( ctr );
var position = computedStyle.getPropertyValue('position');
if( position === 'static' ){
util.warn('A Cytoscape container has style position:static and so can not use UI extensions properly');
}
}
r.selection = [ undefined, undefined, undefined, undefined, 0]; // Coordinates for selection box, plus enabled flag
r.bezierProjPcts = [ 0.05, 0.225, 0.4, 0.5, 0.6, 0.775, 0.95 ];
//--Pointer-related data
r.hoverData = {down: null, last: null,
downTime: null, triggerMode: null,
dragging: false,
initialPan: [ null, null ], capture: false};
r.dragData = {possibleDragElements: []};
r.touchData = {
start: null, capture: false,
// These 3 fields related to tap, taphold events
startPosition: [ null, null, null, null, null, null ],
singleTouchStartTime: null,
singleTouchMoved: true,
now: [ null, null, null, null, null, null ],
earlier: [ null, null, null, null, null, null ]
};
r.redraws = 0;
r.showFps = options.showFps;
r.debug = options.debug;
r.webgl = options.webgl;
r.hideEdgesOnViewport = options.hideEdgesOnViewport;
r.textureOnViewport = options.textureOnViewport;
r.wheelSensitivity = options.wheelSensitivity;
r.motionBlurEnabled = options.motionBlur; // on by default
r.forcedPixelRatio = is.number(options.pixelRatio) ? options.pixelRatio : null;
r.motionBlur = options.motionBlur; // for initial kick off
r.motionBlurOpacity = options.motionBlurOpacity;
r.motionBlurTransparency = 1 - r.motionBlurOpacity;
r.motionBlurPxRatio = 1;
r.mbPxRBlurry = 1; //0.8;
r.minMbLowQualFrames = 4;
r.fullQualityMb = false;
r.clearedForMotionBlur = [];
r.desktopTapThreshold = options.desktopTapThreshold;
r.desktopTapThreshold2 = options.desktopTapThreshold * options.desktopTapThreshold;
r.touchTapThreshold = options.touchTapThreshold;
r.touchTapThreshold2 = options.touchTapThreshold * options.touchTapThreshold;
r.tapholdDuration = 500;
r.bindings = [];
r.beforeRenderCallbacks = [];
r.beforeRenderPriorities = { // higher priority execs before lower one
animations: 400,
eleCalcs: 300,
eleTxrDeq: 200,
lyrTxrDeq: 150,
lyrTxrSkip: 100,
};
r.registerNodeShapes();
r.registerArrowShapes();
r.registerCalculationListeners();
};
BRp.notify = function( eventName, eles ) {
var r = this;
var cy = r.cy;
// the renderer can't be notified after it's destroyed
if( this.destroyed ){ return; }
if( eventName === 'init' ){
r.load();
return;
}
if( eventName === 'destroy' ){
r.destroy();
return;
}
if(
eventName === 'add'
|| eventName === 'remove'
|| (eventName === 'move' && cy.hasCompoundNodes())
|| eventName === 'load'
|| eventName === 'zorder'
|| eventName === 'mount'
){
r.invalidateCachedZSortedEles();
}
if( eventName === 'viewport' ){
r.redrawHint( 'select', true );
}
if( eventName === 'gc' ){
r.redrawHint( 'gc', true );
}
if( eventName === 'load' || eventName === 'resize' || eventName === 'mount' ){
r.invalidateContainerClientCoordsCache();
r.matchCanvasSize( r.container );
}
r.redrawHint( 'eles', true );
r.redrawHint( 'drag', true );
this.startRenderLoop();
this.redraw();
};
BRp.destroy = function(){
var r = this;
r.destroyed = true;
r.cy.stopAnimationLoop();
for( var i = 0; i < r.bindings.length; i++ ){
var binding = r.bindings[ i ];
var b = binding;
var tgt = b.target;
( tgt.off || tgt.removeEventListener ).apply( tgt, b.args );
}
r.bindings = [];
r.beforeRenderCallbacks = [];
r.onUpdateEleCalcsFns = [];
if( r.removeObserver ){
r.removeObserver.disconnect();
}
if( r.styleObserver ){
r.styleObserver.disconnect();
}
if( r.resizeObserver ){
r.resizeObserver.disconnect();
}
if( r.labelCalcDiv ){
try {
document.body.removeChild( r.labelCalcDiv ); // eslint-disable-line no-undef
} catch( e ){
// ie10 issue #1014
}
}
};
BRp.isHeadless = function(){
return false;
};
[
arrowShapes,
coordEleMath,
images,
loadListeners,
nodeShapes,
redraw
].forEach( function( props ){
util.extend( BRp, props );
} );
export default BR;

View File

@@ -0,0 +1,431 @@
/* global Path2D */
import * as util from '../../../util/index.mjs';
import {drawPreparedRoundCorner} from "../../../round.mjs";
let CRp = {};
CRp.drawEdge = function( context, edge, shiftToOriginWithBb, drawLabel = true, shouldDrawOverlay = true, shouldDrawOpacity = true ){
let r = this;
let rs = edge._private.rscratch;
if( shouldDrawOpacity && !edge.visible() ){ return; }
// if bezier ctrl pts can not be calculated, then die
if( rs.badLine || rs.allpts == null || isNaN(rs.allpts[0]) ){ // isNaN in case edge is impossible and browser bugs (e.g. safari)
return;
}
let bb;
if( shiftToOriginWithBb ){
bb = shiftToOriginWithBb;
context.translate( -bb.x1, -bb.y1 );
}
let opacity = shouldDrawOpacity ? edge.pstyle('opacity').value : 1;
let lineOpacity = shouldDrawOpacity ? edge.pstyle('line-opacity').value : 1;
let curveStyle = edge.pstyle('curve-style').value;
let lineStyle = edge.pstyle('line-style').value;
let edgeWidth = edge.pstyle('width').pfValue;
let lineCap = edge.pstyle('line-cap').value;
let lineOutlineWidth = edge.pstyle('line-outline-width').value;
let lineOutlineColor = edge.pstyle('line-outline-color').value;
let effectiveLineOpacity = opacity * lineOpacity;
// separate arrow opacity would require arrow-opacity property
let effectiveArrowOpacity = opacity * lineOpacity;
let drawLine = ( strokeOpacity = effectiveLineOpacity) => {
if (curveStyle === 'straight-triangle') {
r.eleStrokeStyle( context, edge, strokeOpacity );
r.drawEdgeTrianglePath(
edge,
context,
rs.allpts
);
} else {
context.lineWidth = edgeWidth;
context.lineCap = lineCap;
r.eleStrokeStyle( context, edge, strokeOpacity );
r.drawEdgePath(
edge,
context,
rs.allpts,
lineStyle
);
context.lineCap = 'butt'; // reset for other drawing functions
}
};
let drawLineOutline = ( strokeOpacity = effectiveLineOpacity) => {
context.lineWidth = edgeWidth + lineOutlineWidth;
context.lineCap = lineCap;
if (lineOutlineWidth > 0) {
r.colorStrokeStyle( context, lineOutlineColor[0], lineOutlineColor[1], lineOutlineColor[2], strokeOpacity );
} else {
// do not draw any lineOutline
context.lineCap = 'butt'; // reset for other drawing functions
return;
}
if (curveStyle === 'straight-triangle') {
r.drawEdgeTrianglePath(
edge,
context,
rs.allpts
);
} else {
r.drawEdgePath(
edge,
context,
rs.allpts,
lineStyle
);
context.lineCap = 'butt'; // reset for other drawing functions
}
};
let drawOverlay = () => {
if( !shouldDrawOverlay ){ return; }
r.drawEdgeOverlay( context, edge );
};
let drawUnderlay = () => {
if( !shouldDrawOverlay ){ return; }
r.drawEdgeUnderlay( context, edge );
};
let drawArrows = ( arrowOpacity = effectiveArrowOpacity) => {
r.drawArrowheads( context, edge, arrowOpacity );
};
let drawText = () => {
r.drawElementText( context, edge, null, drawLabel );
};
context.lineJoin = 'round';
let ghost = edge.pstyle('ghost').value === 'yes';
if( ghost ){
let gx = edge.pstyle('ghost-offset-x').pfValue;
let gy = edge.pstyle('ghost-offset-y').pfValue;
let ghostOpacity = edge.pstyle('ghost-opacity').value;
let effectiveGhostOpacity = effectiveLineOpacity * ghostOpacity;
context.translate( gx, gy );
drawLine( effectiveGhostOpacity );
drawArrows( effectiveGhostOpacity );
context.translate( -gx, -gy );
} else {
drawLineOutline();
}
drawUnderlay();
drawLine();
drawArrows();
drawOverlay();
drawText();
if( shiftToOriginWithBb ){
context.translate( bb.x1, bb.y1 );
}
};
const drawEdgeOverlayUnderlay = function( overlayOrUnderlay ) {
if (!['overlay', 'underlay'].includes(overlayOrUnderlay)) {
throw new Error('Invalid state');
}
return function( context, edge ){
if( !edge.visible() ){ return; }
let opacity = edge.pstyle(`${overlayOrUnderlay}-opacity`).value;
if( opacity === 0 ){ return; }
let r = this;
let usePaths = r.usePaths();
let rs = edge._private.rscratch;
let padding = edge.pstyle(`${overlayOrUnderlay}-padding`).pfValue;
let width = 2 * padding;
let color = edge.pstyle(`${overlayOrUnderlay}-color`).value;
context.lineWidth = width;
if( rs.edgeType === 'self' && !usePaths ){
context.lineCap = 'butt';
} else {
context.lineCap = 'round';
}
r.colorStrokeStyle( context, color[0], color[1], color[2], opacity );
r.drawEdgePath(
edge,
context,
rs.allpts,
'solid'
);
};
};
CRp.drawEdgeOverlay = drawEdgeOverlayUnderlay('overlay');
CRp.drawEdgeUnderlay = drawEdgeOverlayUnderlay('underlay');
CRp.drawEdgePath = function( edge, context, pts, type ){
let rs = edge._private.rscratch;
let canvasCxt = context;
let path;
let pathCacheHit = false;
let usePaths = this.usePaths();
let lineDashPattern = edge.pstyle('line-dash-pattern').pfValue;
let lineDashOffset = edge.pstyle('line-dash-offset').pfValue;
if( usePaths ){
let pathCacheKey = pts.join( '$' );
let keyMatches = rs.pathCacheKey && rs.pathCacheKey === pathCacheKey;
if( keyMatches ){
path = context = rs.pathCache;
pathCacheHit = true;
} else {
path = context = new Path2D();
rs.pathCacheKey = pathCacheKey;
rs.pathCache = path;
}
}
if( canvasCxt.setLineDash ){ // for very outofdate browsers
switch( type ){
case 'dotted':
canvasCxt.setLineDash( [ 1, 1 ] );
break;
case 'dashed':
canvasCxt.setLineDash( lineDashPattern );
canvasCxt.lineDashOffset = lineDashOffset;
break;
case 'solid':
canvasCxt.setLineDash( [ ] );
break;
}
}
if( !pathCacheHit && !rs.badLine ){
if( context.beginPath ){ context.beginPath(); }
context.moveTo( pts[0], pts[1] );
switch( rs.edgeType ){
case 'bezier':
case 'self':
case 'compound':
case 'multibezier':
for( let i = 2; i + 3 < pts.length; i += 4 ){
context.quadraticCurveTo( pts[ i ], pts[ i + 1], pts[ i + 2], pts[ i + 3] );
}
break;
case 'straight':
case 'haystack':
for( let i = 2; i + 1 < pts.length; i += 2 ) {
context.lineTo( pts[ i ], pts[ i + 1] );
}
break;
case 'segments':
if (rs.isRound) {
for( let corner of rs.roundCorners ){
drawPreparedRoundCorner(context, corner);
}
context.lineTo( pts[ pts.length - 2 ], pts[ pts.length - 1] );
} else {
for( let i = 2; i + 1 < pts.length; i += 2 ) {
context.lineTo( pts[ i ], pts[ i + 1] );
}
}
break;
}
}
context = canvasCxt;
if( usePaths ){
context.stroke( path );
} else {
context.stroke();
}
// reset any line dashes
if( context.setLineDash ){ // for very outofdate browsers
context.setLineDash( [ ] );
}
};
CRp.drawEdgeTrianglePath = function( edge, context, pts ){
// use line stroke style for triangle fill style
context.fillStyle = context.strokeStyle;
let edgeWidth = edge.pstyle('width').pfValue;
for( let i = 0; i + 1 < pts.length; i += 2 ){
const vector = [ pts[ i + 2 ] - pts[ i ], pts[ i + 3 ] - pts[ i + 1 ] ];
const length = Math.sqrt( vector[0] * vector[0] + vector[1] * vector[1] );
const normal = [ vector[1] / length, -vector[0] / length ];
const triangleHead = [ normal[0] * edgeWidth / 2, normal[1] * edgeWidth / 2 ];
context.beginPath();
context.moveTo( pts[ i ] - triangleHead[0], pts[ i + 1 ] - triangleHead[1] );
context.lineTo( pts[ i ] + triangleHead[0], pts[ i + 1 ] + triangleHead[1] );
context.lineTo( pts[ i + 2 ], pts[ i + 3 ] );
context.closePath();
context.fill();
}
};
CRp.drawArrowheads = function( context, edge, opacity ){
let rs = edge._private.rscratch;
let isHaystack = rs.edgeType === 'haystack';
if( !isHaystack ){
this.drawArrowhead( context, edge, 'source', rs.arrowStartX, rs.arrowStartY, rs.srcArrowAngle, opacity );
}
this.drawArrowhead( context, edge, 'mid-target', rs.midX, rs.midY, rs.midtgtArrowAngle, opacity );
this.drawArrowhead( context, edge, 'mid-source', rs.midX, rs.midY, rs.midsrcArrowAngle, opacity );
if( !isHaystack ){
this.drawArrowhead( context, edge, 'target', rs.arrowEndX, rs.arrowEndY, rs.tgtArrowAngle, opacity );
}
};
CRp.drawArrowhead = function( context, edge, prefix, x, y, angle, opacity ){
if( isNaN( x ) || x == null || isNaN( y ) || y == null || isNaN( angle ) || angle == null ){ return; }
let self = this;
let arrowShape = edge.pstyle( prefix + '-arrow-shape' ).value;
if( arrowShape === 'none' ) { return; }
let arrowClearFill = edge.pstyle( prefix + '-arrow-fill' ).value === 'hollow' ? 'both' : 'filled';
let arrowFill = edge.pstyle( prefix + '-arrow-fill' ).value;
let edgeWidth = edge.pstyle( 'width' ).pfValue;
let pArrowWidth = edge.pstyle( prefix + '-arrow-width' );
let arrowWidth = pArrowWidth.value === 'match-line' ? edgeWidth : pArrowWidth.pfValue;
if (pArrowWidth.units === '%') arrowWidth *= edgeWidth;
let edgeOpacity = edge.pstyle( 'opacity' ).value;
if( opacity === undefined ){
opacity = edgeOpacity;
}
let gco = context.globalCompositeOperation;
if( opacity !== 1 || arrowFill === 'hollow' ){ // then extra clear is needed
context.globalCompositeOperation = 'destination-out';
self.colorFillStyle( context, 255, 255, 255, 1 );
self.colorStrokeStyle( context, 255, 255, 255, 1 );
self.drawArrowShape( edge, context,
arrowClearFill, edgeWidth, arrowShape, arrowWidth, x, y, angle
);
context.globalCompositeOperation = gco;
} // otherwise, the opaque arrow clears it for free :)
let color = edge.pstyle( prefix + '-arrow-color' ).value;
self.colorFillStyle( context, color[0], color[1], color[2], opacity );
self.colorStrokeStyle( context, color[0], color[1], color[2], opacity );
self.drawArrowShape( edge, context,
arrowFill, edgeWidth, arrowShape, arrowWidth, x, y, angle
);
};
CRp.drawArrowShape = function( edge, context, fill, edgeWidth, shape, shapeWidth, x, y, angle ){
let r = this;
let usePaths = this.usePaths() && shape !== 'triangle-cross';
let pathCacheHit = false;
let path;
let canvasContext = context;
let translation = { x, y };
let scale = edge.pstyle( 'arrow-scale' ).value;
let size = this.getArrowWidth( edgeWidth, scale );
let shapeImpl = r.arrowShapes[ shape ];
if( usePaths ){
let cache = r.arrowPathCache = r.arrowPathCache || [];
let key = util.hashString(shape);
let cachedPath = cache[ key ];
if( cachedPath != null ){
path = context = cachedPath;
pathCacheHit = true;
} else {
path = context = new Path2D();
cache[ key ] = path;
}
}
if( !pathCacheHit ){
if( context.beginPath ){ context.beginPath(); }
if( usePaths ){ // store in the path cache with values easily manipulated later
shapeImpl.draw( context, 1, 0, { x: 0, y: 0 }, 1 );
} else {
shapeImpl.draw( context, size, angle, translation, edgeWidth );
}
if( context.closePath ){ context.closePath(); }
}
context = canvasContext;
if( usePaths ){ // set transform to arrow position/orientation
context.translate( x, y );
context.rotate( angle );
context.scale( size, size );
}
if( fill === 'filled' || fill === 'both' ){
if( usePaths ){
context.fill( path );
} else {
context.fill();
}
}
if( fill === 'hollow' || fill === 'both' ){
context.lineWidth = shapeWidth / (usePaths ? size : 1);
context.lineJoin = 'miter';
if( usePaths ){
context.stroke( path );
} else {
context.stroke();
}
}
if( usePaths ){ // reset transform by applying inverse
context.scale( 1/size, 1/size );
context.rotate( -angle );
context.translate( -x, -y );
}
};
export default CRp;

View File

@@ -0,0 +1,218 @@
import * as math from '../../../math.mjs';
let CRp = {};
CRp.drawElement = function( context, ele, shiftToOriginWithBb, showLabel, showOverlay, showOpacity ){
let r = this;
if( ele.isNode() ){
r.drawNode( context, ele, shiftToOriginWithBb, showLabel, showOverlay, showOpacity );
} else {
r.drawEdge( context, ele, shiftToOriginWithBb, showLabel, showOverlay, showOpacity );
}
};
CRp.drawElementOverlay = function( context, ele ){
let r = this;
if( ele.isNode() ){
r.drawNodeOverlay( context, ele );
} else {
r.drawEdgeOverlay( context, ele );
}
};
CRp.drawElementUnderlay = function( context, ele ){
let r = this;
if( ele.isNode() ){
r.drawNodeUnderlay( context, ele );
} else {
r.drawEdgeUnderlay( context, ele );
}
};
CRp.drawCachedElementPortion = function( context, ele, eleTxrCache, pxRatio, lvl, reason, getRotation, getOpacity ){
let r = this;
let bb = eleTxrCache.getBoundingBox(ele);
if( bb.w === 0 || bb.h === 0 ){ return; } // ignore zero size case
let eleCache = eleTxrCache.getElement( ele, bb, pxRatio, lvl, reason );
if( eleCache != null ){
let opacity = getOpacity(r, ele);
if( opacity === 0 ){ return; }
let theta = getRotation(r, ele);
let { x1, y1, w, h } = bb;
let x, y, sx, sy, smooth;
if( theta !== 0 ){
let rotPt = eleTxrCache.getRotationPoint(ele);
sx = rotPt.x;
sy = rotPt.y;
context.translate(sx, sy);
context.rotate(theta);
smooth = r.getImgSmoothing(context);
if( !smooth ){ r.setImgSmoothing(context, true); }
let off = eleTxrCache.getRotationOffset(ele);
x = off.x;
y = off.y;
} else {
x = x1;
y = y1;
}
let oldGlobalAlpha;
if( opacity !== 1 ){
oldGlobalAlpha = context.globalAlpha;
context.globalAlpha = oldGlobalAlpha * opacity;
}
context.drawImage( eleCache.texture.canvas, eleCache.x, 0, eleCache.width, eleCache.height, x, y, w, h );
if( opacity !== 1 ){
context.globalAlpha = oldGlobalAlpha;
}
if( theta !== 0 ){
context.rotate(-theta);
context.translate(-sx, -sy);
if( !smooth ){ r.setImgSmoothing(context, false); }
}
} else {
eleTxrCache.drawElement( context, ele ); // direct draw fallback
}
};
const getZeroRotation = () => 0;
const getLabelRotation = (r, ele) => r.getTextAngle(ele, null);
const getSourceLabelRotation = (r, ele) => r.getTextAngle(ele, 'source');
const getTargetLabelRotation = (r, ele) => r.getTextAngle(ele, 'target');
const getOpacity = (r, ele) => ele.effectiveOpacity();
const getTextOpacity = (e, ele) => ele.pstyle('text-opacity').pfValue * ele.effectiveOpacity();
CRp.drawCachedElement = function( context, ele, pxRatio, extent, lvl, requestHighQuality ){
let r = this;
let { eleTxrCache, lblTxrCache, slbTxrCache, tlbTxrCache } = r.data;
let bb = ele.boundingBox();
let reason = requestHighQuality === true ? eleTxrCache.reasons.highQuality : null;
if( bb.w === 0 || bb.h === 0 || !ele.visible() ){ return; }
if( !extent || math.boundingBoxesIntersect( bb, extent ) ){
let isEdge = ele.isEdge();
let badLine = ele.element()._private.rscratch.badLine;
r.drawElementUnderlay( context, ele );
r.drawCachedElementPortion( context, ele, eleTxrCache, pxRatio, lvl, reason, getZeroRotation, getOpacity );
if( !isEdge || !badLine ){
r.drawCachedElementPortion( context, ele, lblTxrCache, pxRatio, lvl, reason, getLabelRotation, getTextOpacity );
}
if( isEdge && !badLine ){
r.drawCachedElementPortion( context, ele, slbTxrCache, pxRatio, lvl, reason, getSourceLabelRotation, getTextOpacity );
r.drawCachedElementPortion( context, ele, tlbTxrCache, pxRatio, lvl, reason, getTargetLabelRotation, getTextOpacity );
}
r.drawElementOverlay( context, ele );
}
};
CRp.drawElements = function( context, eles ){
let r = this;
for( let i = 0; i < eles.length; i++ ){
let ele = eles[ i ];
r.drawElement( context, ele );
}
};
CRp.drawCachedElements = function( context, eles, pxRatio, extent ){
let r = this;
for( let i = 0; i < eles.length; i++ ){
let ele = eles[ i ];
r.drawCachedElement( context, ele, pxRatio, extent );
}
};
CRp.drawCachedNodes = function( context, eles, pxRatio, extent ){
let r = this;
for( let i = 0; i < eles.length; i++ ){
let ele = eles[ i ];
if( !ele.isNode() ){ continue; }
r.drawCachedElement( context, ele, pxRatio, extent );
}
};
CRp.drawLayeredElements = function( context, eles, pxRatio, extent ){
let r = this;
let layers = r.data.lyrTxrCache.getLayers( eles, pxRatio );
if( layers ){
for( let i = 0; i < layers.length; i++ ){
let layer = layers[i];
let bb = layer.bb;
if( bb.w === 0 || bb.h === 0 ){ continue; }
context.drawImage( layer.canvas, bb.x1, bb.y1, bb.w, bb.h );
}
} else { // fall back on plain caching if no layers
r.drawCachedElements( context, eles, pxRatio, extent );
}
};
if( process.env.NODE_ENV !== 'production' ){
CRp.drawDebugPoints = function( context, eles ){
let draw = function( x, y, color ){
context.fillStyle = color;
context.fillRect( x - 1, y - 1, 3, 3 );
};
for( let i = 0; i < eles.length; i++ ){
let ele = eles[i];
let rs = ele._private.rscratch;
if( ele.isNode() ){
let p = ele.position();
draw( rs.labelX, rs.labelY, 'red' );
draw( p.x, p.y, 'magenta' );
} else {
let pts = rs.allpts;
for( let j = 0; j + 1 < pts.length; j += 2 ){
let x = pts[ j ];
let y = pts[ j + 1 ];
draw( x, y, 'cyan' );
}
draw( rs.midX, rs.midY, 'yellow' );
}
}
};
}
export default CRp;

View File

@@ -0,0 +1,804 @@
/* global Path2D */
import * as is from '../../../is.mjs';
import {expandPolygon, joinLines} from '../../../math.mjs';
import * as util from '../../../util/index.mjs';
import * as round from "../../../round.mjs";
import * as math from "../../../math.mjs";
let CRp = {};
CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, shouldDrawOverlay = true, shouldDrawOpacity = true ){
let r = this;
let nodeWidth, nodeHeight;
let _p = node._private;
let rs = _p.rscratch;
let pos = node.position();
if( !is.number( pos.x ) || !is.number( pos.y ) ){
return; // can't draw node with undefined position
}
if( shouldDrawOpacity && !node.visible() ){ return; }
let eleOpacity = shouldDrawOpacity ? node.effectiveOpacity() : 1;
let usePaths = r.usePaths();
let path;
let pathCacheHit = false;
let padding = node.padding();
nodeWidth = node.width() + 2 * padding;
nodeHeight = node.height() + 2 * padding;
//
// setup shift
let bb;
if( shiftToOriginWithBb ){
bb = shiftToOriginWithBb;
context.translate( -bb.x1, -bb.y1 );
}
//
// load bg image
let bgImgProp = node.pstyle( 'background-image' );
let urls = bgImgProp.value;
let urlDefined = new Array( urls.length );
let image = new Array( urls.length );
let numImages = 0;
for( let i = 0; i < urls.length; i++ ){
let url = urls[i];
let defd = urlDefined[i] = url != null && url !== 'none';
if( defd ){
let bgImgCrossOrigin = node.cy().style().getIndexedStyle(node, 'background-image-crossorigin', 'value', i);
numImages++;
// get image, and if not loaded then ask to redraw when later loaded
image[i] = r.getCachedImage( url, bgImgCrossOrigin, function(){
_p.backgroundTimestamp = Date.now();
node.emitAndNotify('background');
} );
}
}
//
// setup styles
let darkness = node.pstyle('background-blacken').value;
let borderWidth = node.pstyle('border-width').pfValue;
let bgOpacity = node.pstyle('background-opacity').value * eleOpacity;
let borderColor = node.pstyle('border-color').value;
let borderStyle = node.pstyle('border-style').value;
let borderJoin = node.pstyle('border-join').value;
let borderCap = node.pstyle('border-cap').value;
let borderPosition = node.pstyle('border-position').value;
let borderPattern = node.pstyle('border-dash-pattern').pfValue;
let borderOffset = node.pstyle('border-dash-offset').pfValue;
let borderOpacity = node.pstyle('border-opacity').value * eleOpacity;
let outlineWidth = node.pstyle('outline-width').pfValue;
let outlineColor = node.pstyle('outline-color').value;
let outlineStyle = node.pstyle('outline-style').value;
let outlineOpacity = node.pstyle('outline-opacity').value * eleOpacity;
let outlineOffset = node.pstyle('outline-offset').value;
let cornerRadius = node.pstyle('corner-radius').value;
if (cornerRadius !== 'auto') cornerRadius = node.pstyle('corner-radius').pfValue;
let setupShapeColor = ( bgOpy = bgOpacity ) => {
r.eleFillStyle( context, node, bgOpy );
};
let setupBorderColor = ( bdrOpy = borderOpacity ) => {
r.colorStrokeStyle( context, borderColor[0], borderColor[1], borderColor[2], bdrOpy );
};
let setupOutlineColor = ( otlnOpy = outlineOpacity ) => {
r.colorStrokeStyle( context, outlineColor[0], outlineColor[1], outlineColor[2], otlnOpy );
};
//
// setup shape
let getPath = (width, height, shape, points) => {
let pathCache = r.nodePathCache = r.nodePathCache || [];
let key = util.hashStrings(
shape === 'polygon' ? shape + ',' + points.join(',') : shape,
'' + height,
'' + width,
'' + cornerRadius
);
let cachedPath = pathCache[ key ];
let path;
let cacheHit = false;
if( cachedPath != null ){
path = cachedPath;
cacheHit = true;
rs.pathCache = path;
} else {
path = new Path2D();
pathCache[ key ] = rs.pathCache = path;
}
return {
path,
cacheHit
};
};
let styleShape = node.pstyle('shape').strValue;
let shapePts = node.pstyle('shape-polygon-points').pfValue;
if( usePaths ){
context.translate( pos.x, pos.y );
const shapePath = getPath(nodeWidth, nodeHeight, styleShape, shapePts);
path = shapePath.path;
pathCacheHit = shapePath.cacheHit;
}
let drawShape = () => {
if( !pathCacheHit ){
let npos = pos;
if( usePaths ){
npos = {
x: 0,
y: 0
};
}
r.nodeShapes[ r.getNodeShape( node ) ].draw(
( path || context ),
npos.x,
npos.y,
nodeWidth,
nodeHeight, cornerRadius, rs );
}
if( usePaths ){
context.fill( path );
} else {
context.fill();
}
};
let drawImages = ( nodeOpacity = eleOpacity, inside = true ) => {
let prevBging = _p.backgrounding;
let totalCompleted = 0;
for( let i = 0; i < image.length; i++ ){
const bgContainment = node.cy().style().getIndexedStyle(node, 'background-image-containment', 'value', i);
if( inside && bgContainment === 'over' || !inside && bgContainment === 'inside' ){
totalCompleted++;
continue;
}
if( urlDefined[i] && image[i].complete && !image[i].error ){
totalCompleted++;
r.drawInscribedImage( context, image[i], node, i, nodeOpacity );
}
}
_p.backgrounding = !(totalCompleted === numImages);
if( prevBging !== _p.backgrounding ){ // update style b/c :backgrounding state changed
node.updateStyle( false );
}
};
let drawPie = ( redrawShape = false, pieOpacity = eleOpacity ) => {
if( r.hasPie( node ) ){
r.drawPie( context, node, pieOpacity );
// redraw/restore path if steps after pie need it
if( redrawShape ){
if( !usePaths ){
r.nodeShapes[ r.getNodeShape( node ) ].draw(
context,
pos.x,
pos.y,
nodeWidth,
nodeHeight, cornerRadius, rs );
}
}
}
};
let drawStripe = (redrawShape = false, stripeOpacity = eleOpacity) => {
if( r.hasStripe( node ) ){
context.save();
if (usePaths) {
context.clip( rs.pathCache );
} else {
r.nodeShapes[ r.getNodeShape( node ) ].draw(
context,
pos.x,
pos.y,
nodeWidth,
nodeHeight, cornerRadius, rs );
context.clip();
}
r.drawStripe( context, node, stripeOpacity );
context.restore();
// redraw/restore path if steps after stripes need it
if( redrawShape ){
if( !usePaths ){
r.nodeShapes[ r.getNodeShape( node ) ].draw(
context,
pos.x,
pos.y,
nodeWidth,
nodeHeight, cornerRadius, rs );
}
}
}
};
let darken = ( darkenOpacity = eleOpacity ) => {
let opacity = ( darkness > 0 ? darkness : -darkness ) * darkenOpacity;
let c = darkness > 0 ? 0 : 255;
if( darkness !== 0 ){
r.colorFillStyle( context, c, c, c, opacity );
if( usePaths ){
context.fill( path );
} else {
context.fill();
}
}
};
let drawBorder = () => {
if( borderWidth > 0 ){
context.lineWidth = borderWidth;
context.lineCap = borderCap;
context.lineJoin = borderJoin;
if( context.setLineDash ){ // for very outofdate browsers
switch( borderStyle ){
case 'dotted':
context.setLineDash( [ 1, 1 ] );
break;
case 'dashed':
context.setLineDash( borderPattern );
context.lineDashOffset = borderOffset;
break;
case 'solid':
case 'double':
context.setLineDash( [ ] );
break;
}
}
if ( borderPosition !== 'center') {
context.save();
context.lineWidth *= 2;
if (borderPosition === 'inside') {
usePaths ? context.clip(path) : context.clip();
} else {
const region = new Path2D();
region.rect(
-nodeWidth / 2 - borderWidth,
-nodeHeight / 2 - borderWidth,
nodeWidth + 2 * borderWidth,
nodeHeight + 2 * borderWidth
);
region.addPath(path);
context.clip(region, 'evenodd');
}
usePaths ? context.stroke(path) : context.stroke();
context.restore();
} else {
usePaths ? context.stroke(path) : context.stroke();
}
if( borderStyle === 'double' ){
context.lineWidth = borderWidth / 3;
let gco = context.globalCompositeOperation;
context.globalCompositeOperation = 'destination-out';
if( usePaths ){
context.stroke( path );
} else {
context.stroke();
}
context.globalCompositeOperation = gco;
}
// reset in case we changed the border style
if( context.setLineDash ){ // for very outofdate browsers
context.setLineDash( [ ] );
}
}
};
let drawOutline = () => {
if( outlineWidth > 0 ){
context.lineWidth = outlineWidth;
context.lineCap = 'butt';
if( context.setLineDash ){ // for very outofdate browsers
switch( outlineStyle ){
case 'dotted':
context.setLineDash( [ 1, 1 ] );
break;
case 'dashed':
context.setLineDash( [ 4, 2 ] );
break;
case 'solid':
case 'double':
context.setLineDash( [ ] );
break;
}
}
let npos = pos;
if( usePaths ){
npos = {
x: 0,
y: 0
};
}
let shape = r.getNodeShape( node );
let bWidth = borderWidth;
if( borderPosition === 'inside' ) bWidth = 0;
if( borderPosition === 'outside' ) bWidth *= 2;
let scaleX = (nodeWidth + bWidth + (outlineWidth + outlineOffset)) / nodeWidth;
let scaleY = (nodeHeight + bWidth + (outlineWidth + outlineOffset)) / nodeHeight;
let sWidth = nodeWidth * scaleX;
let sHeight = nodeHeight * scaleY;
let points = r.nodeShapes[ shape ].points;
let path;
if (usePaths) {
let outlinePath = getPath(sWidth, sHeight, shape, points);
path = outlinePath.path;
}
// draw the outline path, either by using expanded points or by scaling
// the dimensions, depending on shape
if (shape === "ellipse") {
r.drawEllipsePath(path || context, npos.x, npos.y, sWidth, sHeight);
} else if ([
'round-diamond', 'round-heptagon', 'round-hexagon', 'round-octagon',
'round-pentagon', 'round-polygon', 'round-triangle', 'round-tag'
].includes(shape)) {
let sMult = 0;
let offsetX = 0;
let offsetY = 0;
if (shape === 'round-diamond') {
sMult = (bWidth + outlineOffset + outlineWidth) * 1.4;
} else if (shape === 'round-heptagon') {
sMult = (bWidth + outlineOffset + outlineWidth) * 1.075;
offsetY = -(bWidth/2 + outlineOffset + outlineWidth) / 35;
} else if (shape === 'round-hexagon') {
sMult = (bWidth + outlineOffset + outlineWidth) * 1.12;
} else if (shape === 'round-pentagon') {
sMult = (bWidth + outlineOffset + outlineWidth) * 1.13;
offsetY = -(bWidth/2 + outlineOffset + outlineWidth) / 15;
} else if (shape === 'round-tag') {
sMult = (bWidth + outlineOffset + outlineWidth) * 1.12;
offsetX = (bWidth/2 + outlineWidth + outlineOffset) * .07;
} else if (shape === 'round-triangle') {
sMult = (bWidth + outlineOffset + outlineWidth) * (Math.PI/2);
offsetY = -(bWidth + outlineOffset/2 + outlineWidth) / Math.PI;
}
if (sMult !== 0) {
scaleX = (nodeWidth + sMult)/nodeWidth;
sWidth = nodeWidth * scaleX;
if ( ! ['round-hexagon', 'round-tag'].includes(shape) ) {
scaleY = (nodeHeight + sMult)/nodeHeight;
sHeight = nodeHeight * scaleY;
}
}
cornerRadius = cornerRadius === 'auto' ? math.getRoundPolygonRadius( sWidth, sHeight ) : cornerRadius;
const halfW = sWidth / 2;
const halfH = sHeight / 2;
const radius = cornerRadius + ( bWidth + outlineWidth + outlineOffset ) / 2;
const p = new Array( points.length / 2 );
const corners = new Array( points.length / 2 );
for ( let i = 0; i < points.length / 2; i++ ){
p[i] = {
x: npos.x + offsetX + halfW * points[ i * 2 ],
y: npos.y + offsetY + halfH * points[ i * 2 + 1 ]
};
}
let i, p1, p2, p3, len = p.length;
p1 = p[ len - 1 ];
// for each point
for( i = 0; i < len; i++ ){
p2 = p[ (i) % len ];
p3 = p[ (i + 1) % len ];
corners[ i ] = round.getRoundCorner( p1, p2, p3, radius );
p1 = p2;
p2 = p3;
}
r.drawRoundPolygonPath(path || context, npos.x + offsetX, npos.y + offsetY, nodeWidth * scaleX, nodeHeight * scaleY, points, corners );
} else if (['roundrectangle', 'round-rectangle'].includes(shape)) {
cornerRadius = cornerRadius === 'auto' ? math.getRoundRectangleRadius( sWidth, sHeight ) : cornerRadius;
r.drawRoundRectanglePath(path || context, npos.x, npos.y, sWidth, sHeight, cornerRadius + ( bWidth + outlineWidth + outlineOffset ) / 2 );
} else if (['cutrectangle', 'cut-rectangle'].includes(shape)) {
cornerRadius = cornerRadius === 'auto' ? math.getCutRectangleCornerLength( sWidth, sHeight ) : cornerRadius;
r.drawCutRectanglePath(path || context, npos.x, npos.y, sWidth, sHeight, null ,cornerRadius + ( bWidth + outlineWidth + outlineOffset ) / 4 );
} else if (['bottomroundrectangle', 'bottom-round-rectangle'].includes(shape)) {
cornerRadius = cornerRadius === 'auto' ? math.getRoundRectangleRadius( sWidth, sHeight ) : cornerRadius;
r.drawBottomRoundRectanglePath(path || context, npos.x, npos.y, sWidth, sHeight, cornerRadius + ( bWidth + outlineWidth + outlineOffset ) / 2 );
} else if (shape === "barrel") {
r.drawBarrelPath(path || context, npos.x, npos.y, sWidth, sHeight);
} else if (shape.startsWith("polygon") || ['rhomboid', 'right-rhomboid', 'round-tag', 'tag', 'vee'].includes(shape)) {
let pad = (bWidth + outlineWidth + outlineOffset) / nodeWidth;
points = joinLines(expandPolygon(points, pad));
r.drawPolygonPath(path || context, npos.x, npos.y, nodeWidth, nodeHeight, points);
} else {
let pad = (bWidth + outlineWidth + outlineOffset) / nodeWidth;
points = joinLines(expandPolygon(points, -pad));
r.drawPolygonPath(path || context, npos.x, npos.y, nodeWidth, nodeHeight, points);
}
if( usePaths ){
context.stroke( path );
} else {
context.stroke();
}
if( outlineStyle === 'double' ){
context.lineWidth = bWidth / 3;
let gco = context.globalCompositeOperation;
context.globalCompositeOperation = 'destination-out';
if( usePaths ){
context.stroke( path );
} else {
context.stroke();
}
context.globalCompositeOperation = gco;
}
// reset in case we changed the border style
if( context.setLineDash ){ // for very outofdate browsers
context.setLineDash( [ ] );
}
}
};
let drawOverlay = () => {
if( shouldDrawOverlay ){
r.drawNodeOverlay( context, node, pos, nodeWidth, nodeHeight );
}
};
let drawUnderlay = () => {
if( shouldDrawOverlay ){
r.drawNodeUnderlay( context, node, pos, nodeWidth, nodeHeight );
}
};
let drawText = () => {
r.drawElementText( context, node, null, drawLabel );
};
let ghost = node.pstyle('ghost').value === 'yes';
if( ghost ){
let gx = node.pstyle('ghost-offset-x').pfValue;
let gy = node.pstyle('ghost-offset-y').pfValue;
let ghostOpacity = node.pstyle('ghost-opacity').value;
let effGhostOpacity = ghostOpacity * eleOpacity;
context.translate( gx, gy );
setupOutlineColor();
drawOutline();
setupShapeColor( ghostOpacity * bgOpacity );
drawShape();
drawImages( effGhostOpacity, true );
setupBorderColor( ghostOpacity * borderOpacity );
drawBorder();
drawPie( darkness !== 0 || borderWidth !== 0 );
drawStripe( darkness !== 0 || borderWidth !== 0 );
drawImages( effGhostOpacity, false );
darken( effGhostOpacity );
context.translate( -gx, -gy );
}
if( usePaths ){
context.translate( -pos.x, -pos.y );
}
drawUnderlay();
if( usePaths ){
context.translate( pos.x, pos.y );
}
setupOutlineColor();
drawOutline();
setupShapeColor();
drawShape();
drawImages(eleOpacity, true);
setupBorderColor();
drawBorder();
drawPie( darkness !== 0 || borderWidth !== 0 );
drawStripe( darkness !== 0 || borderWidth !== 0 );
drawImages(eleOpacity, false);
darken();
if( usePaths ){
context.translate( -pos.x, -pos.y );
}
drawText();
drawOverlay();
//
// clean up shift
if( shiftToOriginWithBb ){
context.translate( bb.x1, bb.y1 );
}
};
const drawNodeOverlayUnderlay = function( overlayOrUnderlay ) {
if (!['overlay', 'underlay'].includes(overlayOrUnderlay)) {
throw new Error('Invalid state');
}
return function( context, node, pos, nodeWidth, nodeHeight ){
let r = this;
if( !node.visible() ){ return; }
let padding = node.pstyle( `${overlayOrUnderlay}-padding` ).pfValue;
let opacity = node.pstyle( `${overlayOrUnderlay}-opacity` ).value;
let color = node.pstyle( `${overlayOrUnderlay}-color` ).value;
let shape = node.pstyle( `${overlayOrUnderlay}-shape` ).value;
let radius = node.pstyle( `${overlayOrUnderlay}-corner-radius` ).value;
if( opacity > 0 ){
pos = pos || node.position();
if( nodeWidth == null || nodeHeight == null ){
let padding = node.padding();
nodeWidth = node.width() + 2 * padding;
nodeHeight = node.height() + 2 * padding;
}
r.colorFillStyle( context, color[0], color[1], color[2], opacity );
r.nodeShapes[shape].draw(
context,
pos.x,
pos.y,
nodeWidth + padding * 2,
nodeHeight + padding * 2,
radius
);
context.fill();
}
};
};
CRp.drawNodeOverlay = drawNodeOverlayUnderlay('overlay');
CRp.drawNodeUnderlay = drawNodeOverlayUnderlay('underlay');
// does the node have at least one pie piece?
CRp.hasPie = function( node ){
node = node[0]; // ensure ele ref
return node._private.hasPie;
};
CRp.hasStripe = function( node ){
node = node[0]; // ensure ele ref
return node._private.hasStripe;
};
CRp.drawPie = function( context, node, nodeOpacity, pos ){
node = node[0]; // ensure ele ref
pos = pos || node.position();
let cyStyle = node.cy().style();
let pieSize = node.pstyle( 'pie-size' );
let hole = node.pstyle('pie-hole');
let overallStartAngle = node.pstyle('pie-start-angle').pfValue;
let x = pos.x;
let y = pos.y;
let nodeW = node.width();
let nodeH = node.height();
let radius = Math.min( nodeW, nodeH ) / 2; // must fit in node
let holeRadius;
let lastPercent = 0; // what % to continue drawing pie slices from on [0, 1]
let usePaths = this.usePaths();
if( usePaths ){
x = 0;
y = 0;
}
if( pieSize.units === '%' ){
radius = radius * pieSize.pfValue;
} else if( pieSize.pfValue !== undefined ){
radius = pieSize.pfValue / 2; // diameter in pixels => radius
}
if (hole.units === '%') {
holeRadius = radius * hole.pfValue;
} else if (hole.pfValue !== undefined) {
holeRadius = hole.pfValue / 2; // diameter in pixels => radius
}
if (holeRadius >= radius) {
return; // the pie would be invisible anyway
}
for( let i = 1; i <= cyStyle.pieBackgroundN; i++ ){ // 1..N
let size = node.pstyle( 'pie-' + i + '-background-size' ).value;
let color = node.pstyle( 'pie-' + i + '-background-color' ).value;
let opacity = node.pstyle( 'pie-' + i + '-background-opacity' ).value * nodeOpacity;
let percent = size / 100; // map integer range [0, 100] to [0, 1]
// percent can't push beyond 1
if( percent + lastPercent > 1 ){
percent = 1 - lastPercent;
}
let angleStart = (1.5 * Math.PI) + (2 * Math.PI * lastPercent); // start at 12 o'clock and go clockwise
angleStart += overallStartAngle; // shift by the overall pie start angle
let angleDelta = 2 * Math.PI * percent;
let angleEnd = angleStart + angleDelta;
// ignore if
// - zero size
// - we're already beyond the full circle
// - adding the current slice would go beyond the full circle
if( size === 0 || lastPercent >= 1 || lastPercent + percent > 1 ){
continue;
}
if (holeRadius === 0) { // make a pie slice
context.beginPath();
context.moveTo( x, y );
context.arc( x, y, radius, angleStart, angleEnd );
context.closePath();
} else { // make a pie slice that's like the above but with a hole in the middle
context.beginPath();
context.arc(x, y, radius, angleStart, angleEnd);
context.arc(x, y, holeRadius, angleEnd, angleStart, true); // true for anticlockwise
context.closePath();
}
this.colorFillStyle( context, color[0], color[1], color[2], opacity );
context.fill();
lastPercent += percent;
}
};
CRp.drawStripe = function( context, node, nodeOpacity, pos ){
node = node[0]; // ensure ele ref
pos = pos || node.position();
let cyStyle = node.cy().style();
let x = pos.x;
let y = pos.y;
let nodeW = node.width();
let nodeH = node.height();
let lastPercent = 0; // what % to continue drawing pie slices from on [0, 1]
let usePaths = this.usePaths();
context.save();
let direction = node.pstyle('stripe-direction').value;
let stripeSize = node.pstyle('stripe-size');
switch (direction) {
case 'vertical':
break; // default
case 'righward':
context.rotate(-Math.PI / 2);
break;
}
let stripeW = nodeW;
let stripeH = nodeH;
if( stripeSize.units === '%' ){
stripeW = stripeW * stripeSize.pfValue;
stripeH = stripeH * stripeSize.pfValue;
} else if( stripeSize.pfValue !== undefined ){
stripeW = stripeSize.pfValue;
stripeH = stripeSize.pfValue;
}
if( usePaths ){
x = 0;
y = 0;
}
// shift up from the centre of the node to the top-left corner
y -= stripeW / 2;
x -= stripeH / 2;
for( let i = 1; i <= cyStyle.stripeBackgroundN; i++ ){ // 1..N
let size = node.pstyle( 'stripe-' + i + '-background-size' ).value;
let color = node.pstyle( 'stripe-' + i + '-background-color' ).value;
let opacity = node.pstyle( 'stripe-' + i + '-background-opacity' ).value * nodeOpacity;
let percent = size / 100; // map integer range [0, 100] to [0, 1]
// percent can't push beyond 1
if( percent + lastPercent > 1 ){
percent = 1 - lastPercent;
}
// ignore if
// - zero size
// - we're already beyond the full chart
// - adding the current slice would go beyond the full chart
if( size === 0 || lastPercent >= 1 || lastPercent + percent > 1 ){
continue;
}
// draw rect for the current stripe
context.beginPath();
context.rect( x, y + stripeH * lastPercent, stripeW, stripeH * percent );
context.closePath();
this.colorFillStyle( context, color[0], color[1], color[2], opacity );
context.fill();
lastPercent += percent;
}
context.restore();
};
export default CRp;

View File

@@ -0,0 +1,555 @@
import * as math from '../../../math.mjs';
import { trueify, falsify, removeFromArray, clearArray, MAX_INT, assign, defaults } from '../../../util/index.mjs';
import Heap from '../../../heap.mjs';
import defs from './texture-cache-defs.mjs';
import ElementTextureCacheLookup from './ele-texture-cache-lookup.mjs';
const minTxrH = 25; // the size of the texture cache for small height eles (special case)
const txrStepH = 50; // the min size of the regular cache, and the size it increases with each step up
const minLvl = -4; // when scaling smaller than that we don't need to re-render
export const maxLvl = 3; // when larger than this scale just render directly (caching is not helpful)
export const maxZoom = 7.99; // beyond this zoom level, layered textures are not used
const eleTxrSpacing = 8; // spacing between elements on textures to avoid blitting overlaps
const defTxrWidth = 1024; // default/minimum texture width
const maxTxrW = 1024; // the maximum width of a texture
const maxTxrH = 1024; // the maximum height of a texture
const minUtility = 0.2; // if usage of texture is less than this, it is retired
const maxFullness = 0.8; // fullness of texture after which queue removal is checked
const maxFullnessChecks = 10; // dequeued after this many checks
const deqCost = 0.15; // % of add'l rendering cost allowed for dequeuing ele caches each frame
const deqAvgCost = 0.1; // % of add'l rendering cost compared to average overall redraw time
const deqNoDrawCost = 0.9; // % of avg frame time that can be used for dequeueing when not drawing
const deqFastCost = 0.9; // % of frame time to be used when >60fps
const deqRedrawThreshold = 100; // time to batch redraws together from dequeueing to allow more dequeueing calcs to happen in the meanwhile
const maxDeqSize = 1; // number of eles to dequeue and render at higher texture in each batch
const getTxrReasons = {
dequeue: 'dequeue',
downscale: 'downscale',
highQuality: 'highQuality'
};
const initDefaults = defaults({
getKey: null,
doesEleInvalidateKey: falsify,
drawElement: null,
getBoundingBox: null,
getRotationPoint: null,
getRotationOffset: null,
isVisible: trueify,
allowEdgeTxrCaching: true,
allowParentTxrCaching: true
});
const ElementTextureCache = function( renderer, initOptions ){
let self = this;
self.renderer = renderer;
self.onDequeues = [];
let opts = initDefaults(initOptions);
assign(self, opts);
self.lookup = new ElementTextureCacheLookup(opts.getKey, opts.doesEleInvalidateKey);
self.setupDequeueing();
};
const ETCp = ElementTextureCache.prototype;
ETCp.reasons = getTxrReasons;
// the list of textures in which new subtextures for elements can be placed
ETCp.getTextureQueue = function( txrH ){
let self = this;
self.eleImgCaches = self.eleImgCaches || {};
return ( self.eleImgCaches[ txrH ] = self.eleImgCaches[ txrH ] || [] );
};
// the list of usused textures which can be recycled (in use in texture queue)
ETCp.getRetiredTextureQueue = function( txrH ){
let self = this;
let rtxtrQs = self.eleImgCaches.retired = self.eleImgCaches.retired || {};
let rtxtrQ = rtxtrQs[ txrH ] = rtxtrQs[ txrH ] || [];
return rtxtrQ;
};
// queue of element draw requests at different scale levels
ETCp.getElementQueue = function(){
let self = this;
let q = self.eleCacheQueue = self.eleCacheQueue || new Heap(function( a, b ){
return b.reqs - a.reqs;
});
return q;
};
// queue of element draw requests at different scale levels (element id lookup)
ETCp.getElementKeyToQueue = function(){
let self = this;
let k2q = self.eleKeyToCacheQueue = self.eleKeyToCacheQueue || {};
return k2q;
};
ETCp.getElement = function( ele, bb, pxRatio, lvl, reason ){
let self = this;
let r = this.renderer;
let zoom = r.cy.zoom();
let lookup = this.lookup;
if( !bb || bb.w === 0 || bb.h === 0 || isNaN(bb.w) || isNaN(bb.h) || !ele.visible() || ele.removed() ){ return null; }
if(
( !self.allowEdgeTxrCaching && ele.isEdge() )
|| ( !self.allowParentTxrCaching && ele.isParent() )
){
return null;
}
if( lvl == null ){
lvl = Math.ceil( math.log2( zoom * pxRatio ) );
}
if( lvl < minLvl ){
lvl = minLvl;
} else if( zoom >= maxZoom || lvl > maxLvl ){
return null;
}
let scale = Math.pow( 2, lvl );
let eleScaledH = bb.h * scale;
let eleScaledW = bb.w * scale;
let scaledLabelShown = r.eleTextBiggerThanMin( ele, scale );
if( !this.isVisible(ele, scaledLabelShown) ){ return null; }
let eleCache = lookup.get( ele, lvl );
// if this get was on an unused/invalidated cache, then restore the texture usage metric
if( eleCache && eleCache.invalidated ){
eleCache.invalidated = false;
eleCache.texture.invalidatedWidth -= eleCache.width;
}
if( eleCache ){
return eleCache;
}
let txrH; // which texture height this ele belongs to
if( eleScaledH <= minTxrH ){
txrH = minTxrH;
} else if( eleScaledH <= txrStepH ){
txrH = txrStepH;
} else {
txrH = Math.ceil( eleScaledH / txrStepH ) * txrStepH;
}
if( eleScaledH > maxTxrH || eleScaledW > maxTxrW ){
return null; // caching large elements is not efficient
}
let txrQ = self.getTextureQueue( txrH );
// first try the second last one in case it has space at the end
let txr = txrQ[ txrQ.length - 2 ];
let addNewTxr = function(){
return self.recycleTexture( txrH, eleScaledW ) || self.addTexture( txrH, eleScaledW );
};
// try the last one if there is no second last one
if( !txr ){
txr = txrQ[ txrQ.length - 1 ];
}
// if the last one doesn't exist, we need a first one
if( !txr ){
txr = addNewTxr();
}
// if there's no room in the current texture, we need a new one
if( txr.width - txr.usedWidth < eleScaledW ){
txr = addNewTxr();
}
let scalableFrom = function( otherCache ){
return otherCache && otherCache.scaledLabelShown === scaledLabelShown;
};
let deqing = reason && reason === getTxrReasons.dequeue;
let highQualityReq = reason && reason === getTxrReasons.highQuality;
let downscaleReq = reason && reason === getTxrReasons.downscale;
let higherCache; // the nearest cache with a higher level
for( let l = lvl + 1; l <= maxLvl; l++ ){
let c = lookup.get( ele, l );
if( c ){ higherCache = c; break; }
}
let oneUpCache = higherCache && higherCache.level === lvl + 1 ? higherCache : null;
let downscale = function(){
txr.context.drawImage(
oneUpCache.texture.canvas,
oneUpCache.x, 0,
oneUpCache.width, oneUpCache.height,
txr.usedWidth, 0,
eleScaledW, eleScaledH
);
};
// reset ele area in texture
txr.context.setTransform( 1, 0, 0, 1, 0, 0 );
txr.context.clearRect( txr.usedWidth, 0, eleScaledW, txrH );
if( scalableFrom(oneUpCache) ){
// then we can relatively cheaply rescale the existing image w/o rerendering
downscale();
} else if( scalableFrom(higherCache) ){
// then use the higher cache for now and queue the next level down
// to cheaply scale towards the smaller level
if( highQualityReq ){
for( let l = higherCache.level; l > lvl; l-- ){
oneUpCache = self.getElement( ele, bb, pxRatio, l, getTxrReasons.downscale );
}
downscale();
} else {
self.queueElement( ele, higherCache.level - 1 );
return higherCache;
}
} else {
let lowerCache; // the nearest cache with a lower level
if( !deqing && !highQualityReq && !downscaleReq ){
for( let l = lvl - 1; l >= minLvl; l-- ){
let c = lookup.get( ele, l );
if( c ){ lowerCache = c; break; }
}
}
if( scalableFrom(lowerCache) ){
// then use the lower quality cache for now and queue the better one for later
self.queueElement( ele, lvl );
return lowerCache;
}
txr.context.translate( txr.usedWidth, 0 );
txr.context.scale( scale, scale );
this.drawElement( txr.context, ele, bb, scaledLabelShown, false );
txr.context.scale( 1/scale, 1/scale );
txr.context.translate( -txr.usedWidth, 0 );
}
eleCache = {
x: txr.usedWidth,
texture: txr,
level: lvl,
scale: scale,
width: eleScaledW,
height: eleScaledH,
scaledLabelShown: scaledLabelShown
};
txr.usedWidth += Math.ceil( eleScaledW + eleTxrSpacing );
txr.eleCaches.push( eleCache );
lookup.set( ele, lvl, eleCache );
self.checkTextureFullness( txr );
return eleCache;
};
ETCp.invalidateElements = function( eles ){
for( let i = 0; i < eles.length; i++ ){
this.invalidateElement(eles[i]);
}
};
ETCp.invalidateElement = function( ele ){
let self = this;
let lookup = self.lookup;
let caches = [];
let invalid = lookup.isInvalid(ele);
if( !invalid ){
return; // override the invalidation request if the element key has not changed
}
for( let lvl = minLvl; lvl <= maxLvl; lvl++ ){
let cache = lookup.getForCachedKey( ele, lvl );
if( cache ){
caches.push( cache );
}
}
let noOtherElesUseCache = lookup.invalidate(ele);
if( noOtherElesUseCache ){
for( let i = 0; i < caches.length; i++ ){
let cache = caches[i];
let txr = cache.texture;
// remove space from the texture it belongs to
txr.invalidatedWidth += cache.width;
// mark the cache as invalidated
cache.invalidated = true;
// retire the texture if its utility is low
self.checkTextureUtility( txr );
}
}
// remove from queue since the old req was for the old state
self.removeFromQueue( ele );
};
ETCp.checkTextureUtility = function( txr ){
// invalidate all entries in the cache if the cache size is small
if( txr.invalidatedWidth >= minUtility * txr.width ){
this.retireTexture( txr );
}
};
ETCp.checkTextureFullness = function( txr ){
// if texture has been mostly filled and passed over several times, remove
// it from the queue so we don't need to waste time looking at it to put new things
let self = this;
let txrQ = self.getTextureQueue( txr.height );
if( txr.usedWidth / txr.width > maxFullness && txr.fullnessChecks >= maxFullnessChecks ){
removeFromArray( txrQ, txr );
} else {
txr.fullnessChecks++;
}
};
ETCp.retireTexture = function( txr ){
let self = this;
let txrH = txr.height;
let txrQ = self.getTextureQueue( txrH );
let lookup = this.lookup;
// retire the texture from the active / searchable queue:
removeFromArray( txrQ, txr );
txr.retired = true;
// remove the refs from the eles to the caches:
let eleCaches = txr.eleCaches;
for( let i = 0; i < eleCaches.length; i++ ){
let eleCache = eleCaches[i];
lookup.deleteCache( eleCache.key, eleCache.level );
}
clearArray( eleCaches );
// add the texture to a retired queue so it can be recycled in future:
let rtxtrQ = self.getRetiredTextureQueue( txrH );
rtxtrQ.push( txr );
};
ETCp.addTexture = function( txrH, minW ){
let self = this;
let txrQ = self.getTextureQueue( txrH );
let txr = {};
txrQ.push( txr );
txr.eleCaches = [];
txr.height = txrH;
txr.width = Math.max( defTxrWidth, minW );
txr.usedWidth = 0;
txr.invalidatedWidth = 0;
txr.fullnessChecks = 0;
txr.canvas = self.renderer.makeOffscreenCanvas(txr.width, txr.height);
txr.context = txr.canvas.getContext('2d');
return txr;
};
ETCp.recycleTexture = function( txrH, minW ){
let self = this;
let txrQ = self.getTextureQueue( txrH );
let rtxtrQ = self.getRetiredTextureQueue( txrH );
for( let i = 0; i < rtxtrQ.length; i++ ){
let txr = rtxtrQ[i];
if( txr.width >= minW ){
txr.retired = false;
txr.usedWidth = 0;
txr.invalidatedWidth = 0;
txr.fullnessChecks = 0;
clearArray( txr.eleCaches );
txr.context.setTransform( 1, 0, 0, 1, 0, 0 );
txr.context.clearRect( 0, 0, txr.width, txr.height );
removeFromArray( rtxtrQ, txr );
txrQ.push( txr );
return txr;
}
}
};
ETCp.queueElement = function( ele, lvl ){
let self = this;
let q = self.getElementQueue();
let k2q = self.getElementKeyToQueue();
let key = this.getKey(ele);
let existingReq = k2q[key];
if( existingReq ){
// use the max lvl b/c in between lvls are cheap to make
existingReq.level = Math.max( existingReq.level, lvl );
existingReq.eles.merge(ele);
existingReq.reqs++;
q.updateItem( existingReq );
} else {
let req = {
eles: ele.spawn().merge(ele),
level: lvl,
reqs: 1,
key
};
q.push( req );
k2q[key] = req;
}
};
ETCp.dequeue = function( pxRatio /*, extent*/ ){
let self = this;
let q = self.getElementQueue();
let k2q = self.getElementKeyToQueue();
let dequeued = [];
let lookup = self.lookup;
for( let i = 0; i < maxDeqSize; i++ ){
if( q.size() > 0 ){
let req = q.pop();
let key = req.key;
let ele = req.eles[0]; // all eles have the same key
let cacheExists = lookup.hasCache(ele, req.level);
// clear out the key to req lookup
k2q[key] = null;
// dequeueing isn't necessary with an existing cache
if( cacheExists ){ continue; }
dequeued.push( req );
let bb = self.getBoundingBox( ele );
self.getElement( ele, bb, pxRatio, req.level, getTxrReasons.dequeue );
} else {
break;
}
}
return dequeued;
};
ETCp.removeFromQueue = function( ele ){
let self = this;
let q = self.getElementQueue();
let k2q = self.getElementKeyToQueue();
let key = this.getKey(ele);
let req = k2q[key];
if( req != null ){
if( req.eles.length === 1 ){ // remove if last ele in the req
// bring to front of queue
req.reqs = MAX_INT;
q.updateItem(req);
q.pop(); // remove from queue
k2q[key] = null; // remove from lookup map
} else { // otherwise just remove ele from req
req.eles.unmerge(ele);
}
}
};
ETCp.onDequeue = function( fn ){ this.onDequeues.push( fn ); };
ETCp.offDequeue = function( fn ){ removeFromArray( this.onDequeues, fn ); };
ETCp.setupDequeueing = defs.setupDequeueing({
deqRedrawThreshold: deqRedrawThreshold,
deqCost: deqCost,
deqAvgCost: deqAvgCost,
deqNoDrawCost: deqNoDrawCost,
deqFastCost: deqFastCost,
deq: function( self, pxRatio, extent ){
return self.dequeue( pxRatio, extent );
},
onDeqd: function( self, deqd ){
for( let i = 0; i < self.onDequeues.length; i++ ){
let fn = self.onDequeues[i];
fn( deqd );
}
},
shouldRedraw: function( self, deqd, pxRatio, extent ){
for( let i = 0; i < deqd.length; i++ ){
let eles = deqd[i].eles;
for( let j = 0; j < eles.length; j++ ){
let bb = eles[j].boundingBox();
if( math.boundingBoxesIntersect( bb, extent ) ){
return true;
}
}
}
return false;
},
priority: function( self ){
return self.renderer.beforeRenderPriorities.eleTxrDeq;
}
});
export default ElementTextureCache;

View File

@@ -0,0 +1,390 @@
/*
The canvas renderer was written by Yue Dong.
Modifications tracked on Github.
*/
/* global OffscreenCanvas */
import * as util from '../../../util/index.mjs';
import * as is from '../../../is.mjs';
import { makeBoundingBox } from '../../../math.mjs';
import ElementTextureCache from './ele-texture-cache.mjs';
import LayeredTextureCache from './layered-texture-cache.mjs';
import arrowShapes from './arrow-shapes.mjs';
import drawingElements from './drawing-elements.mjs';
import drawingEdges from './drawing-edges.mjs';
import drawingImages from './drawing-images.mjs';
import drawingLabelText from './drawing-label-text.mjs';
import drawingNodes from './drawing-nodes.mjs';
import drawingRedraw from './drawing-redraw.mjs';
import drawingRedrawWebGL from './webgl/drawing-redraw-webgl.mjs';
import drawingShapes from './drawing-shapes.mjs';
import exportImage from './export-image.mjs';
import nodeShapes from './node-shapes.mjs';
var CR = CanvasRenderer;
var CRp = CanvasRenderer.prototype;
CRp.CANVAS_LAYERS = 3;
//
CRp.SELECT_BOX = 0;
CRp.DRAG = 1;
CRp.NODE = 2;
CRp.WEBGL = 3;
CRp.CANVAS_TYPES = [ '2d', '2d', '2d', 'webgl2' ];
CRp.BUFFER_COUNT = 3;
//
CRp.TEXTURE_BUFFER = 0;
CRp.MOTIONBLUR_BUFFER_NODE = 1;
CRp.MOTIONBLUR_BUFFER_DRAG = 2;
function CanvasRenderer( options ){
var r = this;
var containerWindow = r.cy.window();
var document = containerWindow.document;
if( options.webgl ){
CRp.CANVAS_LAYERS = r.CANVAS_LAYERS = 4;
console.log('webgl rendering enabled');
}
r.data = {
canvases: new Array( CRp.CANVAS_LAYERS ),
contexts: new Array( CRp.CANVAS_LAYERS ),
canvasNeedsRedraw: new Array( CRp.CANVAS_LAYERS ),
bufferCanvases: new Array( CRp.BUFFER_COUNT ),
bufferContexts: new Array( CRp.CANVAS_LAYERS ),
};
var tapHlOffAttr = '-webkit-tap-highlight-color';
var tapHlOffStyle = 'rgba(0,0,0,0)';
r.data.canvasContainer = document.createElement( 'div' ); // eslint-disable-line no-undef
var containerStyle = r.data.canvasContainer.style;
r.data.canvasContainer.style[tapHlOffAttr] = tapHlOffStyle;
containerStyle.position = 'relative';
containerStyle.zIndex = '0';
containerStyle.overflow = 'hidden';
var container = options.cy.container();
container.appendChild( r.data.canvasContainer );
container.style[tapHlOffAttr] = tapHlOffStyle;
var styleMap = {
'-webkit-user-select': 'none',
'-moz-user-select': '-moz-none',
'user-select': 'none',
'-webkit-tap-highlight-color': 'rgba(0,0,0,0)',
'outline-style': 'none',
};
if(is.ms()) {
styleMap['-ms-touch-action'] = 'none';
styleMap['touch-action'] = 'none';
}
for( var i = 0; i < CRp.CANVAS_LAYERS; i++ ){
var canvas = r.data.canvases[ i ] = document.createElement( 'canvas' ); // eslint-disable-line no-undef
var type = CRp.CANVAS_TYPES[ i ];
r.data.contexts[ i ] = canvas.getContext( type );
if( !r.data.contexts[ i ] ) {
util.error( 'Could not create canvas of type ' + type );
}
Object.keys(styleMap).forEach((k) => {
canvas.style[k] = styleMap[k];
});
canvas.style.position = 'absolute';
canvas.setAttribute( 'data-id', 'layer' + i );
canvas.style.zIndex = String( CRp.CANVAS_LAYERS - i );
r.data.canvasContainer.appendChild( canvas );
r.data.canvasNeedsRedraw[ i ] = false;
}
r.data.topCanvas = r.data.canvases[0];
r.data.canvases[ CRp.NODE ].setAttribute( 'data-id', 'layer' + CRp.NODE + '-node' );
r.data.canvases[ CRp.SELECT_BOX ].setAttribute( 'data-id', 'layer' + CRp.SELECT_BOX + '-selectbox' );
r.data.canvases[ CRp.DRAG ].setAttribute( 'data-id', 'layer' + CRp.DRAG + '-drag' );
if( r.data.canvases[ CRp.WEBGL ] ) {
r.data.canvases[ CRp.WEBGL ].setAttribute( 'data-id', 'layer' + CRp.WEBGL + '-webgl' );
}
for( var i = 0; i < CRp.BUFFER_COUNT; i++ ){
r.data.bufferCanvases[ i ] = document.createElement( 'canvas' ); // eslint-disable-line no-undef
r.data.bufferContexts[ i ] = r.data.bufferCanvases[ i ].getContext( '2d' );
r.data.bufferCanvases[ i ].style.position = 'absolute';
r.data.bufferCanvases[ i ].setAttribute( 'data-id', 'buffer' + i );
r.data.bufferCanvases[ i ].style.zIndex = String( -i - 1 );
r.data.bufferCanvases[ i ].style.visibility = 'hidden';
//r.data.canvasContainer.appendChild(r.data.bufferCanvases[i]);
}
r.pathsEnabled = true;
let emptyBb = makeBoundingBox();
let getBoxCenter = bb => ({ x: (bb.x1 + bb.x2)/2, y: (bb.y1 + bb.y2)/2 });
let getCenterOffset = bb => ({ x: -bb.w/2, y: -bb.h/2 });
let backgroundTimestampHasChanged = ele => {
let _p = ele[0]._private;
let same = _p.oldBackgroundTimestamp === _p.backgroundTimestamp;
return !same;
};
let getStyleKey = ele => ele[0]._private.nodeKey;
let getLabelKey = ele => ele[0]._private.labelStyleKey;
let getSourceLabelKey = ele => ele[0]._private.sourceLabelStyleKey;
let getTargetLabelKey = ele => ele[0]._private.targetLabelStyleKey;
let drawElement = (context, ele, bb, scaledLabelShown, useEleOpacity) => r.drawElement( context, ele, bb, false, false, useEleOpacity );
let drawLabel = (context, ele, bb, scaledLabelShown, useEleOpacity) => r.drawElementText( context, ele, bb, scaledLabelShown, 'main', useEleOpacity );
let drawSourceLabel = (context, ele, bb, scaledLabelShown, useEleOpacity) => r.drawElementText( context, ele, bb, scaledLabelShown, 'source', useEleOpacity );
let drawTargetLabel = (context, ele, bb, scaledLabelShown, useEleOpacity) => r.drawElementText( context, ele, bb, scaledLabelShown, 'target', useEleOpacity );
let getElementBox = ele => { ele.boundingBox(); return ele[0]._private.bodyBounds; };
let getLabelBox = ele => { ele.boundingBox(); return ele[0]._private.labelBounds.main || emptyBb; };
let getSourceLabelBox = ele => { ele.boundingBox(); return ele[0]._private.labelBounds.source || emptyBb; };
let getTargetLabelBox = ele => { ele.boundingBox(); return ele[0]._private.labelBounds.target || emptyBb; };
let isLabelVisibleAtScale = (ele, scaledLabelShown) => scaledLabelShown;
let getElementRotationPoint = ele => getBoxCenter( getElementBox(ele) );
let addTextMargin = (prefix, pt, ele) => {
let pre = prefix ? prefix + '-' : '';
return {
x: pt.x + ele.pstyle(pre + 'text-margin-x').pfValue,
y: pt.y + ele.pstyle(pre + 'text-margin-y').pfValue
};
};
let getRsPt = (ele, x, y) => {
let rs = ele[0]._private.rscratch;
return { x: rs[x], y: rs[y] };
};
let getLabelRotationPoint = ele => addTextMargin('', getRsPt(ele, 'labelX', 'labelY'), ele);
let getSourceLabelRotationPoint = ele => addTextMargin('source', getRsPt(ele, 'sourceLabelX', 'sourceLabelY'), ele);
let getTargetLabelRotationPoint = ele => addTextMargin('target', getRsPt(ele, 'targetLabelX', 'targetLabelY'), ele);
let getElementRotationOffset = ele => getCenterOffset( getElementBox(ele) );
let getSourceLabelRotationOffset = ele => getCenterOffset( getSourceLabelBox(ele) );
let getTargetLabelRotationOffset = ele => getCenterOffset( getTargetLabelBox(ele) );
let getLabelRotationOffset = ele => {
let bb = getLabelBox(ele);
let p = getCenterOffset( getLabelBox(ele) );
if( ele.isNode() ){
switch( ele.pstyle('text-halign').value ){
case 'left':
p.x = -bb.w - (bb.leftPad || 0);
break;
case 'right':
p.x = -(bb.rightPad || 0);
break;
}
switch( ele.pstyle('text-valign').value ){
case 'top':
p.y = -bb.h - (bb.topPad || 0);
break;
case 'bottom':
p.y = -(bb.botPad || 0);
break;
}
}
return p;
};
let eleTxrCache = r.data.eleTxrCache = new ElementTextureCache( r, {
getKey: getStyleKey,
doesEleInvalidateKey: backgroundTimestampHasChanged,
drawElement: drawElement,
getBoundingBox: getElementBox,
getRotationPoint: getElementRotationPoint,
getRotationOffset: getElementRotationOffset,
allowEdgeTxrCaching: false,
allowParentTxrCaching: false
} );
let lblTxrCache = r.data.lblTxrCache = new ElementTextureCache( r, {
getKey: getLabelKey,
drawElement: drawLabel,
getBoundingBox: getLabelBox,
getRotationPoint: getLabelRotationPoint,
getRotationOffset: getLabelRotationOffset,
isVisible: isLabelVisibleAtScale
} );
let slbTxrCache = r.data.slbTxrCache = new ElementTextureCache( r, {
getKey: getSourceLabelKey,
drawElement: drawSourceLabel,
getBoundingBox: getSourceLabelBox,
getRotationPoint: getSourceLabelRotationPoint,
getRotationOffset: getSourceLabelRotationOffset,
isVisible: isLabelVisibleAtScale
} );
let tlbTxrCache = r.data.tlbTxrCache = new ElementTextureCache( r, {
getKey: getTargetLabelKey,
drawElement: drawTargetLabel,
getBoundingBox: getTargetLabelBox,
getRotationPoint: getTargetLabelRotationPoint,
getRotationOffset: getTargetLabelRotationOffset,
isVisible: isLabelVisibleAtScale
} );
let lyrTxrCache = r.data.lyrTxrCache = new LayeredTextureCache( r );
r.onUpdateEleCalcs(function invalidateTextureCaches( willDraw, eles ){
// each cache should check for sub-key diff to see that the update affects that cache particularly
eleTxrCache.invalidateElements( eles );
lblTxrCache.invalidateElements( eles );
slbTxrCache.invalidateElements( eles );
tlbTxrCache.invalidateElements( eles );
// any change invalidates the layers
lyrTxrCache.invalidateElements( eles );
// update the old bg timestamp so diffs can be done in the ele txr caches
for( let i = 0; i < eles.length; i++ ){
let _p = eles[i]._private;
_p.oldBackgroundTimestamp = _p.backgroundTimestamp;
}
});
let refineInLayers = reqs => {
for( var i = 0; i < reqs.length; i++ ){
lyrTxrCache.enqueueElementRefinement( reqs[i].ele );
}
};
eleTxrCache.onDequeue(refineInLayers);
lblTxrCache.onDequeue(refineInLayers);
slbTxrCache.onDequeue(refineInLayers);
tlbTxrCache.onDequeue(refineInLayers);
if( options.webgl ) {
r.initWebgl( options, {
getStyleKey,
getLabelKey,
getSourceLabelKey,
getTargetLabelKey,
drawElement,
drawLabel,
drawSourceLabel,
drawTargetLabel,
getElementBox,
getLabelBox,
getSourceLabelBox,
getTargetLabelBox,
getElementRotationPoint,
getElementRotationOffset,
getLabelRotationPoint,
getSourceLabelRotationPoint,
getTargetLabelRotationPoint,
getLabelRotationOffset,
getSourceLabelRotationOffset,
getTargetLabelRotationOffset
} );
}
}
CRp.redrawHint = function( group, bool ){
var r = this;
switch( group ){
case 'eles':
r.data.canvasNeedsRedraw[ CRp.NODE ] = bool;
break;
case 'drag':
r.data.canvasNeedsRedraw[ CRp.DRAG ] = bool;
break;
case 'select':
r.data.canvasNeedsRedraw[ CRp.SELECT_BOX ] = bool;
break;
case 'gc':
r.data.gc = true;
break;
}
};
// whether to use Path2D caching for drawing
var pathsImpld = typeof Path2D !== 'undefined';
CRp.path2dEnabled = function( on ){
if( on === undefined ){
return this.pathsEnabled;
}
this.pathsEnabled = on ? true : false;
};
CRp.usePaths = function(){
return pathsImpld && this.pathsEnabled;
};
CRp.setImgSmoothing = function( context, bool ){
if( context.imageSmoothingEnabled != null ){
context.imageSmoothingEnabled = bool;
} else {
context.webkitImageSmoothingEnabled = bool;
context.mozImageSmoothingEnabled = bool;
context.msImageSmoothingEnabled = bool;
}
};
CRp.getImgSmoothing = function( context ){
if( context.imageSmoothingEnabled != null ){
return context.imageSmoothingEnabled;
} else {
return context.webkitImageSmoothingEnabled || context.mozImageSmoothingEnabled || context.msImageSmoothingEnabled;
}
};
CRp.makeOffscreenCanvas = function(width, height){
let canvas;
if( typeof OffscreenCanvas !== typeof undefined ){
canvas = new OffscreenCanvas(width, height);
} else {
var containerWindow = this.cy.window();
var document = containerWindow.document;
canvas = document.createElement('canvas'); // eslint-disable-line no-undef
canvas.width = width;
canvas.height = height;
}
return canvas;
};
[
arrowShapes,
drawingElements,
drawingEdges,
drawingImages,
drawingLabelText,
drawingNodes,
drawingRedraw,
drawingRedrawWebGL,
drawingShapes,
exportImage,
nodeShapes
].forEach( function( props ){
util.extend( CRp, props );
} );
export default CR;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,208 @@
// Canvas Upscaling Plugin
class MiscUpscaler {
constructor(options = {}) {
// Plugin Settings
this.options = Object.assign({
useEdgeDetection: true, // Edge Detection
scaleFactor: 2.0, // Upscaling Value
}, options);
this.gl = null;
this.program = null;
this.frameBuffer = null;
this.texture = null;
this.outputTexture = null;
}
init(inputCanvas, outputCanvas) {
this.inputCanvas = inputCanvas;
this.outputCanvas = outputCanvas;
// Set output canvas size based on scale factor
this.outputCanvas.width = this.inputCanvas.width * this.options.scaleFactor;
this.outputCanvas.height = this.inputCanvas.height * this.options.scaleFactor;
// Initialize WebGL context
this.gl = this.outputCanvas.getContext('webgl2');
if (!this.gl) {
console.error('WebGL not supported');
return false;
}
// Initialize shaders and buffers
this.initShaders();
this.initBuffers();
this.initTextures();
return true;
}
initShaders() {
// Vertex shader
const vertexShaderSource = `
attribute vec2 a_position;
attribute vec2 a_texCoord;
varying vec2 vUv;
void main() {
vUv = a_texCoord;
gl_Position = vec4(a_position, 0.0, 1.0);
}
`;
// Fragment shader
const fragmentShaderSource = `
precision mediump float;
uniform sampler2D tDiffuse;
uniform vec2 resolution;
varying vec2 vUv;
void main() {
vec2 texelSize = 1.0 / resolution;
// Get Neighbor Pixels
vec4 color = texture2D(tDiffuse, vUv);
vec4 colorUp = texture2D(tDiffuse, vUv + vec2(0.0, texelSize.y));
vec4 colorDown = texture2D(tDiffuse, vUv - vec2(0.0, texelSize.y));
vec4 colorLeft = texture2D(tDiffuse, vUv - vec2(texelSize.x, 0.0));
vec4 colorRight = texture2D(tDiffuse, vUv + vec2(texelSize.x, 0.0));
// Work with edges
float edgeStrength = 1.0 - smoothstep(0.1, 0.3, length(color.rgb - colorUp.rgb));
edgeStrength += 1.0 - smoothstep(0.1, 0.3, length(color.rgb - colorDown.rgb));
edgeStrength += 1.0 - smoothstep(0.1, 0.3, length(color.rgb - colorLeft.rgb));
edgeStrength += 1.0 - smoothstep(0.1, 0.3, length(color.rgb - colorRight.rgb));
edgeStrength = clamp(edgeStrength, 0.0, 1.0);
// Applying edges incresing and filtering
vec3 enhancedColor = mix(color.rgb, vec3(1.0) - (1.0 - color.rgb) * edgeStrength, 0.5);
gl_FragColor = vec4(enhancedColor, color.a);
}
`;
// Create shader program
const vertexShader = this.createShader(this.gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = this.createShader(this.gl.FRAGMENT_SHADER, fragmentShaderSource);
this.program = this.createProgram(vertexShader, fragmentShader);
// Get attribute and uniform locations
this.positionLocation = this.gl.getAttribLocation(this.program, 'a_position');
this.texCoordLocation = this.gl.getAttribLocation(this.program, 'a_texCoord');
this.resolutionLocation = this.gl.getUniformLocation(this.program, 'resolution');
this.textureLocation = this.gl.getUniformLocation(this.program, 'tDiffuse');
}
createShader(type, source) {
const shader = this.gl.createShader(type);
this.gl.shaderSource(shader, source);
this.gl.compileShader(shader);
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
console.error('Shader compile error:', this.gl.getShaderInfoLog(shader));
this.gl.deleteShader(shader);
return null;
}
return shader;
}
createProgram(vertexShader, fragmentShader) {
const program = this.gl.createProgram();
this.gl.attachShader(program, vertexShader);
this.gl.attachShader(program, fragmentShader);
this.gl.linkProgram(program);
if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
console.error('Program link error:', this.gl.getProgramInfoLog(program));
return null;
}
return program;
}
initBuffers() {
// Create a buffer for positions
this.positionBuffer = this.gl.createBuffer();
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);
// Full screen quad (2 triangles)
const positions = [
-1, -1,
1, -1,
-1, 1,
-1, 1,
1, -1,
1, 1
];
this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(positions), this.gl.STATIC_DRAW);
// Create a buffer for texture coordinates
this.texCoordBuffer = this.gl.createBuffer();
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.texCoordBuffer);
// Texture coordinates for the quad with Y-flipped to match canvas coordinates
const texCoords = [
0, 1, // top-left (flipped from 0,0)
1, 1, // top-right (flipped from 1,0)
0, 0, // bottom-left (flipped from 0,1)
0, 0, // bottom-left (flipped from 0,1)
1, 1, // top-right (flipped from 1,0)
1, 0 // bottom-right (flipped from 1,1)
];
this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(texCoords), this.gl.STATIC_DRAW);
}
initTextures() {
// Create texture for input canvas
this.texture = this.gl.createTexture();
this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);
// Set parameters for texture
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR);
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);
}
render() {
if (!this.gl) return;
// Update texture from input canvas
this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);
this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, this.inputCanvas);
// Set viewport and clear
this.gl.viewport(0, 0, this.outputCanvas.width, this.outputCanvas.height);
this.gl.clearColor(0, 0, 0, 0);
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
// Use shader program
this.gl.useProgram(this.program);
// Set uniforms
this.gl.uniform2f(this.resolutionLocation, this.inputCanvas.width, this.inputCanvas.height);
this.gl.uniform1i(this.textureLocation, 0);
// Set position attribute
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);
this.gl.enableVertexAttribArray(this.positionLocation);
this.gl.vertexAttribPointer(this.positionLocation, 2, this.gl.FLOAT, false, 0, 0);
// Set texture coordinate attribute
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.texCoordBuffer);
this.gl.enableVertexAttribArray(this.texCoordLocation);
this.gl.vertexAttribPointer(this.texCoordLocation, 2, this.gl.FLOAT, false, 0, 0);
// Draw
this.gl.drawArrays(this.gl.TRIANGLES, 0, 6);
}
resize() {
if (this.inputCanvas && this.outputCanvas) {
this.outputCanvas.width = this.inputCanvas.width * this.options.scaleFactor;
this.outputCanvas.height = this.inputCanvas.height * this.options.scaleFactor;
}
}
}
export { MiscUpscaler as UpscalerPlugin };

View File

@@ -0,0 +1,9 @@
import nullRenderer from './null/index.mjs';
import baseRenderer from './base/index.mjs';
import canvasRenderer from './canvas/index.mjs';
export default [
{ name: 'null', impl: nullRenderer },
{ name: 'base', impl: baseRenderer },
{ name: 'canvas', impl: canvasRenderer },
];

145
frontend/node_modules/cytoscape/src/is.mjs generated vendored Normal file
View File

@@ -0,0 +1,145 @@
/*global HTMLElement DocumentTouch */
import window from './window.mjs';
let navigator = window ? window.navigator : null;
let document = window ? window.document : null;
let typeofstr = typeof '';
let typeofobj = typeof {};
let typeoffn = typeof function(){};
let typeofhtmlele = typeof HTMLElement;
let instanceStr = function( obj ){
return obj && obj.instanceString && fn( obj.instanceString ) ? obj.instanceString() : null;
};
export const defined = obj =>
obj != null; // not undefined or null
export const string = obj =>
obj != null && typeof obj == typeofstr;
export const fn = obj =>
obj != null && typeof obj === typeoffn;
export const array = obj =>
!(elementOrCollection(obj)) && (Array.isArray ? Array.isArray( obj ) : obj != null && obj instanceof Array);
export const plainObject = obj =>
obj != null && typeof obj === typeofobj && !array( obj ) && obj.constructor === Object;
export const object = obj =>
obj != null && typeof obj === typeofobj;
export const number = obj =>
obj != null && typeof obj === typeof 1 && !isNaN( obj );
export const integer = obj =>
number( obj ) && Math.floor( obj ) === obj;
export const bool = obj =>
obj != null && typeof obj === typeof true;
export const htmlElement = obj => {
if( 'undefined' === typeofhtmlele ){
return undefined;
} else {
return null != obj && obj instanceof HTMLElement;
}
};
export const elementOrCollection = obj =>
element( obj ) || collection( obj );
export const element = obj =>
instanceStr( obj ) === 'collection' && obj._private.single;
export const collection = obj =>
instanceStr( obj ) === 'collection' && !obj._private.single;
export const core = obj =>
instanceStr( obj ) === 'core';
export const style = obj =>
instanceStr( obj ) === 'style';
export const stylesheet = obj =>
instanceStr( obj ) === 'stylesheet';
export const event = obj =>
instanceStr( obj ) === 'event';
export const thread = obj =>
instanceStr( obj ) === 'thread';
export const fabric = obj =>
instanceStr( obj ) === 'fabric';
export const emptyString = obj => {
if( obj === undefined || obj === null ){ // null is empty
return true;
} else if( obj === '' || obj.match( /^\s+$/ ) ){
return true; // empty string is empty
}
return false; // otherwise, we don't know what we've got
};
export const nonemptyString = obj => {
if( obj && string( obj ) && obj !== '' && !obj.match( /^\s+$/ ) ){
return true;
}
return false;
};
export const domElement = obj => {
if( typeof HTMLElement === 'undefined' ){
return false; // we're not in a browser so it doesn't matter
} else {
return obj instanceof HTMLElement;
}
};
export const boundingBox = obj =>
plainObject( obj ) &&
number( obj.x1 ) && number( obj.x2 ) &&
number( obj.y1 ) && number( obj.y2 )
;
export const promise = obj =>
object( obj ) && fn( obj.then );
export const touch = () =>
window && ( ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch );
export const gecko = () =>
window && ( typeof InstallTrigger !== 'undefined' || ('MozAppearance' in document.documentElement.style) );
export const webkit = () =>
window && ( typeof webkitURL !== 'undefined' || ('WebkitAppearance' in document.documentElement.style) );
export const chromium = () =>
window && ( typeof chrome !== 'undefined' );
export const khtml = () =>
navigator && navigator.vendor.match( /kde/i ); // probably a better way to detect this...
export const khtmlEtc = () =>
khtml() || webkit() || chromium();
export const ms = () =>
navigator && navigator.userAgent.match( /msie|trident|edge/i ); // probably a better way to detect this...
export const windows = () =>
navigator && navigator.appVersion.match( /Win/i );
export const mac = () =>
navigator && navigator.appVersion.match( /Mac/i );
export const linux = () =>
navigator && navigator.appVersion.match( /Linux/i );
export const unix = () =>
navigator && navigator.appVersion.match( /X11/i );

View File

@@ -0,0 +1,13 @@
/**
* Make a new query object
*
* @prop type {Type} The type enum (int) of the query
* @prop checks List of checks to make against an ele to test for a match
*/
let newQuery = function(){
return {
checks: []
};
};
export default newQuery;

254
frontend/node_modules/cytoscape/src/selector/parse.mjs generated vendored Normal file
View File

@@ -0,0 +1,254 @@
import { warn } from '../util/index.mjs';
import * as is from '../is.mjs';
import exprs from './expressions.mjs';
import newQuery from './new-query.mjs';
import Type from './type.mjs';
/**
* Of all the expressions, find the first match in the remaining text.
* @param {string} remaining The remaining text to parse
* @returns The matched expression and the newly remaining text `{ expr, match, name, remaining }`
*/
const consumeExpr = ( remaining ) => {
let expr;
let match;
let name;
for( let j = 0; j < exprs.length; j++ ){
let e = exprs[ j ];
let n = e.name;
let m = remaining.match( e.regexObj );
if( m != null ){
match = m;
expr = e;
name = n;
let consumed = m[0];
remaining = remaining.substring( consumed.length );
break; // we've consumed one expr, so we can return now
}
}
return {
expr: expr,
match: match,
name: name,
remaining: remaining
};
};
/**
* Consume all the leading whitespace
* @param {string} remaining The text to consume
* @returns The text with the leading whitespace removed
*/
const consumeWhitespace = ( remaining ) => {
let match = remaining.match( /^\s+/ );
if( match ){
let consumed = match[0];
remaining = remaining.substring( consumed.length );
}
return remaining;
};
/**
* Parse the string and store the parsed representation in the Selector.
* @param {string} selector The selector string
* @returns `true` if the selector was successfully parsed, `false` otherwise
*/
const parse = function( selector ){
let self = this;
let remaining = self.inputText = selector;
let currentQuery = self[0] = newQuery();
self.length = 1;
remaining = consumeWhitespace( remaining ); // get rid of leading whitespace
for( ;; ){
let exprInfo = consumeExpr( remaining );
if( exprInfo.expr == null ){
warn( 'The selector `' + selector + '`is invalid' );
return false;
} else {
let args = exprInfo.match.slice( 1 );
// let the token populate the selector object in currentQuery
let ret = exprInfo.expr.populate( self, currentQuery, args );
if( ret === false ){
return false; // exit if population failed
} else if( ret != null ){
currentQuery = ret; // change the current query to be filled if the expr specifies
}
}
remaining = exprInfo.remaining;
// we're done when there's nothing left to parse
if( remaining.match( /^\s*$/ ) ){
break;
}
}
let lastQ = self[self.length - 1];
if( self.currentSubject != null ){
lastQ.subject = self.currentSubject;
}
lastQ.edgeCount = self.edgeCount;
lastQ.compoundCount = self.compoundCount;
for( let i = 0; i < self.length; i++ ){
let q = self[i];
// in future, this could potentially be allowed if there were operator precedence and detection of invalid combinations
if( q.compoundCount > 0 && q.edgeCount > 0 ){
warn( 'The selector `' + selector + '` is invalid because it uses both a compound selector and an edge selector' );
return false;
}
if( q.edgeCount > 1 ){
warn( 'The selector `' + selector + '` is invalid because it uses multiple edge selectors' );
return false;
} else if( q.edgeCount === 1 ){
warn( 'The selector `' + selector + '` is deprecated. Edge selectors do not take effect on changes to source and target nodes after an edge is added, for performance reasons. Use a class or data selector on edges instead, updating the class or data of an edge when your app detects a change in source or target nodes.' );
}
}
return true; // success
};
/**
* Get the selector represented as a string. This value uses default formatting,
* so things like spacing may differ from the input text passed to the constructor.
* @returns {string} The selector string
*/
export const toString = function(){
if( this.toStringCache != null ){
return this.toStringCache;
}
let clean = function( obj ){
if( obj == null ){
return '';
} else {
return obj;
}
};
let cleanVal = function( val ){
if( is.string( val ) ){
return '"' + val + '"';
} else {
return clean( val );
}
};
let space = ( val ) => {
return ' ' + val + ' ';
};
let checkToString = ( check, subject ) => {
let { type, value } = check;
switch( type ){
case Type.GROUP: {
let group = clean( value );
return group.substring( 0, group.length - 1 );
}
case Type.DATA_COMPARE: {
let { field, operator } = check;
return '[' + field + space( clean( operator ) ) + cleanVal( value ) + ']';
}
case Type.DATA_BOOL: {
let { operator, field } = check;
return '[' + clean( operator ) + field + ']';
}
case Type.DATA_EXIST: {
let { field } = check;
return '[' + field + ']';
}
case Type.META_COMPARE: {
let { operator, field } = check;
return '[[' + field + space( clean( operator ) ) + cleanVal( value ) + ']]';
}
case Type.STATE: {
return value;
}
case Type.ID: {
return '#' + value;
}
case Type.CLASS: {
return '.' + value;
}
case Type.PARENT:
case Type.CHILD: {
return queryToString(check.parent, subject) + space('>') + queryToString(check.child, subject);
}
case Type.ANCESTOR:
case Type.DESCENDANT: {
return queryToString(check.ancestor, subject) + ' ' + queryToString(check.descendant, subject);
}
case Type.COMPOUND_SPLIT: {
let lhs = queryToString(check.left, subject);
let sub = queryToString(check.subject, subject);
let rhs = queryToString(check.right, subject);
return lhs + (lhs.length > 0 ? ' ' : '') + sub + rhs;
}
case Type.TRUE: {
return '';
}
}
};
let queryToString = ( query, subject ) => {
return query.checks.reduce((str, chk, i) => {
return str + (subject === query && i === 0 ? '$' : '') + checkToString(chk, subject);
}, '');
};
let str = '';
for( let i = 0; i < this.length; i++ ){
let query = this[ i ];
str += queryToString( query, query.subject );
if( this.length > 1 && i < this.length - 1 ){
str += ', ';
}
}
this.toStringCache = str;
return str;
};
export default { parse, toString };

View File

@@ -0,0 +1,122 @@
import Type from './type.mjs';
import { stateSelectorMatches } from './state.mjs';
import { valCmp, boolCmp, existCmp, meta, data } from './data.mjs';
/** A lookup of `match(check, ele)` functions by `Type` int */
export const match = [];
/**
* Returns whether the query matches for the element
* @param query The `{ type, value, ... }` query object
* @param ele The element to compare against
*/
export const matches = (query, ele) => {
return query.checks.every( chk => match[chk.type](chk, ele) );
};
match[Type.GROUP] = (check, ele) => {
let group = check.value;
return group === '*' || group === ele.group();
};
match[Type.STATE] = (check, ele) => {
let stateSelector = check.value;
return stateSelectorMatches( stateSelector, ele );
};
match[Type.ID] = (check, ele) => {
let id = check.value;
return ele.id() === id;
};
match[Type.CLASS] = (check, ele) => {
let cls = check.value;
return ele.hasClass(cls);
};
match[Type.META_COMPARE] = (check, ele) => {
let { field, operator, value } = check;
return valCmp( meta(ele, field), operator, value );
};
match[Type.DATA_COMPARE] = (check, ele) => {
let { field, operator, value } = check;
return valCmp( data(ele, field), operator, value );
};
match[Type.DATA_BOOL] = (check, ele) => {
let { field, operator } = check;
return boolCmp( data(ele, field), operator );
};
match[Type.DATA_EXIST] = (check, ele) => {
let { field, operator } = check;
return existCmp( data(ele, field), operator );
};
match[Type.UNDIRECTED_EDGE] = (check, ele) => {
let qA = check.nodes[0];
let qB = check.nodes[1];
let src = ele.source();
let tgt = ele.target();
return ( matches(qA, src) && matches(qB, tgt) ) || ( matches(qB, src) && matches(qA, tgt) );
};
match[Type.NODE_NEIGHBOR] = (check, ele) => {
return matches(check.node, ele) && ele.neighborhood().some( n => n.isNode() && matches(check.neighbor, n) );
};
match[Type.DIRECTED_EDGE] = (check, ele) => {
return matches(check.source, ele.source()) && matches(check.target, ele.target());
};
match[Type.NODE_SOURCE] = (check, ele) => {
return matches(check.source, ele) && ele.outgoers().some( n => n.isNode() && matches(check.target, n) );
};
match[Type.NODE_TARGET] = (check, ele) => {
return matches(check.target, ele) && ele.incomers().some( n => n.isNode() && matches(check.source, n) );
};
match[Type.CHILD] = (check, ele) => {
return matches(check.child, ele) && matches(check.parent, ele.parent());
};
match[Type.PARENT] = (check, ele) => {
return matches(check.parent, ele) && ele.children().some( c => matches(check.child, c) );
};
match[Type.DESCENDANT] = (check, ele) => {
return matches(check.descendant, ele) && ele.ancestors().some( a => matches(check.ancestor, a) );
};
match[Type.ANCESTOR] = (check, ele) => {
return matches(check.ancestor, ele) && ele.descendants().some( d => matches(check.descendant, d) );
};
match[Type.COMPOUND_SPLIT] = (check, ele) => {
return matches(check.subject, ele) && matches(check.left, ele) && matches(check.right, ele);
};
match[Type.TRUE] = () => true;
match[Type.COLLECTION] = (check, ele) => {
let collection = check.value;
return collection.has(ele);
};
match[Type.FILTER] = (check, ele) => {
let filter = check.value;
return filter(ele);
};

849
frontend/node_modules/cytoscape/src/style/apply.mjs generated vendored Normal file
View File

@@ -0,0 +1,849 @@
import * as util from '../util/index.mjs';
import * as is from '../is.mjs';
import Promise from '../promise.mjs';
const styfn = {};
// keys for style blocks, e.g. ttfftt
const TRUE = 't';
const FALSE = 'f';
// (potentially expensive calculation)
// apply the style to the element based on
// - its bypass
// - what selectors match it
styfn.apply = function( eles ){
let self = this;
let _p = self._private;
let cy = _p.cy;
let updatedEles = cy.collection();
for( let ie = 0; ie < eles.length; ie++ ){
let ele = eles[ ie ];
let cxtMeta = self.getContextMeta( ele );
if( cxtMeta.empty ){
continue;
}
let cxtStyle = self.getContextStyle( cxtMeta );
let app = self.applyContextStyle( cxtMeta, cxtStyle, ele );
if( ele._private.appliedInitStyle ){
self.updateTransitions( ele, app.diffProps );
} else {
ele._private.appliedInitStyle = true;
}
let hintsDiff = self.updateStyleHints( ele );
if( hintsDiff ){
updatedEles.push( ele );
}
} // for elements
return updatedEles;
};
styfn.getPropertiesDiff = function( oldCxtKey, newCxtKey ){
let self = this;
let cache = self._private.propDiffs = self._private.propDiffs || {};
let dualCxtKey = oldCxtKey + '-' + newCxtKey;
let cachedVal = cache[ dualCxtKey ];
if( cachedVal ){
return cachedVal;
}
let diffProps = [];
let addedProp = {};
for( let i = 0; i < self.length; i++ ){
let cxt = self[ i ];
let oldHasCxt = oldCxtKey[ i ] === TRUE;
let newHasCxt = newCxtKey[ i ] === TRUE;
let cxtHasDiffed = oldHasCxt !== newHasCxt;
let cxtHasMappedProps = cxt.mappedProperties.length > 0;
if( cxtHasDiffed || ( newHasCxt && cxtHasMappedProps )){
let props;
if( cxtHasDiffed && cxtHasMappedProps ){
props = cxt.properties; // suffices b/c mappedProperties is a subset of properties
} else if( cxtHasDiffed ){
props = cxt.properties; // need to check them all
} else if( cxtHasMappedProps ){
props = cxt.mappedProperties; // only need to check mapped
}
for( let j = 0; j < props.length; j++ ){
let prop = props[ j ];
let name = prop.name;
// if a later context overrides this property, then the fact that this context has switched/diffed doesn't matter
// (semi expensive check since it makes this function O(n^2) on context length, but worth it since overall result
// is cached)
let laterCxtOverrides = false;
for( let k = i + 1; k < self.length; k++ ){
let laterCxt = self[ k ];
let hasLaterCxt = newCxtKey[ k ] === TRUE;
if( !hasLaterCxt ){ continue; } // can't override unless the context is active
laterCxtOverrides = laterCxt.properties[ prop.name ] != null;
if( laterCxtOverrides ){ break; } // exit early as long as one later context overrides
}
if( !addedProp[ name ] && !laterCxtOverrides ){
addedProp[ name ] = true;
diffProps.push( name );
}
} // for props
} // if
} // for contexts
cache[ dualCxtKey ] = diffProps;
return diffProps;
};
styfn.getContextMeta = function( ele ){
let self = this;
let cxtKey = '';
let diffProps;
let prevKey = ele._private.styleCxtKey || '';
// get the cxt key
for( let i = 0; i < self.length; i++ ){
let context = self[ i ];
let contextSelectorMatches = context.selector && context.selector.matches( ele ); // NB: context.selector may be null for 'core'
if( contextSelectorMatches ){
cxtKey += TRUE;
} else {
cxtKey += FALSE;
}
} // for context
diffProps = self.getPropertiesDiff( prevKey, cxtKey );
ele._private.styleCxtKey = cxtKey;
return {
key: cxtKey,
diffPropNames: diffProps,
empty: diffProps.length === 0
};
};
// gets a computed ele style object based on matched contexts
styfn.getContextStyle = function( cxtMeta ){
let cxtKey = cxtMeta.key;
let self = this;
let cxtStyles = this._private.contextStyles = this._private.contextStyles || {};
// if already computed style, returned cached copy
if( cxtStyles[ cxtKey ] ){ return cxtStyles[ cxtKey ]; }
let style = {
_private: {
key: cxtKey
}
};
for( let i = 0; i < self.length; i++ ){
let cxt = self[ i ];
let hasCxt = cxtKey[ i ] === TRUE;
if( !hasCxt ){ continue; }
for( let j = 0; j < cxt.properties.length; j++ ){
let prop = cxt.properties[ j ];
style[ prop.name ] = prop;
}
}
cxtStyles[ cxtKey ] = style;
return style;
};
styfn.applyContextStyle = function( cxtMeta, cxtStyle, ele ){
let self = this;
let diffProps = cxtMeta.diffPropNames;
let retDiffProps = {};
let types = self.types;
for( let i = 0; i < diffProps.length; i++ ){
let diffPropName = diffProps[ i ];
let cxtProp = cxtStyle[ diffPropName ];
let eleProp = ele.pstyle( diffPropName );
if( !cxtProp ){ // no context prop means delete
if( !eleProp ){
continue; // no existing prop means nothing needs to be removed
// nb affects initial application on mapped values like control-point-distances
} else if( eleProp.bypass ){
cxtProp = { name: diffPropName, deleteBypassed: true };
} else {
cxtProp = { name: diffPropName, delete: true };
}
}
// save cycles when the context prop doesn't need to be applied
if( eleProp === cxtProp ){ continue; }
// save cycles when a mapped context prop doesn't need to be applied
if(
cxtProp.mapped === types.fn // context prop is function mapper
&& eleProp != null // some props can be null even by default (e.g. a prop that overrides another one)
&& eleProp.mapping != null // ele prop is a concrete value from from a mapper
&& eleProp.mapping.value === cxtProp.value // the current prop on the ele is a flat prop value for the function mapper
){ // NB don't write to cxtProp, as it's shared among eles (stored in stylesheet)
let mapping = eleProp.mapping; // can write to mapping, as it's a per-ele copy
let fnValue = mapping.fnValue = cxtProp.value( ele ); // temporarily cache the value in case of a miss
if( fnValue === mapping.prevFnValue ){ continue; }
}
let retDiffProp = retDiffProps[ diffPropName ] = {
prev: eleProp
};
self.applyParsedProperty( ele, cxtProp );
retDiffProp.next = ele.pstyle( diffPropName );
if( retDiffProp.next && retDiffProp.next.bypass ){
retDiffProp.next = retDiffProp.next.bypassed;
}
}
return {
diffProps: retDiffProps
};
};
styfn.updateStyleHints = function(ele){
let _p = ele._private;
let self = this;
let propNames = self.propertyGroupNames;
let propGrKeys = self.propertyGroupKeys;
let propHash = ( ele, propNames, seedKey ) => self.getPropertiesHash( ele, propNames, seedKey );
let oldStyleKey = _p.styleKey;
if( ele.removed() ){ return false; }
let isNode = _p.group === 'nodes';
// get the style key hashes per prop group
// but lazily -- only use non-default prop values to reduce the number of hashes
//
let overriddenStyles = ele._private.style;
propNames = Object.keys( overriddenStyles );
for( let i = 0; i < propGrKeys.length; i++ ){
let grKey = propGrKeys[i];
_p.styleKeys[ grKey ] = [ util.DEFAULT_HASH_SEED, util.DEFAULT_HASH_SEED_ALT ];
}
let updateGrKey1 = (val, grKey) => _p.styleKeys[ grKey ][0] = util.hashInt( val, _p.styleKeys[ grKey ][0] );
let updateGrKey2 = (val, grKey) => _p.styleKeys[ grKey ][1] = util.hashIntAlt( val, _p.styleKeys[ grKey ][1] );
let updateGrKey = (val, grKey) => {
updateGrKey1(val, grKey);
updateGrKey2(val, grKey);
};
let updateGrKeyWStr = (strVal, grKey) => {
for( let j = 0; j < strVal.length; j++ ){
let ch = strVal.charCodeAt(j);
updateGrKey1(ch, grKey);
updateGrKey2(ch, grKey);
}
};
// - hashing works on 32 bit ints b/c we use bitwise ops
// - small numbers get cut off (e.g. 0.123 is seen as 0 by the hashing function)
// - raise up small numbers so more significant digits are seen by hashing
// - make small numbers larger than a normal value to avoid collisions
// - works in practice and it's relatively cheap
let N = 2000000000;
let cleanNum = val => (-128 < val && val < 128) && Math.floor(val) !== val ? N - ((val * 1024) | 0) : val;
for( let i = 0; i < propNames.length; i++ ){
let name = propNames[i];
let parsedProp = overriddenStyles[ name ];
if( parsedProp == null ){ continue; }
let propInfo = this.properties[name];
let type = propInfo.type;
let grKey = propInfo.groupKey;
let normalizedNumberVal;
if( propInfo.hashOverride != null ){
normalizedNumberVal = propInfo.hashOverride(ele, parsedProp);
} else if( parsedProp.pfValue != null ){
normalizedNumberVal = parsedProp.pfValue;
}
// might not be a number if it allows enums
let numberVal = propInfo.enums == null ? parsedProp.value : null;
let haveNormNum = normalizedNumberVal != null;
let haveUnitedNum = numberVal != null;
let haveNum = haveNormNum || haveUnitedNum;
let units = parsedProp.units;
// numbers are cheaper to hash than strings
// 1 hash op vs n hash ops (for length n string)
if( type.number && haveNum && !type.multiple ){
let v = haveNormNum ? normalizedNumberVal : numberVal;
updateGrKey(cleanNum(v), grKey);
if( !haveNormNum && units != null ){
updateGrKeyWStr(units, grKey);
}
} else {
updateGrKeyWStr(parsedProp.strValue, grKey);
}
}
// overall style key
//
let hash = [ util.DEFAULT_HASH_SEED, util.DEFAULT_HASH_SEED_ALT ];
for( let i = 0; i < propGrKeys.length; i++ ){
let grKey = propGrKeys[i];
let grHash = _p.styleKeys[ grKey ];
hash[0] = util.hashInt( grHash[0], hash[0] );
hash[1] = util.hashIntAlt( grHash[1], hash[1] );
}
_p.styleKey = util.combineHashes(hash[0], hash[1]);
// label dims
//
let sk = _p.styleKeys;
_p.labelDimsKey = util.combineHashesArray(sk.labelDimensions);
let labelKeys = propHash( ele, ['label'], sk.labelDimensions );
_p.labelKey = util.combineHashesArray(labelKeys);
_p.labelStyleKey = util.combineHashesArray(util.hashArrays(sk.commonLabel, labelKeys));
if( !isNode ){
let sourceLabelKeys = propHash( ele, ['source-label'], sk.labelDimensions );
_p.sourceLabelKey = util.combineHashesArray(sourceLabelKeys);
_p.sourceLabelStyleKey = util.combineHashesArray(util.hashArrays(sk.commonLabel, sourceLabelKeys));
let targetLabelKeys = propHash( ele, ['target-label'], sk.labelDimensions );
_p.targetLabelKey = util.combineHashesArray(targetLabelKeys);
_p.targetLabelStyleKey = util.combineHashesArray(util.hashArrays(sk.commonLabel, targetLabelKeys));
}
// node
//
if( isNode ){
let { nodeBody, nodeBorder, nodeOutline, backgroundImage, compound, pie, stripe } = _p.styleKeys;
let nodeKeys = [ nodeBody, nodeBorder, nodeOutline, backgroundImage, compound, pie, stripe ].filter(k => k != null).reduce(util.hashArrays, [
util.DEFAULT_HASH_SEED,
util.DEFAULT_HASH_SEED_ALT
]);
_p.nodeKey = util.combineHashesArray(nodeKeys);
_p.hasPie = pie != null && pie[0] !== util.DEFAULT_HASH_SEED && pie[1] !== util.DEFAULT_HASH_SEED_ALT;
_p.hasStripe = stripe != null && stripe[0] !== util.DEFAULT_HASH_SEED && stripe[1] !== util.DEFAULT_HASH_SEED_ALT;
}
return oldStyleKey !== _p.styleKey;
};
styfn.clearStyleHints = function(ele){
let _p = ele._private;
_p.styleCxtKey = '';
_p.styleKeys = {};
_p.styleKey = null;
_p.labelKey = null;
_p.labelStyleKey = null;
_p.sourceLabelKey = null;
_p.sourceLabelStyleKey = null;
_p.targetLabelKey = null;
_p.targetLabelStyleKey = null;
_p.nodeKey = null;
_p.hasPie = null;
_p.hasStripe = null;
};
// apply a property to the style (for internal use)
// returns whether application was successful
//
// now, this function flattens the property, and here's how:
//
// for parsedProp:{ bypass: true, deleteBypass: true }
// no property is generated, instead the bypass property in the
// element's style is replaced by what's pointed to by the `bypassed`
// field in the bypass property (i.e. restoring the property the
// bypass was overriding)
//
// for parsedProp:{ mapped: truthy }
// the generated flattenedProp:{ mapping: prop }
//
// for parsedProp:{ bypass: true }
// the generated flattenedProp:{ bypassed: parsedProp }
styfn.applyParsedProperty = function( ele, parsedProp ){
let self = this;
let prop = parsedProp;
let style = ele._private.style;
let flatProp;
let types = self.types;
let type = self.properties[ prop.name ].type;
let propIsBypass = prop.bypass;
let origProp = style[ prop.name ];
let origPropIsBypass = origProp && origProp.bypass;
let _p = ele._private;
let flatPropMapping = 'mapping';
let getVal = p => {
if( p == null ){
return null;
} else if( p.pfValue != null ){
return p.pfValue;
} else {
return p.value;
}
};
let checkTriggers = () => {
let fromVal = getVal(origProp);
let toVal = getVal(prop);
self.checkTriggers( ele, prop.name, fromVal, toVal );
};
// edge sanity checks to prevent the client from making serious mistakes
if(
parsedProp.name === 'curve-style'
&& ele.isEdge()
&& (
( // loops must be bundled beziers
parsedProp.value !== 'bezier'
&& ele.isLoop()
) || ( // edges connected to compound nodes can not be haystacks
parsedProp.value === 'haystack'
&& ( ele.source().isParent() || ele.target().isParent() )
)
)
){
prop = parsedProp = this.parse( parsedProp.name, 'bezier', propIsBypass );
}
if( prop.delete ){ // delete the property and use the default value on falsey value
style[ prop.name ] = undefined;
checkTriggers();
return true;
}
if( prop.deleteBypassed ){ // delete the property that the
if( !origProp ){
checkTriggers();
return true; // can't delete if no prop
} else if( origProp.bypass ){ // delete bypassed
origProp.bypassed = undefined;
checkTriggers();
return true;
} else {
return false; // we're unsuccessful deleting the bypassed
}
}
// check if we need to delete the current bypass
if( prop.deleteBypass ){ // then this property is just here to indicate we need to delete
if( !origProp ){
checkTriggers();
return true; // property is already not defined
} else if( origProp.bypass ){ // then replace the bypass property with the original
// because the bypassed property was already applied (and therefore parsed), we can just replace it (no reapplying necessary)
style[ prop.name ] = origProp.bypassed;
checkTriggers();
return true;
} else {
return false; // we're unsuccessful deleting the bypass
}
}
let printMappingErr = function(){
util.warn( 'Do not assign mappings to elements without corresponding data (i.e. ele `' + ele.id() + '` has no mapping for property `' + prop.name + '` with data field `' + prop.field + '`); try a `[' + prop.field + ']` selector to limit scope to elements with `' + prop.field + '` defined' );
};
// put the property in the style objects
switch( prop.mapped ){ // flatten the property if mapped
case types.mapData: {
// flatten the field (e.g. data.foo.bar)
let fields = prop.field.split( '.' );
let fieldVal = _p.data;
for( let i = 0; i < fields.length && fieldVal; i++ ){
let field = fields[ i ];
fieldVal = fieldVal[ field ];
}
if( fieldVal == null ){
printMappingErr();
return false;
}
let percent;
if( !is.number( fieldVal ) ){ // then don't apply and fall back on the existing style
util.warn('Do not use continuous mappers without specifying numeric data (i.e. `' + prop.field + ': ' + fieldVal + '` for `' + ele.id() + '` is non-numeric)');
return false;
} else {
let fieldWidth = prop.fieldMax - prop.fieldMin;
if( fieldWidth === 0 ){ // safety check -- not strictly necessary as no props of zero range should be passed here
percent = 0;
} else {
percent = (fieldVal - prop.fieldMin) / fieldWidth;
}
}
// make sure to bound percent value
if( percent < 0 ){
percent = 0;
} else if( percent > 1 ){
percent = 1;
}
if( type.color ){
let r1 = prop.valueMin[0];
let r2 = prop.valueMax[0];
let g1 = prop.valueMin[1];
let g2 = prop.valueMax[1];
let b1 = prop.valueMin[2];
let b2 = prop.valueMax[2];
let a1 = prop.valueMin[3] == null ? 1 : prop.valueMin[3];
let a2 = prop.valueMax[3] == null ? 1 : prop.valueMax[3];
let clr = [
Math.round( r1 + (r2 - r1) * percent ),
Math.round( g1 + (g2 - g1) * percent ),
Math.round( b1 + (b2 - b1) * percent ),
Math.round( a1 + (a2 - a1) * percent )
];
flatProp = { // colours are simple, so just create the flat property instead of expensive string parsing
bypass: prop.bypass, // we're a bypass if the mapping property is a bypass
name: prop.name,
value: clr,
strValue: 'rgb(' + clr[0] + ', ' + clr[1] + ', ' + clr[2] + ')'
};
} else if( type.number ){
let calcValue = prop.valueMin + (prop.valueMax - prop.valueMin) * percent;
flatProp = this.parse( prop.name, calcValue, prop.bypass, flatPropMapping );
} else {
return false; // can only map to colours and numbers
}
if( !flatProp ){ // if we can't flatten the property, then don't apply the property and fall back on the existing style
printMappingErr();
return false;
}
flatProp.mapping = prop; // keep a reference to the mapping
prop = flatProp; // the flattened (mapped) property is the one we want
break;
}
// direct mapping
case types.data: {
// flatten the field (e.g. data.foo.bar)
let fields = prop.field.split( '.' );
let fieldVal = _p.data;
for( let i = 0; i < fields.length && fieldVal; i++ ){
let field = fields[ i ];
fieldVal = fieldVal[ field ];
}
if( fieldVal != null ){
flatProp = this.parse( prop.name, fieldVal, prop.bypass, flatPropMapping );
}
if( !flatProp ){ // if we can't flatten the property, then don't apply and fall back on the existing style
printMappingErr();
return false;
}
flatProp.mapping = prop; // keep a reference to the mapping
prop = flatProp; // the flattened (mapped) property is the one we want
break;
}
case types.fn: {
let fn = prop.value;
let fnRetVal = prop.fnValue != null ? prop.fnValue : fn( ele ); // check for cached value before calling function
prop.prevFnValue = fnRetVal;
if( fnRetVal == null ){
util.warn('Custom function mappers may not return null (i.e. `' + prop.name + '` for ele `' + ele.id() + '` is null)');
return false;
}
flatProp = this.parse( prop.name, fnRetVal, prop.bypass, flatPropMapping );
if( !flatProp ){
util.warn('Custom function mappers may not return invalid values for the property type (i.e. `' + prop.name + '` for ele `' + ele.id() + '` is invalid)');
return false;
}
flatProp.mapping = util.copy( prop ); // keep a reference to the mapping
prop = flatProp; // the flattened (mapped) property is the one we want
break;
}
case undefined:
break; // just set the property
default:
return false; // not a valid mapping
}
// if the property is a bypass property, then link the resultant property to the original one
if( propIsBypass ){
if( origPropIsBypass ){ // then this bypass overrides the existing one
prop.bypassed = origProp.bypassed; // steal bypassed prop from old bypass
} else { // then link the orig prop to the new bypass
prop.bypassed = origProp;
}
style[ prop.name ] = prop; // and set
} else { // prop is not bypass
if( origPropIsBypass ){ // then keep the orig prop (since it's a bypass) and link to the new prop
origProp.bypassed = prop;
} else { // then just replace the old prop with the new one
style[ prop.name ] = prop;
}
}
checkTriggers();
return true;
};
styfn.cleanElements = function( eles, keepBypasses ){
for( let i = 0; i < eles.length; i++ ){
let ele = eles[i];
this.clearStyleHints(ele);
ele.dirtyCompoundBoundsCache();
ele.dirtyBoundingBoxCache();
if( !keepBypasses ){
ele._private.style = {};
} else {
let style = ele._private.style;
let propNames = Object.keys(style);
for( let j = 0; j < propNames.length; j++ ){
let propName = propNames[j];
let eleProp = style[ propName ];
if( eleProp != null ){
if( eleProp.bypass ){
eleProp.bypassed = null;
} else {
style[ propName ] = null;
}
}
}
}
}
};
// updates the visual style for all elements (useful for manual style modification after init)
styfn.update = function(){
let cy = this._private.cy;
let eles = cy.mutableElements();
eles.updateStyle();
};
// diffProps : { name => { prev, next } }
styfn.updateTransitions = function( ele, diffProps ){
let self = this;
let _p = ele._private;
let props = ele.pstyle( 'transition-property' ).value;
let duration = ele.pstyle( 'transition-duration' ).pfValue;
let delay = ele.pstyle( 'transition-delay' ).pfValue;
if( props.length > 0 && duration > 0 ){
let style = {};
// build up the style to animate towards
let anyPrev = false;
for( let i = 0; i < props.length; i++ ){
let prop = props[ i ];
let styProp = ele.pstyle( prop );
let diffProp = diffProps[ prop ];
if( !diffProp ){ continue; }
let prevProp = diffProp.prev;
let fromProp = prevProp;
let toProp = diffProp.next != null ? diffProp.next : styProp;
let diff = false;
let initVal;
let initDt = 0.000001; // delta time % value for initVal (allows animating out of init zero opacity)
if( !fromProp ){ continue; }
// consider px values
if( is.number( fromProp.pfValue ) && is.number( toProp.pfValue ) ){
diff = toProp.pfValue - fromProp.pfValue; // nonzero is truthy
initVal = fromProp.pfValue + initDt * diff;
// consider numerical values
} else if( is.number( fromProp.value ) && is.number( toProp.value ) ){
diff = toProp.value - fromProp.value; // nonzero is truthy
initVal = fromProp.value + initDt * diff;
// consider colour values
} else if( is.array( fromProp.value ) && is.array( toProp.value ) ){
diff = fromProp.value[0] !== toProp.value[0]
|| fromProp.value[1] !== toProp.value[1]
|| fromProp.value[2] !== toProp.value[2]
;
initVal = fromProp.strValue;
}
// the previous value is good for an animation only if it's different
if( diff ){
style[ prop ] = toProp.strValue; // to val
this.applyBypass( ele, prop, initVal ); // from val
anyPrev = true;
}
} // end if props allow ani
// can't transition if there's nothing previous to transition from
if( !anyPrev ){ return; }
_p.transitioning = true;
( new Promise(function( resolve ){
if( delay > 0 ){
ele.delayAnimation( delay ).play().promise().then( resolve );
} else {
resolve();
}
}) ).then(function(){
return ele.animation( {
style: style,
duration: duration,
easing: ele.pstyle( 'transition-timing-function' ).value,
queue: false
} ).play().promise();
}).then(function(){
// if( !isBypass ){
self.removeBypasses( ele, props );
ele.emitAndNotify('style');
// }
_p.transitioning = false;
});
} else if( _p.transitioning ){
this.removeBypasses( ele, props );
ele.emitAndNotify('style');
_p.transitioning = false;
}
};
styfn.checkTrigger = function( ele, name, fromValue, toValue, getTrigger, onTrigger ){
let prop = this.properties[ name ];
let triggerCheck = getTrigger( prop );
if (ele.removed()) { return; }
if( triggerCheck != null && triggerCheck( fromValue, toValue, ele ) ){
onTrigger(prop);
}
};
styfn.checkZOrderTrigger = function( ele, name, fromValue, toValue ){
this.checkTrigger( ele, name, fromValue, toValue, prop => prop.triggersZOrder, () => {
this._private.cy.notify('zorder', ele);
});
};
styfn.checkBoundsTrigger = function( ele, name, fromValue, toValue ){
this.checkTrigger( ele, name, fromValue, toValue, prop => prop.triggersBounds, prop => {
ele.dirtyCompoundBoundsCache();
ele.dirtyBoundingBoxCache();
});
};
styfn.checkConnectedEdgesBoundsTrigger = function( ele, name, fromValue, toValue ){
this.checkTrigger( ele, name, fromValue, toValue, prop => prop.triggersBoundsOfConnectedEdges, prop => {
ele.connectedEdges().forEach(edge => {
edge.dirtyBoundingBoxCache();
});
});
};
styfn.checkParallelEdgesBoundsTrigger = function( ele, name, fromValue, toValue ){
this.checkTrigger( ele, name, fromValue, toValue, prop => prop.triggersBoundsOfParallelEdges, prop => {
ele.parallelEdges().forEach(pllEdge => {
pllEdge.dirtyBoundingBoxCache();
});
});
};
styfn.checkTriggers = function( ele, name, fromValue, toValue ){
ele.dirtyStyleCache();
this.checkZOrderTrigger( ele, name, fromValue, toValue );
this.checkBoundsTrigger( ele, name, fromValue, toValue );
this.checkConnectedEdgesBoundsTrigger( ele, name, fromValue, toValue );
this.checkParallelEdgesBoundsTrigger( ele, name, fromValue, toValue );
};
export default styfn;

204
frontend/node_modules/cytoscape/src/style/index.mjs generated vendored Normal file
View File

@@ -0,0 +1,204 @@
import * as is from '../is.mjs';
import * as util from '../util/index.mjs';
import Selector from '../selector/index.mjs';
import apply from './apply.mjs';
import bypass from './bypass.mjs';
import container from './container.mjs';
import getForEle from './get-for-ele.mjs';
import json from './json.mjs';
import stringSheet from './string-sheet.mjs';
import properties from './properties.mjs';
import parse from './parse.mjs';
let Style = function( cy ){
if( !(this instanceof Style) ){
return new Style( cy );
}
if( !is.core( cy ) ){
util.error( 'A style must have a core reference' );
return;
}
this._private = {
cy: cy,
coreStyle: {}
};
this.length = 0;
this.resetToDefault();
};
let styfn = Style.prototype;
styfn.instanceString = function(){
return 'style';
};
// remove all contexts
styfn.clear = function(){
let _p = this._private;
let cy = _p.cy;
let eles = cy.elements();
for( let i = 0; i < this.length; i++ ){
this[ i ] = undefined;
}
this.length = 0;
_p.contextStyles = {};
_p.propDiffs = {};
this.cleanElements( eles, true );
eles.forEach(ele => {
let ele_p = ele[0]._private;
ele_p.styleDirty = true;
ele_p.appliedInitStyle = false;
});
return this; // chaining
};
styfn.resetToDefault = function(){
this.clear();
this.addDefaultStylesheet();
return this;
};
// builds a style object for the 'core' selector
styfn.core = function( propName ){
return this._private.coreStyle[ propName ] || this.getDefaultProperty( propName );
};
// create a new context from the specified selector string and switch to that context
styfn.selector = function( selectorStr ){
// 'core' is a special case and does not need a selector
let selector = selectorStr === 'core' ? null : new Selector( selectorStr );
let i = this.length++; // new context means new index
this[ i ] = {
selector: selector,
properties: [],
mappedProperties: [],
index: i
};
return this; // chaining
};
// add one or many css rules to the current context
styfn.css = function(){
let self = this;
let args = arguments;
if( args.length === 1 ){
let map = args[0];
for( let i = 0; i < self.properties.length; i++ ){
let prop = self.properties[ i ];
let mapVal = map[ prop.name ];
if( mapVal === undefined ){
mapVal = map[ util.dash2camel( prop.name ) ];
}
if( mapVal !== undefined ){
this.cssRule( prop.name, mapVal );
}
}
} else if( args.length === 2 ){
this.cssRule( args[0], args[1] );
}
// do nothing if args are invalid
return this; // chaining
};
styfn.style = styfn.css;
// add a single css rule to the current context
styfn.cssRule = function( name, value ){
// name-value pair
let property = this.parse( name, value );
// add property to current context if valid
if( property ){
let i = this.length - 1;
this[ i ].properties.push( property );
this[ i ].properties[ property.name ] = property; // allow access by name as well
if( property.name.match( /pie-(\d+)-background-size/ ) && property.value ){
this._private.hasPie = true;
}
if( property.name.match( /stripe-(\d+)-background-size/ ) && property.value ){
this._private.hasStripe = true;
}
if( property.mapped ){
this[ i ].mappedProperties.push( property );
}
// add to core style if necessary
let currentSelectorIsCore = !this[ i ].selector;
if( currentSelectorIsCore ){
this._private.coreStyle[ property.name ] = property;
}
}
return this; // chaining
};
styfn.append = function( style ){
if( is.stylesheet( style ) ){
style.appendToStyle( this );
} else if( is.array( style ) ){
this.appendFromJson( style );
} else if( is.string( style ) ){
this.appendFromString( style );
} // you probably wouldn't want to append a Style, since you'd duplicate the default parts
return this;
};
// static function
Style.fromJson = function( cy, json ){
let style = new Style( cy );
style.fromJson( json );
return style;
};
Style.fromString = function( cy, string ){
return new Style( cy ).fromString( string );
};
[
apply,
bypass,
container,
getForEle,
json,
stringSheet,
properties,
parse
].forEach( function( props ){
util.extend( styfn, props );
} );
Style.types = styfn.types;
Style.properties = styfn.properties;
Style.propertyGroups = styfn.propertyGroups;
Style.propertyGroupNames = styfn.propertyGroupNames;
Style.propertyGroupKeys = styfn.propertyGroupKeys;
export default Style;

59
frontend/node_modules/cytoscape/src/style/json.mjs generated vendored Normal file
View File

@@ -0,0 +1,59 @@
let styfn = {};
styfn.appendFromJson = function( json ){
let style = this;
for( let i = 0; i < json.length; i++ ){
let context = json[ i ];
let selector = context.selector;
let props = context.style || context.css;
let names = Object.keys( props );
style.selector( selector ); // apply selector
for( let j = 0; j < names.length; j++ ){
let name = names[j];
let value = props[ name ];
style.css( name, value ); // apply property
}
}
return style;
};
// accessible cy.style() function
styfn.fromJson = function( json ){
let style = this;
style.resetToDefault();
style.appendFromJson( json );
return style;
};
// get json from cy.style() api
styfn.json = function(){
let json = [];
for( let i = this.defaultLength; i < this.length; i++ ){
let cxt = this[ i ];
let selector = cxt.selector;
let props = cxt.properties;
let css = {};
for( let j = 0; j < props.length; j++ ){
let prop = props[ j ];
css[ prop.name ] = prop.strValue;
}
json.push( {
selector: !selector ? 'core' : selector.toString(),
style: css
} );
}
return json;
};
export default styfn;

435
frontend/node_modules/cytoscape/src/style/parse.mjs generated vendored Normal file
View File

@@ -0,0 +1,435 @@
import * as util from '../util/index.mjs';
import * as is from '../is.mjs';
import * as math from '../math.mjs';
let styfn = {};
// a caching layer for property parsing
styfn.parse = function( name, value, propIsBypass, propIsFlat ){
let self = this;
// function values can't be cached in all cases, and there isn't much benefit of caching them anyway
if( is.fn( value ) ){
return self.parseImplWarn( name, value, propIsBypass, propIsFlat );
}
let flatKey = ( propIsFlat === 'mapping' || propIsFlat === true || propIsFlat === false || propIsFlat == null ) ? 'dontcare' : propIsFlat;
let bypassKey = propIsBypass ? 't' : 'f';
let valueKey = '' + value;
let argHash = util.hashStrings( name, valueKey, bypassKey, flatKey );
let propCache = self.propCache = self.propCache || [];
let ret;
if( !(ret = propCache[ argHash ]) ){
ret = propCache[ argHash ] = self.parseImplWarn( name, value, propIsBypass, propIsFlat );
}
// - bypasses can't be shared b/c the value can be changed by animations or otherwise overridden
// - mappings can't be shared b/c mappings are per-element
if( propIsBypass || propIsFlat === 'mapping' ){
// need a copy since props are mutated later in their lifecycles
ret = util.copy( ret );
if( ret ){
ret.value = util.copy( ret.value ); // because it could be an array, e.g. colour
}
}
return ret;
};
styfn.parseImplWarn = function( name, value, propIsBypass, propIsFlat ){
let prop = this.parseImpl( name, value, propIsBypass, propIsFlat );
if( !prop && value != null ){
util.warn(`The style property \`${name}: ${value}\` is invalid`);
}
if( prop && (prop.name === 'width' || prop.name === 'height') && value === 'label' ){
util.warn('The style value of `label` is deprecated for `' + prop.name + '`');
}
return prop;
};
// parse a property; return null on invalid; return parsed property otherwise
// fields :
// - name : the name of the property
// - value : the parsed, native-typed value of the property
// - strValue : a string value that represents the property value in valid css
// - bypass : true iff the property is a bypass property
styfn.parseImpl = function( name, value, propIsBypass, propIsFlat ){
let self = this;
name = util.camel2dash( name ); // make sure the property name is in dash form (e.g. 'property-name' not 'propertyName')
let property = self.properties[ name ];
let passedValue = value;
let types = self.types;
if( !property ){ return null; } // return null on property of unknown name
if( value === undefined ){ return null; } // can't assign undefined
// the property may be an alias
if( property.alias ){
property = property.pointsTo;
name = property.name;
}
let valueIsString = is.string( value );
if( valueIsString ){ // trim the value to make parsing easier
value = value.trim();
}
let type = property.type;
if( !type ){ return null; } // no type, no luck
// check if bypass is null or empty string (i.e. indication to delete bypass property)
if( propIsBypass && (value === '' || value === null) ){
return {
name: name,
value: value,
bypass: true,
deleteBypass: true
};
}
// check if value is a function used as a mapper
if( is.fn( value ) ){
return {
name: name,
value: value,
strValue: 'fn',
mapped: types.fn,
bypass: propIsBypass
};
}
// check if value is mapped
let data, mapData;
if( !valueIsString || propIsFlat || value.length < 7 || value[1] !== 'a' ){
// then don't bother to do the expensive regex checks
} else if(value.length >= 7 && value[0] === 'd' && ( data = new RegExp( types.data.regex ).exec( value ) )){
if( propIsBypass ){ return false; } // mappers not allowed in bypass
let mapped = types.data;
return {
name: name,
value: data,
strValue: '' + value,
mapped: mapped,
field: data[1],
bypass: propIsBypass
};
} else if(value.length >= 10 && value[0] === 'm' && ( mapData = new RegExp( types.mapData.regex ).exec( value ) )){
if( propIsBypass ){ return false; } // mappers not allowed in bypass
if( type.multiple ){ return false; } // impossible to map to num
let mapped = types.mapData;
// we can map only if the type is a colour or a number
if( !(type.color || type.number) ){ return false; }
let valueMin = this.parse( name, mapData[4] ); // parse to validate
if( !valueMin || valueMin.mapped ){ return false; } // can't be invalid or mapped
let valueMax = this.parse( name, mapData[5] ); // parse to validate
if( !valueMax || valueMax.mapped ){ return false; } // can't be invalid or mapped
// check if valueMin and valueMax are the same
if( valueMin.pfValue === valueMax.pfValue || valueMin.strValue === valueMax.strValue ){
util.warn('`' + name + ': ' + value + '` is not a valid mapper because the output range is zero; converting to `' + name + ': ' + valueMin.strValue + '`');
return this.parse(name, valueMin.strValue); // can't make much of a mapper without a range
} else if( type.color ){
let c1 = valueMin.value;
let c2 = valueMax.value;
let same = c1[0] === c2[0] // red
&& c1[1] === c2[1] // green
&& c1[2] === c2[2] // blue
&& ( // optional alpha
c1[3] === c2[3] // same alpha outright
|| (
(c1[3] == null || c1[3] === 1) // full opacity for colour 1?
&&
(c2[3] == null || c2[3] === 1) // full opacity for colour 2?
)
)
;
if( same ){ return false; } // can't make a mapper without a range
}
return {
name: name,
value: mapData,
strValue: '' + value,
mapped: mapped,
field: mapData[1],
fieldMin: parseFloat( mapData[2] ), // min & max are numeric
fieldMax: parseFloat( mapData[3] ),
valueMin: valueMin.value,
valueMax: valueMax.value,
bypass: propIsBypass
};
}
if( type.multiple && propIsFlat !== 'multiple' ){
let vals;
if( valueIsString ){
vals = value.split( /\s+/ );
} else if( is.array( value ) ){
vals = value;
} else {
vals = [ value ];
}
if( type.evenMultiple && vals.length % 2 !== 0 ){ return null; }
let valArr = [];
let unitsArr = [];
let pfValArr = [];
let strVal = '';
let hasEnum = false;
for( let i = 0; i < vals.length; i++ ){
let p = self.parse( name, vals[i], propIsBypass, 'multiple' );
hasEnum = hasEnum || is.string( p.value );
valArr.push( p.value );
pfValArr.push( p.pfValue != null ? p.pfValue : p.value );
unitsArr.push( p.units );
strVal += (i > 0 ? ' ' : '') + p.strValue;
}
if( type.validate && !type.validate( valArr, unitsArr ) ){
return null;
}
if( type.singleEnum && hasEnum ){
if( valArr.length === 1 && is.string( valArr[0] ) ){
return {
name: name,
value: valArr[0],
strValue: valArr[0],
bypass: propIsBypass
};
} else {
return null;
}
}
return {
name: name,
value: valArr,
pfValue: pfValArr,
strValue: strVal,
bypass: propIsBypass,
units: unitsArr
};
}
// several types also allow enums
let checkEnums = function(){
for( let i = 0; i < type.enums.length; i++ ){
let en = type.enums[ i ];
if( en === value ){
return {
name: name,
value: value,
strValue: '' + value,
bypass: propIsBypass
};
}
}
return null;
};
// check the type and return the appropriate object
if( type.number ){
let units;
let implicitUnits = 'px'; // not set => px
if( type.units ){ // use specified units if set
units = type.units;
}
if( type.implicitUnits ){
implicitUnits = type.implicitUnits;
}
if( !type.unitless ){
if( valueIsString ){
let unitsRegex = 'px|em' + (type.allowPercent ? '|\\%' : '');
if( units ){ unitsRegex = units; } // only allow explicit units if so set
let match = value.match( '^(' + util.regex.number + ')(' + unitsRegex + ')?' + '$' );
if( match ){
value = match[1];
units = match[2] || implicitUnits;
}
} else if( !units || type.implicitUnits ){
units = implicitUnits; // implicitly px if unspecified
}
}
value = parseFloat( value );
// if not a number and enums not allowed, then the value is invalid
if( isNaN( value ) && type.enums === undefined ){
return null;
}
// check if this number type also accepts special keywords in place of numbers
// (i.e. `left`, `auto`, etc)
if( isNaN( value ) && type.enums !== undefined ){
value = passedValue;
return checkEnums();
}
// check if value must be an integer
if( type.integer && !is.integer( value ) ){
return null;
}
// check value is within range
if( ( type.min !== undefined && ( value < type.min || (type.strictMin && value === type.min) ) )
|| ( type.max !== undefined && ( value > type.max || (type.strictMax && value === type.max) ) )
){
return null;
}
let ret = {
name: name,
value: value,
strValue: '' + value + (units ? units : ''),
units: units,
bypass: propIsBypass
};
// normalise value in pixels
if( type.unitless || (units !== 'px' && units !== 'em') ){
ret.pfValue = value;
} else {
ret.pfValue = ( units === 'px' || !units ? (value) : (this.getEmSizeInPixels() * value) );
}
// normalise value in ms
if( units === 'ms' || units === 's' ){
ret.pfValue = units === 'ms' ? value : 1000 * value;
}
// normalise value in rad
if( units === 'deg' || units === 'rad' ){
ret.pfValue = units === 'rad' ? value : math.deg2rad( value );
}
// normalize value in %
if( units === '%' ){
ret.pfValue = value / 100;
}
return ret;
} else if( type.propList ){
let props = [];
let propsStr = '' + value;
if( propsStr === 'none' ){
// leave empty
} else { // go over each prop
let propsSplit = propsStr.split( /\s*,\s*|\s+/ );
for( let i = 0; i < propsSplit.length; i++ ){
let propName = propsSplit[ i ].trim();
if( self.properties[ propName ] ){
props.push( propName );
} else {
util.warn('`' + propName + '` is not a valid property name');
}
}
if( props.length === 0 ){ return null; }
}
return {
name: name,
value: props,
strValue: props.length === 0 ? 'none' : props.join(' '),
bypass: propIsBypass
};
} else if( type.color ){
let tuple = util.color2tuple( value );
if( !tuple ){ return null; }
return {
name: name,
value: tuple,
pfValue: tuple,
strValue: 'rgb(' + tuple[0] + ',' + tuple[1] + ',' + tuple[2] + ')', // n.b. no spaces b/c of multiple support
bypass: propIsBypass
};
} else if( type.regex || type.regexes ){
// first check enums
if( type.enums ){
let enumProp = checkEnums();
if( enumProp ){ return enumProp; }
}
let regexes = type.regexes ? type.regexes : [ type.regex ];
for( let i = 0; i < regexes.length; i++ ){
let regex = new RegExp( regexes[ i ] ); // make a regex from the type string
let m = regex.exec( value );
if( m ){ // regex matches
return {
name: name,
value: type.singleRegexMatchValue ? m[1] : m,
strValue: '' + value,
bypass: propIsBypass
};
}
}
return null; // didn't match any
} else if( type.string ){
// just return
return {
name: name,
value: '' + value,
strValue: '' + value,
bypass: propIsBypass
};
} else if( type.enums ){ // check enums last because it's a combo type in others
return checkEnums();
} else {
return null; // not a type we can handle
}
};
export default styfn;

97
frontend/node_modules/cytoscape/src/stylesheet.mjs generated vendored Normal file
View File

@@ -0,0 +1,97 @@
import * as is from './is.mjs';
import Style from './style/index.mjs';
import { dash2camel } from './util/index.mjs';
// a dummy stylesheet object that doesn't need a reference to the core
// (useful for init)
let Stylesheet = function(){
if( !(this instanceof Stylesheet) ){
return new Stylesheet();
}
this.length = 0;
};
let sheetfn = Stylesheet.prototype;
sheetfn.instanceString = function(){
return 'stylesheet';
};
// just store the selector to be parsed later
sheetfn.selector = function( selector ){
let i = this.length++;
this[ i ] = {
selector: selector,
properties: []
};
return this; // chaining
};
// just store the property to be parsed later
sheetfn.css = function( name, value ){
let i = this.length - 1;
if( is.string( name ) ){
this[ i ].properties.push( {
name: name,
value: value
} );
} else if( is.plainObject( name ) ){
let map = name;
let propNames = Object.keys( map );
for( let j = 0; j < propNames.length; j++ ){
let key = propNames[ j ];
let mapVal = map[ key ];
if( mapVal == null ){ continue; }
let prop = Style.properties[key] || Style.properties[dash2camel(key)];
if( prop == null ){ continue; }
let name = prop.name;
let value = mapVal;
this[ i ].properties.push( {
name: name,
value: value
} );
}
}
return this; // chaining
};
sheetfn.style = sheetfn.css;
// generate a real style object from the dummy stylesheet
sheetfn.generateStyle = function( cy ){
let style = new Style( cy );
return this.appendToStyle( style );
};
// append a dummy stylesheet object on a real style object
sheetfn.appendToStyle = function( style ){
for( let i = 0; i < this.length; i++ ){
let context = this[ i ];
let selector = context.selector;
let props = context.properties;
style.selector( selector ); // apply selector
for( let j = 0; j < props.length; j++ ){
let prop = props[ j ];
style.css( prop.name, prop.value ); // apply property
}
}
return style;
};
export default Stylesheet;

18
frontend/node_modules/cytoscape/src/test.mjs generated vendored Normal file
View File

@@ -0,0 +1,18 @@
/*
This file tells the Mocha tests what build of Cytoscape to use.
*/
// For manual build tests, use the ESM build
// NB : Must do `npm run build` before `npm test`
let cytoscape;
if (process.env.TEST_BUILD) {
// Dynamically import the ESM build
cytoscape = await import('../build/cytoscape.esm.mjs'); // Assuming the ESM build uses `.esm.js`
} else {
// Dynamically import the unbundled, unbabelified raw source
cytoscape = await import('./index.mjs');
}
// Export the module (adjust based on whether you need default or named exports)
export default cytoscape.default || cytoscape;

290
frontend/node_modules/cytoscape/src/util/colors.mjs generated vendored Normal file
View File

@@ -0,0 +1,290 @@
import * as is from '../is.mjs';
import * as regex from './regex.mjs';
// get [r, g, b] from #abc or #aabbcc
export const hex2tuple = hex => {
if( !(hex.length === 4 || hex.length === 7) || hex[0] !== '#' ){ return; }
let shortHex = hex.length === 4;
let r, g, b;
let base = 16;
if( shortHex ){
r = parseInt( hex[1] + hex[1], base );
g = parseInt( hex[2] + hex[2], base );
b = parseInt( hex[3] + hex[3], base );
} else {
r = parseInt( hex[1] + hex[2], base );
g = parseInt( hex[3] + hex[4], base );
b = parseInt( hex[5] + hex[6], base );
}
return [ r, g, b ];
};
// get [r, g, b, a] from hsl(0, 0, 0) or hsla(0, 0, 0, 0)
export const hsl2tuple = hsl => {
let ret;
let h, s, l, a, r, g, b;
function hue2rgb( p, q, t ){
if( t < 0 ) t += 1;
if( t > 1 ) t -= 1;
if( t < 1 / 6 ) return p + (q - p) * 6 * t;
if( t < 1 / 2 ) return q;
if( t < 2 / 3 ) return p + (q - p) * (2 / 3 - t) * 6;
return p;
}
let m = new RegExp( '^' + regex.hsla + '$' ).exec( hsl );
if( m ){
// get hue
h = parseInt( m[1] );
if( h < 0 ){
h = ( 360 - (-1 * h % 360) ) % 360;
} else if( h > 360 ){
h = h % 360;
}
h /= 360; // normalise on [0, 1]
s = parseFloat( m[2] );
if( s < 0 || s > 100 ){ return; } // saturation is [0, 100]
s = s / 100; // normalise on [0, 1]
l = parseFloat( m[3] );
if( l < 0 || l > 100 ){ return; } // lightness is [0, 100]
l = l / 100; // normalise on [0, 1]
a = m[4];
if( a !== undefined ){
a = parseFloat( a );
if( a < 0 || a > 1 ){ return; } // alpha is [0, 1]
}
// now, convert to rgb
// code from http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript
if( s === 0 ){
r = g = b = Math.round( l * 255 ); // achromatic
} else {
let q = l < 0.5 ? l * (1 + s) : l + s - l * s;
let p = 2 * l - q;
r = Math.round( 255 * hue2rgb( p, q, h + 1 / 3 ) );
g = Math.round( 255 * hue2rgb( p, q, h ) );
b = Math.round( 255 * hue2rgb( p, q, h - 1 / 3 ) );
}
ret = [ r, g, b, a ];
}
return ret;
};
// get [r, g, b, a] from rgb(0, 0, 0) or rgba(0, 0, 0, 0)
export const rgb2tuple = rgb => {
let ret;
let m = new RegExp( '^' + regex.rgba + '$' ).exec( rgb );
if( m ){
ret = [];
let isPct = [];
for( let i = 1; i <= 3; i++ ){
let channel = m[ i ];
if( channel[ channel.length - 1 ] === '%' ){
isPct[ i ] = true;
}
channel = parseFloat( channel );
if( isPct[ i ] ){
channel = channel / 100 * 255; // normalise to [0, 255]
}
if( channel < 0 || channel > 255 ){ return; } // invalid channel value
ret.push( Math.floor( channel ) );
}
let atLeastOneIsPct = isPct[1] || isPct[2] || isPct[3];
let allArePct = isPct[1] && isPct[2] && isPct[3];
if( atLeastOneIsPct && !allArePct ){ return; } // must all be percent values if one is
let alpha = m[4];
if( alpha !== undefined ){
alpha = parseFloat( alpha );
if( alpha < 0 || alpha > 1 ){ return; } // invalid alpha value
ret.push( alpha );
}
}
return ret;
};
export const colorname2tuple = color => {
return colors[ color.toLowerCase() ];
};
export const color2tuple = color => {
return ( is.array( color ) ? color : null )
|| colorname2tuple( color )
|| hex2tuple( color )
|| rgb2tuple( color )
|| hsl2tuple( color );
};
export const colors = {
// special colour names
transparent: [0, 0, 0, 0], // NB alpha === 0
// regular colours
aliceblue: [ 240, 248, 255 ],
antiquewhite: [ 250, 235, 215 ],
aqua: [0, 255, 255 ],
aquamarine: [ 127, 255, 212 ],
azure: [ 240, 255, 255 ],
beige: [ 245, 245, 220 ],
bisque: [ 255, 228, 196 ],
black: [0, 0, 0],
blanchedalmond: [ 255, 235, 205 ],
blue: [0, 0, 255 ],
blueviolet: [ 138, 43, 226 ],
brown: [ 165, 42, 42 ],
burlywood: [ 222, 184, 135 ],
cadetblue: [ 95, 158, 160 ],
chartreuse: [ 127, 255, 0],
chocolate: [ 210, 105, 30 ],
coral: [ 255, 127, 80 ],
cornflowerblue: [ 100, 149, 237 ],
cornsilk: [ 255, 248, 220 ],
crimson: [ 220, 20, 60 ],
cyan: [0, 255, 255 ],
darkblue: [0, 0, 139 ],
darkcyan: [0, 139, 139 ],
darkgoldenrod: [ 184, 134, 11 ],
darkgray: [ 169, 169, 169 ],
darkgreen: [0, 100, 0],
darkgrey: [ 169, 169, 169 ],
darkkhaki: [ 189, 183, 107 ],
darkmagenta: [ 139, 0, 139 ],
darkolivegreen: [ 85, 107, 47 ],
darkorange: [ 255, 140, 0],
darkorchid: [ 153, 50, 204 ],
darkred: [ 139, 0, 0],
darksalmon: [ 233, 150, 122 ],
darkseagreen: [ 143, 188, 143 ],
darkslateblue: [ 72, 61, 139 ],
darkslategray: [ 47, 79, 79 ],
darkslategrey: [ 47, 79, 79 ],
darkturquoise: [0, 206, 209 ],
darkviolet: [ 148, 0, 211 ],
deeppink: [ 255, 20, 147 ],
deepskyblue: [0, 191, 255 ],
dimgray: [ 105, 105, 105 ],
dimgrey: [ 105, 105, 105 ],
dodgerblue: [ 30, 144, 255 ],
firebrick: [ 178, 34, 34 ],
floralwhite: [ 255, 250, 240 ],
forestgreen: [ 34, 139, 34 ],
fuchsia: [ 255, 0, 255 ],
gainsboro: [ 220, 220, 220 ],
ghostwhite: [ 248, 248, 255 ],
gold: [ 255, 215, 0],
goldenrod: [ 218, 165, 32 ],
gray: [ 128, 128, 128 ],
grey: [ 128, 128, 128 ],
green: [0, 128, 0],
greenyellow: [ 173, 255, 47 ],
honeydew: [ 240, 255, 240 ],
hotpink: [ 255, 105, 180 ],
indianred: [ 205, 92, 92 ],
indigo: [ 75, 0, 130 ],
ivory: [ 255, 255, 240 ],
khaki: [ 240, 230, 140 ],
lavender: [ 230, 230, 250 ],
lavenderblush: [ 255, 240, 245 ],
lawngreen: [ 124, 252, 0],
lemonchiffon: [ 255, 250, 205 ],
lightblue: [ 173, 216, 230 ],
lightcoral: [ 240, 128, 128 ],
lightcyan: [ 224, 255, 255 ],
lightgoldenrodyellow: [ 250, 250, 210 ],
lightgray: [ 211, 211, 211 ],
lightgreen: [ 144, 238, 144 ],
lightgrey: [ 211, 211, 211 ],
lightpink: [ 255, 182, 193 ],
lightsalmon: [ 255, 160, 122 ],
lightseagreen: [ 32, 178, 170 ],
lightskyblue: [ 135, 206, 250 ],
lightslategray: [ 119, 136, 153 ],
lightslategrey: [ 119, 136, 153 ],
lightsteelblue: [ 176, 196, 222 ],
lightyellow: [ 255, 255, 224 ],
lime: [0, 255, 0],
limegreen: [ 50, 205, 50 ],
linen: [ 250, 240, 230 ],
magenta: [ 255, 0, 255 ],
maroon: [ 128, 0, 0],
mediumaquamarine: [ 102, 205, 170 ],
mediumblue: [0, 0, 205 ],
mediumorchid: [ 186, 85, 211 ],
mediumpurple: [ 147, 112, 219 ],
mediumseagreen: [ 60, 179, 113 ],
mediumslateblue: [ 123, 104, 238 ],
mediumspringgreen: [0, 250, 154 ],
mediumturquoise: [ 72, 209, 204 ],
mediumvioletred: [ 199, 21, 133 ],
midnightblue: [ 25, 25, 112 ],
mintcream: [ 245, 255, 250 ],
mistyrose: [ 255, 228, 225 ],
moccasin: [ 255, 228, 181 ],
navajowhite: [ 255, 222, 173 ],
navy: [0, 0, 128 ],
oldlace: [ 253, 245, 230 ],
olive: [ 128, 128, 0],
olivedrab: [ 107, 142, 35 ],
orange: [ 255, 165, 0],
orangered: [ 255, 69, 0],
orchid: [ 218, 112, 214 ],
palegoldenrod: [ 238, 232, 170 ],
palegreen: [ 152, 251, 152 ],
paleturquoise: [ 175, 238, 238 ],
palevioletred: [ 219, 112, 147 ],
papayawhip: [ 255, 239, 213 ],
peachpuff: [ 255, 218, 185 ],
peru: [ 205, 133, 63 ],
pink: [ 255, 192, 203 ],
plum: [ 221, 160, 221 ],
powderblue: [ 176, 224, 230 ],
purple: [ 128, 0, 128 ],
red: [ 255, 0, 0],
rosybrown: [ 188, 143, 143 ],
royalblue: [ 65, 105, 225 ],
saddlebrown: [ 139, 69, 19 ],
salmon: [ 250, 128, 114 ],
sandybrown: [ 244, 164, 96 ],
seagreen: [ 46, 139, 87 ],
seashell: [ 255, 245, 238 ],
sienna: [ 160, 82, 45 ],
silver: [ 192, 192, 192 ],
skyblue: [ 135, 206, 235 ],
slateblue: [ 106, 90, 205 ],
slategray: [ 112, 128, 144 ],
slategrey: [ 112, 128, 144 ],
snow: [ 255, 250, 250 ],
springgreen: [0, 255, 127 ],
steelblue: [ 70, 130, 180 ],
tan: [ 210, 180, 140 ],
teal: [0, 128, 128 ],
thistle: [ 216, 191, 216 ],
tomato: [ 255, 99, 71 ],
turquoise: [ 64, 224, 208 ],
violet: [ 238, 130, 238 ],
wheat: [ 245, 222, 179 ],
white: [ 255, 255, 255 ],
whitesmoke: [ 245, 245, 245 ],
yellow: [ 255, 255, 0],
yellowgreen: [ 154, 205, 50 ]
};