import * as is from '../../is'; import { assignBoundingBox, expandBoundingBoxSides, clearBoundingBox, expandBoundingBox, makeBoundingBox, copyBoundingBox, shiftBoundingBox, updateBoundingBox } from '../../math'; import { defaults, getPrefixedProperty, hashIntsArray } from '../../util'; let fn, elesfn; fn = elesfn = {}; elesfn.renderedBoundingBox = function( options ){ let bb = this.boundingBox( options ); let cy = this.cy(); let zoom = cy.zoom(); let pan = cy.pan(); let x1 = bb.x1 * zoom + pan.x; let x2 = bb.x2 * zoom + pan.x; let y1 = bb.y1 * zoom + pan.y; let y2 = bb.y2 * zoom + pan.y; return { x1: x1, x2: x2, y1: y1, y2: y2, w: x2 - x1, h: y2 - y1 }; }; elesfn.dirtyCompoundBoundsCache = function(silent = false){ let cy = this.cy(); if( !cy.styleEnabled() || !cy.hasCompoundNodes() ){ return this; } this.forEachUp( ele => { if( ele.isParent() ){ let _p = ele._private; _p.compoundBoundsClean = false; _p.bbCache = null; if(!silent){ ele.emitAndNotify('bounds'); } } } ); return this; }; elesfn.updateCompoundBounds = function(force = false){ let cy = this.cy(); // not possible to do on non-compound graphs or with the style disabled if( !cy.styleEnabled() || !cy.hasCompoundNodes() ){ return this; } // save cycles when batching -- but bounds will be stale (or not exist yet) if( !force && cy.batching() ){ return this; } function update( parent ){ if( !parent.isParent() ){ return; } let _p = parent._private; let children = parent.children(); let includeLabels = parent.pstyle( 'compound-sizing-wrt-labels' ).value === 'include'; let min = { width: { val: parent.pstyle( 'min-width' ).pfValue, left: parent.pstyle( 'min-width-bias-left' ), right: parent.pstyle( 'min-width-bias-right' ) }, height: { val: parent.pstyle( 'min-height' ).pfValue, top: parent.pstyle( 'min-height-bias-top' ), bottom: parent.pstyle( 'min-height-bias-bottom' ) } }; let bb = children.boundingBox( { includeLabels: includeLabels, includeOverlays: false, // updating the compound bounds happens outside of the regular // cache cycle (i.e. before fired events) useCache: false } ); let pos = _p.position; // if children take up zero area then keep position and fall back on stylesheet w/h if( bb.w === 0 || bb.h === 0 ){ bb = { w: parent.pstyle('width').pfValue, h: parent.pstyle('height').pfValue }; bb.x1 = pos.x - bb.w/2; bb.x2 = pos.x + bb.w/2; bb.y1 = pos.y - bb.h/2; bb.y2 = pos.y + bb.h/2; } function computeBiasValues( propDiff, propBias, propBiasComplement ){ let biasDiff = 0; let biasComplementDiff = 0; let biasTotal = propBias + propBiasComplement; if( propDiff > 0 && biasTotal > 0 ){ biasDiff = ( propBias / biasTotal ) * propDiff; biasComplementDiff = ( propBiasComplement / biasTotal ) * propDiff; } return { biasDiff: biasDiff, biasComplementDiff: biasComplementDiff }; } function computePaddingValues( width, height, paddingObject, relativeTo ) { // Assuming percentage is number from 0 to 1 if(paddingObject.units === '%') { switch(relativeTo) { case 'width': return width > 0 ? paddingObject.pfValue * width : 0; case 'height': return height > 0 ? paddingObject.pfValue * height : 0; case 'average': return ( width > 0 ) && ( height > 0 ) ? paddingObject.pfValue * ( width + height ) / 2 : 0; case 'min': return ( width > 0 ) && ( height > 0 ) ? ( ( width > height ) ? paddingObject.pfValue * height : paddingObject.pfValue * width ) : 0; case 'max': return ( width > 0 ) && ( height > 0 ) ? ( ( width > height ) ? paddingObject.pfValue * width : paddingObject.pfValue * height ) : 0; default: return 0; } } else if(paddingObject.units === 'px') { return paddingObject.pfValue; } else { return 0; } } let leftVal = min.width.left.value; if( min.width.left.units === 'px' && min.width.val > 0 ){ leftVal = ( leftVal * 100 ) / min.width.val; } let rightVal = min.width.right.value; if( min.width.right.units === 'px' && min.width.val > 0 ){ rightVal = ( rightVal * 100 ) / min.width.val; } let topVal = min.height.top.value; if( min.height.top.units === 'px' && min.height.val > 0 ){ topVal = ( topVal * 100 ) / min.height.val; } let bottomVal = min.height.bottom.value; if( min.height.bottom.units === 'px' && min.height.val > 0 ){ bottomVal = ( bottomVal * 100 ) / min.height.val; } let widthBiasDiffs = computeBiasValues( min.width.val - bb.w, leftVal, rightVal ); let diffLeft = widthBiasDiffs.biasDiff; let diffRight = widthBiasDiffs.biasComplementDiff; let heightBiasDiffs = computeBiasValues( min.height.val - bb.h, topVal, bottomVal ); let diffTop = heightBiasDiffs.biasDiff; let diffBottom = heightBiasDiffs.biasComplementDiff; _p.autoPadding = computePaddingValues( bb.w, bb.h, parent.pstyle( 'padding' ), parent.pstyle( 'padding-relative-to' ).value ); _p.autoWidth = Math.max(bb.w, min.width.val); pos.x = (- diffLeft + bb.x1 + bb.x2 + diffRight) / 2; _p.autoHeight = Math.max(bb.h, min.height.val); pos.y = (- diffTop + bb.y1 + bb.y2 + diffBottom) / 2; } for( let i = 0; i < this.length; i++ ){ let ele = this[i]; let _p = ele._private; if( !_p.compoundBoundsClean || force ){ update( ele ); if( !cy.batching() ){ _p.compoundBoundsClean = true; } } } return this; }; let noninf = function( x ){ if( x === Infinity || x === -Infinity ){ return 0; } return x; }; let updateBounds = function( b, x1, y1, x2, y2 ){ // don't update with zero area boxes if( x2 - x1 === 0 || y2 - y1 === 0 ){ return; } // don't update with null dim if( x1 == null || y1 == null || x2 == null || y2 == null ){ return; } b.x1 = x1 < b.x1 ? x1 : b.x1; b.x2 = x2 > b.x2 ? x2 : b.x2; b.y1 = y1 < b.y1 ? y1 : b.y1; b.y2 = y2 > b.y2 ? y2 : b.y2; b.w = b.x2 - b.x1; b.h = b.y2 - b.y1; }; let updateBoundsFromBox = function( b, b2 ){ if( b2 == null ){ return b; } return updateBounds( b, b2.x1, b2.y1, b2.x2, b2.y2 ); }; let prefixedProperty = function( obj, field, prefix ){ return getPrefixedProperty( obj, field, prefix ); }; let updateBoundsFromArrow = function( bounds, ele, prefix ){ if( ele.cy().headless() ){ return; } let _p = ele._private; let rstyle = _p.rstyle; let halfArW = rstyle.arrowWidth / 2; let arrowType = ele.pstyle( prefix + '-arrow-shape' ).value; let x; let y; if( arrowType !== 'none' ){ if( prefix === 'source' ){ x = rstyle.srcX; y = rstyle.srcY; } else if( prefix === 'target' ){ x = rstyle.tgtX; y = rstyle.tgtY; } else { x = rstyle.midX; y = rstyle.midY; } // always store the individual arrow bounds let bbs = _p.arrowBounds = _p.arrowBounds || {}; let bb = bbs[prefix] = bbs[prefix] || {}; bb.x1 = x - halfArW; bb.y1 = y - halfArW; bb.x2 = x + halfArW; bb.y2 = y + halfArW; bb.w = bb.x2 - bb.x1; bb.h = bb.y2 - bb.y1; expandBoundingBox(bb, 1); updateBounds( bounds, bb.x1, bb.y1, bb.x2, bb.y2 ); } }; let updateBoundsFromLabel = function( bounds, ele, prefix ){ if( ele.cy().headless() ){ return; } let prefixDash; if( prefix ){ prefixDash = prefix + '-'; } else { prefixDash = ''; } let _p = ele._private; let rstyle = _p.rstyle; let label = ele.pstyle( prefixDash + 'label' ).strValue; if( label ){ let halign = ele.pstyle( 'text-halign' ); let valign = ele.pstyle( 'text-valign' ); let labelWidth = prefixedProperty( rstyle, 'labelWidth', prefix ); let labelHeight = prefixedProperty( rstyle, 'labelHeight', prefix ); let labelX = prefixedProperty( rstyle, 'labelX', prefix ); let labelY = prefixedProperty( rstyle, 'labelY', prefix ); let marginX = ele.pstyle( prefixDash + 'text-margin-x' ).pfValue; let marginY = ele.pstyle( prefixDash + 'text-margin-y' ).pfValue; let isEdge = ele.isEdge(); let rotation = ele.pstyle( prefixDash + 'text-rotation' ); let outlineWidth = ele.pstyle( 'text-outline-width' ).pfValue; let borderWidth = ele.pstyle( 'text-border-width' ).pfValue; let halfBorderWidth = borderWidth / 2; let padding = ele.pstyle( 'text-background-padding' ).pfValue; let marginOfError = 2; // expand to work around browser dimension inaccuracies let lh = labelHeight; let lw = labelWidth; let lw_2 = lw / 2; let lh_2 = lh / 2; let lx1, lx2, ly1, ly2; if( isEdge ){ lx1 = labelX - lw_2; lx2 = labelX + lw_2; ly1 = labelY - lh_2; ly2 = labelY + lh_2; } else { switch( halign.value ){ case 'left': lx1 = labelX - lw; lx2 = labelX; break; case 'center': lx1 = labelX - lw_2; lx2 = labelX + lw_2; break; case 'right': lx1 = labelX; lx2 = labelX + lw; break; } switch( valign.value ){ case 'top': ly1 = labelY - lh; ly2 = labelY; break; case 'center': ly1 = labelY - lh_2; ly2 = labelY + lh_2; break; case 'bottom': ly1 = labelY; ly2 = labelY + lh; break; } } // shift by margin and expand by outline and border let leftPad = marginX - Math.max( outlineWidth, halfBorderWidth ) - padding - marginOfError; let rightPad = marginX + Math.max( outlineWidth, halfBorderWidth ) + padding + marginOfError; let topPad = marginY - Math.max( outlineWidth, halfBorderWidth ) - padding - marginOfError; let botPad = marginY + Math.max( outlineWidth, halfBorderWidth ) + padding + marginOfError; lx1 += leftPad; lx2 += rightPad; ly1 += topPad; ly2 += botPad; // always store the unrotated label bounds separately let bbPrefix = prefix || 'main'; let bbs = _p.labelBounds; let bb = bbs[bbPrefix] = bbs[bbPrefix] || {}; bb.x1 = lx1; bb.y1 = ly1; bb.x2 = lx2; bb.y2 = ly2; bb.w = lx2 - lx1; bb.h = ly2 - ly1; bb.leftPad = leftPad; bb.rightPad = rightPad; bb.topPad = topPad; bb.botPad = botPad; let isAutorotate = ( isEdge && rotation.strValue === 'autorotate' ); let isPfValue = ( rotation.pfValue != null && rotation.pfValue !== 0 ); if( isAutorotate || isPfValue ){ let theta = isAutorotate ? prefixedProperty( _p.rstyle, 'labelAngle', prefix ) : rotation.pfValue; let cos = Math.cos( theta ); let sin = Math.sin( theta ); // rotation point (default value for center-center) let xo = (lx1 + lx2)/2; let yo = (ly1 + ly2)/2; if( !isEdge ){ switch( halign.value ){ case 'left': xo = lx2; break; case 'right': xo = lx1; break; } switch( valign.value ){ case 'top': yo = ly2; break; case 'bottom': yo = ly1; break; } } let rotate = function( x, y ){ x = x - xo; y = y - yo; return { x: x * cos - y * sin + xo, y: x * sin + y * cos + yo }; }; let px1y1 = rotate( lx1, ly1 ); let px1y2 = rotate( lx1, ly2 ); let px2y1 = rotate( lx2, ly1 ); let px2y2 = rotate( lx2, ly2 ); lx1 = Math.min( px1y1.x, px1y2.x, px2y1.x, px2y2.x ); lx2 = Math.max( px1y1.x, px1y2.x, px2y1.x, px2y2.x ); ly1 = Math.min( px1y1.y, px1y2.y, px2y1.y, px2y2.y ); ly2 = Math.max( px1y1.y, px1y2.y, px2y1.y, px2y2.y ); } let bbPrefixRot = bbPrefix + 'Rot'; let bbRot = bbs[bbPrefixRot] = bbs[bbPrefixRot] || {}; bbRot.x1 = lx1; bbRot.y1 = ly1; bbRot.x2 = lx2; bbRot.y2 = ly2; bbRot.w = lx2 - lx1; bbRot.h = ly2 - ly1; updateBounds( bounds, lx1, ly1, lx2, ly2 ); updateBounds( _p.labelBounds.all, lx1, ly1, lx2, ly2 ); } return bounds; }; let updateBoundsFromOutline = function( bounds, ele ){ if( ele.cy().headless() ){ return; } let outlineOpacity = ele.pstyle('outline-opacity').value; let outlineWidth = ele.pstyle('outline-width').value; if (outlineOpacity > 0 && outlineWidth > 0) { let outlineOffset = ele.pstyle('outline-offset').value; let nodeShape = ele.pstyle( 'shape' ).value; let outlineSize = outlineWidth + outlineOffset; let scaleX = (bounds.w + outlineSize * 2) / bounds.w; let scaleY = (bounds.h + outlineSize * 2) / bounds.h; let xOffset = 0; let yOffset = 0; if (["diamond", "pentagon", "round-triangle"].includes(nodeShape)) { scaleX = (bounds.w + outlineSize * 2.4) / bounds.w; yOffset = -outlineSize/3.6; } else if (["concave-hexagon", "rhomboid", "right-rhomboid"].includes(nodeShape)) { scaleX = (bounds.w + outlineSize * 2.4) / bounds.w; } else if (nodeShape === "star") { scaleX = (bounds.w + outlineSize * 2.8) / bounds.w; scaleY = (bounds.h + outlineSize * 2.6) / bounds.h; yOffset = -outlineSize / 3.8; } else if (nodeShape === "triangle") { scaleX = (bounds.w + outlineSize * 2.8) / bounds.w; scaleY = (bounds.h + outlineSize * 2.4) / bounds.h; yOffset = -outlineSize/1.4; } else if (nodeShape === "vee") { scaleX = (bounds.w + outlineSize * 4.4) / bounds.w; scaleY = (bounds.h + outlineSize * 3.8) / bounds.h; yOffset = -outlineSize * .5; } let hDelta = (bounds.h * scaleY) - bounds.h; let wDelta = (bounds.w * scaleX) - bounds.w; expandBoundingBoxSides(bounds, [Math.ceil(hDelta/2), Math.ceil(wDelta/2)]); if (xOffset != 0 || yOffset !== 0) { let oBounds = shiftBoundingBox(bounds, xOffset, yOffset); updateBoundingBox(bounds, oBounds); } } }; // get the bounding box of the elements (in raw model position) let boundingBoxImpl = function( ele, options ){ let cy = ele._private.cy; let styleEnabled = cy.styleEnabled(); let headless = cy.headless(); let bounds = makeBoundingBox(); let _p = ele._private; let isNode = ele.isNode(); let isEdge = ele.isEdge(); let ex1, ex2, ey1, ey2; // extrema of body / lines let x, y; // node pos let rstyle = _p.rstyle; let manualExpansion = isNode && styleEnabled ? ele.pstyle('bounds-expansion').pfValue : [0]; // must use `display` prop only, as reading `compound.width()` causes recursion // (other factors like width values will be considered later in this function anyway) let isDisplayed = ele => ele.pstyle('display').value !== 'none'; let displayed = ( !styleEnabled || ( isDisplayed(ele) // must take into account connected nodes b/c of implicit edge hiding on display:none node && ( !isEdge || ( isDisplayed(ele.source()) && isDisplayed(ele.target()) ) ) ) ); if( displayed ){ // displayed suffices, since we will find zero area eles anyway let overlayOpacity = 0; let overlayPadding = 0; if( styleEnabled && options.includeOverlays ){ overlayOpacity = ele.pstyle( 'overlay-opacity' ).value; if( overlayOpacity !== 0 ){ overlayPadding = ele.pstyle( 'overlay-padding' ).value; } } let underlayOpacity = 0; let underlayPadding = 0; if( styleEnabled && options.includeUnderlays ){ underlayOpacity = ele.pstyle( 'underlay-opacity' ).value; if( underlayOpacity !== 0 ){ underlayPadding = ele.pstyle( 'underlay-padding' ).value; } } let padding = Math.max(overlayPadding, underlayPadding); let w = 0; let wHalf = 0; if( styleEnabled ){ w = ele.pstyle( 'width' ).pfValue; wHalf = w / 2; } if( isNode && options.includeNodes ){ let pos = ele.position(); x = pos.x; y = pos.y; let w = ele.outerWidth(); let halfW = w / 2; let h = ele.outerHeight(); let halfH = h / 2; // handle node dimensions ///////////////////////// ex1 = x - halfW; ex2 = x + halfW; ey1 = y - halfH; ey2 = y + halfH; updateBounds( bounds, ex1, ey1, ex2, ey2 ); if( styleEnabled && options.includeOutlines ){ updateBoundsFromOutline( bounds, ele ); } } else if( isEdge && options.includeEdges ){ if( styleEnabled && !headless ){ let curveStyle = ele.pstyle( 'curve-style').strValue; // handle edge dimensions (rough box estimate) ////////////////////////////////////////////// ex1 = Math.min( rstyle.srcX, rstyle.midX, rstyle.tgtX ); ex2 = Math.max( rstyle.srcX, rstyle.midX, rstyle.tgtX ); ey1 = Math.min( rstyle.srcY, rstyle.midY, rstyle.tgtY ); ey2 = Math.max( rstyle.srcY, rstyle.midY, rstyle.tgtY ); // take into account edge width ex1 -= wHalf; ex2 += wHalf; ey1 -= wHalf; ey2 += wHalf; updateBounds( bounds, ex1, ey1, ex2, ey2 ); // precise edges //////////////// if( curveStyle === 'haystack' ){ let hpts = rstyle.haystackPts; if( hpts && hpts.length === 2 ){ ex1 = hpts[0].x; ey1 = hpts[0].y; ex2 = hpts[1].x; ey2 = hpts[1].y; if( ex1 > ex2 ){ let temp = ex1; ex1 = ex2; ex2 = temp; } if( ey1 > ey2 ){ let temp = ey1; ey1 = ey2; ey2 = temp; } updateBounds( bounds, ex1 - wHalf, ey1 - wHalf, ex2 + wHalf, ey2 + wHalf ); } } else if( curveStyle === 'bezier' || curveStyle === 'unbundled-bezier' || curveStyle.endsWith('segments') || curveStyle.endsWith('taxi') ){ let pts; switch( curveStyle ){ case 'bezier': case 'unbundled-bezier': pts = rstyle.bezierPts; break; case 'segments': case 'taxi': case 'round-segments': case 'round-taxi': pts = rstyle.linePts; break; } if( pts != null ){ for( let j = 0; j < pts.length; j++ ){ let pt = pts[ j ]; ex1 = pt.x - wHalf; ex2 = pt.x + wHalf; ey1 = pt.y - wHalf; ey2 = pt.y + wHalf; updateBounds( bounds, ex1, ey1, ex2, ey2 ); } } } // bezier-like or segment-like edge } else { // headless or style disabled // fallback on source and target positions ////////////////////////////////////////// let n1 = ele.source(); let n1pos = n1.position(); let n2 = ele.target(); let n2pos = n2.position(); ex1 = n1pos.x; ex2 = n2pos.x; ey1 = n1pos.y; ey2 = n2pos.y; if( ex1 > ex2 ){ let temp = ex1; ex1 = ex2; ex2 = temp; } if( ey1 > ey2 ){ let temp = ey1; ey1 = ey2; ey2 = temp; } // take into account edge width ex1 -= wHalf; ex2 += wHalf; ey1 -= wHalf; ey2 += wHalf; updateBounds( bounds, ex1, ey1, ex2, ey2 ); } // headless or style disabled } // edges // handle edge arrow size ///////////////////////// if( styleEnabled && options.includeEdges && isEdge ){ updateBoundsFromArrow( bounds, ele, 'mid-source', options ); updateBoundsFromArrow( bounds, ele, 'mid-target', options ); updateBoundsFromArrow( bounds, ele, 'source', options ); updateBoundsFromArrow( bounds, ele, 'target', options ); } // ghost //////// if( styleEnabled ){ let ghost = ele.pstyle('ghost').value === 'yes'; if( ghost ){ let gx = ele.pstyle('ghost-offset-x').pfValue; let gy = ele.pstyle('ghost-offset-y').pfValue; updateBounds( bounds, bounds.x1 + gx, bounds.y1 + gy, bounds.x2 + gx, bounds.y2 + gy ); } } // always store the body bounds separately from the labels let bbBody = _p.bodyBounds = _p.bodyBounds || {}; assignBoundingBox(bbBody, bounds); expandBoundingBoxSides(bbBody, manualExpansion); expandBoundingBox(bbBody, 1); // expand to work around browser dimension inaccuracies // overlay ////////// if( styleEnabled ){ ex1 = bounds.x1; ex2 = bounds.x2; ey1 = bounds.y1; ey2 = bounds.y2; updateBounds( bounds, ex1 - padding, ey1 - padding, ex2 + padding, ey2 + padding ); } // always store the body bounds separately from the labels let bbOverlay = _p.overlayBounds = _p.overlayBounds || {}; assignBoundingBox(bbOverlay, bounds); expandBoundingBoxSides(bbOverlay, manualExpansion); expandBoundingBox(bbOverlay, 1); // expand to work around browser dimension inaccuracies // handle label dimensions ////////////////////////// let bbLabels = _p.labelBounds = _p.labelBounds || {}; if( bbLabels.all != null ){ clearBoundingBox(bbLabels.all); } else { bbLabels.all = makeBoundingBox(); } if( styleEnabled && options.includeLabels ){ if( options.includeMainLabels ){ updateBoundsFromLabel( bounds, ele, null, options ); } if( isEdge ){ if( options.includeSourceLabels ){ updateBoundsFromLabel( bounds, ele, 'source', options ); } if( options.includeTargetLabels ){ updateBoundsFromLabel( bounds, ele, 'target', options ); } } } // style enabled for labels } // if displayed bounds.x1 = noninf( bounds.x1 ); bounds.y1 = noninf( bounds.y1 ); bounds.x2 = noninf( bounds.x2 ); bounds.y2 = noninf( bounds.y2 ); bounds.w = noninf( bounds.x2 - bounds.x1 ); bounds.h = noninf( bounds.y2 - bounds.y1 ); if( bounds.w > 0 && bounds.h > 0 && displayed ){ expandBoundingBoxSides( bounds, manualExpansion ); // expand bounds by 1 because antialiasing can increase the visual/effective size by 1 on all sides expandBoundingBox( bounds, 1 ); } return bounds; }; let getKey = function( opts ){ let i = 0; let tf = val => (val ? 1 : 0) << i++; let key = 0; key += tf( opts.incudeNodes ); key += tf( opts.includeEdges ); key += tf( opts.includeLabels ); key += tf( opts.includeMainLabels ); key += tf( opts.includeSourceLabels ); key += tf( opts.includeTargetLabels ); key += tf( opts.includeOverlays ); key += tf( opts.includeOutlines ); return key; }; let getBoundingBoxPosKey = ele => { if( ele.isEdge() ){ let p1 = ele.source().position(); let p2 = ele.target().position(); let r = x => Math.round(x); return hashIntsArray([ r(p1.x), r(p1.y), r(p2.x), r(p2.y) ]); } else { return 0; } }; let cachedBoundingBoxImpl = function( ele, opts ){ let _p = ele._private; let bb; let isEdge = ele.isEdge(); let key = opts == null ? defBbOptsKey : getKey( opts ); let usingDefOpts = key === defBbOptsKey; let currPosKey = getBoundingBoxPosKey( ele ); let isPosKeySame = _p.bbCachePosKey === currPosKey; let useCache = opts.useCache && isPosKeySame; let isDirty = ele => ele._private.bbCache == null || ele._private.styleDirty; let needRecalc = !useCache || isDirty(ele) || (isEdge && (isDirty(ele.source()) || isDirty(ele.target()))); if( needRecalc ){ if( !isPosKeySame ){ ele.recalculateRenderedStyle(useCache); } bb = boundingBoxImpl( ele, defBbOpts ); _p.bbCache = bb; _p.bbCachePosKey = currPosKey; } else { bb = _p.bbCache; } // not using def opts => need to build up bb from combination of sub bbs if( !usingDefOpts ){ let isNode = ele.isNode(); bb = makeBoundingBox(); if( (opts.includeNodes && isNode) || (opts.includeEdges && !isNode) ){ if( opts.includeOverlays ){ updateBoundsFromBox(bb, _p.overlayBounds); } else { updateBoundsFromBox(bb, _p.bodyBounds); } } if( opts.includeLabels ){ if( opts.includeMainLabels && (!isEdge || (opts.includeSourceLabels && opts.includeTargetLabels)) ){ updateBoundsFromBox(bb, _p.labelBounds.all); } else { if( opts.includeMainLabels ){ updateBoundsFromBox(bb, _p.labelBounds.mainRot); } if( opts.includeSourceLabels ){ updateBoundsFromBox(bb, _p.labelBounds.sourceRot); } if( opts.includeTargetLabels ){ updateBoundsFromBox(bb, _p.labelBounds.targetRot); } } } bb.w = bb.x2 - bb.x1; bb.h = bb.y2 - bb.y1; } return bb; }; let defBbOpts = { includeNodes: true, includeEdges: true, includeLabels: true, includeMainLabels: true, includeSourceLabels: true, includeTargetLabels: true, includeOverlays: true, includeUnderlays: true, includeOutlines: true, useCache: true }; const defBbOptsKey = getKey( defBbOpts ); const filledBbOpts = defaults( defBbOpts ); elesfn.boundingBox = function( options ){ let bounds; // the main usecase is ele.boundingBox() for a single element with no/def options // specified s.t. the cache is used, so check for this case to make it faster by // avoiding the overhead of the rest of the function if( this.length === 1 && this[0]._private.bbCache != null && !this[0]._private.styleDirty && (options === undefined || options.useCache === undefined || options.useCache === true) ){ if( options === undefined ){ options = defBbOpts; } else { options = filledBbOpts( options ); } bounds = cachedBoundingBoxImpl( this[0], options ); } else { bounds = makeBoundingBox(); options = options || defBbOpts; let opts = filledBbOpts( options ); let eles = this; let cy = eles.cy(); let styleEnabled = cy.styleEnabled(); if( styleEnabled ){ for( let i = 0; i < eles.length; i++ ){ let ele = eles[i]; let _p = ele._private; let currPosKey = getBoundingBoxPosKey( ele ); let isPosKeySame = _p.bbCachePosKey === currPosKey; let useCache = opts.useCache && isPosKeySame && !_p.styleDirty; ele.recalculateRenderedStyle( useCache ); } } this.updateCompoundBounds(!options.useCache); for( let i = 0; i < eles.length; i++ ){ let ele = eles[i]; updateBoundsFromBox( bounds, cachedBoundingBoxImpl( ele, opts ) ); } } bounds.x1 = noninf( bounds.x1 ); bounds.y1 = noninf( bounds.y1 ); bounds.x2 = noninf( bounds.x2 ); bounds.y2 = noninf( bounds.y2 ); bounds.w = noninf( bounds.x2 - bounds.x1 ); bounds.h = noninf( bounds.y2 - bounds.y1 ); return bounds; }; elesfn.dirtyBoundingBoxCache = function(){ for( let i = 0; i < this.length; i++ ){ let _p = this[i]._private; _p.bbCache = null; _p.bbCachePosKey = null; _p.bodyBounds = null; _p.overlayBounds = null; _p.labelBounds.all = null; _p.labelBounds.source = null; _p.labelBounds.target = null; _p.labelBounds.main = null; _p.labelBounds.sourceRot = null; _p.labelBounds.targetRot = null; _p.labelBounds.mainRot = null; _p.arrowBounds.source = null; _p.arrowBounds.target = null; _p.arrowBounds['mid-source'] = null; _p.arrowBounds['mid-target'] = null; } this.emitAndNotify('bounds'); return this; }; // private helper to get bounding box for custom node positions // - good for perf in certain cases but currently requires dirtying the rendered style // - would be better to not modify the nodes but the nodes are read directly everywhere in the renderer... // - try to use for only things like discrete layouts where the node position would change anyway elesfn.boundingBoxAt = function( fn ){ let nodes = this.nodes(); let cy = this.cy(); let hasCompoundNodes = cy.hasCompoundNodes(); let parents = cy.collection(); if( hasCompoundNodes ){ parents = nodes.filter(node => node.isParent()); nodes = nodes.not(parents); } if( is.plainObject( fn ) ){ let obj = fn; fn = function(){ return obj; }; } let storeOldPos = (node, i) => node._private.bbAtOldPos = fn(node, i); let getOldPos = (node) => node._private.bbAtOldPos; cy.startBatch(); ( nodes .forEach(storeOldPos) .silentPositions(fn) ); if( hasCompoundNodes ){ parents.dirtyCompoundBoundsCache(); parents.dirtyBoundingBoxCache(); parents.updateCompoundBounds(true); // force update b/c we're inside a batch cycle } let bb = copyBoundingBox( this.boundingBox({ useCache: false }) ); nodes.silentPositions(getOldPos); if( hasCompoundNodes ){ parents.dirtyCompoundBoundsCache(); parents.dirtyBoundingBoxCache(); parents.updateCompoundBounds(true); // force update b/c we're inside a batch cycle } cy.endBatch(); return bb; }; fn.boundingbox = fn.bb = fn.boundingBox; fn.renderedBoundingbox = fn.renderedBoundingBox; export default elesfn;