@statelyai/layout 0.1.3 → 0.2.0
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/NOTICE.md +43 -0
- package/README.md +82 -4
- package/dist/elkjs/index.cjs +8 -0
- package/dist/elkjs/index.d.cts +146 -0
- package/dist/elkjs/index.d.mts +42 -39
- package/dist/elkjs/index.mjs +2 -1483
- package/dist/elkjs/main.cjs +17 -0
- package/dist/elkjs/main.d.cts +10 -0
- package/dist/elkjs/main.d.mts +11 -0
- package/dist/elkjs/main.mjs +17 -0
- package/dist/elkjs/worker-api.cjs +3 -0
- package/dist/elkjs/worker-api.d.cts +3 -0
- package/dist/elkjs/worker-api.d.mts +3 -0
- package/dist/elkjs/worker-api.mjs +3 -0
- package/dist/elkjs/worker.cjs +86 -0
- package/dist/elkjs/worker.d.cts +17 -0
- package/dist/elkjs/worker.d.mts +17 -0
- package/dist/elkjs/worker.mjs +86 -0
- package/dist/elkjs-BVnDwnBQ.cjs +1551 -0
- package/dist/elkjs-CFlfkEBz.mjs +1528 -0
- package/dist/{index-8xYkohbz.d.mts → index-BSH2bloA.d.mts} +77 -3
- package/dist/index-CG712jzV.d.cts +1286 -0
- package/dist/index.cjs +188 -0
- package/dist/index.d.cts +77 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +24 -25
- package/dist/layered/index.cjs +38 -0
- package/dist/layered/index.d.cts +2 -0
- package/dist/layered/index.d.mts +1 -1
- package/dist/layered/index.mjs +1 -1
- package/dist/{layered-CoeH9LRO.mjs → layered-Bz2nWmVp.mjs} +2261 -47
- package/dist/layered-Cf9Rn8l7.cjs +12061 -0
- package/dist/layout-engine-BNex9jxg.cjs +712 -0
- package/dist/{spore-BtTSZbb6.mjs → layout-engine-WbC0YjjT.mjs} +159 -5
- package/dist/public-types-B8rAALJq.d.mts +109 -0
- package/dist/public-types-CVB0hXGk.d.cts +109 -0
- package/dist/worker-client-D3HMK33r.cjs +98 -0
- package/dist/worker-client-DAT_z2OR.d.mts +19 -0
- package/dist/worker-client-DVhklF6A.d.cts +19 -0
- package/dist/worker-client-DkwS4sHx.mjs +92 -0
- package/package.json +39 -9
- package/src/authoring/coordinates.ts +106 -0
- package/src/authoring/partial.ts +584 -0
- package/src/authoring/routing.ts +211 -0
- package/src/box.ts +22 -0
- package/src/constraints.ts +222 -0
- package/src/elkjs/index.ts +100 -37
- package/src/elkjs/main.ts +19 -0
- package/src/elkjs/public-types.ts +125 -0
- package/src/elkjs/worker-api.ts +29 -0
- package/src/elkjs/worker-client.ts +138 -0
- package/src/elkjs/worker.ts +117 -0
- package/src/index.ts +13 -0
- package/src/internal/elkjs-compatibility.ts +17 -0
- package/src/internal/layout-engine.ts +126 -0
- package/src/layered/index.ts +31 -3
- package/src/layout.ts +35 -28
- package/src/packing.ts +22 -0
- package/src/random.ts +75 -3
- package/src/types.ts +17 -2
package/src/elkjs/index.ts
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
import { createGraph, type Graph } from "@statelyai/graph";
|
|
2
|
-
import {
|
|
3
|
-
import { getFixedLayout } from "../fixed";
|
|
4
|
-
import { getLayeredLayout } from "../layered";
|
|
2
|
+
import type { getLayeredLayout } from "../layered";
|
|
5
3
|
import {
|
|
6
4
|
elkLayeredOptionDefinitions,
|
|
7
5
|
type ElkLayeredOptionValueByName,
|
|
8
6
|
type LayeredAdvancedOptions,
|
|
9
7
|
} from "../layered/elk-options";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { getSporeCompactionLayout, getSporeOverlapRemovalLayout } from "../spore";
|
|
8
|
+
import { executeElkjs0111Layout } from "../internal/layout-engine";
|
|
9
|
+
import { elkjs0111ResultPolicy, type Elkjs0111ResultPolicy } from "../internal/elkjs-compatibility";
|
|
13
10
|
import type {
|
|
14
11
|
ElkConstructorArguments,
|
|
15
12
|
ElkLayoutAlgorithmDescription,
|
|
@@ -25,6 +22,11 @@ import type {
|
|
|
25
22
|
ElkShape,
|
|
26
23
|
LaidOutElkNode,
|
|
27
24
|
} from "./types";
|
|
25
|
+
import type {
|
|
26
|
+
ElkLayoutArguments as PublicElkLayoutArguments,
|
|
27
|
+
ElkNode as PublicElkNode,
|
|
28
|
+
LaidOutElkNode as PublicLaidOutElkNode,
|
|
29
|
+
} from "./public-types";
|
|
28
30
|
|
|
29
31
|
function isLayoutEdgeLabel(label: ElkLabel): boolean {
|
|
30
32
|
return (
|
|
@@ -34,24 +36,27 @@ function isLayoutEdgeLabel(label: ElkLabel): boolean {
|
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
export type {
|
|
37
|
-
|
|
39
|
+
ELK,
|
|
40
|
+
ELKConstructorArguments,
|
|
38
41
|
ElkCommonDescription,
|
|
39
|
-
ElkGraphElement,
|
|
40
|
-
ElkId,
|
|
41
42
|
ElkEdge,
|
|
42
43
|
ElkEdgeSection,
|
|
44
|
+
ElkExtendedEdge,
|
|
45
|
+
ElkGraphElement,
|
|
43
46
|
ElkLabel,
|
|
44
47
|
ElkLayoutArguments,
|
|
45
48
|
ElkLayoutAlgorithmDescription,
|
|
46
49
|
ElkLayoutCategoryDescription,
|
|
47
50
|
ElkLayoutOptionDescription,
|
|
48
|
-
ElkLogging,
|
|
49
51
|
ElkNode,
|
|
50
52
|
ElkPoint,
|
|
51
53
|
ElkPort,
|
|
54
|
+
ElkPrimitiveEdge,
|
|
52
55
|
ElkShape,
|
|
56
|
+
LayoutOptions,
|
|
53
57
|
LaidOutElkNode,
|
|
54
|
-
} from "./types";
|
|
58
|
+
} from "./public-types";
|
|
59
|
+
export type { ElkId, ElkLogging } from "./types";
|
|
55
60
|
|
|
56
61
|
export default class ELK {
|
|
57
62
|
readonly #options: ElkConstructorArguments;
|
|
@@ -119,7 +124,19 @@ export default class ELK {
|
|
|
119
124
|
|
|
120
125
|
terminateWorker(): void {}
|
|
121
126
|
|
|
122
|
-
|
|
127
|
+
layout<T extends PublicElkNode>(
|
|
128
|
+
graph: T,
|
|
129
|
+
arguments_?: PublicElkLayoutArguments,
|
|
130
|
+
): Promise<PublicElkNode & PublicLaidOutElkNode<T>>;
|
|
131
|
+
layout<T extends ElkNode>(graph: T, arguments_?: ElkLayoutArguments): Promise<LaidOutElkNode<T>>;
|
|
132
|
+
layout<T extends ElkNode>(
|
|
133
|
+
graph: T,
|
|
134
|
+
arguments_: ElkLayoutArguments = {},
|
|
135
|
+
): Promise<LaidOutElkNode<T>> {
|
|
136
|
+
return this.#layout(graph, arguments_);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async #layout<T extends ElkNode>(
|
|
123
140
|
graph: T,
|
|
124
141
|
arguments_: ElkLayoutArguments = {},
|
|
125
142
|
): Promise<LaidOutElkNode<T>> {
|
|
@@ -246,7 +263,7 @@ export default class ELK {
|
|
|
246
263
|
}
|
|
247
264
|
for (const child of graph.children ?? []) {
|
|
248
265
|
if ((child.children?.length ?? 0) === 0) continue;
|
|
249
|
-
await this
|
|
266
|
+
await this.#layout(child, {
|
|
250
267
|
...arguments_,
|
|
251
268
|
layoutOptions: {
|
|
252
269
|
...arguments_.layoutOptions,
|
|
@@ -361,7 +378,7 @@ export default class ELK {
|
|
|
361
378
|
children: [...(child.children ?? []), ...proxyByKind.values()],
|
|
362
379
|
edges: temporaryEdges,
|
|
363
380
|
};
|
|
364
|
-
await this
|
|
381
|
+
await this.#layout(temporaryChild, {
|
|
365
382
|
...arguments_,
|
|
366
383
|
layoutOptions: {
|
|
367
384
|
...arguments_.layoutOptions,
|
|
@@ -527,31 +544,49 @@ export default class ELK {
|
|
|
527
544
|
getOption(layoutOptions, "padding"),
|
|
528
545
|
algorithm === "layered" ? 12 : 0,
|
|
529
546
|
);
|
|
530
|
-
const laidOut =
|
|
531
|
-
|
|
532
|
-
|
|
547
|
+
const laidOut = await (algorithm === "sporeCompaction"
|
|
548
|
+
? executeElkjs0111Layout({
|
|
549
|
+
algorithm: "sporeCompaction",
|
|
550
|
+
graph: graph_,
|
|
551
|
+
options: {
|
|
533
552
|
padding,
|
|
534
553
|
spacing: getNumberOption(layoutOptions, "spacing.nodeNode"),
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
|
|
554
|
+
},
|
|
555
|
+
})
|
|
556
|
+
: algorithm === "sporeOverlap"
|
|
557
|
+
? executeElkjs0111Layout({
|
|
558
|
+
algorithm: "sporeOverlap",
|
|
559
|
+
graph: graph_,
|
|
560
|
+
options: {
|
|
538
561
|
padding,
|
|
539
562
|
spacing: getNumberOption(layoutOptions, "spacing.nodeNode"),
|
|
563
|
+
},
|
|
564
|
+
})
|
|
565
|
+
: algorithm === "rectpacking"
|
|
566
|
+
? executeElkjs0111Layout({
|
|
567
|
+
algorithm: "rectpacking",
|
|
568
|
+
graph: graph_,
|
|
569
|
+
options: {
|
|
570
|
+
padding: getOption(layoutOptions, "padding") === undefined ? 15 : padding,
|
|
571
|
+
spacing: getNumberOption(layoutOptions, "spacing.nodeNode") ?? 15,
|
|
572
|
+
},
|
|
540
573
|
})
|
|
541
|
-
: algorithm === "
|
|
542
|
-
?
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
: algorithm === "random"
|
|
547
|
-
? getRandomLayout(graph_, {
|
|
574
|
+
: algorithm === "random"
|
|
575
|
+
? executeElkjs0111Layout({
|
|
576
|
+
algorithm: "random",
|
|
577
|
+
graph: graph_,
|
|
578
|
+
options: {
|
|
548
579
|
padding: getOption(layoutOptions, "padding") === undefined ? 15 : padding,
|
|
549
580
|
spacing: getNumberOption(layoutOptions, "spacing.nodeNode"),
|
|
550
581
|
aspectRatio: getNumberOption(layoutOptions, "aspectRatio"),
|
|
551
582
|
seed: getNumberOption(layoutOptions, "randomSeed"),
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
|
|
583
|
+
},
|
|
584
|
+
})
|
|
585
|
+
: algorithm === "box"
|
|
586
|
+
? executeElkjs0111Layout({
|
|
587
|
+
algorithm: "box",
|
|
588
|
+
graph: graph_,
|
|
589
|
+
options: {
|
|
555
590
|
padding: getOption(layoutOptions, "padding") === undefined ? 15 : padding,
|
|
556
591
|
spacing: getNumberOption(layoutOptions, "spacing.nodeNode"),
|
|
557
592
|
aspectRatio: getNumberOption(layoutOptions, "aspectRatio"),
|
|
@@ -563,10 +598,18 @@ export default class ELK {
|
|
|
563
598
|
);
|
|
564
599
|
return getNumberOption(child?.layoutOptions ?? {}, "priority");
|
|
565
600
|
},
|
|
601
|
+
},
|
|
602
|
+
})
|
|
603
|
+
: algorithm === "fixed"
|
|
604
|
+
? executeElkjs0111Layout({
|
|
605
|
+
algorithm: "fixed",
|
|
606
|
+
graph: graph_,
|
|
607
|
+
options: { direction: getDirection(layoutOptions) },
|
|
566
608
|
})
|
|
567
|
-
:
|
|
568
|
-
|
|
569
|
-
|
|
609
|
+
: executeElkjs0111Layout({
|
|
610
|
+
algorithm: "layered",
|
|
611
|
+
graph: graph_,
|
|
612
|
+
options: {
|
|
570
613
|
direction: getDirection(layoutOptions),
|
|
571
614
|
spacing: {
|
|
572
615
|
node:
|
|
@@ -638,7 +681,8 @@ export default class ELK {
|
|
|
638
681
|
),
|
|
639
682
|
} as ElkLayeredOptionValueByName;
|
|
640
683
|
},
|
|
641
|
-
}
|
|
684
|
+
},
|
|
685
|
+
}));
|
|
642
686
|
if (hierarchyRestorations.length > 0) {
|
|
643
687
|
const direction = getDirection(layoutOptions);
|
|
644
688
|
const horizontal = direction === "right" || direction === "left";
|
|
@@ -750,7 +794,16 @@ export default class ELK {
|
|
|
750
794
|
);
|
|
751
795
|
}
|
|
752
796
|
}
|
|
753
|
-
|
|
797
|
+
const resultPolicy = (
|
|
798
|
+
laidOut as typeof laidOut & {
|
|
799
|
+
[elkjs0111ResultPolicy]?: Elkjs0111ResultPolicy;
|
|
800
|
+
}
|
|
801
|
+
)[elkjs0111ResultPolicy];
|
|
802
|
+
applyLayout(graph, laidOut, padding, layoutOptions, resultPolicy);
|
|
803
|
+
if (resultPolicy?.providerBounds) {
|
|
804
|
+
graph.width = resultPolicy.providerBounds.width;
|
|
805
|
+
graph.height = resultPolicy.providerBounds.height;
|
|
806
|
+
}
|
|
754
807
|
for (const restoration of hierarchyRestorations) {
|
|
755
808
|
restoration.edge.sources = restoration.sources;
|
|
756
809
|
restoration.edge.targets = restoration.targets;
|
|
@@ -771,7 +824,7 @@ export default class ELK {
|
|
|
771
824
|
for (const child of graph.children ?? []) {
|
|
772
825
|
if ((child.children?.length ?? 0) === 0) continue;
|
|
773
826
|
const position = { x: child.x, y: child.y };
|
|
774
|
-
await this
|
|
827
|
+
await this.#layout(child, {
|
|
775
828
|
...arguments_,
|
|
776
829
|
logging: false,
|
|
777
830
|
measureExecutionTime: false,
|
|
@@ -1338,6 +1391,7 @@ function applyLayout(
|
|
|
1338
1391
|
graph: ReturnType<typeof getLayeredLayout>,
|
|
1339
1392
|
padding: { top: number; right: number; bottom: number; left: number },
|
|
1340
1393
|
layoutOptions: Readonly<Record<string, unknown>>,
|
|
1394
|
+
resultPolicy?: Elkjs0111ResultPolicy,
|
|
1341
1395
|
): void {
|
|
1342
1396
|
const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
1343
1397
|
const edgeById = new Map(graph.edges.map((edge) => [edge.id, edge]));
|
|
@@ -1361,6 +1415,13 @@ function applyLayout(
|
|
|
1361
1415
|
placePortLabels(child, layoutOptions);
|
|
1362
1416
|
}
|
|
1363
1417
|
for (const edge of root.edges ?? []) {
|
|
1418
|
+
if (resultPolicy?.preserveEdgeSections) {
|
|
1419
|
+
for (const section of edge.sections ?? []) {
|
|
1420
|
+
section.incomingShape ??= edge.sources?.[0] ?? edge.source;
|
|
1421
|
+
section.outgoingShape ??= edge.targets?.[0] ?? edge.target;
|
|
1422
|
+
}
|
|
1423
|
+
continue;
|
|
1424
|
+
}
|
|
1364
1425
|
const laidOutEdge = edgeById.get(String(edge.id));
|
|
1365
1426
|
if (!laidOutEdge) {
|
|
1366
1427
|
if (getBooleanOption(edge.layoutOptions ?? {}, "noLayout") === true) {
|
|
@@ -1417,7 +1478,9 @@ function applyLayout(
|
|
|
1417
1478
|
labelY += (label.height ?? 0) + labelLabelSpacing;
|
|
1418
1479
|
}
|
|
1419
1480
|
}
|
|
1420
|
-
|
|
1481
|
+
if (!resultPolicy?.skipBoundsNormalization) {
|
|
1482
|
+
normalizeElkGraphBounds(root, padding, layoutOptions);
|
|
1483
|
+
}
|
|
1421
1484
|
}
|
|
1422
1485
|
|
|
1423
1486
|
function normalizeElkGraphBounds(
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*******************************************************************************
|
|
2
|
+
* Copyright (c) 2021 Kiel University and others.
|
|
3
|
+
*
|
|
4
|
+
* Adapted from elkjs 0.11.1 lib/main.js.
|
|
5
|
+
* SPDX-License-Identifier: EPL-2.0
|
|
6
|
+
*******************************************************************************/
|
|
7
|
+
import type { ELKConstructorArguments } from "./public-types";
|
|
8
|
+
import { Worker } from "./worker";
|
|
9
|
+
import { WorkerELK } from "./worker-client";
|
|
10
|
+
|
|
11
|
+
/** Node-compatible elkjs entry point with an in-process worker fallback. */
|
|
12
|
+
export default class ELK extends WorkerELK {
|
|
13
|
+
constructor(options: ELKConstructorArguments = {}) {
|
|
14
|
+
super({
|
|
15
|
+
...options,
|
|
16
|
+
workerFactory: options.workerFactory ?? ((url?: string) => new Worker(url)),
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/*******************************************************************************
|
|
2
|
+
* Copyright (c) 2019 TypeFox and others.
|
|
3
|
+
*
|
|
4
|
+
* Adapted from elkjs 0.11.1 typings/elk-api.d.ts.
|
|
5
|
+
* SPDX-License-Identifier: EPL-2.0
|
|
6
|
+
*******************************************************************************/
|
|
7
|
+
|
|
8
|
+
export interface LayoutOptions {
|
|
9
|
+
[key: string]: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ElkPoint {
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ElkGraphElement {
|
|
18
|
+
id?: string;
|
|
19
|
+
labels?: ElkLabel[];
|
|
20
|
+
layoutOptions?: LayoutOptions;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ElkShape extends ElkGraphElement {
|
|
24
|
+
x?: number;
|
|
25
|
+
y?: number;
|
|
26
|
+
width?: number;
|
|
27
|
+
height?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ElkNode extends ElkShape {
|
|
31
|
+
id: string;
|
|
32
|
+
children?: ElkNode[];
|
|
33
|
+
ports?: ElkPort[];
|
|
34
|
+
edges?: ElkExtendedEdge[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ElkPort extends ElkShape {
|
|
38
|
+
id: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ElkLabel extends ElkShape {
|
|
42
|
+
text?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** @deprecated Use ElkExtendedEdge directly. */
|
|
46
|
+
export interface ElkEdge extends ElkGraphElement {
|
|
47
|
+
id: string;
|
|
48
|
+
container?: string;
|
|
49
|
+
junctionPoints?: ElkPoint[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** @deprecated Use ElkExtendedEdge instead. */
|
|
53
|
+
export interface ElkPrimitiveEdge extends ElkEdge {
|
|
54
|
+
source: string;
|
|
55
|
+
sourcePort?: string;
|
|
56
|
+
target: string;
|
|
57
|
+
targetPort?: string;
|
|
58
|
+
sourcePoint?: ElkPoint;
|
|
59
|
+
targetPoint?: ElkPoint;
|
|
60
|
+
bendPoints?: ElkPoint[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ElkExtendedEdge extends ElkEdge {
|
|
64
|
+
sources: string[];
|
|
65
|
+
targets: string[];
|
|
66
|
+
sections?: ElkEdgeSection[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface ElkEdgeSection extends ElkGraphElement {
|
|
70
|
+
id: string;
|
|
71
|
+
startPoint: ElkPoint;
|
|
72
|
+
endPoint: ElkPoint;
|
|
73
|
+
bendPoints?: ElkPoint[];
|
|
74
|
+
incomingShape?: string;
|
|
75
|
+
outgoingShape?: string;
|
|
76
|
+
incomingSections?: string[];
|
|
77
|
+
outgoingSections?: string[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface ElkLayoutArguments {
|
|
81
|
+
layoutOptions?: LayoutOptions;
|
|
82
|
+
logging?: boolean;
|
|
83
|
+
measureExecutionTime?: boolean;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface ElkCommonDescription {
|
|
87
|
+
id?: string;
|
|
88
|
+
name?: string;
|
|
89
|
+
description?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface ElkLayoutAlgorithmDescription extends ElkCommonDescription {
|
|
93
|
+
category?: string;
|
|
94
|
+
knownOptions?: string[];
|
|
95
|
+
supportedFeatures?: string[];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface ElkLayoutOptionDescription extends ElkCommonDescription {
|
|
99
|
+
group?: string;
|
|
100
|
+
type?: string;
|
|
101
|
+
targets?: string[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface ElkLayoutCategoryDescription extends ElkCommonDescription {
|
|
105
|
+
knownLayouters?: string[];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export type LaidOutElkNode<T extends ElkNode> = Omit<T, "children"> & {
|
|
109
|
+
children?: Array<NonNullable<T["children"]>[number] & ElkNode>;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export interface ELK {
|
|
113
|
+
layout<T extends ElkNode>(graph: T, args?: ElkLayoutArguments): Promise<LaidOutElkNode<T>>;
|
|
114
|
+
knownLayoutAlgorithms(): Promise<ElkLayoutAlgorithmDescription[]>;
|
|
115
|
+
knownLayoutOptions(): Promise<ElkLayoutOptionDescription[]>;
|
|
116
|
+
knownLayoutCategories(): Promise<ElkLayoutCategoryDescription[]>;
|
|
117
|
+
terminateWorker(): void;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface ELKConstructorArguments {
|
|
121
|
+
defaultLayoutOptions?: LayoutOptions;
|
|
122
|
+
algorithms?: string[];
|
|
123
|
+
workerUrl?: string;
|
|
124
|
+
workerFactory?: (url?: string) => Worker;
|
|
125
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/*******************************************************************************
|
|
2
|
+
* Copyright (c) 2017 Kiel University and others.
|
|
3
|
+
*
|
|
4
|
+
* Adapted from elkjs 0.11.1 lib/elk-api.js.
|
|
5
|
+
* SPDX-License-Identifier: EPL-2.0
|
|
6
|
+
*******************************************************************************/
|
|
7
|
+
export { WorkerELK as default } from "./worker-client";
|
|
8
|
+
|
|
9
|
+
export type {
|
|
10
|
+
ELK,
|
|
11
|
+
ELKConstructorArguments,
|
|
12
|
+
ElkCommonDescription,
|
|
13
|
+
ElkEdge,
|
|
14
|
+
ElkEdgeSection,
|
|
15
|
+
ElkExtendedEdge,
|
|
16
|
+
ElkGraphElement,
|
|
17
|
+
ElkLabel,
|
|
18
|
+
ElkLayoutArguments,
|
|
19
|
+
ElkLayoutAlgorithmDescription,
|
|
20
|
+
ElkLayoutCategoryDescription,
|
|
21
|
+
ElkLayoutOptionDescription,
|
|
22
|
+
ElkNode,
|
|
23
|
+
ElkPoint,
|
|
24
|
+
ElkPort,
|
|
25
|
+
ElkPrimitiveEdge,
|
|
26
|
+
ElkShape,
|
|
27
|
+
LayoutOptions,
|
|
28
|
+
LaidOutElkNode,
|
|
29
|
+
} from "./public-types";
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ELK as ElkInterface,
|
|
3
|
+
ELKConstructorArguments,
|
|
4
|
+
ElkLayoutAlgorithmDescription,
|
|
5
|
+
ElkLayoutArguments,
|
|
6
|
+
ElkLayoutCategoryDescription,
|
|
7
|
+
ElkLayoutOptionDescription,
|
|
8
|
+
ElkNode,
|
|
9
|
+
LaidOutElkNode,
|
|
10
|
+
LayoutOptions,
|
|
11
|
+
} from "./public-types";
|
|
12
|
+
|
|
13
|
+
interface WorkerRequest {
|
|
14
|
+
id?: number;
|
|
15
|
+
cmd: "register" | "layout" | "algorithms" | "options" | "categories";
|
|
16
|
+
algorithms?: string[];
|
|
17
|
+
graph?: ElkNode;
|
|
18
|
+
layoutOptions?: LayoutOptions;
|
|
19
|
+
options?: Pick<ElkLayoutArguments, "logging" | "measureExecutionTime">;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface WorkerAnswer {
|
|
23
|
+
id: number;
|
|
24
|
+
data?: unknown;
|
|
25
|
+
error?: unknown;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface WorkerLike {
|
|
29
|
+
onmessage: ((event: MessageEvent<WorkerAnswer>) => void) | null;
|
|
30
|
+
onerror: ((event: ErrorEvent) => void) | null;
|
|
31
|
+
onmessageerror: ((event: MessageEvent) => void) | null;
|
|
32
|
+
postMessage(message: WorkerRequest): void;
|
|
33
|
+
terminate(): void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class PromisedWorker {
|
|
37
|
+
readonly #worker: WorkerLike;
|
|
38
|
+
readonly #resolvers = new Map<
|
|
39
|
+
number,
|
|
40
|
+
{ resolve(value: unknown): void; reject(reason?: unknown): void }
|
|
41
|
+
>();
|
|
42
|
+
#id = 0;
|
|
43
|
+
#failure: Error | undefined;
|
|
44
|
+
|
|
45
|
+
constructor(worker: WorkerLike) {
|
|
46
|
+
this.#worker = worker;
|
|
47
|
+
worker.onmessage = (event) => {
|
|
48
|
+
const resolver = this.#resolvers.get(event.data.id);
|
|
49
|
+
if (!resolver) return;
|
|
50
|
+
this.#resolvers.delete(event.data.id);
|
|
51
|
+
if (event.data.error !== undefined) resolver.reject(event.data.error);
|
|
52
|
+
else resolver.resolve(event.data.data);
|
|
53
|
+
};
|
|
54
|
+
worker.onerror = (event) => this.#rejectAll(event.error ?? event.message);
|
|
55
|
+
worker.onmessageerror = (event) => this.#rejectAll(event.data);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
postMessage<T>(message: WorkerRequest): Promise<T> {
|
|
59
|
+
if (this.#failure) return Promise.reject(this.#failure);
|
|
60
|
+
const id = this.#id++;
|
|
61
|
+
return new Promise<T>((resolve, reject) => {
|
|
62
|
+
this.#resolvers.set(id, { resolve: resolve as (value: unknown) => void, reject });
|
|
63
|
+
this.#worker.postMessage({ ...message, id });
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
terminate(): void {
|
|
68
|
+
this.#worker.terminate();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#rejectAll(reason: unknown): void {
|
|
72
|
+
const error = reason instanceof Error ? reason : new Error(String(reason ?? "Worker failed"));
|
|
73
|
+
this.#failure = error;
|
|
74
|
+
for (const resolver of this.#resolvers.values()) resolver.reject(error);
|
|
75
|
+
this.#resolvers.clear();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export class WorkerELK implements ElkInterface {
|
|
80
|
+
readonly #defaultLayoutOptions: LayoutOptions;
|
|
81
|
+
readonly #worker: PromisedWorker;
|
|
82
|
+
readonly #ready: Promise<unknown>;
|
|
83
|
+
|
|
84
|
+
constructor({
|
|
85
|
+
defaultLayoutOptions = {},
|
|
86
|
+
algorithms,
|
|
87
|
+
workerFactory,
|
|
88
|
+
workerUrl,
|
|
89
|
+
}: ELKConstructorArguments = {}) {
|
|
90
|
+
if (workerUrl === undefined && workerFactory === undefined) {
|
|
91
|
+
throw new Error("Cannot construct an ELK without both 'workerUrl' and 'workerFactory'.");
|
|
92
|
+
}
|
|
93
|
+
const factory = workerFactory ?? ((url?: string) => new globalThis.Worker(url!));
|
|
94
|
+
const worker = factory(workerUrl) as WorkerLike;
|
|
95
|
+
if (!worker || typeof worker.postMessage !== "function") {
|
|
96
|
+
throw new TypeError("Created worker does not provide the required 'postMessage' function.");
|
|
97
|
+
}
|
|
98
|
+
this.#defaultLayoutOptions = defaultLayoutOptions;
|
|
99
|
+
this.#worker = new PromisedWorker(worker);
|
|
100
|
+
this.#ready = this.#worker.postMessage({ cmd: "register", algorithms });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async layout<T extends ElkNode>(
|
|
104
|
+
graph: T,
|
|
105
|
+
args: ElkLayoutArguments = {},
|
|
106
|
+
): Promise<LaidOutElkNode<T>> {
|
|
107
|
+
if (!graph) throw new Error("Missing mandatory parameter 'graph'.");
|
|
108
|
+
await this.#ready;
|
|
109
|
+
return this.#worker.postMessage({
|
|
110
|
+
cmd: "layout",
|
|
111
|
+
graph,
|
|
112
|
+
layoutOptions: { ...this.#defaultLayoutOptions, ...args.layoutOptions },
|
|
113
|
+
options: {
|
|
114
|
+
logging: args.logging ?? false,
|
|
115
|
+
measureExecutionTime: args.measureExecutionTime ?? false,
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async knownLayoutAlgorithms(): Promise<ElkLayoutAlgorithmDescription[]> {
|
|
121
|
+
await this.#ready;
|
|
122
|
+
return this.#worker.postMessage({ cmd: "algorithms" });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async knownLayoutOptions(): Promise<ElkLayoutOptionDescription[]> {
|
|
126
|
+
await this.#ready;
|
|
127
|
+
return this.#worker.postMessage({ cmd: "options" });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async knownLayoutCategories(): Promise<ElkLayoutCategoryDescription[]> {
|
|
131
|
+
await this.#ready;
|
|
132
|
+
return this.#worker.postMessage({ cmd: "categories" });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
terminateWorker(): void {
|
|
136
|
+
this.#worker.terminate();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/*******************************************************************************
|
|
2
|
+
* Copyright (c) 2019 Kiel University and others.
|
|
3
|
+
*
|
|
4
|
+
* Adapted from elkjs 0.11.1 lib/elk-worker.js.
|
|
5
|
+
* SPDX-License-Identifier: EPL-2.0
|
|
6
|
+
*******************************************************************************/
|
|
7
|
+
import ELK from "./index";
|
|
8
|
+
import type { ElkLayoutArguments, ElkNode } from "./types";
|
|
9
|
+
|
|
10
|
+
type WorkerCommand =
|
|
11
|
+
| { id: number; cmd: "register"; algorithms?: string[] }
|
|
12
|
+
| {
|
|
13
|
+
id: number;
|
|
14
|
+
cmd: "layout";
|
|
15
|
+
graph: ElkNode;
|
|
16
|
+
layoutOptions?: Record<string, unknown>;
|
|
17
|
+
options?: ElkLayoutArguments;
|
|
18
|
+
}
|
|
19
|
+
| { id: number; cmd: "algorithms" | "options" | "categories" };
|
|
20
|
+
|
|
21
|
+
type WorkerAnswer = { id: number; data?: unknown; error?: unknown };
|
|
22
|
+
|
|
23
|
+
/** In-process implementation of the message interface shipped by elkjs workers. */
|
|
24
|
+
export class Worker implements globalThis.Worker {
|
|
25
|
+
onerror: ((this: AbstractWorker, event: ErrorEvent) => unknown) | null = null;
|
|
26
|
+
onmessage: ((this: globalThis.Worker, event: MessageEvent) => unknown) | null = null;
|
|
27
|
+
onmessageerror: ((this: globalThis.Worker, event: MessageEvent) => unknown) | null = null;
|
|
28
|
+
|
|
29
|
+
readonly #listeners = new Map<string, Set<EventListenerOrEventListenerObject>>();
|
|
30
|
+
#elk = new ELK();
|
|
31
|
+
#terminated = false;
|
|
32
|
+
|
|
33
|
+
constructor(_url?: string) {}
|
|
34
|
+
|
|
35
|
+
postMessage(message: unknown, _transfer: Transferable[]): void;
|
|
36
|
+
postMessage(message: unknown, _options?: StructuredSerializeOptions): void;
|
|
37
|
+
postMessage(message: unknown): void {
|
|
38
|
+
if (this.#terminated) return;
|
|
39
|
+
queueMicrotask(() => void this.#handle(message as WorkerCommand));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
terminate(): void {
|
|
43
|
+
this.#terminated = true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
addEventListener<K extends keyof WorkerEventMap>(
|
|
47
|
+
type: K,
|
|
48
|
+
listener: (this: globalThis.Worker, event: WorkerEventMap[K]) => unknown,
|
|
49
|
+
options?: boolean | AddEventListenerOptions,
|
|
50
|
+
): void;
|
|
51
|
+
addEventListener(
|
|
52
|
+
type: string,
|
|
53
|
+
listener: EventListenerOrEventListenerObject,
|
|
54
|
+
_options?: boolean | AddEventListenerOptions,
|
|
55
|
+
): void {
|
|
56
|
+
const listeners = this.#listeners.get(type) ?? new Set();
|
|
57
|
+
listeners.add(listener);
|
|
58
|
+
this.#listeners.set(type, listeners);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
removeEventListener<K extends keyof WorkerEventMap>(
|
|
62
|
+
type: K,
|
|
63
|
+
listener: (this: globalThis.Worker, event: WorkerEventMap[K]) => unknown,
|
|
64
|
+
options?: boolean | EventListenerOptions,
|
|
65
|
+
): void;
|
|
66
|
+
removeEventListener(
|
|
67
|
+
type: string,
|
|
68
|
+
listener: EventListenerOrEventListenerObject,
|
|
69
|
+
_options?: boolean | EventListenerOptions,
|
|
70
|
+
): void {
|
|
71
|
+
this.#listeners.get(type)?.delete(listener);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
dispatchEvent(event: Event): boolean {
|
|
75
|
+
for (const listener of this.#listeners.get(event.type) ?? []) {
|
|
76
|
+
if (typeof listener === "function") listener.call(this, event);
|
|
77
|
+
else listener.handleEvent(event);
|
|
78
|
+
}
|
|
79
|
+
return !event.defaultPrevented;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async #handle(message: WorkerCommand): Promise<void> {
|
|
83
|
+
try {
|
|
84
|
+
let data: unknown;
|
|
85
|
+
switch (message.cmd) {
|
|
86
|
+
case "register":
|
|
87
|
+
this.#elk = new ELK({ algorithms: message.algorithms });
|
|
88
|
+
break;
|
|
89
|
+
case "layout":
|
|
90
|
+
data = await this.#elk.layout(message.graph, {
|
|
91
|
+
...message.options,
|
|
92
|
+
layoutOptions: message.layoutOptions,
|
|
93
|
+
});
|
|
94
|
+
break;
|
|
95
|
+
case "algorithms":
|
|
96
|
+
data = await this.#elk.knownLayoutAlgorithms();
|
|
97
|
+
break;
|
|
98
|
+
case "options":
|
|
99
|
+
data = await this.#elk.knownLayoutOptions();
|
|
100
|
+
break;
|
|
101
|
+
case "categories":
|
|
102
|
+
data = await this.#elk.knownLayoutCategories();
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
this.#answer({ id: message.id, data });
|
|
106
|
+
} catch (error) {
|
|
107
|
+
this.#answer({ id: message.id, error });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#answer(answer: WorkerAnswer): void {
|
|
112
|
+
if (this.#terminated) return;
|
|
113
|
+
const event = { data: answer, type: "message" } as MessageEvent<WorkerAnswer>;
|
|
114
|
+
this.onmessage?.call(this, event);
|
|
115
|
+
this.dispatchEvent(event);
|
|
116
|
+
}
|
|
117
|
+
}
|