UNPKG

81.7 kBPlain TextView Raw
1/*
2* Copyright (C) 1998-2021 by Northwoods Software Corporation. All Rights Reserved.
3*/
4
5/*
6* This is an extension and not part of the main GoJS library.
7* Note that the API for this class may change with any version, even point releases.
8* If you intend to use an extension in production, you should copy the code to your own source directory.
9* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
10* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
11*/
12
13import * as go from '../release/go-module.js';
14import { Quadtree } from './Quadtree.js';
15
16/**
17 * @hidden @internal
18 * Used to represent the perimeter of the currently packed
19 * shape when packing rectangles. Segments are always assumed
20 * to be either horizontal or vertical, and store whether or
21 * not their first point is concave (this makes sense in the
22 * context of representing a perimeter, as the next segment
23 * will always be connected to the last).
24 */
25class Segment {
26 public x1: number;
27 public y1: number;
28 public x2: number;
29 public y2: number;
30 public bounds: go.Rect;
31 public p1Concave: boolean;
32 public isHorizontal: boolean; // if the segment is not horizontal, it is assumed to be vertical
33
34 /**
35 * @hidden @internal
36 * Constructs a new Segment. Segments are assumed to be either
37 * horizontal or vertical, and the given coordinates should
38 * reflect that.
39 * @param x1 the x coordinate of the first point
40 * @param y1 the y coordinate of the first point
41 * @param x2 the x coordinate of the second point
42 * @param y2 the y coordinate of the second point
43 * @param p1Concave whether or not the first point is concave
44 */
45 constructor(x1: number, y1: number, x2: number, y2: number, p1Concave: boolean) {
46 this.x1 = x1;
47 this.y1 = y1;
48 this.x2 = x2;
49 this.y2 = y2;
50 this.bounds = Segment.rectFromSegment(this);
51 this.p1Concave = p1Concave;
52 this.isHorizontal = Math.abs(y2 - y1) < 1e-7;
53 }
54
55
56 /**
57 * @hidden @internal
58 * Gets a rectangle representing the bounds of a given segment.
59 * Used to supply bounds of segments to the quadtree.
60 * @this {VirtualizedPackedLayout}
61 * @param segment the segment to get a rectangle for
62 */
63 public static rectFromSegment(segment: Segment): go.Rect {
64 if (Math.abs(segment.x1 - segment.x2) < 1e-7) {
65 return new go.Rect(segment.x1, Math.min(segment.y1, segment.y2), 0, Math.abs(segment.y1 - segment.y2));
66 }
67 return new go.Rect(Math.min(segment.x1, segment.x2), segment.y1, Math.abs(segment.x1 - segment.x2), 0);
68 }
69}
70
71/**
72 * @hidden @internal
73 * Defines the possible orientations that two adjacent
74 * horizontal/vertical segments can form.
75 */
76enum Orientation {
77 NE,
78 NW,
79 SW,
80 SE
81}
82
83/**
84 * @hidden @internal
85 * Structure for storing possible placements when packing
86 * rectangles. Fits have a cost associated with them (lower
87 * cost placements are preferred), and can be placed relative
88 * to either one or two segments. If the fit is only placed
89 * relative to one segment, s2 will be undefined. Fits placed
90 * relative to multiple segments will hereafter be referred to
91 * as "skip fits".
92 */
93class Fit {
94 public bounds: go.Rect;
95 public cost: number;
96 public s1: ListNode<Segment>;
97 public s2: ListNode<Segment> | undefined;
98
99 /**
100 * @hidden @internal
101 * Constructs a new Fit.
102 * @param bounds the boundaries of the placement, including defined x and y coordinates
103 * @param cost the cost of the placement, lower cost fits will be preferred
104 * @param s1 the segment that the placement was made relative to
105 * @param s2 the second segment that the placement was made relative to, if the fit is a skip fit
106 */
107 constructor(bounds: go.Rect, cost: number, s1: ListNode<Segment>, s2?: ListNode<Segment>) {
108 this.bounds = bounds;
109 this.cost = cost;
110 this.s1 = s1;
111 this.s2 = s2;
112 }
113}
114
115interface IBounded { bounds: go.Rect };
116
117/**
118 * A Custom Layout that attempts to pack nodes as close together as possible
119 * without overlap. Each node is assumed to be either rectangular or
120 * circular (dictated by the {@link #hasCircularNodes} property). This layout
121 * supports packing nodes into either a rectangle or an ellipse, with the
122 * shape determined by the packShape property and the aspect ratio determined
123 * by either the aspectRatio property or the specified width and height
124 * (depending on the packMode).
125 *
126 * Nodes with 0 width or height cannot be packed, so they are treated by this
127 * layout as having a width or height of 0.1 instead.
128 *
129 * Unlike other "Virtualized..." layouts, this does not inherit from {@link PackedLayout}
130 * because there were a lot of internal changes that needed to be made. That may
131 * change in the future if PackedLayout's implementation is generalized.
132 * @category Layout Extension
133 */
134export class VirtualizedPackedLayout extends go.Layout {
135 /**
136 * Gets or sets the shape that nodes will be packed into. Valid values are
137 * {@link VirtualizedPackedLayout.Elliptical}, {@link VirtualizedPackedLayout.Rectangular}, and
138 * {@link VirtualizedPackedLayout.Spiral}.
139 *
140 * In {@link VirtualizedPackedLayout.Spiral} mode, nodes are not packed into a particular
141 * shape, but rather packed consecutively one after another in a spiral fashion.
142 * The {@link #aspectRatio} property is ignored in this mode, and
143 * the {@link #size} property (if provided) is expected to be square.
144 * If it is not square, the largest dimension given will be used. This mode
145 * currently only works with circular nodes, so setting it cause the assume that
146 * layout to assume that {@link #hasCircularNodes} is true.
147 *
148 * Note that this property sets only the shape, not the aspect ratio. The aspect
149 * ratio of this shape is determined by either {@link #aspectRatio}
150 * or {@link #size}, depending on the {@link #packMode}.
151 *
152 * When the {@link #packMode} is {@link VirtualizedPackedLayout.Fit} or
153 * {@link VirtualizedPackedLayout.ExpandToFit} and this property is set to true, the
154 * layout will attempt to make the diameter of the enclosing circle of the
155 * layout approximately equal to the greater dimension of the given
156 * {@link #size} property.
157 *
158 * The default value is {@link VirtualizedPackedLayout.Elliptical}.
159 */
160 get packShape(): number { return this._packShape; }
161 set packShape(value: number) {
162 if (this._packShape !== value && (value === VirtualizedPackedLayout.Elliptical || value === VirtualizedPackedLayout.Rectangular || value === VirtualizedPackedLayout.Spiral)) {
163 this._packShape = value;
164 this.invalidateLayout();
165 }
166 }
167
168 /**
169 * Gets or sets the mode that the layout will use to determine its size. Valid values
170 * are {@link VirtualizedPackedLayout.AspectOnly}, {@link VirtualizedPackedLayout.Fit}, and {@link VirtualizedPackedLayout.ExpandToFit}.
171 *
172 * The default value is {@link VirtualizedPackedLayout.AspectOnly}. In this mode, the layout will simply
173 * grow as needed, attempting to keep the aspect ratio defined by {@link #aspectRatio}.
174 */
175 get packMode(): number { return this._packMode; }
176 set packMode(value: number) {
177 if (value === VirtualizedPackedLayout.AspectOnly || value === VirtualizedPackedLayout.Fit || value === VirtualizedPackedLayout.ExpandToFit) {
178 this._packMode = value;
179 this.invalidateLayout();
180 }
181 }
182
183 /**
184 * Gets or sets the method by which nodes will be sorted before being packed. To change
185 * the order, see {@link #sortOrder}.
186 *
187 * The default value is {@link VirtualizedPackedLayout.None}, in which nodes will not be sorted at all.
188 */
189 get sortMode(): number { return this._sortMode; }
190 set sortMode(value: number) {
191 if (value === VirtualizedPackedLayout.None || value === VirtualizedPackedLayout.MaxSide || value === VirtualizedPackedLayout.Area) {
192 this._sortMode = value;
193 this.invalidateLayout();
194 }
195 }
196
197 /**
198 * Gets or sets the order that nodes will be sorted in before being packed. To change
199 * the sort method, see {@link #sortMode}.
200 *
201 * The default value is {@link VirtualizedPackedLayout.Descending}
202 */
203 get sortOrder(): number { return this._sortOrder; }
204 set sortOrder(value: number) {
205 if (value === VirtualizedPackedLayout.Descending || value === VirtualizedPackedLayout.Ascending) {
206 this._sortOrder = value;
207 this.invalidateLayout();
208 }
209 }
210
211 /**
212 * Gets or sets the comparison function used for sorting nodes.
213 *
214 * By default, the comparison function is set according to the values of {@link #sortMode}
215 * and {@link #sortOrder}.
216 *
217 * Whether this comparison function is used is determined by the value of {@link #sortMode}.
218 * Any value except {@link VirtualizedPackedLayout.None} will result in the comparison function being used.
219 * ```js
220 * $(VirtualizedPackedLayout,
221 * {
222 * sortMode: VirtualizedPackedLayout.Area,
223 * comparer: function(na, nb) {
224 * var na = na.data;
225 * var nb = nb.data;
226 * if (da.someProperty < db.someProperty) return -1;
227 * if (da.someProperty > db.someProperty) return 1;
228 * return 0;
229 * }
230 * }
231 * )
232 * ```
233 */
234 get comparer(): ((a: IBounded, b: IBounded) => number) | undefined { return this._comparer; }
235 set comparer(value: ((a: IBounded, b: IBounded) => number) | undefined) {
236 if (typeof value === 'function') {
237 this._comparer = value;
238 }
239 }
240
241 /**
242 * Gets or sets the aspect ratio for the shape that nodes will be packed into.
243 * The provided aspect ratio should be a nonzero postive number.
244 *
245 * Note that this only applies if the {@link #packMode} is
246 * {@link VirtualizedPackedLayout.AspectOnly}. Otherwise, the {@link #size}
247 * will determine the aspect ratio of the packed shape.
248 *
249 * The default value is 1.
250 */
251 get aspectRatio(): number { return this._aspectRatio; }
252 set aspectRatio(value: number) {
253 if (this.isNumeric(value) && isFinite(value) && value > 0) {
254 this._aspectRatio = value;
255 this.invalidateLayout();
256 }
257 }
258
259 /**
260 * Gets or sets the size for the shape that nodes will be packed into.
261 * To fill the viewport, set a size with a width and height of NaN. Size
262 * values of 0 are considered for layout purposes to instead be 1.
263 *
264 * If the width and height are set to NaN (to fill the viewport), but this
265 * layout has no diagram associated with it, the default value of size will
266 * be used instead.
267 *
268 * Note that this only applies if the {@link #packMode} is
269 * {@link VirtualizedPackedLayout.Fit} or {@link VirtualizedPackedLayout.ExpandToFit}.
270 *
271 * The default value is 500x500.
272 */
273 get size(): go.Size { return this._size; }
274 set size(value: go.Size) {
275 // check if both width and height are NaN, as per https://stackoverflow.com/a/16988441
276 if (value.width !== value.width && value.height !== value.height) {
277 this._size = value;
278 this._fillViewport = true;
279 this.invalidateLayout();
280 } else if (this.isNumeric(value.width) && isFinite(value.width) && value.width >= 0
281 && this.isNumeric(value.height) && isFinite(value.height) && value.height >= 0) {
282 this._size = value;
283 this.invalidateLayout();
284 }
285 }
286
287 /**
288 * Gets or sets the spacing between nodes. This value can be set to any
289 * real number (a negative spacing will compress nodes together, and a
290 * positive spacing will leave space between them).
291 *
292 * Note that the spacing value is only respected in the {@link VirtualizedPackedLayout.Fit}
293 * {@link #packMode} if it does not cause the layout to grow outside
294 * of the specified bounds. In the {@link VirtualizedPackedLayout.ExpandToFit}
295 * {@link #packMode}, this property does not do anything.
296 *
297 * The default value is 0.
298 */
299 get spacing(): number { return this._spacing; }
300 set spacing(value: number) {
301 if (this.isNumeric(value) && isFinite(value)) {
302 this._spacing = value;
303 this.invalidateLayout();
304 }
305 }
306
307 /**
308 * Gets or sets whether or not to assume that nodes are circular. This changes
309 * the packing algorithm to one that is much more efficient for circular nodes.
310 *
311 * As this algorithm expects circles, it is assumed that if this property is set
312 * to true that the given nodes will all have the same height and width. All
313 * calculations are done using the width of the given nodes, so unexpected results
314 * may occur if the height differs from the width.
315 *
316 * The default value is false.
317 */
318 get hasCircularNodes(): boolean { return this._hasCircularNodes; }
319 set hasCircularNodes(value: boolean) {
320 if (typeof(value) === typeof(true) && value !== this._hasCircularNodes) {
321 this._hasCircularNodes = value;
322 this.invalidateLayout();
323 }
324 }
325
326 /**
327 * This read-only property is the effective spacing calculated after {@link VirtualizedPackedLayout#doLayout}.
328 *
329 * If the {@link #packMode} is {@link VirtualizedPackedLayout.AspectOnly}, this will simply be the
330 * {@link #spacing} property. However, in the {@link VirtualizedPackedLayout.Fit} and
331 * {@link VirtualizedPackedLayout.ExpandToFit} modes, this property will include the forced spacing added by
332 * the modes themselves.
333 *
334 * Note that this property will only return a valid value after a layout has been performed. Before
335 * then, its behavior is undefined.
336 */
337 get actualSpacing(): number { return this.spacing + this._fixedSizeModeSpacing; }
338
339 /**
340 * This read-only property returns the actual rectangular bounds occupied by the packed nodes.
341 * This property does not take into account any kind of spacing around the packed nodes.
342 *
343 * Note that this property will only return a valid value after a layout has been performed. Before
344 * then, its behavior is undefined.
345 */
346 get actualBounds(): go.Rect { return this._actualBounds; }
347
348 /**
349 * This read-only property returns the smallest enclosing circle around the packed nodes. It makes
350 * use of the {@link #hasCircularNodes} property to determine whether or not to make
351 * enclosing circle calculations for rectangles or for circles. This property does not take into
352 * account any kind of spacing around the packed nodes. The enclosing circle calculation is
353 * performed the first time this property is retrieved, and then cached to prevent slow accesses
354 * in the future.
355 *
356 * Note that this property will only return a valid value after a layout has been performed. Before
357 * then, its behavior is undefined.
358 *
359 * This property is included as it may be useful for some data visualizations.
360 */
361 get enclosingCircle(): go.Rect {
362 if (this._enclosingCircle === null) {
363 if (this.hasCircularNodes || this.packShape === VirtualizedPackedLayout.Spiral) { // remember, spiral mode assumes hasCircularNodes
364 const circles = new Array<Circle>(this._nodeBounds.length);
365 for (let i = 0; i < circles.length; i++) {
366 const bounds = this._nodeBounds[i];
367 const r = bounds.width / 2;
368 circles[i] = new Circle(bounds.x + r, bounds.y + r, r);
369 }
370 this._enclosingCircle = enclose(circles);
371 } else {
372 const points = new Array<go.Point>(); // TODO: make this work with segments, not the whole nodeboudns list
373 let segment = this._segments.start;
374 if (segment !== null) {
375 do {
376 points.push(new go.Point(segment.data.x1, segment.data.y1));
377 segment = segment.next;
378 } while (segment !== this._segments.start);
379 }
380 this._enclosingCircle = enclose(points);
381 }
382 }
383
384 return this._enclosingCircle;
385 }
386
387 /**
388 * Gets or sets whether or not to use the {@link Layout#arrangementOrigin}
389 * property when placing nodes.
390 *
391 * The default value is true.
392 */
393 get arrangesToOrigin(): boolean { return this._arrangesToOrigin; }
394 set arrangesToOrigin(value: boolean) {
395 if (typeof(value) === typeof(true) && value !== this._arrangesToOrigin) {
396 this._arrangesToOrigin = value;
397 this.invalidateLayout();
398 }
399 }
400
401
402 /********************** Configuration constants **********************/
403
404 // These values determine the shape of the final layout
405 /**
406 * This value for {@link #packShape} causes nodes to be packed
407 * into an ellipse.
408 *
409 * The aspect ratio of this ellipse is determined by either
410 * {@link #aspectRatio} or {@link #size}.
411 * @constant
412 */
413 public static readonly Elliptical = 0;
414 /**
415 * Causes nodes to be packed into a rectangle; this value is used for
416 * {@link #packShape}.
417 *
418 * The aspect ratio of this rectangle is determined by either
419 * {@link #aspectRatio} or {@link #size}.
420 * @constant
421 */
422 public static readonly Rectangular = 1;
423 /**
424 * Causes nodes to be packed into a spiral shape; this value is used
425 * for {@link #packShape}.
426 *
427 * The {@link #aspectRatio} property is ignored in this mode, the
428 * {@link #size} is expected to be square, and {@link #hasCircularNodes}
429 * will be assumed 'true'. Please see {@link #packShape} for more details.
430 */
431 public static readonly Spiral = 2;
432
433 // These values determine the size of the layout
434 /**
435 * Nodes will be packed using the {@link #aspectRatio} property, with
436 * no size considerations; this value is used for {@link #packMode}.
437 *
438 * The {@link #spacing} property will be respected in this mode.
439 * @constant
440 */
441 public static readonly AspectOnly = 10;
442 /**
443 * Nodes will be compressed if necessary (using negative spacing) to fit the given
444 * {@link #size}. However, if the {@link #size} is bigger
445 * than the packed shape (with 0 spacing), it will not expand to fit it. This value
446 * is used for {@link #packMode}.
447 *
448 * The {@link #spacing} property will be respected in this mode, but only
449 * if it does not cause the layout to grow larger than the {@link #size}.
450 * @constant
451 */
452 public static readonly Fit = 11;
453 /**
454 * Nodes will be either compressed or spaced evenly to fit the given
455 * {@link #size}; this value is used for {@link #packMode}.
456 *
457 * The {@link #spacing} property will not be respected in this mode, and
458 * will not do anything if set.
459 * @constant
460 */
461 public static readonly ExpandToFit = 12;
462
463 // These values specify an optional method by which to sort nodes before packing
464 /**
465 * Nodes will not be sorted before packing; this value is used for {@link #sortMode}.
466 * @constant
467 */
468 public static readonly None = 20;
469 /**
470 * Nodes will be sorted by their maximum side length before packing; this value is
471 * used for {@link #sortMode}.
472 * @constant
473 */
474 public static readonly MaxSide = 21;
475 /**
476 * Nodes will be sorted by their area; this value is used for {@link #sortMode}.
477 * @constant
478 */
479 public static readonly Area = 22;
480
481 // These values specify the order that nodes will be sorted, if applicable
482 /**
483 * Nodes will be sorted in descending order; this value is used for {@link #sortOrder}.
484 *
485 * Does nothing if {@link #sortMode} is set to {@link VirtualizedPackedLayout.None}.
486 * @constant
487 */
488 public static readonly Descending = 30;
489 /**
490 * Nodes will be sorted in ascending order; this value is used for {@link #sortOrder}.
491 *
492 * Does nothing if {@link #sortMode} is set to {@link VirtualizedPackedLayout.None}.
493 * @constant
494 */
495 public static readonly Ascending = 31;
496
497
498 // configuration defaults
499 /** @hidden @internal */ private _packShape = VirtualizedPackedLayout.Elliptical;
500 /** @hidden @internal */ private _packMode = VirtualizedPackedLayout.AspectOnly;
501 /** @hidden @internal */ private _sortMode = VirtualizedPackedLayout.None;
502 /** @hidden @internal */ private _sortOrder = VirtualizedPackedLayout.Descending;
503 /** @hidden @internal */ private _comparer: ((a: IBounded, b: IBounded) => number) | undefined = undefined;
504 /** @hidden @internal */ private _aspectRatio: number = 1;
505 /** @hidden @internal */ private _size: go.Size = new go.Size(500, 500);
506 /** @hidden @internal */ private _defaultSize: go.Size = this._size.copy();
507 /** @hidden @internal */ private _fillViewport: boolean = false; // true if size is (NaN, NaN)
508 /** @hidden @internal */ private _spacing: number = 0;
509 /** @hidden @internal */ private _hasCircularNodes: boolean = false;
510 /** @hidden @internal */ private _arrangesToOrigin: boolean = true;
511
512 /**
513 * @hidden @internal
514 * The forced spacing value applied in the {@link VirtualizedPackedLayout.Fit}
515 * and {@link VirtualizedPackedLayout.ExpandToFit} modes.
516 */
517 private _fixedSizeModeSpacing: number = 0;
518
519 /**
520 * @hidden @internal
521 * The actual target aspect ratio, set from either {@link #aspectRatio}
522 * or from the {@link #size}, depending on the {@link #packMode}.
523 */
524 private _eAspectRatio: number = this._aspectRatio;
525
526 // layout state
527 /** @hidden @internal */ private _center = new go.Point();
528 /** @hidden @internal */ private _bounds = new go.Rect();
529 /** @hidden @internal */ private _actualBounds = new go.Rect();
530 /** @hidden @internal */ private _enclosingCircle: go.Rect | null = null;
531 /** @hidden @internal */ private _minXSegment: Segment | null = null;
532 /** @hidden @internal */ private _minYSegment: Segment | null = null;
533 /** @hidden @internal */ private _maxXSegment: Segment | null = null;
534 /** @hidden @internal */ private _maxYSegment: Segment | null = null;
535 /** @hidden @internal */ private _tree = new Quadtree<Segment>();
536
537 // saved node bounds and segment list to use to calculate enclosing circle in the enclosingCircle getter
538 /** @hidden @internal */ private _nodeBounds: Array<go.Rect> = [];
539 /** @hidden @internal */ private _segments: CircularDoublyLinkedList<Segment> = new CircularDoublyLinkedList<Segment>();
540
541
542 /**
543 * Performs the VirtualizedPackedLayout.
544 * @this {VirtualizedPackedLayout}
545 * @param {Diagram|Group|Iterable.<Part>} coll A {@link Diagram} or a {@link Group} or a collection of {@link Part}s.
546 */
547 public performLayout(nodes: Array<IBounded>) {
548 const diagram = this.diagram;
549 if (diagram !== null) diagram.startTransaction('Layout');
550 this._bounds = new go.Rect();
551 this._enclosingCircle = null;
552
553 // push all nodes in parts iterator to an array for easy sorting
554 let averageSize = 0;
555 let maxSize = 0;
556 nodes.forEach(function(node) {
557 averageSize += node.bounds.width + node.bounds.height;
558 if (node.bounds.width > maxSize) {
559 maxSize = node.bounds.width;
560 } else if (node.bounds.height > maxSize) {
561 maxSize = node.bounds.height;
562 }
563 });
564 averageSize = averageSize / (nodes.length * 2);
565 if (averageSize < 1) {
566 averageSize = 1;
567 }
568
569 if (this.sortMode !== VirtualizedPackedLayout.None) {
570 if (!this.comparer) {
571 const sortOrder = this.sortOrder;
572 const sortMode = this.sortMode;
573 this.comparer = (a: IBounded, b: IBounded): number => {
574 const sortVal = sortOrder === VirtualizedPackedLayout.Ascending ? 1 : -1;
575 if (sortMode === VirtualizedPackedLayout.MaxSide) {
576 const aMax = Math.max(a.bounds.width, a.bounds.height);
577 const bMax = Math.max(b.bounds.width, b.bounds.height);
578 if (aMax > bMax) {
579 return sortVal;
580 } else if (bMax > aMax) {
581 return -sortVal;
582 }
583 return 0;
584 } else if (sortMode === VirtualizedPackedLayout.Area) {
585 const area1 = a.bounds.width * a.bounds.height;
586 const area2 = b.bounds.width * b.bounds.height;
587 if (area1 > area2) {
588 return sortVal;
589 } else if (area2 > area1) {
590 return -sortVal;
591 }
592 return 0;
593 }
594 return 0;
595 };
596 }
597 nodes.sort(this.comparer);
598 }
599
600 let targetWidth = this.size.width !== 0 ? this.size.width : 1;
601 let targetHeight = this.size.height !== 0 ? this.size.height : 1;
602 if (this._fillViewport && this.diagram !== null) {
603 targetWidth = this.diagram.viewportBounds.width !== 0 ? this.diagram.viewportBounds.width : 1;
604 targetHeight = this.diagram.viewportBounds.height !== 0 ? this.diagram.viewportBounds.height : 1;
605 } else if (this._fillViewport) {
606 targetWidth = this._defaultSize.width !== 0 ? this._defaultSize.width : 1;
607 targetHeight = this._defaultSize.height !== 0 ? this._defaultSize.height : 1;
608 }
609
610 // set the target aspect ratio using the given bounds if necessary
611 if (this.packMode === VirtualizedPackedLayout.Fit || this.packMode === VirtualizedPackedLayout.ExpandToFit) {
612 this._eAspectRatio = targetWidth / targetHeight;
613 } else {
614 this._eAspectRatio = this.aspectRatio;
615 }
616
617 let fits = this.hasCircularNodes || this.packShape === VirtualizedPackedLayout.Spiral ? this.fitCircles(nodes) : this.fitRects(nodes);
618
619 // in the Fit and ExpandToFit modes, we need to run the packing another time to figure out what the correct
620 // _fixedModeSpacing should be. Then the layout is run a final time with the correct spacing.
621 if (this.packMode === VirtualizedPackedLayout.Fit || this.packMode === VirtualizedPackedLayout.ExpandToFit) {
622 const bounds0 = this._bounds.copy();
623 this._bounds = new go.Rect();
624 this._fixedSizeModeSpacing = Math.floor(averageSize);
625 fits = this.hasCircularNodes || this.packShape === VirtualizedPackedLayout.Spiral ? this.fitCircles(nodes) : this.fitRects(nodes);
626
627 if ((this.hasCircularNodes || this.packShape === VirtualizedPackedLayout.Spiral) && this.packShape === VirtualizedPackedLayout.Spiral) {
628 const targetDiameter = Math.max(targetWidth, targetHeight);
629 const oldDiameter = targetDiameter === targetWidth ? bounds0.width : bounds0.height;
630 const newDiameter = targetDiameter === targetWidth ? this._bounds.width : this._bounds.height;
631
632 const diff = (newDiameter - oldDiameter) / this._fixedSizeModeSpacing;
633
634 this._fixedSizeModeSpacing = (targetDiameter - oldDiameter) / diff;
635 } else {
636 const dx = (this._bounds.width - bounds0.width) / this._fixedSizeModeSpacing;
637 const dy = (this._bounds.height - bounds0.height) / this._fixedSizeModeSpacing;
638 const paddingX = (targetWidth - bounds0.width) / dx;
639 const paddingY = (targetHeight - bounds0.height) / dy;
640
641 this._fixedSizeModeSpacing = Math.abs(paddingX) > Math.abs(paddingY) ? paddingX : paddingY;
642 }
643
644
645 if (this.packMode === VirtualizedPackedLayout.Fit) {
646 // make sure that the spacing is not positive in this mode
647 this._fixedSizeModeSpacing = Math.min(this._fixedSizeModeSpacing, 0);
648 }
649 if (this._fixedSizeModeSpacing === Infinity) {
650 this._fixedSizeModeSpacing = -maxSize;
651 }
652
653 this._bounds = new go.Rect();
654 fits = this.hasCircularNodes || this.packShape === VirtualizedPackedLayout.Spiral ? this.fitCircles(nodes) : this.fitRects(nodes);
655 }
656
657 // move the nodes and calculate the actualBounds property
658 if (this.arrangesToOrigin) {
659 this._actualBounds = new go.Rect(this.arrangementOrigin.x, this.arrangementOrigin.y, 0, 0);
660 }
661 const nodeBounds = new Array<go.Rect>(nodes.length);
662 for (let i = 0; i < nodes.length; i++) {
663 const fit = fits[i];
664 const node = nodes[i];
665 if (this.arrangesToOrigin) {
666 // translate coordinates to respect this.arrangementOrigin
667 // this.arrangementOrigin should be the top left corner of the bounding box around the layout
668 fit.x = fit.x - this._bounds.x + this.arrangementOrigin.x;
669 fit.y = fit.y - this._bounds.y + this.arrangementOrigin.y;
670 }
671 this.moveNode(node, fit.x, fit.y);
672 nodeBounds[i] = node.bounds;
673 this._actualBounds.unionRect(node.bounds);
674 }
675 this._nodeBounds = nodeBounds; // save node bounds in case we want to calculate the smallest enclosing circle later
676
677 // can be overriden to change layout behavior, doesn't do anything by default
678 this.commitLayout();
679
680 if (diagram !== null) diagram.commitTransaction('Layout');
681
682 this.isValidLayout = true;
683 }
684
685 /**
686 * Cause the vertex to be moved so that its position is at (nx,ny).
687 * The default implementation assumes the node.bounds is a Rect that may be modified.
688 * @expose
689 * @param node
690 * @param nx
691 * @param ny
692 */
693 public moveNode(node: IBounded, nx: number, ny: number) {
694 node.bounds.x = nx;
695 node.bounds.y = ny;
696 }
697
698 /**
699 * This method is called at the end of {@link #doLayout}, but
700 * before the layout transaction is committed. It can be overriden and
701 * used to customize layout behavior. By default, the method does nothing.
702 * @expose
703 * @this {VirtualizedPackedLayout}
704 */
705 public commitLayout(): void {}
706
707 /**
708 * @hidden @internal
709 * Runs a circle packing algorithm on the given array of nodes. The
710 * algorithm used is a slightly modified version of the one proposed
711 * by Wang et al. in "Visualization of large hierarchical data by
712 * circle packing", 2006.
713 * @this {VirtualizedPackedLayout}
714 * @param nodes the array of Nodes to pack
715 * @return {Array<Rect>} an array of positioned rectangles corresponding to the nodes argument
716 */
717 private fitCircles(nodes: Array<IBounded>): Array<go.Rect> {
718 function place(a: go.Rect, b: go.Rect, c: go.Rect): go.Rect {
719 const ax = a.centerX;
720 const ay = a.centerY;
721 let da = (b.width + c.width) / 2;
722 let db = (a.width + c.width) / 2;
723 const dx = b.centerX - ax;
724 const dy = b.centerY - ay;
725 const dc = dx * dx + dy * dy;
726 if (dc) {
727 const x = 0.5 + ((db *= db) - (da *= da)) / (2 * dc);
728 const y = Math.sqrt(Math.max(0, 2 * da * (db + dc) - (db -= dc) * db - da * da)) / (2 * dc);
729 c.x = (ax + x * dx + y * dy) - (c.width / 2);
730 c.y = (ay + x * dy - y * dx) - (c.height / 2);
731 } else {
732 c.x = ax + db;
733 c.y = ay;
734 }
735 return c;
736 }
737
738 function intersects(a: go.Rect, b: go.Rect): boolean {
739 const ar = a.height / 2;
740 const br = b.height / 2;
741 const dist = Math.sqrt(a.center.distanceSquaredPoint(b.center));
742 const difference = dist - (ar + br);
743 return difference < -0.0000001;
744 }
745
746 const aspect = this._eAspectRatio;
747 const shape = this.packShape;
748 const placementCost = this.placementCost;
749 function score(n: ListNode<go.Rect>) {
750 const a = n.data;
751 const b = n.next.data;
752 const ar = a.width / 2;
753 const br = b.width / 2;
754 const ab = ar + br;
755 const dx = (a.centerX * br + b.centerX * ar) / ab;
756 const dy = (a.centerY * br + b.centerY * ar) / ab * aspect;
757 return shape === VirtualizedPackedLayout.Elliptical ? dx * dx + dy * dy : Math.max(dx * dx, dy * dy);
758 }
759
760
761 const sideSpacing = (this.spacing + this._fixedSizeModeSpacing) / 2;
762 const fits: Array<go.Rect> = [];
763 const frontChain = new CircularDoublyLinkedList<go.Rect>();
764
765 if (!nodes.length) return fits;
766
767 let r1: go.Rect = nodes[0].bounds.copy().inflate(sideSpacing, sideSpacing);
768 r1.setTo(0, 0, r1.width === 0 ? 0.1 : r1.width, r1.height === 0 ? 0.1 : r1.height);
769 fits.push(r1.setTo(0, 0, r1.width, r1.height));
770 this._bounds.unionRect(r1);
771 if (nodes.length < 2) return fits;
772
773 let r2: go.Rect = nodes[1].bounds.copy().inflate(sideSpacing, sideSpacing);
774 r2.setTo(0, 0, r2.width === 0 ? 0.1 : r2.width, r2.height === 0 ? 0.1 : r2.height);
775 fits.push(r2.setTo(-r2.width, r1.centerY - r2.width / 2, r2.width, r2.height));
776 this._bounds.unionRect(r2);
777 if (nodes.length < 3) return fits;
778
779 let r3: go.Rect = nodes[2].bounds.copy().inflate(sideSpacing, sideSpacing);
780 r3.setTo(0, 0, r3.width === 0 ? 0.1 : r3.width, r3.height === 0 ? 0.1 : r3.height);
781 fits.push(place(r2, r1, r3));
782 this._bounds.unionRect(r3);
783
784
785 let n2: ListNode<go.Rect> = frontChain.push(r2);
786 let n3: ListNode<go.Rect> = frontChain.push(r3);
787 let n1: ListNode<go.Rect> = frontChain.push(r1);
788
789 pack: for (let i = 3; i < nodes.length; i++) {
790 r3 = nodes[i].bounds.copy().inflate(sideSpacing, sideSpacing);
791 r3.setTo(0, 0, r3.width === 0 ? 0.1 : r3.width, r3.height === 0 ? 0.1 : r3.height);
792 place(n1.data, n2.data, r3);
793
794 let j = n2.next;
795 let k = n1.prev;
796 let sj = n2.data.width / 2;
797 let sk = n1.data.width / 2;
798 do {
799 if (sj <= sk) {
800 if (intersects(j.data, r3)) {
801 n2 = frontChain.removeBetween(n1, j), i--;
802 continue pack;
803 }
804 sj += j.data.width / 2, j = j.next;
805 } else {
806 if (intersects(k.data, r3)) {
807 frontChain.removeBetween(k, n2);
808 n1 = k, i--;
809 continue pack;
810 }
811 sk += k.data.width / 2, k = k.prev;
812 }
813 } while (j !== k.next);
814
815 fits.push(r3);
816 this._bounds.unionRect(r3);
817
818 n2 = n3 = frontChain.insertAfter(r3, n1);
819
820 if (this.packShape !== VirtualizedPackedLayout.Spiral) {
821 let aa = score(n1);
822 while ((n3 = n3.next) !== n2) {
823 const ca = score(n3);
824 if (ca < aa) {
825 n1 = n3, aa = ca;
826 }
827 }
828 n2 = n1.next;
829 }
830
831 }
832
833 return fits;
834 }
835
836 /**
837 * @hidden @internal
838 * Runs a rectangle packing algorithm on the given array of nodes.
839 * The algorithm presented is original, and operates by maintaining
840 * a representation (with segments) of the perimeter of the already
841 * packed shape. The perimeter of segments is stored in both a linked
842 * list (for ordered iteration) and a quadtree (for fast intersection
843 * detection). Similar to the circle packing algorithm presented
844 * above, this is a greedy algorithm.
845 *
846 * For each node, a large list of possible placements is created,
847 * each one relative to a segment on the perimeter. These placements
848 * are sorted according to a cost function, and then the lowest cost
849 * placement with no intersections is picked. The perimeter
850 * representation is then updated according to the new placement.
851 *
852 * However, in addition to placements made relative to a single segment
853 * on the perimeter, the algorithm also attempts to make placements
854 * between two nonsequential segments ("skip fits"), closing gaps in the
855 * packed shape. If a placement made in this way has no intersections
856 * and a lower cost than any of the original placements, it is picked
857 * instead. This step occurs simultaneously to checking intersections on
858 * the original placement list.
859 *
860 * Intersections for new placements are checked only against the current
861 * perimeter of segments, rather than the entire packed shape.
862 * Additionally, before the quadtree is queried at all, a few closely
863 * surrounding segments to the placement are checked in case an
864 * intersection can be found more quickly. The combination of these two
865 * strategies enables intersection checking to take place extremely
866 * quickly, when it would normally be the slowest part of the entire
867 * algorithm.
868 *
869 * @this {VirtualizedPackedLayout}
870 * @param nodes the array of Nodes to pack
871 * @return {Array<Rect>} an array of positioned rectangles corresponding to the nodes argument
872 */
873 private fitRects(nodes: Array<IBounded>): Array<go.Rect> {
874 const sideSpacing = (this.spacing + this._fixedSizeModeSpacing) / 2;
875 const fits: Array<go.Rect> = [];
876 const segments = new CircularDoublyLinkedList<Segment>();
877
878 // reset layout state
879 this._tree.clear();
880 this._minXSegment = null;
881 this._maxXSegment = null;
882 this._minYSegment = null;
883 this._maxYSegment = null;
884
885 if (nodes.length < 1) {
886 return fits;
887 }
888
889 // place first node at 0, 0
890 const bounds0 = nodes[0].bounds;
891 fits.push(new go.Rect(sideSpacing, sideSpacing, bounds0.width, bounds0.height));
892 fits[0].inflate(sideSpacing, sideSpacing);
893 fits[0].setTo(0, 0, fits[0].width === 0 ? 0.1 : fits[0].width, fits[0].height === 0 ? 0.1 : fits[0].height);
894 this._bounds.unionRect(fits[0]);
895 this._center = fits[0].center;
896
897 const s1 = new Segment(0, 0, fits[0].width, 0, false);
898 const s2 = new Segment(fits[0].width, 0, fits[0].width, fits[0].height, false);
899 const s3 = new Segment(fits[0].width, fits[0].height, 0, fits[0].height, false);
900 const s4 = new Segment(0, fits[0].height, 0, 0, false);
901 this._tree.add(s1);
902 this._tree.add(s2);
903 this._tree.add(s3);
904 this._tree.add(s4);
905 segments.push(s1, s2, s3, s4);
906 this.fixMissingMinMaxSegments(true);
907
908 for (let i = 1; i < nodes.length; i++) {
909 const node = nodes[i];
910 const bounds = node.bounds.copy().inflate(sideSpacing, sideSpacing);
911 bounds.setTo(0, 0, bounds.width === 0 ? 0.1 : bounds.width, bounds.height === 0 ? 0.1 : bounds.height);
912 const possibleFits = new Array<Fit>(segments.length);
913 let j = 0;
914 let s = segments.start as ListNode<Segment>;
915 do {
916
917 // make sure segment is perfectly straight (fixing some floating point error)
918 const sdata = s.data;
919 sdata.x1 = s.prev.data.x2;
920 sdata.y1 = s.prev.data.y2;
921 if (sdata.isHorizontal) {
922 sdata.y2 = sdata.y1;
923 } else {
924 sdata.x2 = sdata.x1;
925 }
926
927 const fitBounds = this.getBestFitRect(s, bounds.width, bounds.height);
928 const cost = this.placementCost(fitBounds);
929 possibleFits[j] = new Fit(fitBounds, cost, s);
930
931 s = s.next;
932 j++;
933 } while (s !== segments.start);
934
935 possibleFits.sort((a, b) => {
936 return a.cost - b.cost;
937 });
938
939 /* scales the cost of skip fits. a number below
940 * one makes skip fits more likely to appear,
941 * which is preferable because they are more
942 * aesthetically pleasing and reduce the total
943 * number of segments.
944 */
945 const skipFitScaleFactor = 0.98;
946
947 let bestFit: Fit | null = null;
948 let onlyCheckSkipFits = false;
949 for (const fit of possibleFits) {
950 if (bestFit && bestFit.cost <= fit.cost) {
951 onlyCheckSkipFits = true;
952 }
953
954 let hasIntersections = true; // set initially to true to make skip fit checking work when onlyCheckSkipFits = true
955 if (!onlyCheckSkipFits) {
956 hasIntersections = this.fastFitHasIntersections(fit) || this.fitHasIntersections(fit);
957 if (!hasIntersections) {
958 bestFit = fit;
959 continue;
960 }
961 }
962
963 // check skip fits
964 if (hasIntersections && !fit.s1.data.p1Concave && (fit.s1.next.data.p1Concave || fit.s1.next.next.data.p1Concave)) {
965 let [nextSegment, usePreviousSegment] = this.findNextOrientedSegment(fit, fit.s1.next);
966 let nextSegmentTouchesFit = false;
967 while (hasIntersections && nextSegment !== null) {
968 fit.bounds = this.rectAgainstMultiSegment(fit.s1, nextSegment, bounds.width, bounds.height);
969 hasIntersections = this.fastFitHasIntersections(fit) || this.fitHasIntersections(fit);
970 nextSegmentTouchesFit = this.segmentIsOnFitPerimeter(nextSegment.data, fit.bounds);
971
972 if (hasIntersections || !nextSegmentTouchesFit) {
973 [nextSegment, usePreviousSegment] = this.findNextOrientedSegment(fit, nextSegment);
974 }
975 }
976
977 if (!hasIntersections && nextSegment !== null && nextSegmentTouchesFit) {
978 fit.cost = this.placementCost(fit.bounds) * skipFitScaleFactor;
979 if (bestFit === null || fit.cost <= bestFit.cost) {
980 bestFit = fit;
981 bestFit.s2 = nextSegment;
982 if (usePreviousSegment) {
983 bestFit.s1 = bestFit.s1.prev;
984 }
985 }
986 }
987 }
988 }
989
990 if (bestFit !== null) {
991 this.updateSegments(bestFit, segments);
992
993 fits.push(bestFit.bounds);
994 this._bounds.unionRect(bestFit.bounds);
995 }
996
997 }
998
999 // save segments in case we want to calculate the enclosing circle later
1000 this._segments = segments;
1001
1002 return fits;
1003 }
1004
1005 /**
1006 * @hidden @internal
1007 * Attempts to find a segment which can be used to create a new skip fit
1008 * between fit.s1 and the found segment. A number of conditions are checked
1009 * before returning a segment, ensuring that if the skip fit *does* intersect
1010 * with the already packed shape, it will do so along the perimeter (so that it
1011 * can be detected with only knowledge about the perimeter). Multiple oriented
1012 * segments can be found for a given fit, so this function starts searching at
1013 * the segment after the given lastSegment parameter.
1014 *
1015 * Oriented segments can be oriented with either fit.s1, or fit.s1.prev. The
1016 * second return value (usePreviousSegment) indicates which the found segment is.
1017 *
1018 * @this {VirtualizedPackedLayout}
1019 * @param fit the fit to search for a new segment for
1020 * @param lastSegment the last segment found.
1021 */
1022 private findNextOrientedSegment(fit: Fit, lastSegment: ListNode<Segment>): [ListNode<Segment> | null, boolean] {
1023 lastSegment = lastSegment.next;
1024 const orientation = this.segmentOrientation(fit.s1.prev.data, fit.s1.data);
1025 const targetOrientation = (orientation + 1) % 4;
1026
1027 while (!this.segmentIsMinOrMax(lastSegment.data)) {
1028 const usePreviousSegment = lastSegment.data.isHorizontal === fit.s1.data.isHorizontal;
1029
1030 let lastOrientation: Orientation;
1031 if (usePreviousSegment) {
1032 lastOrientation = this.segmentOrientation(lastSegment.data, lastSegment.next.data);
1033 if (lastSegment.next.data.p1Concave) {
1034 lastOrientation = (lastOrientation + 1) % 4;
1035 }
1036 } else {
1037 lastOrientation = this.segmentOrientation(lastSegment.prev.data, lastSegment.data);
1038 if (lastSegment.data.p1Concave) {
1039 lastOrientation = (lastOrientation + 1) % 4;
1040 }
1041 }
1042 const validLastOrientation = lastOrientation === targetOrientation;
1043
1044 const exceededPrimaryDimension = fit.s1.data.isHorizontal ?
1045 Math.abs(lastSegment.data.y1 - fit.s1.data.y1) + 1e-7 > fit.bounds.height :
1046 Math.abs(lastSegment.data.x1 - fit.s1.data.x1) + 1e-7 > fit.bounds.width;
1047
1048 let validCornerPlacement: boolean;
1049 let exceededSecondaryDimension: boolean;
1050 switch (orientation) {
1051 case Orientation.NE:
1052 validCornerPlacement = fit.s1.data.x1 < lastSegment.data.x1;
1053 exceededSecondaryDimension = usePreviousSegment ? fit.s1.data.y1 - fit.bounds.height >= lastSegment.data.y1 : fit.s1.data.y2 + fit.bounds.height <= lastSegment.data.y1;
1054 break;
1055 case Orientation.NW:
1056 validCornerPlacement = fit.s1.data.y1 > lastSegment.data.y1;
1057 exceededSecondaryDimension = usePreviousSegment ? fit.s1.data.x1 - fit.bounds.width >= lastSegment.data.x1 : fit.s1.data.x2 + fit.bounds.width <= lastSegment.data.x1;
1058 break;
1059 case Orientation.SW:
1060 validCornerPlacement = fit.s1.data.x1 > lastSegment.data.x1;
1061 exceededSecondaryDimension = usePreviousSegment ? fit.s1.data.y1 + fit.bounds.height <= lastSegment.data.y1 : fit.s1.data.y2 - fit.bounds.height >= lastSegment.data.y1;
1062 break;
1063 case Orientation.SE:
1064 validCornerPlacement = fit.s1.data.y1 < lastSegment.data.y1;
1065 exceededSecondaryDimension = usePreviousSegment ? fit.s1.data.x1 + fit.bounds.width <= lastSegment.data.x1 : fit.s1.data.x2 - fit.bounds.width >= lastSegment.data.x1;
1066 break;
1067 default:
1068 throw new Error('Unknown orientation ' + orientation);
1069 }
1070
1071 if (!exceededPrimaryDimension && !exceededSecondaryDimension && validCornerPlacement && validLastOrientation) {
1072 return [lastSegment, usePreviousSegment];
1073 }
1074
1075 lastSegment = lastSegment.next;
1076 }
1077
1078 return [null, false];
1079 }
1080
1081 /**
1082 * @hidden @internal
1083 * Returns the orientation of two adjacent segments. s2
1084 * is assumed to start at the end of s1.
1085 * @this {VirtualizedPackedLayout}
1086 * @param s1 the first segment
1087 * @param s2 the second segment
1088 */
1089 private segmentOrientation(s1: Segment, s2: Segment): Orientation {
1090 if (s1.isHorizontal) {
1091 if (s1.x1 < s2.x1) {
1092 return s2.p1Concave ? Orientation.SE : Orientation.NE;
1093 } else {
1094 return s2.p1Concave ? Orientation.NW : Orientation.SW;
1095 }
1096 } else {
1097 if (s1.y1 < s2.y1) {
1098 return s2.p1Concave ? Orientation.SW : Orientation.SE;
1099 } else {
1100 return s2.p1Concave ? Orientation.NE : Orientation.NW;
1101 }
1102 }
1103 }
1104
1105 /**
1106 * @hidden @internal
1107 * Fits a rectangle between two segments (used for skip fits). This is an operation
1108 * related more to corners than segments, so fit.s1 should always be supplied for
1109 * segment a (even if usePreviousSegment was true in the return value for
1110 * {@link #findNextOrientedSegment}).
1111 *
1112 * @this {VirtualizedPackedLayout}
1113 * @param a the first segment to fit between, should always be fit.s1
1114 * @param b the second segment to fit between, found with {@link #findNextOrientedSegment}
1115 * @param width the width of the rectangle, should be fit.width
1116 * @param height the height of the rectangle, should be fit.height
1117 */
1118 private rectAgainstMultiSegment(a: ListNode<Segment>, b: ListNode<Segment>, width: number, height: number): go.Rect {
1119 switch (this.segmentOrientation(a.prev.data, a.data)) {
1120 case Orientation.NE:
1121 if (a.data.y1 > b.data.y2) {
1122 return new go.Rect(b.data.x1 - width, a.data.y1 - height, width, height);
1123 } else {
1124 return new go.Rect(a.data.x1, b.data.y1 - height, width, height);
1125 }
1126 case Orientation.NW:
1127 if (a.data.x1 > b.data.x2) {
1128 return new go.Rect(a.data.x1 - width, b.data.y1, width, height);
1129 } else {
1130 return new go.Rect(b.data.x1 - width, a.data.y1 - height, width, height);
1131 }
1132 case Orientation.SW:
1133 if (a.data.y1 < b.data.y2) {
1134 return new go.Rect(b.data.x1, a.data.y1, width, height);
1135 } else {
1136 return new go.Rect(a.data.x1 - width, b.data.y1, width, height);
1137 }
1138 case Orientation.SE:
1139 if (a.data.x1 < b.data.x2) {
1140 return new go.Rect(a.data.x1, b.data.y1 - height, width, height);
1141 } else {
1142 return new go.Rect(b.data.x1, a.data.y1, width, height);
1143 }
1144 }
1145
1146 }
1147
1148 /**
1149 * @hidden @internal
1150 * Gets the rectangle placed against the given segment with the lowest
1151 * placement cost. Rectangles can be placed against a segment either at
1152 * the top/left side, the bottom/right side, or at the center coordinate
1153 * of the entire packed shape (if the segment goes through either the x
1154 * or y coordinate of the center).
1155 * @this {VirtualizedPackedLayout}
1156 * @param s the segment to place against
1157 * @param width the width of the fit, fit.width
1158 * @param height the height of the fit, fit.height
1159 */
1160 private getBestFitRect(s: ListNode<Segment>, width: number, height: number): go.Rect {
1161 let x1 = s.data.x1;
1162 let y1 = s.data.y1;
1163 let x2 = s.data.x2;
1164 let y2 = s.data.y2;
1165 let dir = this.segmentOrientation(s.prev.data, s.data);
1166 if (s.data.p1Concave) {
1167 dir = (dir + 3) % 4;
1168 }
1169
1170 const coordIsX = dir === Orientation.NW || dir === Orientation.SE;
1171 if (dir === Orientation.NE) {
1172 y2 -= height;
1173 } else if (dir === Orientation.SE) {
1174 x1 -= width;
1175 } else if (dir === Orientation.SW) {
1176 x1 -= width;
1177 y1 -= height;
1178 x2 -= width;
1179 } else if (dir === Orientation.NW) {
1180 y1 -= height;
1181 x2 -= width;
1182 y2 -= height;
1183 }
1184
1185 const r = new go.Rect(x1, y1, width, height);
1186 const cost1 = this.placementCost(r);
1187 const cost2 = this.placementCost(r.setTo(x2, y2, width, height));
1188 let cost3 = Infinity;
1189 if (coordIsX && (this._center.x - (x1 + width / 2)) * (this._center.x - (x2 + width / 2 )) < 0) {
1190 cost3 = this.placementCost(r.setTo(this._center.x - width / 2, y1, width, height));
1191 } else if (!coordIsX && (this._center.y - (y1 + height / 2)) * (this._center.y - (y2 + height / 2)) < 0) {
1192 cost3 = this.placementCost(r.setTo(x1, this._center.y - height / 2, width, height));
1193 }
1194
1195 return cost3 < cost2 && cost3 < cost1 ? r
1196 : (cost2 < cost1 ? r.setTo(x2, y2, width, height)
1197 : r.setTo(x1, y1, width, height));
1198
1199 }
1200
1201 /**
1202 * @hidden @internal
1203 * Checks if a segment is on the perimeter of the given fit bounds.
1204 * Also returns true if the segment is within the rect, but that
1205 * shouldn't matter for any of the cases where this function is used.
1206 * @this {VirtualizedPackedLayout}
1207 * @param s the segment to test
1208 * @param bounds the fit bounds
1209 */
1210 private segmentIsOnFitPerimeter(s: Segment, bounds: go.Rect) {
1211 const xCoordinatesTogether = this.numberIsBetween(s.x1, bounds.left, bounds.right)
1212 || this.numberIsBetween(s.x2, bounds.left, bounds.right)
1213 || this.numberIsBetween(bounds.left, s.x1, s.x2)
1214 || this.numberIsBetween(bounds.right, s.x1, s.x2);
1215 const yCoordinatesTogether = this.numberIsBetween(s.y1, bounds.top, bounds.bottom)
1216 || this.numberIsBetween(s.y2, bounds.top, bounds.bottom)
1217 || this.numberIsBetween(bounds.top, s.y1, s.y2)
1218 || this.numberIsBetween(bounds.bottom, s.y1, s.y2);
1219 return (s.isHorizontal && (this.approxEqual(s.y1, bounds.top) || this.approxEqual(s.y1, bounds.bottom)) && xCoordinatesTogether)
1220 || (!s.isHorizontal && (this.approxEqual(s.x1, bounds.left) || this.approxEqual(s.x1, bounds.right)) && yCoordinatesTogether);
1221 }
1222
1223 /**
1224 * @hidden @internal
1225 * Checks if a point is on the perimeter of the given fit bounds.
1226 * Also returns true if the point is within the rect, but that
1227 * shouldn't matter for any of the cases where this function is used.
1228 * @this {VirtualizedPackedLayout}
1229 * @param x the x coordinate of the point to test
1230 * @param y the y coordinate of the point to test
1231 * @param bounds the fit bounds
1232 */
1233 private pointIsOnFitPerimeter(x: number, y: number, bounds: go.Rect): boolean {
1234 return (x >= bounds.left - 1e-7 && x <= bounds.right + 1e-7 && y >= bounds.top - 1e-7 && y <= bounds.bottom + 1e-7);
1235 }
1236
1237 /**
1238 * @hidden @internal
1239 * Checks if a point is on the corner of the given fit bounds.
1240 * @this {VirtualizedPackedLayout}
1241 * @param x the x coordinate of the point to test
1242 * @param y the y coordinate of the point to test
1243 * @param bounds the fit bounds
1244 */
1245 private pointIsFitCorner(x: number, y: number, bounds: go.Rect) {
1246 return (this.approxEqual(x, bounds.left) && this.approxEqual(y, bounds.top)) ||
1247 (this.approxEqual(x, bounds.right) && this.approxEqual(y, bounds.top)) ||
1248 (this.approxEqual(x, bounds.left) && this.approxEqual(y, bounds.bottom)) ||
1249 (this.approxEqual(x, bounds.right) && this.approxEqual(y, bounds.bottom));
1250 }
1251
1252 /**
1253 * @hidden @internal
1254 * Updates the representation of the perimeter of segments after
1255 * a new placement is made. This modifies the given segments list,
1256 * as well as the quadtree class variable {@link #_tree}.
1257 * Also updates the minimum/maximum segments if they have changed as
1258 * a result of the new placement.
1259 * @this {VirtualizedPackedLayout}
1260 * @param fit the fit to add
1261 * @param segments the list of segments to update
1262 */
1263 private updateSegments(fit: Fit, segments: CircularDoublyLinkedList<Segment>): void {
1264 let s0 = fit.s1;
1265 while (this.pointIsOnFitPerimeter(s0.data.x1, s0.data.y1, fit.bounds)) {
1266 s0 = s0.prev;
1267 }
1268 if (!this.segmentIsMinOrMax(s0.data) || !this.segmentIsMinOrMax(s0.prev.data)) {
1269 let sTest = s0.prev.prev; // test for conflicting segments
1270 let sFound: ListNode<Segment> | null = null;
1271 let minMaxCount = 0;
1272 while (minMaxCount < 2) {
1273 if (this.segmentIsOnFitPerimeter(sTest.data, fit.bounds)) {
1274 sFound = sTest;
1275 }
1276 sTest = sTest.prev;
1277 if (this.segmentIsMinOrMax(sTest.next.data)) {
1278 minMaxCount++;
1279 }
1280 }
1281 if (sFound !== null) {
1282 while (this.pointIsOnFitPerimeter(sFound.data.x1, sFound.data.y1, fit.bounds)) {
1283 sFound = sFound.prev;
1284 }
1285 this.removeBetween(segments, sFound, s0);
1286 s0 = sFound;
1287 }
1288 }
1289
1290 let nextConvexCornerOrientation: Orientation;
1291 let lastConvexCornerOrientation: Orientation;
1292
1293 let testOrientation = this.segmentOrientation(s0.prev.data, s0.data);
1294 if (s0.data.p1Concave) {
1295 testOrientation = (testOrientation + 3) % 4;
1296 }
1297 let [cornerX, cornerY] = this.cornerFromRect(testOrientation, fit.bounds);
1298 const extended0 = this.approxEqual(cornerX, s0.data.x2) && this.approxEqual(cornerY, s0.data.y2);
1299 let shortened0Precond: boolean;
1300 let [cornerX2, cornerY2] = this.cornerFromRect((testOrientation + 1) % 4, fit.bounds);
1301 if (s0.data.isHorizontal) {
1302 shortened0Precond = this.numberIsBetween(cornerX2, s0.data.x1, s0.data.x2) && this.approxEqual(cornerY2, s0.data.y1);
1303 } else {
1304 shortened0Precond = this.numberIsBetween(cornerY2, s0.data.y1, s0.data.y2) && this.approxEqual(cornerX2, s0.data.x1);
1305 }
1306 const shortened0 = !extended0 && this.pointIsFitCorner(s0.data.x2, s0.data.y2, fit.bounds)
1307 || !this.pointIsOnFitPerimeter(s0.data.x2, s0.data.y2, fit.bounds)
1308 || (this.pointIsOnFitPerimeter(s0.data.x2, s0.data.y2, fit.bounds)
1309 && !this.pointIsOnFitPerimeter(s0.data.x1, s0.data.y1, fit.bounds)
1310 && shortened0Precond);
1311 if (extended0) {
1312 // extend s0
1313 [s0.data.x2, s0.data.y2] = this.cornerFromRect((testOrientation + 3) % 4, fit.bounds);
1314 this._tree.setTo(s0.data, Segment.rectFromSegment(s0.data));
1315 nextConvexCornerOrientation = (testOrientation + 3) % 4;
1316 this.updateMinMaxSegments(s0.data);
1317 } else {
1318 if (shortened0) {
1319 [s0.data.x2, s0.data.y2] = this.cornerFromRect((testOrientation + 1) % 4, fit.bounds);
1320 this._tree.setTo(s0.data, Segment.rectFromSegment(s0.data));
1321 }
1322 const newSegment = new Segment(s0.data.x2, s0.data.y2, cornerX, cornerY, true);
1323 s0 = segments.insertAfter(newSegment, s0);
1324 this._tree.add(newSegment);
1325 nextConvexCornerOrientation = testOrientation;
1326 this.updateMinMaxSegments(newSegment);
1327 }
1328
1329 let sNext = fit.s2 ? fit.s2 : s0;
1330 while (this.pointIsOnFitPerimeter(sNext.data.x2, sNext.data.y2, fit.bounds)) {
1331 sNext = sNext.next;
1332 }
1333 if (!this.segmentIsMinOrMax(sNext.data) || !this.segmentIsMinOrMax(sNext.next.data)) {
1334 let sTest = sNext.next.next; // test for conflicting segments
1335 let sFound: ListNode<Segment> | null = null;
1336 let minMaxCount = 0;
1337 while (minMaxCount < 2) {
1338 if (this.segmentIsOnFitPerimeter(sTest.data, fit.bounds)) {
1339 sFound = sTest;
1340 }
1341 sTest = sTest.next;
1342 if (this.segmentIsMinOrMax(sTest.prev.data)) {
1343 minMaxCount++;
1344 }
1345 }
1346 if (sFound !== null) {
1347 sNext = sFound;
1348 while (this.pointIsOnFitPerimeter(sNext.data.x2, sNext.data.y2, fit.bounds)) {
1349 sNext = sNext.next;
1350 }
1351 }
1352 }
1353
1354 testOrientation = this.segmentOrientation(sNext.data, sNext.next.data);
1355 if (sNext.data.p1Concave) {
1356 testOrientation = (testOrientation + 2) % 4;
1357 }
1358 if (sNext.next.data.p1Concave) {
1359 testOrientation = (testOrientation + 1) % 4;
1360 }
1361 [cornerX2, cornerY2] = this.cornerFromRect((testOrientation + 3) % 4, fit.bounds);
1362 if (sNext.data.isHorizontal && this.numberIsBetween(cornerX2, sNext.data.x1, sNext.data.x2) && this.approxEqual(cornerY2, sNext.data.y1)
1363 || (!sNext.data.isHorizontal && this.numberIsBetween(cornerY2, sNext.data.y1, sNext.data.y2) && this.approxEqual(cornerX2, sNext.data.x1))
1364 || (sNext.data.isHorizontal && this.numberIsBetween(fit.bounds.left, sNext.data.x1, sNext.data.x2) && this.numberIsBetween(fit.bounds.right, sNext.data.x1, sNext.data.x2)
1365 && (this.approxEqual(fit.bounds.top, sNext.data.y1) || this.approxEqual(fit.bounds.bottom, sNext.data.y1)))
1366 || (!sNext.data.isHorizontal && this.numberIsBetween(fit.bounds.top, sNext.data.y1, sNext.data.y2) && this.numberIsBetween(fit.bounds.bottom, sNext.data.y1, sNext.data.y2)
1367 && (this.approxEqual(fit.bounds.left, sNext.data.x1) || this.approxEqual(fit.bounds.right, sNext.data.x1)))) {
1368 sNext = sNext.next;
1369 testOrientation = this.segmentOrientation(sNext.data, sNext.next.data);
1370 if (sNext.data.p1Concave) {
1371 testOrientation = (testOrientation + 2) % 4;
1372 }
1373 if (sNext.next.data.p1Concave) {
1374 testOrientation = (testOrientation + 1) % 4;
1375 }
1376 }
1377
1378 this.removeBetween(segments, s0, sNext);
1379
1380 [cornerX, cornerY] = this.cornerFromRect(testOrientation, fit.bounds);
1381
1382 if (this.approxEqual(cornerX, sNext.data.x1) && this.approxEqual(cornerY, sNext.data.y1)) {
1383 // extend sNext
1384 if (s0.data.isHorizontal === sNext.data.isHorizontal && (s0.data.isHorizontal ? this.approxEqual(s0.data.y1, sNext.data.y1) : this.approxEqual(s0.data.x1, sNext.data.x1))) {
1385 s0.data.x2 = sNext.data.x2;
1386 s0.data.y2 = sNext.data.y2;
1387 this.removeSegmentFromLayoutState(sNext);
1388 segments.remove(sNext);
1389 this._tree.setTo(s0.data, Segment.rectFromSegment(s0.data));
1390 lastConvexCornerOrientation = nextConvexCornerOrientation; // no additional segments need to be added
1391 this.updateMinMaxSegments(s0.data);
1392 } else {
1393 [sNext.data.x1, sNext.data.y1] = this.cornerFromRect((testOrientation + 1) % 4, fit.bounds);
1394 this._tree.setTo(sNext.data, Segment.rectFromSegment(sNext.data));
1395 lastConvexCornerOrientation = (testOrientation + 1) % 4;
1396 this.updateMinMaxSegments(sNext.data);
1397 }
1398 } else if (extended0 && (s0.data.isHorizontal ?
1399 this.approxEqual(s0.data.y1, sNext.data.y1) && this.numberIsBetween(sNext.data.x1, s0.data.x1, s0.data.x2) :
1400 this.approxEqual(s0.data.x1, sNext.data.x1) && this.numberIsBetween(sNext.data.y1, s0.data.y1, s0.data.y2))) {
1401 if (s0.data.isHorizontal) {
1402 s0.data.x2 = sNext.data.x1;
1403 } else {
1404 s0.data.y2 = sNext.data.y1;
1405 }
1406 this._tree.setTo(s0.data, Segment.rectFromSegment(s0.data));
1407 lastConvexCornerOrientation = nextConvexCornerOrientation;
1408 sNext.data.p1Concave = true;
1409 this.updateMinMaxSegments(s0.data);
1410 } else if (!this.pointIsFitCorner(sNext.data.x1, sNext.data.y1, fit.bounds)) {
1411 // add concave segment
1412 const newSegment = new Segment(cornerX, cornerY, sNext.data.x1, sNext.data.y1, false);
1413 if (this.pointIsOnFitPerimeter(sNext.data.x1, sNext.data.y1, fit.bounds)) {
1414 sNext.data.p1Concave = true;
1415 } else {
1416 newSegment.p1Concave = true;
1417 }
1418 if (this.approxEqual(sNext.prev.data.x1, cornerX) && this.approxEqual(sNext.prev.data.y1, cornerY) && newSegment.isHorizontal === sNext.prev.data.isHorizontal) {
1419 sNext.prev.data.x2 = sNext.data.x1;
1420 sNext.prev.data.y2 = sNext.data.y1;
1421 this._tree.setTo(sNext.prev.data, Segment.rectFromSegment(sNext.prev.data));
1422 lastConvexCornerOrientation = nextConvexCornerOrientation;
1423 } else {
1424 segments.insertAfter(newSegment, sNext.prev);
1425 this._tree.add(newSegment);
1426 lastConvexCornerOrientation = testOrientation;
1427 this.updateMinMaxSegments(newSegment);
1428 }
1429 } else { // if (this.pointIsOnFitPerimeter(sNext.data.x1, sNext.data.y1, fit.bounds))
1430 // shorten existing segment
1431 [sNext.data.x1, sNext.data.y1] = this.cornerFromRect((testOrientation + 3) % 4, fit.bounds);
1432 sNext.data.p1Concave = true;
1433 this._tree.setTo(sNext.data, Segment.rectFromSegment(sNext.data));
1434 lastConvexCornerOrientation = (testOrientation + 3) % 4;
1435 }
1436
1437 while (nextConvexCornerOrientation !== lastConvexCornerOrientation) {
1438 [cornerX, cornerY] = this.cornerFromRect((nextConvexCornerOrientation + 3) % 4, fit.bounds);
1439 const newSegment = new Segment(s0.data.x2, s0.data.y2, cornerX, cornerY, false);
1440 s0 = segments.insertAfter(newSegment, s0);
1441 this._tree.add(newSegment);
1442 nextConvexCornerOrientation = (nextConvexCornerOrientation + 3) % 4;
1443 this.updateMinMaxSegments(newSegment);
1444 }
1445
1446 this.fixMissingMinMaxSegments();
1447 }
1448
1449 /**
1450 * @hidden @internal
1451 * Finds the new minimum and maximum segments in the packed shape if
1452 * any of them have been deleted. To do this quickly, the quadtree
1453 * is used.
1454 * @this{VirtualizedPackedLayout}
1455 * @param force whether or not to force an update based on the quadtree even if none of the segments were deleted
1456 */
1457 private fixMissingMinMaxSegments(force = false) {
1458 if (!this._minXSegment || !this._maxXSegment || !this._minYSegment || !this._maxYSegment || force) {
1459 [this._minXSegment, this._maxXSegment, this._minYSegment, this._maxYSegment] = this._tree.findExtremeObjects();
1460 }
1461 }
1462
1463 /**
1464 * @hidden @internal
1465 * Updates the minimum or maximum segments with a new segment if that
1466 * segment is a new minimum or maximum.
1467 * @this {VirtualizedPackedLayout}
1468 * @param s the new segment to test
1469 */
1470 private updateMinMaxSegments(s: Segment) {
1471 const centerX = (s.x1 + s.x2) / 2;
1472 const centerY = (s.y1 + s.y2) / 2;
1473 if (this._minXSegment && centerX < (this._minXSegment.x1 + this._minXSegment.x2) / 2) {
1474 this._minXSegment = s;
1475 }
1476 if (this._minYSegment && centerY < (this._minYSegment.y1 + this._minYSegment.y2) / 2) {
1477 this._minYSegment = s;
1478 }
1479 if (this._maxXSegment && centerX > (this._maxXSegment.x1 + this._maxXSegment.x2) / 2) {
1480 this._maxXSegment = s;
1481 }
1482 if (this._maxYSegment && centerY > (this._maxYSegment.y1 + this._maxYSegment.y2) / 2) {
1483 this._maxYSegment = s;
1484 }
1485 }
1486
1487 /**
1488 * @hidden @internal
1489 * Gets the x and y coordinates of a corner of a given rectangle.
1490 * @this {VirtualizedPackedLayout}
1491 * @param orientation the orientation of the corner to get
1492 * @param bounds the bounds of the rectangle to get the corner from
1493 */
1494 private cornerFromRect(orientation: Orientation, bounds: go.Rect): [number, number] {
1495 let x = bounds.x;
1496 let y = bounds.y;
1497 if (orientation === Orientation.NE || orientation === Orientation.SE) {
1498 x = bounds.right;
1499 }
1500 if (orientation === Orientation.SW || orientation === Orientation.SE) {
1501 y = bounds.bottom;
1502 }
1503 return [x, y];
1504 }
1505
1506 /**
1507 * @hidden @internal
1508 * Tests if a number is in between two other numbers, with included
1509 * allowance for some floating point error with the supplied values.
1510 * The order of the given boundaries does not matter.
1511 * @this {VirtualizedPackedLayout}
1512 * @param n the number to test
1513 * @param b1 the first boundary
1514 * @param b2 the second boundary
1515 */
1516 private numberIsBetween(n: number, b1: number, b2: number) {
1517 const tmp = b1;
1518 b1 = Math.min(b1, b2);
1519 b2 = Math.max(tmp, b2);
1520 return n + 1e-7 >= b1 && n - 1e-7 <= b2;
1521 }
1522
1523 /**
1524 * @hidden @internal
1525 * Tests whether or not a given segment is a minimum or maximum segment.
1526 * @this {VirtualizedPackedLayout}
1527 * @param s the segment to test
1528 */
1529 private segmentIsMinOrMax(s: Segment): boolean {
1530 return s === this._minXSegment || s === this._minYSegment || s === this._maxXSegment || s === this._maxYSegment;
1531 }
1532
1533 /**
1534 * @hidden @internal
1535 * Removes a segment from the layout state. This includes removing it
1536 * from the quadtree, as well as setting the corresponding minimum or
1537 * maximum segment to null if the given segment is a minimum or
1538 * maximum.
1539 * @this {VirtualizedPackedLayout}
1540 * @param s the segment to remove
1541 */
1542 private removeSegmentFromLayoutState(s: ListNode<Segment>) {
1543 if (s.data === this._minXSegment) {
1544 this._minXSegment = null;
1545 }
1546 if (s.data === this._maxXSegment) {
1547 this._maxXSegment = null;
1548 }
1549 if (s.data === this._minYSegment) {
1550 this._minYSegment = null;
1551 }
1552 if (s.data === this._maxYSegment) {
1553 this._maxYSegment = null;
1554 }
1555
1556 this._tree.remove(s.data);
1557 }
1558
1559 /**
1560 * @hidden @internal
1561 * Removes all segments between the two given segments (exclusive).
1562 * This includes removing them from the layout state, as well as
1563 * the given segment list.
1564 * @this {VirtualizedPackedLayout}
1565 * @param segments the full list of segments
1566 * @param s1 the first segment
1567 * @param s2 the second segment
1568 */
1569 private removeBetween(segments: CircularDoublyLinkedList<Segment>, s1: ListNode<Segment>, s2: ListNode<Segment>) {
1570 if (s1 === s2) return;
1571 let last = s1.next;
1572 let count = 0;
1573 while (last !== s2) {
1574 if (last === segments.start) {
1575 segments.start = s2;
1576 }
1577
1578 this.removeSegmentFromLayoutState(last);
1579
1580 count++;
1581 last = last.next;
1582 }
1583 s1.next = s2;
1584 s2.prev = s1;
1585 segments.length -= count;
1586 }
1587
1588 /**
1589 * @hidden @internal
1590 * Calculates the cost of a given fit placement, depending on the
1591 * {@link #packShape} and {@link #_eAspectRatio}.
1592 * @this {VirtualizedPackedLayout}
1593 * @param fit the fit to calculate the cost of
1594 */
1595 private placementCost(fit: go.Rect): number {
1596 if (this.packShape === VirtualizedPackedLayout.Rectangular) {
1597 if (this._bounds.containsRect(fit)) {
1598 return 0;
1599 }
1600 return Math.max(Math.abs(this._center.x - fit.center.x), Math.abs(this._center.y - fit.center.y) * this._eAspectRatio);
1601 } else { // if (this.packShape === VirtualizedPackedLayout.Elliptical)
1602 return Math.pow((fit.center.x - this._center.x) / this._eAspectRatio, 2) + Math.pow(fit.center.y - this._center.y, 2);
1603 }
1604 }
1605
1606 /**
1607 * @hidden @internal
1608 * Uses the quadtree to determine if the given fit has any
1609 * intersections anywhere along the perimeter.
1610 * @this {VirtualizedPackedLayout}
1611 * @param fit the fit to check
1612 */
1613 private fitHasIntersections(fit: Fit) {
1614 return this._tree.intersecting(fit.bounds).length > 0;
1615 }
1616
1617 /**
1618 * @hidden @internal
1619 * Checks if a few nearby segments intersect with the given fit,
1620 * producing faster interesection detection than a complete check
1621 * with the quadtree in many cases. However, since it doesn't check
1622 * the entire perimeter, this function is susceptible to false
1623 * negatives and should only be used with a more comprehensive check.
1624 * @this {VirtualizedPackedLayout}
1625 * @param fit the fit to check
1626 */
1627 private fastFitHasIntersections(fit: Fit): boolean {
1628 let sNext = fit.s1.next;
1629 let sPrev = fit.s1.prev;
1630 for (let i = 0; i < 2; i++) {
1631 if (this.segmentIntersectsRect(sNext.data, fit.bounds)) {
1632 return true;
1633 }
1634 if (this.segmentIntersectsRect(sPrev.data, fit.bounds)) {
1635 return true;
1636 }
1637 sNext = sNext.next;
1638 sPrev = sPrev.prev;
1639 }
1640 return false;
1641 }
1642
1643 /**
1644 * @hidden @internal
1645 * Checks whether or not a segment intersects with a given rect.
1646 * Used for {@link #fastFitHasIntersections}.
1647 * @this {VirtualizedPackedLayout}
1648 * @param s the segment to test
1649 * @param r the rectangle to test
1650 */
1651 private segmentIntersectsRect(s: Segment, r: go.Rect): boolean {
1652 const left = Math.min(s.x1, s.x2);
1653 const right = Math.max(s.x1, s.x2);
1654 const top = Math.min(s.y1, s.y2);
1655 const bottom = Math.min(s.y1, s.y2);
1656 return !(left + 1e-7 >= r.right || right - 1e-7 <= r.left || top + 1e-7 >= r.bottom || bottom - 1e-7 <= r.top);
1657 }
1658
1659 /**
1660 * @hidden @internal
1661 * Checks if two numbers are approximately equal, used for
1662 * eliminating mistakes caused by floating point error.
1663 * @this {VirtualizedPackedLayout}
1664 * @param x the first number
1665 * @param y the second number
1666 */
1667 private approxEqual(x: number, y: number): boolean {
1668 return Math.abs(x - y) < 1e-7;
1669 }
1670
1671 /**
1672 * @hidden @internal
1673 * Checks if a value is a number, used for parameter validation
1674 * @this {VirtualizedPackedLayout}
1675 * @param value the value to check
1676 */
1677 private isNumeric(value: number): boolean {
1678 return !isNaN(Number(value.toString()));
1679 }
1680
1681 /**
1682 * @hidden @internal
1683 * Copies properties to a cloned Layout.
1684 * @this {VirtualizedPackedLayout}
1685 * @param {?} copy
1686 */
1687 public cloneProtected(copy: this): void {
1688 copy._packShape = this._packShape;
1689 copy._packMode = this._packMode;
1690 copy._sortMode = this._sortMode;
1691 copy._sortOrder = this._sortOrder;
1692 copy._comparer = this._comparer;
1693 copy._aspectRatio = this._aspectRatio;
1694 copy._size = this._size;
1695 copy._spacing = this._spacing;
1696 copy._hasCircularNodes = this._hasCircularNodes;
1697 copy._arrangesToOrigin = this._arrangesToOrigin;
1698 }
1699
1700}
1701
1702
1703/**
1704 * @hidden @internal
1705 * Class for a node in a {{@link CircularDoublyLinkedList}.
1706 * Stores a pointer to the previous and next node.
1707 */
1708class ListNode<T> {
1709 public data: T;
1710 public prev: ListNode<T>;
1711 public next: ListNode<T>;
1712
1713 constructor(data: T, prev?: ListNode<T>, next?: ListNode<T>) {
1714 this.data = data;
1715 this.prev = prev !== undefined ? prev : this;
1716 this.next = next !== undefined ? next : this;
1717 }
1718}
1719
1720/**
1721 * @hidden @internal
1722 * A Circular doubly linked list, used by {@link VirtualizedPackedLayout} to
1723 * efficiently store {@link Segment}s with fast insertion and
1724 * deletion time.
1725 */
1726class CircularDoublyLinkedList<T> {
1727
1728 /**
1729 * The start of the list, null when the list is empty.
1730 */
1731 public start: ListNode<T> | null = null;
1732 /**
1733 * The length of the list.
1734 */
1735 public length = 0;
1736
1737 /**
1738 * Constructs a new list with an optional list of values
1739 * @param vals values to create the list with
1740 */
1741 constructor(...vals: Array<T>) {
1742 if (vals.length > 0) {
1743 this.push(...vals);
1744 }
1745 }
1746
1747 /**
1748 * Inserts the given value directly after the given node
1749 * @this {CircularDoublyLinkedList}
1750 * @param val the value to insert
1751 * @param node the node to insert after
1752 * @return {ListNode<T>} the new node
1753 */
1754 public insertAfter(val: T, node: ListNode<T> | null): ListNode<T> {
1755 if (node === null) {
1756 const newnode = new ListNode<T>(val);
1757 newnode.prev = newnode;
1758 newnode.next = newnode;
1759 this.length = 1;
1760 return this.start = newnode;
1761 }
1762 const tmp = node.next;
1763 node.next = new ListNode<T>(val, node, tmp);
1764 tmp.prev = node.next;
1765 this.length++;
1766 return node.next;
1767 }
1768
1769 /**
1770 * Inserts the given value or values at the end of the list
1771 * @this {CircularDoublyLinkedList}
1772 * @param vals the value(s) to insert
1773 * @return {ListNode<T>} the node for the last value inserted (a list of values is inserted sequentially)
1774 */
1775 public push(...vals: Array<T>): ListNode<T> {
1776 if (vals.length === 0) {
1777 throw new Error('You must push at least one element!');
1778 }
1779 const sp = this.start !== null ? this.start.prev : null;
1780 let last = this.insertAfter(vals[0], sp);
1781 for (let i = 1; i < vals.length; i++) {
1782 last = this.insertAfter(vals[i], last);
1783 }
1784 return last;
1785 }
1786
1787 /**
1788 * Removes the given node from the list
1789 * @this {CircularDoublyLinkedList}
1790 * @param node the node to remove
1791 */
1792 public remove(node: ListNode<T>): void {
1793 this.length--;
1794 if (this.length) {
1795 node.prev.next = node.next;
1796 node.next.prev = node.prev;
1797 if (node === this.start) {
1798 this.start = node.next;
1799 }
1800 } else {
1801 this.start = null;
1802 }
1803 }
1804
1805 /**
1806 * Removes all nodes between the given start and end point (exclusive).
1807 * Returns the given end node.
1808 * @this {CircularDoublyLinkedList}
1809 * @param start node to start removing after
1810 * @param end node to stop removing at
1811 * @return {ListNode<T>} the end node
1812 */
1813 public removeBetween(start: ListNode<T>, end: ListNode<T>): ListNode<T> {
1814 if (start !== end) {
1815 let last = start.next;
1816 let count = 0;
1817 while (last !== end) {
1818 if (last === this.start) {
1819 this.start = end;
1820 }
1821 count++;
1822 last = last.next;
1823 }
1824 start.next = end;
1825 end.prev = start;
1826 this.length -= count;
1827 return end;
1828 }
1829 return start;
1830 }
1831
1832}
1833
1834/**
1835 * The following is a BSD-licensed implementation of the
1836 * Matousek-Sharir-Welzl algorithm for finding the smallest
1837 * enclosing circle around a given set of circles. The
1838 * original algorithm was adapted to support enclosing points
1839 * by assuming that the radius of a point is 0.
1840 */
1841
1842/**
1843 * Copyright 2010-2016 Mike Bostock
1844 * All rights reserved.
1845 *
1846 * Redistribution and use in source and binary forms, with or without modification,
1847 * are permitted provided that the following conditions are met:
1848 *
1849 * * Redistributions of source code must retain the above copyright notice, this
1850 * list of conditions and the following disclaimer.
1851 *
1852 * * Redistributions in binary form must reproduce the above copyright notice,
1853 * this list of conditions and the following disclaimer in the documentation
1854 * and/or other materials provided with the distribution.
1855 *
1856 * * Neither the name of the author nor the names of contributors may be used to
1857 * endorse or promote products derived from this software without specific prior
1858 * written permission.
1859 *
1860 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
1861 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
1862 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
1863 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
1864 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
1865 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
1866 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
1867 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
1868 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
1869 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
1870 */
1871
1872/**
1873 * @hidden @internal
1874 * Represents a circle for the purposes of the smallest-enclosing
1875 * circle algorithm. The x and y values represent the center of
1876 * the circle.
1877 */
1878class Circle extends go.Point {
1879 public r: number;
1880 constructor(x: number, y: number, r: number) {
1881 super(x, y);
1882 this.r = r;
1883 }
1884}
1885
1886/**
1887 * @hidden @internal
1888 * @param circles array of circles of points to find the enclosing circle for
1889 */
1890function enclose(circles: Array<Circle | go.Point>): go.Rect {
1891 let i = 0;
1892 const n = (circles = shuffle(circles.slice())).length;
1893 let B = new Array<Circle | go.Point>();
1894 let p: Circle | go.Point;
1895 let e: Circle | null = null;
1896
1897 while (i < n) {
1898 p = circles[i];
1899 if (e !== null && enclosesWeak(e, p)) ++i;
1900 else e = encloseBasis(B = extendBasis(B, p)), i = 0;
1901 }
1902
1903 if (e !== null) {
1904 return circleToRect(e);
1905 } else { // this will never happen, but needs to be here for strict TypeScript compilation
1906 throw new Error('Assertion error');
1907 }
1908}
1909
1910/**
1911 * @hidden @internal
1912 * Converts a Circle to a go.Rect object
1913 * @param c the Circle to convert
1914 */
1915function circleToRect(c: Circle): go.Rect {
1916 return new go.Rect(c.x - c.r, c.y - c.r, c.r * 2, c.r * 2);
1917}
1918
1919/**
1920 * @hidden @internal
1921 */
1922function extendBasis(B: Array<Circle | go.Point>, p: Circle | go.Point): Array<Circle | go.Point> {
1923 if (enclosesWeakAll(p, B)) return [p];
1924
1925 // If we get here then B must have at least one element.
1926 for (let i = 0; i < B.length; ++i) {
1927 if (enclosesNot(p, B[i])
1928 && enclosesWeakAll(encloseBasis2(B[i], p), B)) {
1929 return [B[i], p];
1930 }
1931 }
1932
1933 // If we get here then B must have at least two elements.
1934 for (let i = 0; i < B.length - 1; ++i) {
1935 for (let j = i + 1; j < B.length; ++j) {
1936 if (enclosesNot(encloseBasis2(B[i], B[j]), p)
1937 && enclosesNot(encloseBasis2(B[i], p), B[j])
1938 && enclosesNot(encloseBasis2(B[j], p), B[i])
1939 && enclosesWeakAll(encloseBasis3(B[i], B[j], p), B)) {
1940 return [B[i], B[j], p];
1941 }
1942 }
1943 }
1944
1945 // If we get here then something is very wrong.
1946 throw new Error('Assertion error');
1947}
1948
1949/**
1950 * @hidden @internal
1951 */
1952function enclosesNot(a: Circle | go.Point, b: Circle | go.Point): boolean {
1953 const ar = a instanceof Circle ? a.r : 0;
1954 const br = b instanceof Circle ? b.r : 0;
1955 const dr = ar - br;
1956 const dx = b.x - a.x;
1957 const dy = b.y - a.y;
1958 return dr < 0 || dr * dr < dx * dx + dy * dy;
1959}
1960
1961/**
1962 * @hidden @internal
1963 */
1964function enclosesWeak(a: Circle | go.Point, b: Circle | go.Point): boolean {
1965 const ar = a instanceof Circle ? a.r : 0;
1966 const br = b instanceof Circle ? b.r : 0;
1967 const dr = ar - br + 1e-6;
1968 const dx = b.x - a.x;
1969 const dy = b.y - a.y;
1970 return dr > 0 && dr * dr > dx * dx + dy * dy;
1971}
1972
1973/**
1974 * @hidden @internal
1975 */
1976function enclosesWeakAll(a: Circle | go.Point, B: Array<Circle | go.Point>): boolean {
1977 for (let i = 0; i < B.length; ++i) {
1978 if (!enclosesWeak(a, B[i])) {
1979 return false;
1980 }
1981 }
1982 return true;
1983}
1984
1985/**
1986 * @hidden @internal
1987 */
1988function encloseBasis(B: Array<Circle | go.Point>): Circle {
1989 switch (B.length) {
1990 case 2: return encloseBasis2(B[0], B[1]);
1991 case 3: return encloseBasis3(B[0], B[1], B[2]);
1992 default: return encloseBasis1(B[0]); // case 1
1993 }
1994}
1995
1996/**
1997 * @hidden @internal
1998 */
1999function encloseBasis1(a: Circle | go.Point): Circle {
2000 const ar = a instanceof Circle ? a.r : 0;
2001 return new Circle(a.x, a.y, ar);
2002}
2003
2004/**
2005 * @hidden @internal
2006 */
2007function encloseBasis2(a: Circle | go.Point, b: Circle | go.Point): Circle {
2008 const ar = a instanceof Circle ? a.r : 0;
2009 const br = b instanceof Circle ? b.r : 0;
2010 const x1 = a.x;
2011 const y1 = a.y;
2012 const r1 = ar;
2013 const x2 = b.x;
2014 const y2 = b.y;
2015 const r2 = br;
2016 const x21 = x2 - x1;
2017 const y21 = y2 - y1;
2018 const r21 = r2 - r1;
2019 const l = Math.sqrt(x21 * x21 + y21 * y21);
2020 return new Circle(
2021 (x1 + x2 + x21 / l * r21) / 2,
2022 (y1 + y2 + y21 / l * r21) / 2,
2023 (l + r1 + r2) / 2
2024 );
2025}
2026
2027/**
2028 * @hidden @internal
2029 */
2030function encloseBasis3(a: Circle | go.Point, b: Circle | go.Point, c: Circle | go.Point): Circle {
2031 const ar = a instanceof Circle ? a.r : 0;
2032 const br = b instanceof Circle ? b.r : 0;
2033 const cr = c instanceof Circle ? c.r : 0;
2034 const x1 = a.x;
2035 const y1 = a.y;
2036 const r1 = ar;
2037 const x2 = b.x;
2038 const y2 = b.y;
2039 const r2 = br;
2040 const x3 = c.x;
2041 const y3 = c.y;
2042 const r3 = cr;
2043 const a2 = x1 - x2;
2044 const a3 = x1 - x3;
2045 const b2 = y1 - y2;
2046 const b3 = y1 - y3;
2047 const c2 = r2 - r1;
2048 const c3 = r3 - r1;
2049 const d1 = x1 * x1 + y1 * y1 - r1 * r1;
2050 const d2 = d1 - x2 * x2 - y2 * y2 + r2 * r2;
2051 const d3 = d1 - x3 * x3 - y3 * y3 + r3 * r3;
2052 const ab = a3 * b2 - a2 * b3;
2053 const xa = (b2 * d3 - b3 * d2) / (ab * 2) - x1;
2054 const xb = (b3 * c2 - b2 * c3) / ab;
2055 const ya = (a3 * d2 - a2 * d3) / (ab * 2) - y1;
2056 const yb = (a2 * c3 - a3 * c2) / ab;
2057 const A = xb * xb + yb * yb - 1;
2058 const B = 2 * (r1 + xa * xb + ya * yb);
2059 const C = xa * xa + ya * ya - r1 * r1;
2060 const r = -(A ? (B + Math.sqrt(B * B - 4 * A * C)) / (2 * A) : C / B);
2061 return new Circle(
2062 x1 + xa + xb * r,
2063 y1 + ya + yb * r,
2064 r
2065 );
2066}
2067
2068/**
2069 * @hidden @internal
2070 * Shuffles array in place.
2071 * @param {Array} a items An array containing the items.
2072 */
2073function shuffle(a: Array<any>): Array<any> {
2074 let j: any;
2075 let x: any;
2076 let i: any;
2077 for (i = a.length - 1; i > 0; i--) {
2078 j = Math.floor(Math.random() * (i + 1));
2079 x = a[i];
2080 a[i] = a[j];
2081 a[j] = x;
2082 }
2083 return a;
2084}