UNPKG

6.3 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';
14
15/**
16 * A custom {@link Layout} that lays out a chain of nodes in a snake-like fashion.
17 *
18 * This layout assumes the graph is a chain of Nodes,
19 * positioning nodes in horizontal rows back and forth, alternating between left-to-right
20 * and right-to-left within the {@link #wrap} limit.
21 * {@link #spacing} controls the distance between nodes.
22 *
23 * When this layout is the Diagram.layout, it is automatically invalidated when the viewport changes size.
24 *
25 * If you want to experiment with this extension, try the <a href="../../extensionsJSM/Serpentine.html">Serpentine Layout</a> sample.
26 * @category Layout Extension
27 */
28export class SerpentineLayout extends go.Layout {
29 private _spacing: go.Size = new go.Size(30, 30);
30 private _wrap: number = NaN;
31
32 /**
33 * Constructs a SerpentineLayout and sets the {@link #isViewportSized} property to true.
34 */
35 constructor() {
36 super();
37 this.isViewportSized = true;
38 }
39
40 /**
41 * Gets or sets the {@link Size} whose width specifies the horizontal space between nodes
42 * and whose height specifies the minimum vertical space between nodes.
43 *
44 * The default value is 30x30.
45 */
46 get spacing(): go.Size { return this._spacing; }
47 set spacing(val: go.Size) {
48 if (!(val instanceof go.Size)) throw new Error('new value for SerpentineLayout.spacing must be a Size, not: ' + val);
49 if (!this._spacing.equals(val)) {
50 this._spacing = val;
51 this.invalidateLayout();
52 }
53 }
54
55 /**
56 * Gets or sets the total width of the layout.
57 *
58 * The default value is NaN, which for {@link Diagram#layout}s means that it uses
59 * the {@link Diagram#viewportBounds}.
60 */
61 get wrap(): number { return this._wrap; }
62 set wrap(val: number) {
63 if (this._wrap !== val) {
64 this._wrap = val;
65 this.invalidateLayout();
66 }
67 }
68
69 /**
70 * Copies properties to a cloned Layout.
71 */
72 public cloneProtected(copy: this): void {
73 super.cloneProtected(copy);
74 copy._spacing = this._spacing;
75 copy._wrap = this._wrap;
76 }
77
78 /**
79 * This method actually positions all of the Nodes, assuming that the ordering of the nodes
80 * is given by a single link from one node to the next.
81 * This respects the {@link #spacing} and {@link #wrap} properties to affect the layout.
82 * @param {Iterable.<Part>} coll A collection of {@link Part}s.
83 */
84 public doLayout(coll: go.Iterable<go.Part>): void {
85 const diagram = this.diagram;
86 coll = this.collectParts(coll);
87
88 let root = null;
89 // find a root node -- one without any incoming links
90 const it = coll.iterator;
91 while (it.next()) {
92 const n = it.value;
93 if (!(n instanceof go.Node)) continue;
94 if (root === null) root = n;
95 if (n.findLinksInto().count === 0) {
96 root = n;
97 break;
98 }
99 }
100 // couldn't find a root node
101 if (root === null) return;
102
103 const spacing = this.spacing;
104
105 // calculate the width at which we should start a new row
106 let wrap = this.wrap;
107 if (diagram !== null && isNaN(wrap)) {
108 if (this.group === null) { // for a top-level layout, use the Diagram.viewportBounds
109 const pad = diagram.padding as go.Margin;
110 wrap = Math.max(spacing.width * 2, diagram.viewportBounds.width - 24 - pad.left - pad.right);
111 } else {
112 wrap = 1000; // provide a better default value?
113 }
114 }
115
116 // implementations of doLayout that do not make use of a LayoutNetwork
117 // need to perform their own transactions
118 if (diagram !== null) diagram.startTransaction('Serpentine Layout');
119
120 // start on the left, at Layout.arrangementOrigin
121 this.arrangementOrigin = this.initialOrigin(this.arrangementOrigin);
122 let x = this.arrangementOrigin.x;
123 let rowh = 0;
124 let y = this.arrangementOrigin.y;
125 let increasing = true;
126 let node: go.Node | null = root;
127 while (node !== null) {
128 const b = this.getLayoutBounds(node);
129 // get the next node, if any
130 const nextlink: go.Link | null = node.findLinksOutOf().first();
131 const nextnode: go.Node | null = (nextlink !== null ? nextlink.toNode : null);
132 const nb = (nextnode !== null ? this.getLayoutBounds(nextnode) : new go.Rect());
133 if (increasing) {
134 node.move(new go.Point(x, y));
135 x += b.width;
136 rowh = Math.max(rowh, b.height);
137 if (x + spacing.width + nb.width > wrap) {
138 y += rowh + spacing.height;
139 x = wrap - spacing.width;
140 rowh = 0;
141 increasing = false;
142 if (nextlink !== null) {
143 nextlink.fromSpot = go.Spot.Right;
144 nextlink.toSpot = go.Spot.Right;
145 }
146 } else {
147 x += spacing.width;
148 if (nextlink !== null) {
149 nextlink.fromSpot = go.Spot.Right;
150 nextlink.toSpot = go.Spot.Left;
151 }
152 }
153 } else {
154 x -= b.width;
155 node.move(new go.Point(x, y));
156 rowh = Math.max(rowh, b.height);
157 if (x - spacing.width - nb.width < 0) {
158 y += rowh + spacing.height;
159 x = 0;
160 rowh = 0;
161 increasing = true;
162 if (nextlink !== null) {
163 nextlink.fromSpot = go.Spot.Left;
164 nextlink.toSpot = go.Spot.Left;
165 }
166 } else {
167 x -= spacing.width;
168 if (nextlink !== null) {
169 nextlink.fromSpot = go.Spot.Left;
170 nextlink.toSpot = go.Spot.Right;
171 }
172 }
173 }
174 node = nextnode;
175 }
176
177 if (diagram !== null) diagram.commitTransaction('Serpentine Layout');
178 }
179
180}