@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.
Files changed (60) hide show
  1. package/NOTICE.md +43 -0
  2. package/README.md +82 -4
  3. package/dist/elkjs/index.cjs +8 -0
  4. package/dist/elkjs/index.d.cts +146 -0
  5. package/dist/elkjs/index.d.mts +42 -39
  6. package/dist/elkjs/index.mjs +2 -1483
  7. package/dist/elkjs/main.cjs +17 -0
  8. package/dist/elkjs/main.d.cts +10 -0
  9. package/dist/elkjs/main.d.mts +11 -0
  10. package/dist/elkjs/main.mjs +17 -0
  11. package/dist/elkjs/worker-api.cjs +3 -0
  12. package/dist/elkjs/worker-api.d.cts +3 -0
  13. package/dist/elkjs/worker-api.d.mts +3 -0
  14. package/dist/elkjs/worker-api.mjs +3 -0
  15. package/dist/elkjs/worker.cjs +86 -0
  16. package/dist/elkjs/worker.d.cts +17 -0
  17. package/dist/elkjs/worker.d.mts +17 -0
  18. package/dist/elkjs/worker.mjs +86 -0
  19. package/dist/elkjs-BVnDwnBQ.cjs +1551 -0
  20. package/dist/elkjs-CFlfkEBz.mjs +1528 -0
  21. package/dist/{index-8xYkohbz.d.mts → index-BSH2bloA.d.mts} +77 -3
  22. package/dist/index-CG712jzV.d.cts +1286 -0
  23. package/dist/index.cjs +188 -0
  24. package/dist/index.d.cts +77 -0
  25. package/dist/index.d.mts +2 -2
  26. package/dist/index.mjs +24 -25
  27. package/dist/layered/index.cjs +38 -0
  28. package/dist/layered/index.d.cts +2 -0
  29. package/dist/layered/index.d.mts +1 -1
  30. package/dist/layered/index.mjs +1 -1
  31. package/dist/{layered-CoeH9LRO.mjs → layered-Bz2nWmVp.mjs} +2261 -47
  32. package/dist/layered-Cf9Rn8l7.cjs +12061 -0
  33. package/dist/layout-engine-BNex9jxg.cjs +712 -0
  34. package/dist/{spore-BtTSZbb6.mjs → layout-engine-WbC0YjjT.mjs} +159 -5
  35. package/dist/public-types-B8rAALJq.d.mts +109 -0
  36. package/dist/public-types-CVB0hXGk.d.cts +109 -0
  37. package/dist/worker-client-D3HMK33r.cjs +98 -0
  38. package/dist/worker-client-DAT_z2OR.d.mts +19 -0
  39. package/dist/worker-client-DVhklF6A.d.cts +19 -0
  40. package/dist/worker-client-DkwS4sHx.mjs +92 -0
  41. package/package.json +39 -9
  42. package/src/authoring/coordinates.ts +106 -0
  43. package/src/authoring/partial.ts +584 -0
  44. package/src/authoring/routing.ts +211 -0
  45. package/src/box.ts +22 -0
  46. package/src/constraints.ts +222 -0
  47. package/src/elkjs/index.ts +100 -37
  48. package/src/elkjs/main.ts +19 -0
  49. package/src/elkjs/public-types.ts +125 -0
  50. package/src/elkjs/worker-api.ts +29 -0
  51. package/src/elkjs/worker-client.ts +138 -0
  52. package/src/elkjs/worker.ts +117 -0
  53. package/src/index.ts +13 -0
  54. package/src/internal/elkjs-compatibility.ts +17 -0
  55. package/src/internal/layout-engine.ts +126 -0
  56. package/src/layered/index.ts +31 -3
  57. package/src/layout.ts +35 -28
  58. package/src/packing.ts +22 -0
  59. package/src/random.ts +75 -3
  60. package/src/types.ts +17 -2
@@ -1,15 +1,12 @@
1
1
  import { createGraph, type Graph } from "@statelyai/graph";
2
- import { getBoxLayout } from "../box";
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 { getRectanglePackingLayout } from "../packing";
11
- import { getRandomLayout } from "../random";
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
- ElkConstructorArguments,
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
- async layout<T extends ElkNode>(
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.layout(child, {
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.layout(temporaryChild, {
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
- algorithm === "sporeCompaction"
532
- ? getSporeCompactionLayout(graph_, {
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
- : algorithm === "sporeOverlap"
537
- ? getSporeOverlapRemovalLayout(graph_, {
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 === "rectpacking"
542
- ? getRectanglePackingLayout(graph_, {
543
- padding,
544
- spacing: getNumberOption(layoutOptions, "spacing.nodeNode"),
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
- : algorithm === "box"
554
- ? getBoxLayout(graph_, {
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
- : algorithm === "fixed"
568
- ? getFixedLayout(graph_, { direction: getDirection(layoutOptions) })
569
- : getLayeredLayout(graph_, {
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
- applyLayout(graph, laidOut, padding, layoutOptions);
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.layout(child, {
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
- normalizeElkGraphBounds(root, padding, layoutOptions);
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
+ }