UNPKG

gojs

Version:

Interactive diagrams, charts, and graphs, such as trees, flowcharts, orgcharts, UML, BPMN, or business diagrams

942 lines (941 loc) 101 kB
/* * Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved. */ var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); (function (factory) { if (typeof module === "object" && typeof module.exports === "object") { var v = factory(require, exports); if (v !== undefined) module.exports = v; } else if (typeof define === "function" && define.amd) { define(["require", "exports", "../release/go.js", "./Quadtree.js"], factory); } })(function (require, exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); /* * This is an extension and not part of the main GoJS library. * Note that the API for this class may change with any version, even point releases. * If you intend to use an extension in production, you should copy the code to your own source directory. * Extensions can be found in the GoJS kit under the extensions or extensionsTS folders. * See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information. */ var go = require("../release/go.js"); var Quadtree_js_1 = require("./Quadtree.js"); /** * @hidden @internal * Used to represent the perimeter of the currently packed * shape when packing rectangles. Segments are always assumed * to be either horizontal or vertical, and store whether or * not their first point is concave (this makes sense in the * context of representing a perimeter, as the next segment * will always be connected to the last). */ var Segment = /** @class */ (function () { /** * @hidden @internal * Constructs a new Segment. Segments are assumed to be either * horizontal or vertical, and the given coordinates should * reflect that. * @param x1 the x coordinate of the first point * @param y1 the y coordinate of the first point * @param x2 the x coordinate of the second point * @param y2 the y coordinate of the second point * @param p1Concave whether or not the first point is concave */ function Segment(x1, y1, x2, y2, p1Concave) { this.x1 = x1; this.y1 = y1; this.x2 = x2; this.y2 = y2; this.bounds = Segment.rectFromSegment(this); this.p1Concave = p1Concave; this.isHorizontal = Math.abs(y2 - y1) < 1e-7; } /** * @hidden @internal * Gets a rectangle representing the bounds of a given segment. * Used to supply bounds of segments to the quadtree. * @this {VirtualizedPackedLayout} * @param segment the segment to get a rectangle for */ Segment.rectFromSegment = function (segment) { if (Math.abs(segment.x1 - segment.x2) < 1e-7) { return new go.Rect(segment.x1, Math.min(segment.y1, segment.y2), 0, Math.abs(segment.y1 - segment.y2)); } return new go.Rect(Math.min(segment.x1, segment.x2), segment.y1, Math.abs(segment.x1 - segment.x2), 0); }; return Segment; }()); /** * @hidden @internal * Defines the possible orientations that two adjacent * horizontal/vertical segments can form. */ var Orientation; (function (Orientation) { Orientation[Orientation["NE"] = 0] = "NE"; Orientation[Orientation["NW"] = 1] = "NW"; Orientation[Orientation["SW"] = 2] = "SW"; Orientation[Orientation["SE"] = 3] = "SE"; })(Orientation || (Orientation = {})); /** * @hidden @internal * Structure for storing possible placements when packing * rectangles. Fits have a cost associated with them (lower * cost placements are preferred), and can be placed relative * to either one or two segments. If the fit is only placed * relative to one segment, s2 will be undefined. Fits placed * relative to multiple segments will hereafter be referred to * as "skip fits". */ var Fit = /** @class */ (function () { /** * @hidden @internal * Constructs a new Fit. * @param bounds the boundaries of the placement, including defined x and y coordinates * @param cost the cost of the placement, lower cost fits will be preferred * @param s1 the segment that the placement was made relative to * @param s2 the second segment that the placement was made relative to, if the fit is a skip fit */ function Fit(bounds, cost, s1, s2) { this.bounds = bounds; this.cost = cost; this.s1 = s1; this.s2 = s2; } return Fit; }()); ; /** * A Custom Layout that attempts to pack nodes as close together as possible * without overlap. Each node is assumed to be either rectangular or * circular (dictated by the {@link #hasCircularNodes} property). This layout * supports packing nodes into either a rectangle or an ellipse, with the * shape determined by the packShape property and the aspect ratio determined * by either the aspectRatio property or the specified width and height * (depending on the packMode). * * Nodes with 0 width or height cannot be packed, so they are treated by this * layout as having a width or height of 0.1 instead. * * Unlike other "Virtualized..." layouts, this does not inherit from {@link PackedLayout} * because there were a lot of internal changes that needed to be made. That may * change in the future if PackedLayout's implementation is generalized. * @category Layout Extension */ var VirtualizedPackedLayout = /** @class */ (function (_super) { __extends(VirtualizedPackedLayout, _super); function VirtualizedPackedLayout() { var _this = _super !== null && _super.apply(this, arguments) || this; // configuration defaults /** @hidden @internal */ _this._packShape = VirtualizedPackedLayout.Elliptical; /** @hidden @internal */ _this._packMode = VirtualizedPackedLayout.AspectOnly; /** @hidden @internal */ _this._sortMode = VirtualizedPackedLayout.None; /** @hidden @internal */ _this._sortOrder = VirtualizedPackedLayout.Descending; /** @hidden @internal */ _this._comparer = undefined; /** @hidden @internal */ _this._aspectRatio = 1; /** @hidden @internal */ _this._size = new go.Size(500, 500); /** @hidden @internal */ _this._defaultSize = _this._size.copy(); /** @hidden @internal */ _this._fillViewport = false; // true if size is (NaN, NaN) /** @hidden @internal */ _this._spacing = 0; /** @hidden @internal */ _this._hasCircularNodes = false; /** @hidden @internal */ _this._arrangesToOrigin = true; /** * @hidden @internal * The forced spacing value applied in the {@link VirtualizedPackedLayout.Fit} * and {@link VirtualizedPackedLayout.ExpandToFit} modes. */ _this._fixedSizeModeSpacing = 0; /** * @hidden @internal * The actual target aspect ratio, set from either {@link #aspectRatio} * or from the {@link #size}, depending on the {@link #packMode}. */ _this._eAspectRatio = _this._aspectRatio; // layout state /** @hidden @internal */ _this._center = new go.Point(); /** @hidden @internal */ _this._bounds = new go.Rect(); /** @hidden @internal */ _this._actualBounds = new go.Rect(); /** @hidden @internal */ _this._enclosingCircle = null; /** @hidden @internal */ _this._minXSegment = null; /** @hidden @internal */ _this._minYSegment = null; /** @hidden @internal */ _this._maxXSegment = null; /** @hidden @internal */ _this._maxYSegment = null; /** @hidden @internal */ _this._tree = new Quadtree_js_1.Quadtree(); // saved node bounds and segment list to use to calculate enclosing circle in the enclosingCircle getter /** @hidden @internal */ _this._nodeBounds = []; /** @hidden @internal */ _this._segments = new CircularDoublyLinkedList(); return _this; } Object.defineProperty(VirtualizedPackedLayout.prototype, "packShape", { /** * Gets or sets the shape that nodes will be packed into. Valid values are * {@link VirtualizedPackedLayout.Elliptical}, {@link VirtualizedPackedLayout.Rectangular}, and * {@link VirtualizedPackedLayout.Spiral}. * * In {@link VirtualizedPackedLayout.Spiral} mode, nodes are not packed into a particular * shape, but rather packed consecutively one after another in a spiral fashion. * The {@link #aspectRatio} property is ignored in this mode, and * the {@link #size} property (if provided) is expected to be square. * If it is not square, the largest dimension given will be used. This mode * currently only works with circular nodes, so setting it cause the assume that * layout to assume that {@link #hasCircularNodes} is true. * * Note that this property sets only the shape, not the aspect ratio. The aspect * ratio of this shape is determined by either {@link #aspectRatio} * or {@link #size}, depending on the {@link #packMode}. * * When the {@link #packMode} is {@link VirtualizedPackedLayout.Fit} or * {@link VirtualizedPackedLayout.ExpandToFit} and this property is set to true, the * layout will attempt to make the diameter of the enclosing circle of the * layout approximately equal to the greater dimension of the given * {@link #size} property. * * The default value is {@link VirtualizedPackedLayout.Elliptical}. */ get: function () { return this._packShape; }, set: function (value) { if (this._packShape !== value && (value === VirtualizedPackedLayout.Elliptical || value === VirtualizedPackedLayout.Rectangular || value === VirtualizedPackedLayout.Spiral)) { this._packShape = value; this.invalidateLayout(); } }, enumerable: true, configurable: true }); Object.defineProperty(VirtualizedPackedLayout.prototype, "packMode", { /** * Gets or sets the mode that the layout will use to determine its size. Valid values * are {@link VirtualizedPackedLayout.AspectOnly}, {@link VirtualizedPackedLayout.Fit}, and {@link VirtualizedPackedLayout.ExpandToFit}. * * The default value is {@link VirtualizedPackedLayout.AspectOnly}. In this mode, the layout will simply * grow as needed, attempting to keep the aspect ratio defined by {@link #aspectRatio}. */ get: function () { return this._packMode; }, set: function (value) { if (value === VirtualizedPackedLayout.AspectOnly || value === VirtualizedPackedLayout.Fit || value === VirtualizedPackedLayout.ExpandToFit) { this._packMode = value; this.invalidateLayout(); } }, enumerable: true, configurable: true }); Object.defineProperty(VirtualizedPackedLayout.prototype, "sortMode", { /** * Gets or sets the method by which nodes will be sorted before being packed. To change * the order, see {@link #sortOrder}. * * The default value is {@link VirtualizedPackedLayout.None}, in which nodes will not be sorted at all. */ get: function () { return this._sortMode; }, set: function (value) { if (value === VirtualizedPackedLayout.None || value === VirtualizedPackedLayout.MaxSide || value === VirtualizedPackedLayout.Area) { this._sortMode = value; this.invalidateLayout(); } }, enumerable: true, configurable: true }); Object.defineProperty(VirtualizedPackedLayout.prototype, "sortOrder", { /** * Gets or sets the order that nodes will be sorted in before being packed. To change * the sort method, see {@link #sortMode}. * * The default value is {@link VirtualizedPackedLayout.Descending} */ get: function () { return this._sortOrder; }, set: function (value) { if (value === VirtualizedPackedLayout.Descending || value === VirtualizedPackedLayout.Ascending) { this._sortOrder = value; this.invalidateLayout(); } }, enumerable: true, configurable: true }); Object.defineProperty(VirtualizedPackedLayout.prototype, "comparer", { /** * Gets or sets the comparison function used for sorting nodes. * * By default, the comparison function is set according to the values of {@link #sortMode} * and {@link #sortOrder}. * * Whether this comparison function is used is determined by the value of {@link #sortMode}. * Any value except {@link VirtualizedPackedLayout.None} will result in the comparison function being used. * ```js * $(VirtualizedPackedLayout, * { * sortMode: VirtualizedPackedLayout.Area, * comparer: function(na, nb) { * var na = na.data; * var nb = nb.data; * if (da.someProperty < db.someProperty) return -1; * if (da.someProperty > db.someProperty) return 1; * return 0; * } * } * ) * ``` */ get: function () { return this._comparer; }, set: function (value) { if (typeof value === 'function') { this._comparer = value; } }, enumerable: true, configurable: true }); Object.defineProperty(VirtualizedPackedLayout.prototype, "aspectRatio", { /** * Gets or sets the aspect ratio for the shape that nodes will be packed into. * The provided aspect ratio should be a nonzero postive number. * * Note that this only applies if the {@link #packMode} is * {@link VirtualizedPackedLayout.AspectOnly}. Otherwise, the {@link #size} * will determine the aspect ratio of the packed shape. * * The default value is 1. */ get: function () { return this._aspectRatio; }, set: function (value) { if (this.isNumeric(value) && isFinite(value) && value > 0) { this._aspectRatio = value; this.invalidateLayout(); } }, enumerable: true, configurable: true }); Object.defineProperty(VirtualizedPackedLayout.prototype, "size", { /** * Gets or sets the size for the shape that nodes will be packed into. * To fill the viewport, set a size with a width and height of NaN. Size * values of 0 are considered for layout purposes to instead be 1. * * If the width and height are set to NaN (to fill the viewport), but this * layout has no diagram associated with it, the default value of size will * be used instead. * * Note that this only applies if the {@link #packMode} is * {@link VirtualizedPackedLayout.Fit} or {@link VirtualizedPackedLayout.ExpandToFit}. * * The default value is 500x500. */ get: function () { return this._size; }, set: function (value) { // check if both width and height are NaN, as per https://stackoverflow.com/a/16988441 if (value.width !== value.width && value.height !== value.height) { this._size = value; this._fillViewport = true; this.invalidateLayout(); } else if (this.isNumeric(value.width) && isFinite(value.width) && value.width >= 0 && this.isNumeric(value.height) && isFinite(value.height) && value.height >= 0) { this._size = value; this.invalidateLayout(); } }, enumerable: true, configurable: true }); Object.defineProperty(VirtualizedPackedLayout.prototype, "spacing", { /** * Gets or sets the spacing between nodes. This value can be set to any * real number (a negative spacing will compress nodes together, and a * positive spacing will leave space between them). * * Note that the spacing value is only respected in the {@link VirtualizedPackedLayout.Fit} * {@link #packMode} if it does not cause the layout to grow outside * of the specified bounds. In the {@link VirtualizedPackedLayout.ExpandToFit} * {@link #packMode}, this property does not do anything. * * The default value is 0. */ get: function () { return this._spacing; }, set: function (value) { if (this.isNumeric(value) && isFinite(value)) { this._spacing = value; this.invalidateLayout(); } }, enumerable: true, configurable: true }); Object.defineProperty(VirtualizedPackedLayout.prototype, "hasCircularNodes", { /** * Gets or sets whether or not to assume that nodes are circular. This changes * the packing algorithm to one that is much more efficient for circular nodes. * * As this algorithm expects circles, it is assumed that if this property is set * to true that the given nodes will all have the same height and width. All * calculations are done using the width of the given nodes, so unexpected results * may occur if the height differs from the width. * * The default value is false. */ get: function () { return this._hasCircularNodes; }, set: function (value) { if (typeof (value) === typeof (true) && value !== this._hasCircularNodes) { this._hasCircularNodes = value; this.invalidateLayout(); } }, enumerable: true, configurable: true }); Object.defineProperty(VirtualizedPackedLayout.prototype, "actualSpacing", { /** * This read-only property is the effective spacing calculated after {@link VirtualizedPackedLayout#doLayout}. * * If the {@link #packMode} is {@link VirtualizedPackedLayout.AspectOnly}, this will simply be the * {@link #spacing} property. However, in the {@link VirtualizedPackedLayout.Fit} and * {@link VirtualizedPackedLayout.ExpandToFit} modes, this property will include the forced spacing added by * the modes themselves. * * Note that this property will only return a valid value after a layout has been performed. Before * then, its behavior is undefined. */ get: function () { return this.spacing + this._fixedSizeModeSpacing; }, enumerable: true, configurable: true }); Object.defineProperty(VirtualizedPackedLayout.prototype, "actualBounds", { /** * This read-only property returns the actual rectangular bounds occupied by the packed nodes. * This property does not take into account any kind of spacing around the packed nodes. * * Note that this property will only return a valid value after a layout has been performed. Before * then, its behavior is undefined. */ get: function () { return this._actualBounds; }, enumerable: true, configurable: true }); Object.defineProperty(VirtualizedPackedLayout.prototype, "enclosingCircle", { /** * This read-only property returns the smallest enclosing circle around the packed nodes. It makes * use of the {@link #hasCircularNodes} property to determine whether or not to make * enclosing circle calculations for rectangles or for circles. This property does not take into * account any kind of spacing around the packed nodes. The enclosing circle calculation is * performed the first time this property is retrieved, and then cached to prevent slow accesses * in the future. * * Note that this property will only return a valid value after a layout has been performed. Before * then, its behavior is undefined. * * This property is included as it may be useful for some data visualizations. */ get: function () { if (this._enclosingCircle === null) { if (this.hasCircularNodes || this.packShape === VirtualizedPackedLayout.Spiral) { // remember, spiral mode assumes hasCircularNodes var circles = new Array(this._nodeBounds.length); for (var i = 0; i < circles.length; i++) { var bounds = this._nodeBounds[i]; var r = bounds.width / 2; circles[i] = new Circle(bounds.x + r, bounds.y + r, r); } this._enclosingCircle = enclose(circles); } else { var points = new Array(); // TODO: make this work with segments, not the whole nodeboudns list var segment = this._segments.start; if (segment !== null) { do { points.push(new go.Point(segment.data.x1, segment.data.y1)); segment = segment.next; } while (segment !== this._segments.start); } this._enclosingCircle = enclose(points); } } return this._enclosingCircle; }, enumerable: true, configurable: true }); Object.defineProperty(VirtualizedPackedLayout.prototype, "arrangesToOrigin", { /** * Gets or sets whether or not to use the {@link Layout#arrangementOrigin} * property when placing nodes. * * The default value is true. */ get: function () { return this._arrangesToOrigin; }, set: function (value) { if (typeof (value) === typeof (true) && value !== this._arrangesToOrigin) { this._arrangesToOrigin = value; this.invalidateLayout(); } }, enumerable: true, configurable: true }); /** * Performs the VirtualizedPackedLayout. * @this {VirtualizedPackedLayout} * @param {Diagram|Group|Iterable.<Part>} coll A {@link Diagram} or a {@link Group} or a collection of {@link Part}s. */ VirtualizedPackedLayout.prototype.performLayout = function (nodes) { var diagram = this.diagram; if (diagram !== null) diagram.startTransaction('Layout'); this._bounds = new go.Rect(); this._enclosingCircle = null; // push all nodes in parts iterator to an array for easy sorting var averageSize = 0; var maxSize = 0; nodes.forEach(function (node) { averageSize += node.bounds.width + node.bounds.height; if (node.bounds.width > maxSize) { maxSize = node.bounds.width; } else if (node.bounds.height > maxSize) { maxSize = node.bounds.height; } }); averageSize = averageSize / (nodes.length * 2); if (averageSize < 1) { averageSize = 1; } if (this.sortMode !== VirtualizedPackedLayout.None) { if (!this.comparer) { var sortOrder_1 = this.sortOrder; var sortMode_1 = this.sortMode; this.comparer = function (a, b) { var sortVal = sortOrder_1 === VirtualizedPackedLayout.Ascending ? 1 : -1; if (sortMode_1 === VirtualizedPackedLayout.MaxSide) { var aMax = Math.max(a.bounds.width, a.bounds.height); var bMax = Math.max(b.bounds.width, b.bounds.height); if (aMax > bMax) { return sortVal; } else if (bMax > aMax) { return -sortVal; } return 0; } else if (sortMode_1 === VirtualizedPackedLayout.Area) { var area1 = a.bounds.width * a.bounds.height; var area2 = b.bounds.width * b.bounds.height; if (area1 > area2) { return sortVal; } else if (area2 > area1) { return -sortVal; } return 0; } return 0; }; } nodes.sort(this.comparer); } var targetWidth = this.size.width !== 0 ? this.size.width : 1; var targetHeight = this.size.height !== 0 ? this.size.height : 1; if (this._fillViewport && this.diagram !== null) { targetWidth = this.diagram.viewportBounds.width !== 0 ? this.diagram.viewportBounds.width : 1; targetHeight = this.diagram.viewportBounds.height !== 0 ? this.diagram.viewportBounds.height : 1; } else if (this._fillViewport) { targetWidth = this._defaultSize.width !== 0 ? this._defaultSize.width : 1; targetHeight = this._defaultSize.height !== 0 ? this._defaultSize.height : 1; } // set the target aspect ratio using the given bounds if necessary if (this.packMode === VirtualizedPackedLayout.Fit || this.packMode === VirtualizedPackedLayout.ExpandToFit) { this._eAspectRatio = targetWidth / targetHeight; } else { this._eAspectRatio = this.aspectRatio; } var fits = this.hasCircularNodes || this.packShape === VirtualizedPackedLayout.Spiral ? this.fitCircles(nodes) : this.fitRects(nodes); // in the Fit and ExpandToFit modes, we need to run the packing another time to figure out what the correct // _fixedModeSpacing should be. Then the layout is run a final time with the correct spacing. if (this.packMode === VirtualizedPackedLayout.Fit || this.packMode === VirtualizedPackedLayout.ExpandToFit) { var bounds0 = this._bounds.copy(); this._bounds = new go.Rect(); this._fixedSizeModeSpacing = Math.floor(averageSize); fits = this.hasCircularNodes || this.packShape === VirtualizedPackedLayout.Spiral ? this.fitCircles(nodes) : this.fitRects(nodes); if ((this.hasCircularNodes || this.packShape === VirtualizedPackedLayout.Spiral) && this.packShape === VirtualizedPackedLayout.Spiral) { var targetDiameter = Math.max(targetWidth, targetHeight); var oldDiameter = targetDiameter === targetWidth ? bounds0.width : bounds0.height; var newDiameter = targetDiameter === targetWidth ? this._bounds.width : this._bounds.height; var diff = (newDiameter - oldDiameter) / this._fixedSizeModeSpacing; this._fixedSizeModeSpacing = (targetDiameter - oldDiameter) / diff; } else { var dx = (this._bounds.width - bounds0.width) / this._fixedSizeModeSpacing; var dy = (this._bounds.height - bounds0.height) / this._fixedSizeModeSpacing; var paddingX = (targetWidth - bounds0.width) / dx; var paddingY = (targetHeight - bounds0.height) / dy; this._fixedSizeModeSpacing = Math.abs(paddingX) > Math.abs(paddingY) ? paddingX : paddingY; } if (this.packMode === VirtualizedPackedLayout.Fit) { // make sure that the spacing is not positive in this mode this._fixedSizeModeSpacing = Math.min(this._fixedSizeModeSpacing, 0); } if (this._fixedSizeModeSpacing === Infinity) { this._fixedSizeModeSpacing = -maxSize; } this._bounds = new go.Rect(); fits = this.hasCircularNodes || this.packShape === VirtualizedPackedLayout.Spiral ? this.fitCircles(nodes) : this.fitRects(nodes); } // move the nodes and calculate the actualBounds property if (this.arrangesToOrigin) { this._actualBounds = new go.Rect(this.arrangementOrigin.x, this.arrangementOrigin.y, 0, 0); } var nodeBounds = new Array(nodes.length); for (var i = 0; i < nodes.length; i++) { var fit = fits[i]; var node = nodes[i]; if (this.arrangesToOrigin) { // translate coordinates to respect this.arrangementOrigin // this.arrangementOrigin should be the top left corner of the bounding box around the layout fit.x = fit.x - this._bounds.x + this.arrangementOrigin.x; fit.y = fit.y - this._bounds.y + this.arrangementOrigin.y; } this.moveNode(node, fit.x, fit.y); nodeBounds[i] = node.bounds; this._actualBounds.unionRect(node.bounds); } this._nodeBounds = nodeBounds; // save node bounds in case we want to calculate the smallest enclosing circle later // can be overriden to change layout behavior, doesn't do anything by default this.commitLayout(); if (diagram !== null) diagram.commitTransaction('Layout'); this.isValidLayout = true; }; /** * Cause the vertex to be moved so that its position is at (nx,ny). * The default implementation assumes the node.bounds is a Rect that may be modified. * @expose * @param node * @param nx * @param ny */ VirtualizedPackedLayout.prototype.moveNode = function (node, nx, ny) { node.bounds.x = nx; node.bounds.y = ny; }; /** * This method is called at the end of {@link #doLayout}, but * before the layout transaction is committed. It can be overriden and * used to customize layout behavior. By default, the method does nothing. * @expose * @this {VirtualizedPackedLayout} */ VirtualizedPackedLayout.prototype.commitLayout = function () { }; /** * @hidden @internal * Runs a circle packing algorithm on the given array of nodes. The * algorithm used is a slightly modified version of the one proposed * by Wang et al. in "Visualization of large hierarchical data by * circle packing", 2006. * @this {VirtualizedPackedLayout} * @param nodes the array of Nodes to pack * @return {Array<Rect>} an array of positioned rectangles corresponding to the nodes argument */ VirtualizedPackedLayout.prototype.fitCircles = function (nodes) { function place(a, b, c) { var ax = a.centerX; var ay = a.centerY; var da = (b.width + c.width) / 2; var db = (a.width + c.width) / 2; var dx = b.centerX - ax; var dy = b.centerY - ay; var dc = dx * dx + dy * dy; if (dc) { var x = 0.5 + ((db *= db) - (da *= da)) / (2 * dc); var y = Math.sqrt(Math.max(0, 2 * da * (db + dc) - (db -= dc) * db - da * da)) / (2 * dc); c.x = (ax + x * dx + y * dy) - (c.width / 2); c.y = (ay + x * dy - y * dx) - (c.height / 2); } else { c.x = ax + db; c.y = ay; } return c; } function intersects(a, b) { var ar = a.height / 2; var br = b.height / 2; var dist = Math.sqrt(a.center.distanceSquaredPoint(b.center)); var difference = dist - (ar + br); return difference < -0.0000001; } var aspect = this._eAspectRatio; var shape = this.packShape; var placementCost = this.placementCost; function score(n) { var a = n.data; var b = n.next.data; var ar = a.width / 2; var br = b.width / 2; var ab = ar + br; var dx = (a.centerX * br + b.centerX * ar) / ab; var dy = (a.centerY * br + b.centerY * ar) / ab * aspect; return shape === VirtualizedPackedLayout.Elliptical ? dx * dx + dy * dy : Math.max(dx * dx, dy * dy); } var sideSpacing = (this.spacing + this._fixedSizeModeSpacing) / 2; var fits = []; var frontChain = new CircularDoublyLinkedList(); if (!nodes.length) return fits; var n1 = nodes[0].bounds.copy().inflate(sideSpacing, sideSpacing); n1.setTo(0, 0, n1.width === 0 ? 0.1 : n1.width, n1.height === 0 ? 0.1 : n1.height); fits.push(n1.setTo(0, 0, n1.width, n1.height)); this._bounds.unionRect(n1); if (nodes.length < 2) return fits; var n2 = nodes[1].bounds.copy().inflate(sideSpacing, sideSpacing); n2.setTo(0, 0, n2.width === 0 ? 0.1 : n2.width, n2.height === 0 ? 0.1 : n2.height); fits.push(n2.setTo(-n2.width, n1.centerY - n2.width / 2, n2.width, n2.height)); this._bounds.unionRect(n2); if (nodes.length < 3) return fits; var n3 = nodes[2].bounds.copy().inflate(sideSpacing, sideSpacing); n3.setTo(0, 0, n3.width === 0 ? 0.1 : n3.width, n3.height === 0 ? 0.1 : n3.height); fits.push(place(n2, n1, n3)); this._bounds.unionRect(n3); n2 = frontChain.push(n2); n3 = frontChain.push(n3); n1 = frontChain.push(n1); pack: for (var i = 3; i < nodes.length; i++) { n3 = nodes[i].bounds.copy().inflate(sideSpacing, sideSpacing); n3.setTo(0, 0, n3.width === 0 ? 0.1 : n3.width, n3.height === 0 ? 0.1 : n3.height); place(n1.data, n2.data, n3); var j = n2.next; var k = n1.prev; var sj = n2.data.width / 2; var sk = n1.data.width / 2; do { if (sj <= sk) { if (intersects(j.data, n3)) { n2 = frontChain.removeBetween(n1, j), i--; continue pack; } sj += j.data.width / 2, j = j.next; } else { if (intersects(k.data, n3)) { frontChain.removeBetween(k, n2); n1 = k, i--; continue pack; } sk += k.data.width / 2, k = k.prev; } } while (j !== k.next); fits.push(n3); this._bounds.unionRect(n3); n2 = n3 = frontChain.insertAfter(n3, n1); if (this.packShape !== VirtualizedPackedLayout.Spiral) { var aa = score(n1); while ((n3 = n3.next) !== n2) { var ca = score(n3); if (ca < aa) { n1 = n3, aa = ca; } } n2 = n1.next; } } return fits; }; /** * @hidden @internal * Runs a rectangle packing algorithm on the given array of nodes. * The algorithm presented is original, and operates by maintaining * a representation (with segments) of the perimeter of the already * packed shape. The perimeter of segments is stored in both a linked * list (for ordered iteration) and a quadtree (for fast intersection * detection). Similar to the circle packing algorithm presented * above, this is a greedy algorithm. * * For each node, a large list of possible placements is created, * each one relative to a segment on the perimeter. These placements * are sorted according to a cost function, and then the lowest cost * placement with no intersections is picked. The perimeter * representation is then updated according to the new placement. * * However, in addition to placements made relative to a single segment * on the perimeter, the algorithm also attempts to make placements * between two nonsequential segments ("skip fits"), closing gaps in the * packed shape. If a placement made in this way has no intersections * and a lower cost than any of the original placements, it is picked * instead. This step occurs simultaneously to checking intersections on * the original placement list. * * Intersections for new placements are checked only against the current * perimeter of segments, rather than the entire packed shape. * Additionally, before the quadtree is queried at all, a few closely * surrounding segments to the placement are checked in case an * intersection can be found more quickly. The combination of these two * strategies enables intersection checking to take place extremely * quickly, when it would normally be the slowest part of the entire * algorithm. * * @this {VirtualizedPackedLayout} * @param nodes the array of Nodes to pack * @return {Array<Rect>} an array of positioned rectangles corresponding to the nodes argument */ VirtualizedPackedLayout.prototype.fitRects = function (nodes) { var _a; var sideSpacing = (this.spacing + this._fixedSizeModeSpacing) / 2; var fits = []; var segments = new CircularDoublyLinkedList(); // reset layout state this._tree.clear(); this._minXSegment = null; this._maxXSegment = null; this._minYSegment = null; this._maxYSegment = null; if (nodes.length < 1) { return fits; } // place first node at 0, 0 var bounds0 = nodes[0].bounds; fits.push(new go.Rect(sideSpacing, sideSpacing, bounds0.width, bounds0.height)); fits[0].inflate(sideSpacing, sideSpacing); fits[0].setTo(0, 0, fits[0].width === 0 ? 0.1 : fits[0].width, fits[0].height === 0 ? 0.1 : fits[0].height); this._bounds.unionRect(fits[0]); this._center = fits[0].center; var s1 = new Segment(0, 0, fits[0].width, 0, false); var s2 = new Segment(fits[0].width, 0, fits[0].width, fits[0].height, false); var s3 = new Segment(fits[0].width, fits[0].height, 0, fits[0].height, false); var s4 = new Segment(0, fits[0].height, 0, 0, false); this._tree.add(s1); this._tree.add(s2); this._tree.add(s3); this._tree.add(s4); segments.push(s1, s2, s3, s4); this.fixMissingMinMaxSegments(true); for (var i = 1; i < nodes.length; i++) { var node = nodes[i]; var bounds = node.bounds.copy().inflate(sideSpacing, sideSpacing); bounds.setTo(0, 0, bounds.width === 0 ? 0.1 : bounds.width, bounds.height === 0 ? 0.1 : bounds.height); var possibleFits = new Array(segments.length); var j = 0; var s = segments.start; do { // make sure segment is perfectly straight (fixing some floating point error) var sdata = s.data; sdata.x1 = s.prev.data.x2; sdata.y1 = s.prev.data.y2; if (sdata.isHorizontal) { sdata.y2 = sdata.y1; } else { sdata.x2 = sdata.x1; } var fitBounds = this.getBestFitRect(s, bounds.width, bounds.height); var cost = this.placementCost(fitBounds); possibleFits[j] = new Fit(fitBounds, cost, s); s = s.next; j++; } while (s !== segments.start); possibleFits.sort(function (a, b) { return a.cost - b.cost; }); /* scales the cost of skip fits. a number below * one makes skip fits more likely to appear, * which is preferable because they are more * aesthetically pleasing and reduce the total * number of segments. */ var skipFitScaleFactor = 0.98; var bestFit = null; var onlyCheckSkipFits = false; for (var _i = 0, possibleFits_1 = possibleFits; _i < possibleFits_1.length; _i++) { var fit = possibleFits_1[_i]; if (bestFit && bestFit.cost <= fit.cost) { onlyCheckSkipFits = true; } var hasIntersections = true; // set initially to true to make skip fit checking work when onlyCheckSkipFits = true if (!onlyCheckSkipFits) { hasIntersections = this.fastFitHasIntersections(fit) || this.fitHasIntersections(fit); if (!hasIntersections) { bestFit = fit; continue; } } // check skip fits if (hasIntersections && !fit.s1.data.p1Concave && (fit.s1.next.data.p1Concave || fit.s1.next.next.data.p1Concave)) { var _b = this.findNextOrientedSegment(fit, fit.s1.next), nextSegment = _b[0], usePreviousSegment = _b[1]; var nextSegmentTouchesFit = false; while (hasIntersections && nextSegment !== null) { fit.bounds = this.rectAgainstMultiSegment(fit.s1, nextSegment, bounds.width, bounds.height); hasIntersections = this.fastFitHasIntersections(fit) || this.fitHasIntersections(fit); nextSegmentTouchesFit = this.segmentIsOnFitPerimeter(nextSegment.data, fit.bounds); if (hasIntersections || !nextSegmentTouchesFit) { _a = this.findNextOrientedSegment(fit, nextSegment), nextSegment = _a[0], usePreviousSegment = _a[1]; } } if (!hasIntersections && nextSegment !== null && nextSegmentTouchesFit) { fit.cost = this.placementCost(fit.bounds) * skipFitScaleFactor; if (bestFit === null || fit.cost <= bestFit.cost) { bestFit = fit; bestFit.s2 = nextSegment; if (usePreviousSegment) { bestFit.s1 = bestFit.s1.prev; } } } } } if (bestFit !== null) { this.updateSegments(bestFit, segments); fits.push(bestFit.bounds); this._bounds.unionRect(bestFit.bounds); } } // save segments in case we want to calculate the enclosing circle later this._segments = segments; return fits; }; /** * @hidden @internal * Attempts to find a segment which can be used to create a new skip fit * between fit.s1 and the found segment. A number of conditions are checked * before returning a segment, ensuring that if the skip fit *does* intersect * with the already packed shape, it will do so along the perimeter (so that it * can be detected with only knowledge about the perimeter). Multiple oriented * segments can be found for a given fit, so this function starts searching at * the segment after the given lastSegment parameter. * * Oriented segments can be oriented with either fit.s1, or fit.s1.prev. The * second return value (usePreviousSegment) indicates which the found segment is. * * @this {VirtualizedPackedLayout} * @param fit the fit to search for a new segment for * @param lastSegment the last segment found. */ VirtualizedPackedLayout.prototype.findNextOrientedSegment = function (fit, lastSegment) { lastSegment = lastSegment.next; var orientation = this.segmentOrientation(fit.s1.prev.data, fit.s1.data); var targetOrientation = (orientation + 1) % 4; while (!this.segmentIsMinOrMax(lastSegment.data)) { var usePreviousSegment = lastSegment.data.isHorizontal === fit.s1.data.isHorizontal; var lastOrientation = void 0; if (usePreviousSegment) { lastOrientation = this.segmentOrientation(lastSegment.data, lastSegment.next.data); if (lastSegment.next.data.p1Concave) { lastOrientation = (lastOrientation + 1) % 4; } } else { lastOrientation = this.segmentOrientation(lastSegment.prev.data, lastSegment.data);