UNPKG

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