@statelyai/layout 0.4.0 → 0.4.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.
Files changed (33) hide show
  1. package/dist/elkjs/index.cjs +2 -2
  2. package/dist/elkjs/index.mjs +2 -2
  3. package/dist/elkjs/main.cjs +1 -1
  4. package/dist/elkjs/main.mjs +1 -1
  5. package/dist/elkjs/worker.cjs +2 -2
  6. package/dist/elkjs/worker.mjs +2 -2
  7. package/dist/{elkjs-UciYx7lD.mjs → elkjs-CSi7oB5J.mjs} +181 -69
  8. package/dist/{elkjs-ggjsBuVP.cjs → elkjs-CnBvdyBu.cjs} +181 -69
  9. package/dist/index.cjs +2 -2
  10. package/dist/index.mjs +2 -2
  11. package/dist/layered/index.cjs +1 -1
  12. package/dist/layered/index.mjs +1 -1
  13. package/dist/{layered-CBPg6_6_.cjs → layered-BJu8A21s.cjs} +474 -158
  14. package/dist/{layered-C7ERzZjR.mjs → layered-BsAdm_u8.mjs} +469 -153
  15. package/dist/{layout-engine-DAw06RhN.cjs → layout-engine-BFP1TZbF.cjs} +1 -1
  16. package/dist/{layout-engine-DC3IYkIy.mjs → layout-engine-DnlmGxHL.mjs} +1 -1
  17. package/package.json +1 -1
  18. package/src/elkjs/index.ts +135 -33
  19. package/src/elkjs/layout-quality.ts +81 -33
  20. package/src/layered/bk-node-placement.ts +10 -7
  21. package/src/layered/crossing-counter.ts +27 -9
  22. package/src/layered/crossing-graph.ts +2 -2
  23. package/src/layered/greedy-switch-decider.ts +44 -17
  24. package/src/layered/hierarchy-sweepiness.ts +37 -41
  25. package/src/layered/index.ts +15 -21
  26. package/src/layered/loop-envelopes.ts +10 -4
  27. package/src/layered/node-lookup.ts +29 -0
  28. package/src/layered/opposing-tracks.ts +108 -8
  29. package/src/layered/orthogonal-hypersegments.ts +69 -7
  30. package/src/layered/port-distributor.ts +26 -14
  31. package/src/layered/port-edge-order.ts +2 -1
  32. package/src/layered/spacing.ts +4 -3
  33. package/src/layered/strategies.ts +288 -111
@@ -18,6 +18,8 @@ export interface PortDistributionOptions {
18
18
  }
19
19
 
20
20
  /** Stateful clockwise port distribution across successive layer sweeps. */
21
+ const sideRank = { NORTH: 0, EAST: 1, SOUTH: 2, WEST: 3 };
22
+
21
23
  export class CanonicalPortDistributor {
22
24
  readonly state: PortDistributionState;
23
25
  private topologyGraph?: CrossingGraph;
@@ -73,27 +75,36 @@ export class CanonicalPortDistributor {
73
75
  ): void {
74
76
  this.prepareTopology(graph);
75
77
  let consumed = 0;
78
+ // Only ports with edges toward the free layer take ranks.
79
+ const ends = input ? this.incoming : this.outgoing;
80
+ const ranked = (port: CrossingPort) => ends.get(port.id)!.length > 0;
76
81
  for (const node of layer) {
77
- const ports = node.ports.filter(
78
- (port) => (input ? this.incoming : this.outgoing).get(port.id)!.length > 0,
79
- );
80
- const increment = nodeRelative ? 1 / (ports.length + 1) : 1;
82
+ let count = 0,
83
+ northCount = 0;
84
+ for (const port of node.ports)
85
+ if (ranked(port)) {
86
+ count++;
87
+ if (port.side === "NORTH") northCount++;
88
+ }
89
+ const increment = nodeRelative ? 1 / (count + 1) : 1;
81
90
  if (input) {
82
- let north = consumed + ports.filter((port) => port.side === "NORTH").length * increment;
83
- let rest = nodeRelative ? consumed + 1 - increment : consumed + ports.length;
84
- for (const port of ports) {
91
+ let north = consumed + northCount * increment;
92
+ let rest = nodeRelative ? consumed + 1 - increment : consumed + count;
93
+ for (const port of node.ports) {
94
+ if (!ranked(port)) continue;
85
95
  this.state.ranks[port.id] = port.side === "NORTH" ? north : rest;
86
96
  if (port.side === "NORTH") north -= increment;
87
97
  else rest -= increment;
88
98
  }
89
99
  } else {
90
100
  let position = consumed + increment;
91
- for (const port of ports) {
101
+ for (const port of node.ports) {
102
+ if (!ranked(port)) continue;
92
103
  this.state.ranks[port.id] = position;
93
104
  position += increment;
94
105
  }
95
106
  }
96
- consumed += nodeRelative ? 1 : ports.length;
107
+ consumed += nodeRelative ? 1 : count;
97
108
  }
98
109
  }
99
110
 
@@ -109,14 +120,15 @@ export class CanonicalPortDistributor {
109
120
  const ranks = (layer: CrossingNode[], input: boolean) =>
110
121
  this.calculatePortRanks(graph, layer, input, options.nodeRelative);
111
122
  const distributeNode = (node: CrossingNode, side: "EAST" | "WEST") => {
112
- if (options.fixedOrder.has(node.id)) return;
123
+ if (options.fixedOrder.has(node.id) || !node.ports.length) return;
113
124
  const layerIndex = node.ports.length ? layerByPort.get(node.ports[0]!.id)! : index;
114
125
  const size = graph.layers[layerIndex]!.length;
115
126
  for (const face of [side, "SOUTH", "NORTH"] as const) {
116
127
  let minimum = 0,
117
128
  maximum = 0;
118
129
  const inLayer: CrossingPort[] = [];
119
- for (const port of node.ports.filter((port) => port.side === face)) {
130
+ for (const port of node.ports) {
131
+ if (port.side !== face) continue;
120
132
  let sum = 0;
121
133
  const cross = port.side === "NORTH" || port.side === "SOUTH";
122
134
  if (cross) {
@@ -135,8 +147,8 @@ export class CanonicalPortDistributor {
135
147
  else if (output && !input) sum += position + 1;
136
148
  else if (input && output) sum += port.side === "NORTH" ? 0 : large / 2;
137
149
  } else {
138
- const peers = [...outgoing.get(port.id)!, ...incoming.get(port.id)!];
139
- if (peers.some((peer) => layerByPort.get(peer) === layerIndex)) {
150
+ const sameLayer = (peer: string) => layerByPort.get(peer) === layerIndex;
151
+ if (outgoing.get(port.id)!.some(sameLayer) || incoming.get(port.id)!.some(sameLayer)) {
140
152
  inLayer.push(port);
141
153
  continue;
142
154
  }
@@ -168,7 +180,7 @@ export class CanonicalPortDistributor {
168
180
  : minimum - (size + 1 - value);
169
181
  }
170
182
  }
171
- const sideRank = { NORTH: 0, EAST: 1, SOUTH: 2, WEST: 3 };
183
+ if (node.ports.length < 2) return;
172
184
  node.ports.sort((a, b) => {
173
185
  const side = sideRank[a.side] - sideRank[b.side];
174
186
  if (side) return side;
@@ -1,9 +1,10 @@
1
1
  import type { GraphEdge } from "@statelyai/graph";
2
2
  import type { LayeredPhaseInput } from "./types";
3
+ import { findNode } from "./node-lookup";
3
4
 
4
5
  /** ELK traverses connected edges through the node's port list, preserving each port's edge order. */
5
6
  export function connectedEdgesInPortOrder(input: LayeredPhaseInput, nodeId: string): GraphEdge[] {
6
- const node = input.graph.nodes.find((candidate) => candidate.id === nodeId);
7
+ const node = findNode(input.graph.nodes, nodeId);
7
8
  const portRank = new Map(node?.ports?.map((port, index) => [port.name, index]));
8
9
  const explicitCount = portRank.size;
9
10
  return input.graph.edges
@@ -1,5 +1,6 @@
1
1
  import { externalPortDummyOf } from "./external-port-dummy";
2
2
  import type { LayeredPhaseInput } from "./types";
3
+ import { findNode } from "./node-lookup";
3
4
 
4
5
  export type IndividualSpacing = Readonly<Record<string, number>>;
5
6
 
@@ -13,8 +14,8 @@ export function nodeNodeSpacing(
13
14
  const secondBreakingPoint = secondId.startsWith("__layout_breaking:");
14
15
  const firstLabel = firstId.startsWith("__layout_dummy:label:");
15
16
  const secondLabel = secondId.startsWith("__layout_dummy:label:");
16
- const firstNode = input.graph.nodes.find((node) => node.id === firstId);
17
- const secondNode = input.graph.nodes.find((node) => node.id === secondId);
17
+ const firstNode = findNode(input.graph.nodes, firstId);
18
+ const secondNode = findNode(input.graph.nodes, secondId);
18
19
  const firstExternal = firstNode !== undefined && externalPortDummyOf(firstNode) !== undefined;
19
20
  const secondExternal = secondNode !== undefined && externalPortDummyOf(secondNode) !== undefined;
20
21
  const firstDummy = firstId.startsWith("__layout_dummy:") || firstBreakingPoint || firstExternal;
@@ -47,7 +48,7 @@ export function nodeNodeSpacing(
47
48
  ? Number(input.settings["spacing.edgeNode"] ?? 10)
48
49
  : input.spacing.node;
49
50
  for (const id of [firstId, secondId]) {
50
- const node = input.graph.nodes.find((candidate) => candidate.id === id);
51
+ const node = findNode(input.graph.nodes, id);
51
52
  if (!node) continue;
52
53
  const individual = input.nodeSettings?.(node)?.["spacing.individual"];
53
54
  if (individual && typeof individual === "object") {