@statelyai/layout 0.4.2 → 0.4.3

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/README.md CHANGED
@@ -276,6 +276,14 @@ and Box SIMPLE geometry, including provider bounds and whether edge sections
276
276
  are routed or left authored. Rectangle Packing has an exact default baseline;
277
277
  its full Java packing strategy remains in progress.
278
278
 
279
+ <!-- size snapping from src/elkjs/index.ts snapSizes -->
280
+
281
+ Node, port and label sizes are snapped to the browser's 1/64px layout unit
282
+ before layout, and the result reports the snapped sizes. Measured sizes carry
283
+ float noise, such as a rect divided by a zoom factor, and exact ties in layered
284
+ layout would otherwise let that noise move nodes. Sizes already on that grid,
285
+ including all integer sizes, are unchanged.
286
+
279
287
  The compatibility entry's named graph, edge, option, and result types are
280
288
  mutually assignable with the declarations shipped by `elkjs@0.11.1`. Its
281
289
  default class also accepts the library's broader internal graph inputs.
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, '__esModule', { value: true });
2
2
  require('../layered-BJu8A21s.cjs');
3
- const require_elkjs = require('../elkjs-CnBvdyBu.cjs');
3
+ const require_elkjs = require('../elkjs-Cj-lWiK1.cjs');
4
4
 
5
5
  exports.compileStatechartLayout = require_elkjs.compileStatechartLayout;
6
6
  exports.default = require_elkjs.ELK;
@@ -1,4 +1,4 @@
1
1
  import "../layered-BsAdm_u8.mjs";
2
- import { a as scoreStatechartLayout, i as layoutStatechart, n as getElkRoutes, r as compileStatechartLayout, t as ELK } from "../elkjs-CSi7oB5J.mjs";
2
+ import { a as scoreStatechartLayout, i as layoutStatechart, n as getElkRoutes, r as compileStatechartLayout, t as ELK } from "../elkjs-B5kN_zS6.mjs";
3
3
 
4
4
  export { compileStatechartLayout, ELK as default, getElkRoutes, layoutStatechart, scoreStatechartLayout };
@@ -1,5 +1,5 @@
1
1
  require('../layered-BJu8A21s.cjs');
2
- const require_elkjs = require('../elkjs-CnBvdyBu.cjs');
2
+ const require_elkjs = require('../elkjs-Cj-lWiK1.cjs');
3
3
 
4
4
  //#region src/elkjs/worker.ts
5
5
  /*******************************************************************************
@@ -1,5 +1,5 @@
1
1
  import "../layered-BsAdm_u8.mjs";
2
- import { t as ELK } from "../elkjs-CSi7oB5J.mjs";
2
+ import { t as ELK } from "../elkjs-B5kN_zS6.mjs";
3
3
 
4
4
  //#region src/elkjs/worker.ts
5
5
  /*******************************************************************************
@@ -1049,6 +1049,27 @@ function isLayoutEdgeLabel(label) {
1049
1049
  }
1050
1050
  /** The crossing-model setting a layout passes to crossing minimization. */
1051
1051
  const crossingModel = Symbol("crossingModel");
1052
+ const LAYOUT_UNITS_PER_PX = 64;
1053
+ /**
1054
+ * Measured sizes carry float noise (a browser rect divided by zoom), and a
1055
+ * layered layout is full of exact ties that noise decides. Snap sizes to the
1056
+ * browser's 1/64px layout unit so the same graph always lays out the same way;
1057
+ * sizes already on that grid are unchanged.
1058
+ */
1059
+ function snapSizes(node) {
1060
+ const snap = (shape) => {
1061
+ if (typeof shape.width === "number") shape.width = Math.round(shape.width * LAYOUT_UNITS_PER_PX) / LAYOUT_UNITS_PER_PX;
1062
+ if (typeof shape.height === "number") shape.height = Math.round(shape.height * LAYOUT_UNITS_PER_PX) / LAYOUT_UNITS_PER_PX;
1063
+ };
1064
+ snap(node);
1065
+ for (const label of node.labels ?? []) snap(label);
1066
+ for (const port of node.ports ?? []) {
1067
+ snap(port);
1068
+ for (const label of port.labels ?? []) snap(label);
1069
+ }
1070
+ for (const edge of node.edges ?? []) for (const label of edge.labels ?? []) snap(label);
1071
+ for (const child of node.children ?? []) snapSizes(child);
1072
+ }
1052
1073
  var ELK = class {
1053
1074
  #options;
1054
1075
  #algorithmIds;
@@ -1119,6 +1140,7 @@ var ELK = class {
1119
1140
  }
1120
1141
  terminateWorker() {}
1121
1142
  async layout(graph, arguments_ = {}) {
1143
+ snapSizes(graph);
1122
1144
  const options = {
1123
1145
  ...this.#options.defaultLayoutOptions,
1124
1146
  ...arguments_.layoutOptions
@@ -1049,6 +1049,27 @@ function isLayoutEdgeLabel(label) {
1049
1049
  }
1050
1050
  /** The crossing-model setting a layout passes to crossing minimization. */
1051
1051
  const crossingModel = Symbol("crossingModel");
1052
+ const LAYOUT_UNITS_PER_PX = 64;
1053
+ /**
1054
+ * Measured sizes carry float noise (a browser rect divided by zoom), and a
1055
+ * layered layout is full of exact ties that noise decides. Snap sizes to the
1056
+ * browser's 1/64px layout unit so the same graph always lays out the same way;
1057
+ * sizes already on that grid are unchanged.
1058
+ */
1059
+ function snapSizes(node) {
1060
+ const snap = (shape) => {
1061
+ if (typeof shape.width === "number") shape.width = Math.round(shape.width * LAYOUT_UNITS_PER_PX) / LAYOUT_UNITS_PER_PX;
1062
+ if (typeof shape.height === "number") shape.height = Math.round(shape.height * LAYOUT_UNITS_PER_PX) / LAYOUT_UNITS_PER_PX;
1063
+ };
1064
+ snap(node);
1065
+ for (const label of node.labels ?? []) snap(label);
1066
+ for (const port of node.ports ?? []) {
1067
+ snap(port);
1068
+ for (const label of port.labels ?? []) snap(label);
1069
+ }
1070
+ for (const edge of node.edges ?? []) for (const label of edge.labels ?? []) snap(label);
1071
+ for (const child of node.children ?? []) snapSizes(child);
1072
+ }
1052
1073
  var ELK = class {
1053
1074
  #options;
1054
1075
  #algorithmIds;
@@ -1119,6 +1140,7 @@ var ELK = class {
1119
1140
  }
1120
1141
  terminateWorker() {}
1121
1142
  async layout(graph, arguments_ = {}) {
1143
+ snapSizes(graph);
1122
1144
  const options = {
1123
1145
  ...this.#options.defaultLayoutOptions,
1124
1146
  ...arguments_.layoutOptions
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statelyai/layout",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "Extensible, graph-native layout algorithms for @statelyai/graph",
5
5
  "license": "EPL-2.0 OR GPL-3.0-or-later",
6
6
  "repository": {
@@ -96,6 +96,31 @@ interface PreparedElkScope {
96
96
  finish(orders?: ReadonlyMap<PreparedElkScope, LayerOrder>): Promise<ElkNode>;
97
97
  }
98
98
 
99
+ const LAYOUT_UNITS_PER_PX = 64;
100
+
101
+ /**
102
+ * Measured sizes carry float noise (a browser rect divided by zoom), and a
103
+ * layered layout is full of exact ties that noise decides. Snap sizes to the
104
+ * browser's 1/64px layout unit so the same graph always lays out the same way;
105
+ * sizes already on that grid are unchanged.
106
+ */
107
+ function snapSizes(node: ElkNode): void {
108
+ const snap = (shape: { width?: number; height?: number }) => {
109
+ if (typeof shape.width === "number")
110
+ shape.width = Math.round(shape.width * LAYOUT_UNITS_PER_PX) / LAYOUT_UNITS_PER_PX;
111
+ if (typeof shape.height === "number")
112
+ shape.height = Math.round(shape.height * LAYOUT_UNITS_PER_PX) / LAYOUT_UNITS_PER_PX;
113
+ };
114
+ snap(node);
115
+ for (const label of node.labels ?? []) snap(label);
116
+ for (const port of node.ports ?? []) {
117
+ snap(port);
118
+ for (const label of port.labels ?? []) snap(label);
119
+ }
120
+ for (const edge of node.edges ?? []) for (const label of edge.labels ?? []) snap(label);
121
+ for (const child of node.children ?? []) snapSizes(child);
122
+ }
123
+
99
124
  export default class ELK {
100
125
  readonly #options: ElkConstructorArguments;
101
126
  readonly #algorithmIds: ReadonlySet<string>;
@@ -171,6 +196,7 @@ export default class ELK {
171
196
  graph: T,
172
197
  arguments_: ElkLayoutArguments = {},
173
198
  ): Promise<LaidOutElkNode<T>> {
199
+ snapSizes(graph);
174
200
  const options = { ...this.#options.defaultLayoutOptions, ...arguments_.layoutOptions };
175
201
  const seeds = crossingVariants(options, graph);
176
202
  const probe = mayShareTracks(graph, options)