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 |
|
13 | import * 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 | */
|
28 | export 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 | }
|