basant307/AI_Governance_Project
048
1import * as is from '../../is.mjs';2import { assignBoundingBox, expandBoundingBoxSides, clearBoundingBox, expandBoundingBox, makeBoundingBox, copyBoundingBox, shiftBoundingBox, updateBoundingBox } from '../../math.mjs';3import {defaults, endsWith, getPrefixedProperty, hashIntsArray, memoize} from '../../util/index.mjs';4import { labelHalign, labelValign } from '../../style/align.mjs';5 6let fn, elesfn;7 8fn = elesfn = {};9 10elesfn.renderedBoundingBox = function( options ){11 let bb = this.boundingBox( options );12 let cy = this.cy();13 let zoom = cy.zoom();14 let pan = cy.pan();15 16 let x1 = bb.x1 * zoom + pan.x;17 let x2 = bb.x2 * zoom + pan.x;18 let y1 = bb.y1 * zoom + pan.y;19 let y2 = bb.y2 * zoom + pan.y;20 21 return {22 x1: x1,23 x2: x2,24 y1: y1,25 y2: y2,26 w: x2 - x1,27 h: y2 - y128 };29};30 31elesfn.dirtyCompoundBoundsCache = function(silent = false){32 let cy = this.cy();33 34 if( !cy.styleEnabled() || !cy.hasCompoundNodes() ){ return this; }35 36 this.forEachUp( ele => {37 if( ele.isParent() ){38 let _p = ele._private;39 40 _p.compoundBoundsClean = false;41 _p.bbCache = null;42 43 if(!silent){44 ele.emitAndNotify('bounds');45 }46 }47 } );48 49 return this;50};51 52elesfn.updateCompoundBounds = function(force = false){53 let cy = this.cy();54 55 // not possible to do on non-compound graphs or with the style disabled56 if( !cy.styleEnabled() || !cy.hasCompoundNodes() ){ return this; }57 58 // save cycles when batching -- but bounds will be stale (or not exist yet)59 if( !force && cy.batching() ){ return this; }60 61 function update( parent ){62 if( !parent.isParent() ){ return; }63 64 let _p = parent._private;65 let children = parent.children();66 let includeLabels = parent.pstyle( 'compound-sizing-wrt-labels' ).value === 'include';67 68 let min = {69 width: {70 val: parent.pstyle( 'min-width' ).pfValue,71 left: parent.pstyle( 'min-width-bias-left' ),72 right: parent.pstyle( 'min-width-bias-right' )73 },74 height: {75 val: parent.pstyle( 'min-height' ).pfValue,76 top: parent.pstyle( 'min-height-bias-top' ),77 bottom: parent.pstyle( 'min-height-bias-bottom' )78 }79 };80 81 let bb = children.boundingBox( {82 includeLabels: includeLabels,83 includeOverlays: false,84 85 // updating the compound bounds happens outside of the regular86 // cache cycle (i.e. before fired events)87 useCache: false88 } );89 let pos = _p.position;90 91 // if children take up zero area then keep position and fall back on stylesheet w/h92 if( bb.w === 0 || bb.h === 0 ){93 bb = {94 w: parent.pstyle('width').pfValue,95 h: parent.pstyle('height').pfValue96 };97 98 bb.x1 = pos.x - bb.w/2;99 bb.x2 = pos.x + bb.w/2;100 bb.y1 = pos.y - bb.h/2;101 bb.y2 = pos.y + bb.h/2;102 }103 104 function computeBiasValues( propDiff, propBias, propBiasComplement ){105 let biasDiff = 0;106 let biasComplementDiff = 0;107 let biasTotal = propBias + propBiasComplement;108 109 if( propDiff > 0 && biasTotal > 0 ){110 biasDiff = ( propBias / biasTotal ) * propDiff;111 biasComplementDiff = ( propBiasComplement / biasTotal ) * propDiff;112 }113 return {114 biasDiff: biasDiff,115 biasComplementDiff: biasComplementDiff116 };117 }118 119 function computePaddingValues( width, height, paddingObject, relativeTo ) {120 // Assuming percentage is number from 0 to 1121 if(paddingObject.units === '%') {122 switch(relativeTo) {123 case 'width':124 return width > 0 ? paddingObject.pfValue * width : 0;125 case 'height':126 return height > 0 ? paddingObject.pfValue * height : 0;127 case 'average':128 return ( width > 0 ) && ( height > 0 ) ? paddingObject.pfValue * ( width + height ) / 2 : 0;129 case 'min':130 return ( width > 0 ) && ( height > 0 ) ? ( ( width > height ) ? paddingObject.pfValue * height : paddingObject.pfValue * width ) : 0;131 case 'max':132 return ( width > 0 ) && ( height > 0 ) ? ( ( width > height ) ? paddingObject.pfValue * width : paddingObject.pfValue * height ) : 0;133 default:134 return 0;135 }136 } else if(paddingObject.units === 'px') {137 return paddingObject.pfValue;138 } else {139 return 0;140 }141 }142 143 let leftVal = min.width.left.value;144 if( min.width.left.units === 'px' && min.width.val > 0 ){145 leftVal = ( leftVal * 100 ) / min.width.val;146 }147 let rightVal = min.width.right.value;148 if( min.width.right.units === 'px' && min.width.val > 0 ){149 rightVal = ( rightVal * 100 ) / min.width.val;150 }151 152 let topVal = min.height.top.value;153 if( min.height.top.units === 'px' && min.height.val > 0 ){154 topVal = ( topVal * 100 ) / min.height.val;155 }156 157 let bottomVal = min.height.bottom.value;158 if( min.height.bottom.units === 'px' && min.height.val > 0 ){159 bottomVal = ( bottomVal * 100 ) / min.height.val;160 }161 162 let widthBiasDiffs = computeBiasValues( min.width.val - bb.w, leftVal, rightVal );163 let diffLeft = widthBiasDiffs.biasDiff;164 let diffRight = widthBiasDiffs.biasComplementDiff;165 166 let heightBiasDiffs = computeBiasValues( min.height.val - bb.h, topVal, bottomVal );167 let diffTop = heightBiasDiffs.biasDiff;168 let diffBottom = heightBiasDiffs.biasComplementDiff;169 170 _p.autoPadding = computePaddingValues( bb.w, bb.h, parent.pstyle( 'padding' ), parent.pstyle( 'padding-relative-to' ).value );171 172 _p.autoWidth = Math.max(bb.w, min.width.val);173 pos.x = (- diffLeft + bb.x1 + bb.x2 + diffRight) / 2;174 175 _p.autoHeight = Math.max(bb.h, min.height.val);176 pos.y = (- diffTop + bb.y1 + bb.y2 + diffBottom) / 2;177 }178 179 for( let i = 0; i < this.length; i++ ){180 let ele = this[i];181 let _p = ele._private;182 183 if( !_p.compoundBoundsClean || force ){184 update( ele );185 186 if( !cy.batching() ){187 _p.compoundBoundsClean = true;188 }189 }190 }191 192 return this;193};194 195let noninf = function( x ){196 if( x === Infinity || x === -Infinity ){197 return 0;198 }199 200 return x;201};202 203let updateBounds = function( b, x1, y1, x2, y2 ){204 // don't update with zero area boxes205 if( x2 - x1 === 0 || y2 - y1 === 0 ){ return; }206 207 // don't update with null dim208 if( x1 == null || y1 == null || x2 == null || y2 == null ){ return; }209 210 b.x1 = x1 < b.x1 ? x1 : b.x1;211 b.x2 = x2 > b.x2 ? x2 : b.x2;212 b.y1 = y1 < b.y1 ? y1 : b.y1;213 b.y2 = y2 > b.y2 ? y2 : b.y2;214 b.w = b.x2 - b.x1;215 b.h = b.y2 - b.y1;216};217 218let updateBoundsFromBox = function( b, b2 ){219 if( b2 == null ){ return b; }220 221 return updateBounds( b, b2.x1, b2.y1, b2.x2, b2.y2 );222};223 224let prefixedProperty = function( obj, field, prefix ){225 return getPrefixedProperty( obj, field, prefix );226};227 228let updateBoundsFromArrow = function( bounds, ele, prefix ){229 if( ele.cy().headless() ){ return; }230 231 let _p = ele._private;232 let rstyle = _p.rstyle;233 let halfArW = rstyle.arrowWidth / 2;234 let arrowType = ele.pstyle( prefix + '-arrow-shape' ).value;235 let x;236 let y;237 238 if( arrowType !== 'none' ){239 if( prefix === 'source' ){240 x = rstyle.srcX;241 y = rstyle.srcY;242 } else if( prefix === 'target' ){243 x = rstyle.tgtX;244 y = rstyle.tgtY;245 } else {246 x = rstyle.midX;247 y = rstyle.midY;248 }249 250 // always store the individual arrow bounds251 let bbs = _p.arrowBounds = _p.arrowBounds || {};252 let bb = bbs[prefix] = bbs[prefix] || {};253 bb.x1 = x - halfArW;254 bb.y1 = y - halfArW;255 bb.x2 = x + halfArW;256 bb.y2 = y + halfArW;257 bb.w = bb.x2 - bb.x1;258 bb.h = bb.y2 - bb.y1;259 expandBoundingBox(bb, 1);260 261 updateBounds( bounds, bb.x1, bb.y1, bb.x2, bb.y2 );262 }263};264 265let updateBoundsFromLabel = function( bounds, ele, prefix ){266 if( ele.cy().headless() ){ return; }267 268 let prefixDash;269 270 if( prefix ){271 prefixDash = prefix + '-';272 } else {273 prefixDash = '';274 }275 276 let _p = ele._private;277 let rstyle = _p.rstyle;278 let label = ele.pstyle( prefixDash + 'label' ).strValue;279 280 if( label ){281 let halign = ele.pstyle( 'text-halign' );282 let valign = ele.pstyle( 'text-valign' );283 let labelWidth = prefixedProperty( rstyle, 'labelWidth', prefix );284 let labelHeight = prefixedProperty( rstyle, 'labelHeight', prefix );285 let labelX = prefixedProperty( rstyle, 'labelX', prefix );286 let labelY = prefixedProperty( rstyle, 'labelY', prefix );287 let marginX = ele.pstyle( prefixDash + 'text-margin-x' ).pfValue;288 let marginY = ele.pstyle( prefixDash + 'text-margin-y' ).pfValue;289 let isEdge = ele.isEdge();290 let rotation = ele.pstyle( prefixDash + 'text-rotation' );291 let outlineWidth = ele.pstyle( 'text-outline-width' ).pfValue;292 let borderWidth = ele.pstyle( 'text-border-width' ).pfValue;293 let halfBorderWidth = borderWidth / 2;294 let padding = ele.pstyle( 'text-background-padding' ).pfValue;295 let marginOfError = 2; // expand to work around browser dimension inaccuracies296 297 let lh = labelHeight;298 let lw = labelWidth;299 let lw_2 = lw / 2;300 let lh_2 = lh / 2;301 let lx1, lx2, ly1, ly2;302 303 if( isEdge ){304 lx1 = labelX - lw_2;305 lx2 = labelX + lw_2;306 ly1 = labelY - lh_2;307 ly2 = labelY + lh_2;308 } else {309 switch( labelHalign( halign.value ) ){310 case 'left':311 lx1 = labelX - lw;312 lx2 = labelX;313 break;314 315 case 'center':316 lx1 = labelX - lw_2;317 lx2 = labelX + lw_2;318 break;319 320 case 'right':321 lx1 = labelX;322 lx2 = labelX + lw;323 break;324 }325 326 switch( labelValign( valign.value ) ){327 case 'top':328 ly1 = labelY - lh;329 ly2 = labelY;330 break;331 332 case 'center':333 ly1 = labelY - lh_2;334 ly2 = labelY + lh_2;335 break;336 337 case 'bottom':338 ly1 = labelY;339 ly2 = labelY + lh;340 break;341 }342 }343 344 // shift by margin and expand by outline and border345 let leftPad = marginX - Math.max( outlineWidth, halfBorderWidth ) - padding - marginOfError;346 let rightPad = marginX + Math.max( outlineWidth, halfBorderWidth ) + padding + marginOfError;347 let topPad = marginY - Math.max( outlineWidth, halfBorderWidth ) - padding - marginOfError;348 let botPad = marginY + Math.max( outlineWidth, halfBorderWidth ) + padding + marginOfError;349 350 lx1 += leftPad;351 lx2 += rightPad;352 ly1 += topPad;353 ly2 += botPad;354 355 // always store the unrotated label bounds separately356 let bbPrefix = prefix || 'main';357 let bbs = _p.labelBounds;358 let bb = bbs[bbPrefix] = bbs[bbPrefix] || {};359 bb.x1 = lx1;360 bb.y1 = ly1;361 bb.x2 = lx2;362 bb.y2 = ly2;363 bb.w = lx2 - lx1;364 bb.h = ly2 - ly1;365 bb.leftPad = leftPad;366 bb.rightPad = rightPad;367 bb.topPad = topPad;368 bb.botPad = botPad;369 370 let isAutorotate = ( isEdge && rotation.strValue === 'autorotate' );371 let isPfValue = ( rotation.pfValue != null && rotation.pfValue !== 0 );372 373 if( isAutorotate || isPfValue ){374 let theta = isAutorotate ? prefixedProperty( _p.rstyle, 'labelAngle', prefix ) : rotation.pfValue;375 let cos = Math.cos( theta );376 let sin = Math.sin( theta );377 378 // rotation point (default value for center-center)379 let xo = (lx1 + lx2)/2;380 let yo = (ly1 + ly2)/2;381 382 if( !isEdge ){383 switch( labelHalign( halign.value ) ){384 case 'left':385 xo = lx2;386 break;387 388 case 'right':389 xo = lx1;390 break;391 }392 393 switch( labelValign( valign.value ) ){394 case 'top':395 yo = ly2;396 break;397 398 case 'bottom':399 yo = ly1;400 break;401 }402 }403 404 let rotate = function( x, y ){405 x = x - xo;406 y = y - yo;407 408 return {409 x: x * cos - y * sin + xo,410 y: x * sin + y * cos + yo411 };412 };413 414 let px1y1 = rotate( lx1, ly1 );415 let px1y2 = rotate( lx1, ly2 );416 let px2y1 = rotate( lx2, ly1 );417 let px2y2 = rotate( lx2, ly2 );418 419 lx1 = Math.min( px1y1.x, px1y2.x, px2y1.x, px2y2.x );420 lx2 = Math.max( px1y1.x, px1y2.x, px2y1.x, px2y2.x );421 ly1 = Math.min( px1y1.y, px1y2.y, px2y1.y, px2y2.y );422 ly2 = Math.max( px1y1.y, px1y2.y, px2y1.y, px2y2.y );423 }424 425 let bbPrefixRot = bbPrefix + 'Rot';426 let bbRot = bbs[bbPrefixRot] = bbs[bbPrefixRot] || {};427 bbRot.x1 = lx1;428 bbRot.y1 = ly1;429 bbRot.x2 = lx2;430 bbRot.y2 = ly2;431 bbRot.w = lx2 - lx1;432 bbRot.h = ly2 - ly1;433 434 updateBounds( bounds, lx1, ly1, lx2, ly2 );435 updateBounds( _p.labelBounds.all, lx1, ly1, lx2, ly2 );436 }437 438 return bounds;439};440 441let updateBoundsFromOutline = function (bounds, ele) {442 if (ele.cy().headless()) { return; }443 444 let outlineOpacity = ele.pstyle('outline-opacity').value;445 let outlineWidth = ele.pstyle('outline-width').value;446 let outlineOffset = ele.pstyle('outline-offset').value;447 let expansion = outlineWidth + outlineOffset;448 449 updateBoundsFromMiter( bounds, ele, outlineOpacity, expansion, 'outside', expansion/2 );450};451 452let updateBoundsFromMiter = function( bounds, ele, opacity, expansionSize, expansionPosition, useFallbackValue){453 if (opacity === 0 || expansionSize <= 0 || expansionPosition === 'inside') {454 return;455 }456 457 let cy = ele.cy();458 let r = cy.renderer();459 let rshape = r.nodeShapes[r.getNodeShape(ele)];460 if (!rshape) { return; }461 let { x, y } = ele.position();462 let w = ele.width();463 let h = ele.height();464 465 if (rshape.hasMiterBounds) {466 if (expansionPosition === 'center') {467 expansionSize /= 2;468 }469 470 let mbb = rshape.miterBounds(x, y, w, h, expansionSize);471 472 updateBoundsFromBox(bounds, mbb);473 } else if (useFallbackValue != null && useFallbackValue > 0) {474 expandBoundingBoxSides(bounds, [useFallbackValue, useFallbackValue, useFallbackValue, useFallbackValue]);475 }476};477 478let updateBoundsFromMiterBorder = function( bounds, ele ){479 if (ele.cy().headless()) { return; }480 481 let borderOpacity = ele.pstyle('border-opacity').value;482 let borderWidth = ele.pstyle('border-width').pfValue;483 let borderPosition = ele.pstyle('border-position').value;484 485 updateBoundsFromMiter(bounds, ele, borderOpacity, borderWidth, borderPosition);486};487 488// get the bounding box of the elements (in raw model position)489let boundingBoxImpl = function( ele, options ){490 let cy = ele._private.cy;491 let styleEnabled = cy.styleEnabled();492 let headless = cy.headless();493 494 let bounds = makeBoundingBox();495 496 let _p = ele._private;497 let isNode = ele.isNode();498 let isEdge = ele.isEdge();499 let ex1, ex2, ey1, ey2; // extrema of body / lines500 let x, y; // node pos501 let rstyle = _p.rstyle;502 let manualExpansion = isNode && styleEnabled ? ele.pstyle('bounds-expansion').pfValue : [0];503 504 // must use `display` prop only, as reading `compound.width()` causes recursion505 // (other factors like width values will be considered later in this function anyway)506 let isDisplayed = ele => ele.pstyle('display').value !== 'none';507 508 let displayed = (509 !styleEnabled510 || (511 isDisplayed(ele)512 513 // must take into account connected nodes b/c of implicit edge hiding on display:none node514 && ( !isEdge || ( isDisplayed(ele.source()) && isDisplayed(ele.target()) ) )515 )516 );517 518 if( displayed ){ // displayed suffices, since we will find zero area eles anyway519 let overlayOpacity = 0;520 let overlayPadding = 0;521 522 if( styleEnabled && options.includeOverlays ){523 overlayOpacity = ele.pstyle( 'overlay-opacity' ).value;524 525 if( overlayOpacity !== 0 ){526 overlayPadding = ele.pstyle( 'overlay-padding' ).value;527 }528 }529 530 let underlayOpacity = 0;531 let underlayPadding = 0;532 533 if( styleEnabled && options.includeUnderlays ){534 underlayOpacity = ele.pstyle( 'underlay-opacity' ).value;535 536 if( underlayOpacity !== 0 ){537 underlayPadding = ele.pstyle( 'underlay-padding' ).value;538 }539 }540 541 let padding = Math.max(overlayPadding, underlayPadding);542 543 let w = 0;544 let wHalf = 0;545 546 if( styleEnabled ){547 w = ele.pstyle( 'width' ).pfValue;548 wHalf = w / 2;549 }550 551 if( isNode && options.includeNodes ){552 let pos = ele.position();553 x = pos.x;554 y = pos.y;555 let w = ele.outerWidth();556 let halfW = w / 2;557 let h = ele.outerHeight();558 let halfH = h / 2;559 560 // handle node dimensions561 /////////////////////////562 563 ex1 = x - halfW;564 ex2 = x + halfW;565 ey1 = y - halfH;566 ey2 = y + halfH;567 568 updateBounds( bounds, ex1, ey1, ex2, ey2 );569 570 if( styleEnabled ){571 updateBoundsFromOutline(bounds, ele)572 }573 574 if( styleEnabled && options.includeOutlines && !headless ){575 updateBoundsFromOutline( bounds, ele );576 }577 578 if (styleEnabled) {579 updateBoundsFromMiterBorder(bounds, ele);580 }581 } else if( isEdge && options.includeEdges ){582 583 if( styleEnabled && !headless ){584 let curveStyle = ele.pstyle( 'curve-style').strValue;585 586 587 // handle edge dimensions (rough box estimate)588 //////////////////////////////////////////////589 590 ex1 = Math.min( rstyle.srcX, rstyle.midX, rstyle.tgtX );591 ex2 = Math.max( rstyle.srcX, rstyle.midX, rstyle.tgtX );592 ey1 = Math.min( rstyle.srcY, rstyle.midY, rstyle.tgtY );593 ey2 = Math.max( rstyle.srcY, rstyle.midY, rstyle.tgtY );594 595 // take into account edge width596 ex1 -= wHalf;597 ex2 += wHalf;598 ey1 -= wHalf;599 ey2 += wHalf;600 601 updateBounds( bounds, ex1, ey1, ex2, ey2 );602 603 604 // precise edges605 ////////////////606 607 if( curveStyle === 'haystack' ){608 let hpts = rstyle.haystackPts;609 610 if( hpts && hpts.length === 2 ){611 ex1 = hpts[0].x;612 ey1 = hpts[0].y;613 ex2 = hpts[1].x;614 ey2 = hpts[1].y;615 616 if( ex1 > ex2 ){617 let temp = ex1;618 ex1 = ex2;619 ex2 = temp;620 }621 622 if( ey1 > ey2 ){623 let temp = ey1;624 ey1 = ey2;625 ey2 = temp;626 }627 628 updateBounds( bounds, ex1 - wHalf, ey1 - wHalf, ex2 + wHalf, ey2 + wHalf );629 }630 631 } else if(632 curveStyle === 'bezier' || curveStyle === 'unbundled-bezier'633 || endsWith(curveStyle, 'segments') || endsWith(curveStyle, 'taxi')634 ){635 let pts;636 637 switch( curveStyle ){638 case 'bezier':639 case 'unbundled-bezier':640 pts = rstyle.bezierPts;641 break;642 case 'segments':643 case 'taxi':644 case 'round-segments':645 case 'round-taxi':646 pts = rstyle.linePts;647 break;648 }649 650 if( pts != null ){651 for( let j = 0; j < pts.length; j++ ){652 let pt = pts[ j ];653 654 ex1 = pt.x - wHalf;655 ex2 = pt.x + wHalf;656 ey1 = pt.y - wHalf;657 ey2 = pt.y + wHalf;658 659 updateBounds( bounds, ex1, ey1, ex2, ey2 );660 }661 }662 } // bezier-like or segment-like edge663 } else { // headless or style disabled664 665 // fallback on source and target positions666 //////////////////////////////////////////667 668 let n1 = ele.source();669 let n1pos = n1.position();670 671 let n2 = ele.target();672 let n2pos = n2.position();673 674 ex1 = n1pos.x;675 ex2 = n2pos.x;676 ey1 = n1pos.y;677 ey2 = n2pos.y;678 679 if( ex1 > ex2 ){680 let temp = ex1;681 ex1 = ex2;682 ex2 = temp;683 }684 685 if( ey1 > ey2 ){686 let temp = ey1;687 ey1 = ey2;688 ey2 = temp;689 }690 691 // take into account edge width692 ex1 -= wHalf;693 ex2 += wHalf;694 ey1 -= wHalf;695 ey2 += wHalf;696 697 updateBounds( bounds, ex1, ey1, ex2, ey2 );698 } // headless or style disabled699 700 } // edges701 702 // handle edge arrow size703 /////////////////////////704 705 if( styleEnabled && options.includeEdges && isEdge ){706 updateBoundsFromArrow( bounds, ele, 'mid-source', options );707 updateBoundsFromArrow( bounds, ele, 'mid-target', options );708 updateBoundsFromArrow( bounds, ele, 'source', options );709 updateBoundsFromArrow( bounds, ele, 'target', options );710 }711 712 // ghost713 ////////714 715 if( styleEnabled ){716 let ghost = ele.pstyle('ghost').value === 'yes';717 718 if( ghost ){719 let gx = ele.pstyle('ghost-offset-x').pfValue;720 let gy = ele.pstyle('ghost-offset-y').pfValue;721 722 updateBounds( bounds, bounds.x1 + gx, bounds.y1 + gy, bounds.x2 + gx, bounds.y2 + gy );723 }724 }725 726 // always store the body bounds separately from the labels727 let bbBody = _p.bodyBounds = _p.bodyBounds || {};728 assignBoundingBox(bbBody, bounds);729 expandBoundingBoxSides(bbBody, manualExpansion);730 expandBoundingBox(bbBody, 1); // expand to work around browser dimension inaccuracies731 732 // overlay733 //////////734 735 if( styleEnabled ){736 ex1 = bounds.x1;737 ex2 = bounds.x2;738 ey1 = bounds.y1;739 ey2 = bounds.y2;740 741 updateBounds( bounds, ex1 - padding, ey1 - padding, ex2 + padding, ey2 + padding );742 }743 744 // always store the body bounds separately from the labels745 let bbOverlay = _p.overlayBounds = _p.overlayBounds || {};746 assignBoundingBox(bbOverlay, bounds);747 expandBoundingBoxSides(bbOverlay, manualExpansion);748 expandBoundingBox(bbOverlay, 1); // expand to work around browser dimension inaccuracies749 750 // handle label dimensions751 //////////////////////////752 753 let bbLabels = _p.labelBounds = _p.labelBounds || {};754 755 if( bbLabels.all != null ){756 clearBoundingBox(bbLabels.all);757 } else {758 bbLabels.all = makeBoundingBox();759 }760 761 if( styleEnabled && options.includeLabels ){762 if( options.includeMainLabels ){763 updateBoundsFromLabel( bounds, ele, null, options );764 }765 766 if( isEdge ){767 if( options.includeSourceLabels ){768 updateBoundsFromLabel( bounds, ele, 'source', options );769 }770 771 if( options.includeTargetLabels ){772 updateBoundsFromLabel( bounds, ele, 'target', options );773 }774 }775 } // style enabled for labels776 } // if displayed777 778 779 bounds.x1 = noninf( bounds.x1 );780 bounds.y1 = noninf( bounds.y1 );781 bounds.x2 = noninf( bounds.x2 );782 bounds.y2 = noninf( bounds.y2 );783 bounds.w = noninf( bounds.x2 - bounds.x1 );784 bounds.h = noninf( bounds.y2 - bounds.y1 );785 786 if( bounds.w > 0 && bounds.h > 0 && displayed ){787 expandBoundingBoxSides( bounds, manualExpansion );788 789 // expand bounds by 1 because antialiasing can increase the visual/effective size by 1 on all sides790 expandBoundingBox( bounds, 1 );791 }792 793 return bounds;794};795 796let getKey = function( opts ){797 let i = 0;798 let tf = val => (val ? 1 : 0) << i++;799 let key = 0;800 801 key += tf( opts.incudeNodes );802 key += tf( opts.includeEdges );803 key += tf( opts.includeLabels );804 key += tf( opts.includeMainLabels );805 key += tf( opts.includeSourceLabels );806 key += tf( opts.includeTargetLabels );807 key += tf( opts.includeOverlays );808 key += tf( opts.includeOutlines );809 810 return key;811};812 813let getBoundingBoxPosKey = ele => {814 let r = x => Math.round(x);815 816 if( ele.isEdge() ){817 let p1 = ele.source().position();818 let p2 = ele.target().position();819 820 return hashIntsArray([ r(p1.x), r(p1.y), r(p2.x), r(p2.y) ]);821 } else {822 let p = ele.position();823 824 return hashIntsArray([ r(p.x), r(p.y) ]);825 }826};827 828let cachedBoundingBoxImpl = function( ele, opts ){829 let _p = ele._private;830 let bb;831 let isEdge = ele.isEdge();832 let key = opts == null ? defBbOptsKey : getKey( opts );833 let usingDefOpts = key === defBbOptsKey;834 835 if( _p.bbCache == null ){836 bb = boundingBoxImpl( ele, defBbOpts );837 838 _p.bbCache = bb;839 _p.bbCachePosKey = getBoundingBoxPosKey( ele );840 } else {841 bb = _p.bbCache;842 }843 844 // not using def opts => need to build up bb from combination of sub bbs845 if( !usingDefOpts ){846 let isNode = ele.isNode();847 848 bb = makeBoundingBox();849 850 if( (opts.includeNodes && isNode) || (opts.includeEdges && !isNode) ){851 if( opts.includeOverlays ){852 updateBoundsFromBox(bb, _p.overlayBounds);853 } else {854 updateBoundsFromBox(bb, _p.bodyBounds);855 }856 }857 858 if( opts.includeLabels ){859 if( opts.includeMainLabels && (!isEdge || (opts.includeSourceLabels && opts.includeTargetLabels)) ){860 updateBoundsFromBox(bb, _p.labelBounds.all);861 } else {862 if( opts.includeMainLabels ){863 updateBoundsFromBox(bb, _p.labelBounds.mainRot);864 }865 866 if( opts.includeSourceLabels ){867 updateBoundsFromBox(bb, _p.labelBounds.sourceRot);868 }869 870 if( opts.includeTargetLabels ){871 updateBoundsFromBox(bb, _p.labelBounds.targetRot);872 }873 }874 }875 876 bb.w = bb.x2 - bb.x1;877 bb.h = bb.y2 - bb.y1;878 }879 880 return bb;881};882 883let defBbOpts = {884 includeNodes: true,885 includeEdges: true,886 includeLabels: true,887 includeMainLabels: true,888 includeSourceLabels: true,889 includeTargetLabels: true,890 includeOverlays: true,891 includeUnderlays: true,892 includeOutlines: true,893 useCache: true894};895 896const defBbOptsKey = getKey( defBbOpts );897 898const filledBbOpts = defaults( defBbOpts );899 900elesfn.boundingBox = function( options ){901 let bounds;902 903 let useCache = (options === undefined || options.useCache === undefined || options.useCache === true);904 905 let isDirty = memoize(ele => {906 let _p = ele._private;907 908 return _p.bbCache == null || _p.styleDirty || _p.bbCachePosKey !== getBoundingBoxPosKey(ele);909 }, ele => ele.id());910 911 // the main usecase is ele.boundingBox() for a single element with no/def options912 // specified s.t. the cache is used, so check for this case to make it faster by913 // avoiding the overhead of the rest of the function914 if (useCache && this.length === 1 && !isDirty(this[0])) {915 if (options === undefined) {916 options = defBbOpts;917 } else {918 options = filledBbOpts( options );919 }920 921 bounds = cachedBoundingBoxImpl(this[0], options);922 } else {923 bounds = makeBoundingBox();924 925 options = options || defBbOpts;926 927 let opts = filledBbOpts(options);928 929 let eles = this;930 let cy = eles.cy();931 let styleEnabled = cy.styleEnabled();932 933 // cache the isDirty state for all eles, edges first since they depend on node state934 this.edges().forEach(isDirty);935 this.nodes().forEach(isDirty);936 937 if(styleEnabled) {938 this.recalculateRenderedStyle(useCache);939 }940 941 this.updateCompoundBounds(!useCache);942 943 for (let i = 0; i < eles.length; i++) {944 let ele = eles[i];945 946 if (isDirty(ele)) {947 ele.dirtyBoundingBoxCache();948 }949 950 updateBoundsFromBox(bounds, cachedBoundingBoxImpl(ele, opts));951 }952 }953 954 bounds.x1 = noninf( bounds.x1 );955 bounds.y1 = noninf( bounds.y1 );956 bounds.x2 = noninf( bounds.x2 );957 bounds.y2 = noninf( bounds.y2 );958 bounds.w = noninf( bounds.x2 - bounds.x1 );959 bounds.h = noninf( bounds.y2 - bounds.y1 );960 961 return bounds;962};963 964elesfn.dirtyBoundingBoxCache = function(){965 for( let i = 0; i < this.length; i++ ){966 let _p = this[i]._private;967 968 _p.bbCache = null;969 _p.bbCachePosKey = null;970 _p.bodyBounds = null;971 _p.overlayBounds = null;972 _p.labelBounds.all = null;973 _p.labelBounds.source = null;974 _p.labelBounds.target = null;975 _p.labelBounds.main = null;976 _p.labelBounds.sourceRot = null;977 _p.labelBounds.targetRot = null;978 _p.labelBounds.mainRot = null;979 _p.arrowBounds.source = null;980 _p.arrowBounds.target = null;981 _p.arrowBounds['mid-source'] = null;982 _p.arrowBounds['mid-target'] = null;983 }984 985 this.emitAndNotify('bounds');986 987 return this;988};989 990// private helper to get bounding box for custom node positions991// - good for perf in certain cases but currently requires dirtying the rendered style992// - would be better to not modify the nodes but the nodes are read directly everywhere in the renderer...993// - try to use for only things like discrete layouts where the node position would change anyway994elesfn.boundingBoxAt = function( fn ){995 let nodes = this.nodes();996 let cy = this.cy();997 let hasCompoundNodes = cy.hasCompoundNodes();998 let parents = cy.collection();999 1000 if( hasCompoundNodes ){1001 parents = nodes.filter(node => node.isParent());1002 nodes = nodes.not(parents);1003 }1004 1005 if( is.plainObject( fn ) ){1006 let obj = fn;1007 1008 fn = function(){ return obj; };1009 }1010 1011 let storeOldPos = (node, i) => node._private.bbAtOldPos = fn(node, i);1012 let getOldPos = (node) => node._private.bbAtOldPos;1013 1014 cy.startBatch();1015 1016 (1017 nodes1018 .forEach(storeOldPos)1019 .silentPositions(fn)1020 );1021 1022 if( hasCompoundNodes ){1023 parents.dirtyCompoundBoundsCache();1024 parents.dirtyBoundingBoxCache();1025 parents.updateCompoundBounds(true); // force update b/c we're inside a batch cycle1026 }1027 1028 let bb = copyBoundingBox( this.boundingBox({ useCache: false }) );1029 1030 nodes.silentPositions(getOldPos);1031 1032 if( hasCompoundNodes ){1033 parents.dirtyCompoundBoundsCache();1034 parents.dirtyBoundingBoxCache();1035 parents.updateCompoundBounds(true); // force update b/c we're inside a batch cycle1036 }1037 1038 cy.endBatch();1039 1040 return bb;1041};1042 1043fn.boundingbox = fn.bb = fn.boundingBox;1044fn.renderedBoundingbox = fn.renderedBoundingBox;1045 1046export default elesfn;1047 