@antv/layout 0.2.5 → 0.3.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/layout.min.js +1 -1
- package/dist/layout.min.js.map +1 -1
- package/es/layout/circular.js +4 -4
- package/es/layout/circular.js.map +1 -1
- package/es/layout/concentric.js +1 -1
- package/es/layout/concentric.js.map +1 -1
- package/es/layout/constants.js +1 -0
- package/es/layout/constants.js.map +1 -1
- package/es/layout/dagre.js +3 -3
- package/es/layout/dagre.js.map +1 -1
- package/es/layout/force2/ForceNBody.d.ts +7 -0
- package/es/layout/force2/ForceNBody.js +94 -0
- package/es/layout/force2/ForceNBody.js.map +1 -0
- package/es/layout/force2/index.d.ts +123 -0
- package/es/layout/force2/index.js +609 -0
- package/es/layout/force2/index.js.map +1 -0
- package/es/layout/gForce.js +2 -2
- package/es/layout/gForce.js.map +1 -1
- package/es/layout/gpu/gForce.js +1 -1
- package/es/layout/gpu/gForce.js.map +1 -1
- package/es/layout/grid.js +1 -1
- package/es/layout/grid.js.map +1 -1
- package/es/layout/index.d.ts +2 -1
- package/es/layout/index.js +2 -1
- package/es/layout/index.js.map +1 -1
- package/es/layout/layout.js +2 -0
- package/es/layout/layout.js.map +1 -1
- package/es/layout/types.d.ts +61 -0
- package/es/util/math.d.ts +21 -2
- package/es/util/math.js +111 -4
- package/es/util/math.js.map +1 -1
- package/lib/layout/circular.js +4 -4
- package/lib/layout/circular.js.map +1 -1
- package/lib/layout/concentric.js +1 -1
- package/lib/layout/concentric.js.map +1 -1
- package/lib/layout/constants.js +1 -0
- package/lib/layout/constants.js.map +1 -1
- package/lib/layout/dagre.js +4 -4
- package/lib/layout/dagre.js.map +1 -1
- package/lib/layout/force2/ForceNBody.d.ts +7 -0
- package/lib/layout/force2/ForceNBody.js +98 -0
- package/lib/layout/force2/ForceNBody.js.map +1 -0
- package/lib/layout/force2/index.d.ts +123 -0
- package/lib/layout/force2/index.js +644 -0
- package/lib/layout/force2/index.js.map +1 -0
- package/lib/layout/gForce.js +2 -2
- package/lib/layout/gForce.js.map +1 -1
- package/lib/layout/gpu/gForce.js +1 -1
- package/lib/layout/gpu/gForce.js.map +1 -1
- package/lib/layout/grid.js +1 -1
- package/lib/layout/grid.js.map +1 -1
- package/lib/layout/index.d.ts +2 -1
- package/lib/layout/index.js +3 -1
- package/lib/layout/index.js.map +1 -1
- package/lib/layout/layout.js +2 -0
- package/lib/layout/layout.js.map +1 -1
- package/lib/layout/types.d.ts +61 -0
- package/lib/util/math.d.ts +21 -2
- package/lib/util/math.js +116 -6
- package/lib/util/math.js.map +1 -1
- package/package.json +4 -2
- package/src/layout/circular.ts +7 -6
- package/src/layout/concentric.ts +1 -1
- package/src/layout/constants.ts +1 -0
- package/src/layout/dagre.ts +1 -1
- package/src/layout/force2/ForceNBody.ts +128 -0
- package/src/layout/force2/index.ts +743 -0
- package/src/layout/gForce.ts +7 -6
- package/src/layout/gpu/gForce.ts +4 -3
- package/src/layout/grid.ts +1 -1
- package/src/layout/index.ts +2 -0
- package/src/layout/layout.ts +2 -0
- package/src/layout/types.ts +67 -0
- package/src/util/math.ts +122 -6
package/src/layout/gForce.ts
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
* @author shiwu.wyy@antfin.com
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
7
|
OutNode,
|
|
8
8
|
Edge,
|
|
9
9
|
PointTuple,
|
|
10
10
|
IndexMap,
|
|
11
11
|
Point,
|
|
12
|
-
GForceLayoutOptions
|
|
12
|
+
GForceLayoutOptions,
|
|
13
|
+
Degree
|
|
13
14
|
} from "./types";
|
|
14
15
|
import { Base } from "./base";
|
|
15
16
|
import { isNumber, isFunction, isArray, getDegree, isObject, getEdgeTerminal } from "../util";
|
|
@@ -128,7 +129,7 @@ export class GForceLayout extends Base {
|
|
|
128
129
|
public animate: Boolean = true;
|
|
129
130
|
|
|
130
131
|
/** 存储节点度数 */
|
|
131
|
-
private degrees:
|
|
132
|
+
private degrees: Degree[];
|
|
132
133
|
|
|
133
134
|
/** 迭代中的标识 */
|
|
134
135
|
private timeInterval: number;
|
|
@@ -234,7 +235,7 @@ export class GForceLayout extends Base {
|
|
|
234
235
|
self.degrees = getDegree(nodes.length, self.nodeIdxMap, edges);
|
|
235
236
|
if (!self.getMass) {
|
|
236
237
|
self.getMass = (d) => {
|
|
237
|
-
const mass = d.mass || self.degrees[self.nodeIdxMap[d.id]] || 1;
|
|
238
|
+
const mass = d.mass || self.degrees[self.nodeIdxMap[d.id]].all || 1;
|
|
238
239
|
return mass;
|
|
239
240
|
};
|
|
240
241
|
}
|
|
@@ -406,7 +407,7 @@ export class GForceLayout extends Base {
|
|
|
406
407
|
let gravity = defaultGravity;
|
|
407
408
|
|
|
408
409
|
if (self.getCenter) {
|
|
409
|
-
const customCenterOpt = self.getCenter(node, degrees[i]);
|
|
410
|
+
const customCenterOpt = self.getCenter(node, degrees[i].all);
|
|
410
411
|
if (
|
|
411
412
|
customCenterOpt &&
|
|
412
413
|
isNumber(customCenterOpt[0]) &&
|
|
@@ -484,4 +485,4 @@ export class GForceLayout extends Base {
|
|
|
484
485
|
public getType() {
|
|
485
486
|
return "gForce";
|
|
486
487
|
}
|
|
487
|
-
}
|
|
488
|
+
}
|
package/src/layout/gpu/gForce.ts
CHANGED
|
@@ -9,7 +9,8 @@ import {
|
|
|
9
9
|
OutNode,
|
|
10
10
|
PointTuple,
|
|
11
11
|
Edge,
|
|
12
|
-
GForceGPULayoutOptions
|
|
12
|
+
GForceGPULayoutOptions,
|
|
13
|
+
Degree
|
|
13
14
|
} from "../types";
|
|
14
15
|
import { Base } from "../base";
|
|
15
16
|
import { isNumber } from "../../util";
|
|
@@ -95,7 +96,7 @@ export class GForceGPULayout extends Base {
|
|
|
95
96
|
public onLayoutEnd: () => void;
|
|
96
97
|
|
|
97
98
|
/** 存储节点度数 */
|
|
98
|
-
private degrees:
|
|
99
|
+
private degrees: Degree[];
|
|
99
100
|
|
|
100
101
|
constructor(options?: GForceGPULayoutOptions) {
|
|
101
102
|
super();
|
|
@@ -220,7 +221,7 @@ export class GForceGPULayout extends Base {
|
|
|
220
221
|
);
|
|
221
222
|
|
|
222
223
|
// init degree for mass
|
|
223
|
-
self.degrees = getDegree(nodes.length, self.nodeIdxMap, edges);
|
|
224
|
+
self.degrees = getDegree(nodes.length, self.nodeIdxMap, edges).map(degree => degree.all);
|
|
224
225
|
const masses: number[] = [];
|
|
225
226
|
const nodeStrengths: number[] = [];
|
|
226
227
|
const centerXs: number[] = [];
|
package/src/layout/grid.ts
CHANGED
|
@@ -154,7 +154,7 @@ export class GridLayout extends Base {
|
|
|
154
154
|
if (isNaN(nodes[0].degree)) {
|
|
155
155
|
const values = getDegree(layoutNodes.length, nodeIdxMap, edges);
|
|
156
156
|
layoutNodes.forEach((node, i) => {
|
|
157
|
-
node.degree = values[i];
|
|
157
|
+
node.degree = values[i].all;
|
|
158
158
|
});
|
|
159
159
|
}
|
|
160
160
|
}
|
package/src/layout/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { GridLayout } from "./grid";
|
|
2
2
|
import { RandomLayout } from "./random";
|
|
3
3
|
import { GForceLayout } from "./gForce";
|
|
4
|
+
import { Force2Layout } from "./force2";
|
|
4
5
|
import { ForceLayout } from "./force";
|
|
5
6
|
import { CircularLayout } from "./circular";
|
|
6
7
|
import { DagreLayout } from "./dagre";
|
|
@@ -25,6 +26,7 @@ export {
|
|
|
25
26
|
GridLayout,
|
|
26
27
|
RandomLayout,
|
|
27
28
|
GForceLayout,
|
|
29
|
+
Force2Layout,
|
|
28
30
|
ForceLayout,
|
|
29
31
|
CircularLayout,
|
|
30
32
|
DagreLayout,
|
package/src/layout/layout.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { Model, ILayout } from "./types";
|
|
|
3
3
|
import { getLayoutByName } from "../registy";
|
|
4
4
|
import { GridLayout } from "./grid";
|
|
5
5
|
import { RandomLayout } from "./random";
|
|
6
|
+
import { Force2Layout } from "./force2";
|
|
6
7
|
import { GForceLayout } from "./gForce";
|
|
7
8
|
import { ForceLayout } from "./force";
|
|
8
9
|
import { CircularLayout } from "./circular";
|
|
@@ -59,6 +60,7 @@ export const Layouts: { [key: string]: any } = {
|
|
|
59
60
|
fruchterman: FruchtermanLayout,
|
|
60
61
|
forceAtlas2: ForceAtlas2Layout,
|
|
61
62
|
gForce: GForceLayout,
|
|
63
|
+
force2: Force2Layout,
|
|
62
64
|
dagre: DagreLayout,
|
|
63
65
|
dagreCompound: DagreCompoundLayout,
|
|
64
66
|
circular: CircularLayout,
|
package/src/layout/types.ts
CHANGED
|
@@ -65,6 +65,11 @@ export type IndexMap = {
|
|
|
65
65
|
[key: string]: number;
|
|
66
66
|
};
|
|
67
67
|
|
|
68
|
+
export type NodeMap = {
|
|
69
|
+
[key: string]: INode;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
|
|
68
73
|
export type Matrix = number[];
|
|
69
74
|
|
|
70
75
|
export type Point = {
|
|
@@ -72,6 +77,12 @@ export type Point = {
|
|
|
72
77
|
y: number;
|
|
73
78
|
};
|
|
74
79
|
|
|
80
|
+
export type Degree = {
|
|
81
|
+
in: number;
|
|
82
|
+
out: number;
|
|
83
|
+
all: number;
|
|
84
|
+
};
|
|
85
|
+
|
|
75
86
|
export interface ComboTree {
|
|
76
87
|
id: string;
|
|
77
88
|
children?: ComboTree[];
|
|
@@ -215,6 +226,62 @@ export interface FruchtermanLayoutOptions {
|
|
|
215
226
|
onLayoutEnd?: () => void;
|
|
216
227
|
}
|
|
217
228
|
|
|
229
|
+
export interface CentripetalOptions {
|
|
230
|
+
/** 叶子节点的施加力的因子 */
|
|
231
|
+
leaf?: number | ((node: INode, nodes: INode[], edges: Edge[]) => number);
|
|
232
|
+
/** 孤立节点的施加力的因子 */
|
|
233
|
+
single?: number | ((node: INode) => number);
|
|
234
|
+
/** 其他节点的施加力的因子 */
|
|
235
|
+
others?: number | ((node: INode) => number);
|
|
236
|
+
/** 向心力的中心点,默认为画布的中心 */
|
|
237
|
+
center?: (
|
|
238
|
+
node: INode,
|
|
239
|
+
nodes: INode[],
|
|
240
|
+
edges: Edge[],
|
|
241
|
+
width: number,
|
|
242
|
+
height: number,
|
|
243
|
+
) => {
|
|
244
|
+
x: number;
|
|
245
|
+
y: number;
|
|
246
|
+
centerStrength?: number;
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
export interface Force2LayoutOptions {
|
|
250
|
+
type?: "force2";
|
|
251
|
+
center?: PointTuple;
|
|
252
|
+
width?: number;
|
|
253
|
+
height?: number;
|
|
254
|
+
linkDistance?: number | ((edge?: any, source?: any, target?: any) => number) | undefined;
|
|
255
|
+
defSpringLen: number | ((edge?: any, source?: any, target?: any) => number) | undefined;
|
|
256
|
+
nodeStrength?: number | ((d?: any) => number) | undefined;
|
|
257
|
+
edgeStrength?: number | ((d?: any) => number) | undefined;
|
|
258
|
+
preventOverlap?: boolean;
|
|
259
|
+
nodeSize?: number | number[] | ((d?: any) => number) | undefined;
|
|
260
|
+
nodeSpacing?: number | number[] | ((d?: any) => number) | undefined;
|
|
261
|
+
minMovement?: number;
|
|
262
|
+
maxIteration?: number;
|
|
263
|
+
damping?: number;
|
|
264
|
+
maxSpeed?: number;
|
|
265
|
+
coulombDisScale?: number;
|
|
266
|
+
gravity?: number;
|
|
267
|
+
factor?: number;
|
|
268
|
+
workerEnabled?: boolean;
|
|
269
|
+
centripetalOptions?: CentripetalOptions
|
|
270
|
+
leafCluster?: boolean;
|
|
271
|
+
clustering?: boolean;
|
|
272
|
+
nodeClusterBy?: string;
|
|
273
|
+
clusterNodeStrength?: number | ((node: Node) => number);
|
|
274
|
+
collideStrength?: number;
|
|
275
|
+
distanceThresholdMode?: 'mean' | 'max' | 'min';
|
|
276
|
+
animate?: boolean;
|
|
277
|
+
tick?: () => void;
|
|
278
|
+
onLayoutEnd?: () => void;
|
|
279
|
+
getMass?: ((d?: any) => number) | undefined;
|
|
280
|
+
getCenter?: ((d?: any, degree?: number) => number[]) | undefined;
|
|
281
|
+
monitor?: (params: { energy: number, nodes: INode[], edge: Edge[], iterations: number }) => void;
|
|
282
|
+
|
|
283
|
+
}
|
|
284
|
+
|
|
218
285
|
export interface GForceLayoutOptions {
|
|
219
286
|
type?: "gForce";
|
|
220
287
|
center?: PointTuple;
|
package/src/util/math.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Matrix, Model, IndexMap, Edge, OutNode } from '../layout/types';
|
|
1
|
+
import { Matrix, Model, IndexMap, Edge, Node, OutNode, Degree, NodeMap } from '../layout/types';
|
|
2
2
|
import { isObject } from './object';
|
|
3
3
|
|
|
4
4
|
export const getEdgeTerminal = (edge: Edge, type: 'source' | 'target') => {
|
|
@@ -10,22 +10,54 @@ export const getEdgeTerminal = (edge: Edge, type: 'source' | 'target') => {
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
export const getDegree = (n: number, nodeIdxMap: IndexMap, edges: Edge[] | null) => {
|
|
13
|
-
const degrees:
|
|
13
|
+
const degrees: Degree[] = [];
|
|
14
14
|
for (let i = 0; i < n; i++) {
|
|
15
|
-
degrees[i] =
|
|
15
|
+
degrees[i] = {
|
|
16
|
+
in: 0,
|
|
17
|
+
out: 0,
|
|
18
|
+
all: 0
|
|
19
|
+
};
|
|
16
20
|
}
|
|
17
21
|
if (!edges) return degrees;
|
|
22
|
+
edges.forEach((e) => {
|
|
23
|
+
const source = getEdgeTerminal(e, 'source');
|
|
24
|
+
const target = getEdgeTerminal(e, 'target');
|
|
25
|
+
if (source && degrees[nodeIdxMap[source]]) {
|
|
26
|
+
degrees[nodeIdxMap[source]].out += 1;
|
|
27
|
+
degrees[nodeIdxMap[source]].all += 1;
|
|
28
|
+
}
|
|
29
|
+
if (target && degrees[nodeIdxMap[target]]) {
|
|
30
|
+
degrees[nodeIdxMap[target]].in += 1;
|
|
31
|
+
degrees[nodeIdxMap[target]].all += 1;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
return degrees;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const getDegreeMap = (nodes: Node[], edges: Edge[] | null) => {
|
|
38
|
+
const degreesMap: { [id: string]: Degree } = {}
|
|
39
|
+
nodes.forEach(node => {
|
|
40
|
+
degreesMap[node.id] = {
|
|
41
|
+
in: 0,
|
|
42
|
+
out: 0,
|
|
43
|
+
all: 0
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
if (!edges) return degreesMap;
|
|
18
48
|
edges.forEach((e) => {
|
|
19
49
|
const source = getEdgeTerminal(e, 'source');
|
|
20
50
|
const target = getEdgeTerminal(e, 'target');
|
|
21
51
|
if (source) {
|
|
22
|
-
|
|
52
|
+
degreesMap[source].out += 1;
|
|
53
|
+
degreesMap[source].all += 1;
|
|
23
54
|
}
|
|
24
55
|
if (target) {
|
|
25
|
-
|
|
56
|
+
degreesMap[target].in += 1;
|
|
57
|
+
degreesMap[target].all += 1;
|
|
26
58
|
}
|
|
27
59
|
});
|
|
28
|
-
return
|
|
60
|
+
return degreesMap;
|
|
29
61
|
};
|
|
30
62
|
|
|
31
63
|
export const floydWarshall = (adjMatrix: Matrix[]): Matrix[] => {
|
|
@@ -155,4 +187,88 @@ export const findMinMaxNodeXY = (nodes: OutNode[]) => {
|
|
|
155
187
|
if (maxY < node.y) maxY = node.y;
|
|
156
188
|
});
|
|
157
189
|
return { minX, minY, maxX, maxY };
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* 获取节点集合的平均位置信息
|
|
194
|
+
* @param nodes 节点集合
|
|
195
|
+
* @returns 平局内置
|
|
196
|
+
*/
|
|
197
|
+
export const getAvgNodePosition = (nodes: OutNode[]) => {
|
|
198
|
+
let totalNodes = { x: 0, y: 0 };
|
|
199
|
+
nodes.forEach(node => {
|
|
200
|
+
totalNodes.x += node.x || 0;
|
|
201
|
+
totalNodes.y += node.y || 0;
|
|
202
|
+
});
|
|
203
|
+
// 获取均值向量
|
|
204
|
+
const length = nodes.length || 1;
|
|
205
|
+
return {
|
|
206
|
+
x: totalNodes.x / length,
|
|
207
|
+
y: totalNodes.y / length,
|
|
208
|
+
};
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// 找出指定节点关联的边的起点或终点
|
|
212
|
+
const getCoreNode = (type: 'source' | 'target', node: Node, edges: Edge[]) => {
|
|
213
|
+
if (type === 'source') {
|
|
214
|
+
return (edges?.find(edge => edge.target === node.id)?.source || {}) as Node;
|
|
215
|
+
}
|
|
216
|
+
return (edges?.find(edge => edge.source === node.id)?.target || {}) as Node;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// 找出指定节点为起点或终点的所有一度叶子节点
|
|
220
|
+
const getRelativeNodeIds = (type: 'source' | 'target' | 'both', coreNode: Node, edges: Edge[]) => {
|
|
221
|
+
let relativeNodes: string[] = []
|
|
222
|
+
switch (type) {
|
|
223
|
+
case 'source':
|
|
224
|
+
relativeNodes = edges?.filter(edge => edge.source === coreNode.id).map(edge => edge.target);
|
|
225
|
+
break;
|
|
226
|
+
case 'target':
|
|
227
|
+
relativeNodes = edges?.filter(edge => edge.target === coreNode.id).map(edge => edge.source);
|
|
228
|
+
break;
|
|
229
|
+
case 'both':
|
|
230
|
+
relativeNodes = edges
|
|
231
|
+
?.filter(edge => edge.source === coreNode.id)
|
|
232
|
+
.map(edge => edge.target)
|
|
233
|
+
.concat(edges?.filter(edge => edge.target === coreNode.id).map(edge => edge.source));
|
|
234
|
+
break;
|
|
235
|
+
default:
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
// 去重
|
|
239
|
+
const set = new Set(relativeNodes);
|
|
240
|
+
return Array.from(set);
|
|
241
|
+
};
|
|
242
|
+
// 找出同类型的节点
|
|
243
|
+
const getSameTypeNodes = (type: 'leaf' | 'all', nodeClusterBy: string, node: Node, relativeNodes: Node[], degreesMap: { [id: string]: Degree }) => {
|
|
244
|
+
// @ts-ignore
|
|
245
|
+
const typeName = node[nodeClusterBy] || '';
|
|
246
|
+
// @ts-ignore
|
|
247
|
+
let sameTypeNodes = relativeNodes?.filter(item => item[nodeClusterBy] === typeName) || [];
|
|
248
|
+
if (type === 'leaf') {
|
|
249
|
+
sameTypeNodes = sameTypeNodes.filter(node => degreesMap[node.id]?.in === 0 ||degreesMap[node.id]?.out === 0);
|
|
250
|
+
}
|
|
251
|
+
return sameTypeNodes;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
// 找出与指定节点关联的边的起点或终点出发的所有一度叶子节点
|
|
256
|
+
export const getCoreNodeAndRelativeLeafNodes = (type: 'leaf' | 'all', node: Node, edges: Edge[], nodeClusterBy: string, degreesMap: { [id: string]: Degree }, nodeMap: NodeMap) => {
|
|
257
|
+
const { in: inDegree, out: outDegree } = degreesMap[node.id];
|
|
258
|
+
let coreNode: Node = node;
|
|
259
|
+
let relativeLeafNodes: Node[] = [];
|
|
260
|
+
if (inDegree === 0) {
|
|
261
|
+
// 如果为没有出边的叶子节点,则找出与它关联的边的起点出发的所有一度节点
|
|
262
|
+
coreNode = getCoreNode('source', node, edges);
|
|
263
|
+
relativeLeafNodes = getRelativeNodeIds('both', coreNode, edges).map(nodeId => nodeMap[nodeId]);
|
|
264
|
+
} else if (outDegree === 0) {
|
|
265
|
+
// 如果为没有入边边的叶子节点,则找出与它关联的边的起点出发的所有一度节点
|
|
266
|
+
coreNode = getCoreNode('target', node, edges);
|
|
267
|
+
relativeLeafNodes = getRelativeNodeIds('both', coreNode, edges).map(nodeId => nodeMap[nodeId]);
|
|
268
|
+
}
|
|
269
|
+
relativeLeafNodes = relativeLeafNodes.filter(
|
|
270
|
+
node => degreesMap[node.id] && (degreesMap[node.id].in === 0 || degreesMap[node.id].out === 0),
|
|
271
|
+
);
|
|
272
|
+
const sameTypeLeafNodes = getSameTypeNodes(type, nodeClusterBy, node, relativeLeafNodes, degreesMap);
|
|
273
|
+
return { coreNode, relativeLeafNodes, sameTypeLeafNodes };
|
|
158
274
|
};
|