This is an automated email from the ASF dual-hosted git repository.

hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new fd34192bd2 docs and fixes for auto layout. #7345 (#7351)
fd34192bd2 is described below

commit fd34192bd23028ce4ed4b9e25c83f6fa630c9488
Author: Bart Maertens <[email protected]>
AuthorDate: Fri Jun 26 15:19:47 2026 +0200

    docs and fixes for auto layout. #7345 (#7351)
---
 .../images/getting-started/icons/auto-layout.svg   |   8 ++
 .../apache/hop/core/layout/LayeredGraphLayout.java | 108 +++++++++++++++++++++
 .../apache/hop/pipeline/PipelineMetaLayout.java    |  77 +--------------
 .../apache/hop/workflow/WorkflowMetaLayout.java    |  76 +--------------
 ui/src/main/resources/ui/images/auto-layout.svg    |   8 ++
 5 files changed, 127 insertions(+), 150 deletions(-)

diff --git 
a/docs/hop-user-manual/modules/ROOT/assets/images/getting-started/icons/auto-layout.svg
 
b/docs/hop-user-manual/modules/ROOT/assets/images/getting-started/icons/auto-layout.svg
new file mode 100644
index 0000000000..9ef08337a0
--- /dev/null
+++ 
b/docs/hop-user-manual/modules/ROOT/assets/images/getting-started/icons/auto-layout.svg
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg"; width="16" height="16">
+    <path d="M5 8 H8 V3 H10" fill="none" stroke="#0e3a5a" stroke-width="1"/>
+    <path d="M8 8 V13 H10" fill="none" stroke="#0e3a5a" stroke-width="1"/>
+    <rect x="1" y="6" width="4" height="4" fill="#0e3a5a"/>
+    <rect x="10" y="1" width="4" height="4" fill="#0e3a5a"/>
+    <rect x="10" y="11" width="4" height="4" fill="#0e3a5a"/>
+</svg>
diff --git 
a/engine/src/main/java/org/apache/hop/core/layout/LayeredGraphLayout.java 
b/engine/src/main/java/org/apache/hop/core/layout/LayeredGraphLayout.java
index cac632c0a9..b6b8dec1d8 100644
--- a/engine/src/main/java/org/apache/hop/core/layout/LayeredGraphLayout.java
+++ b/engine/src/main/java/org/apache/hop/core/layout/LayeredGraphLayout.java
@@ -23,6 +23,8 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import org.apache.hop.core.gui.IGuiPosition;
+import org.apache.hop.core.gui.Point;
 
 /**
  * Reusable layered (Sugiyama-style) DAG auto-layout core, decoupled from any 
particular Hop model.
@@ -155,6 +157,112 @@ public final class LayeredGraphLayout {
     return best;
   }
 
+  /**
+   * High-level layout over positioned model objects. Arranges {@code nodes} 
(indexed 0..n-1, with
+   * {@code edges} referring to those indices), writing the result back via 
{@link
+   * IGuiPosition#setLocation(int, int)}. Shared by the pipeline and workflow 
adapters.
+   *
+   * <p>When {@code anchorToOriginal} is true the arranged block is translated 
so its top-left lands
+   * where the supplied nodes originally were (used for "arrange selection 
only"). When {@link
+   * Options#isMoveNotes()} is set, each note in {@code notes} is moved along 
with the node it sits
+   * closest to (within one layer/node spacing); notes far from every node are 
left in place.
+   *
+   * @param nodes the nodes to arrange, in edge-index order
+   * @param edges directed edges as {@code int[]{fromIndex, toIndex}} into 
{@code nodes}
+   * @param notes free-floating items (e.g. notes) to optionally reposition, 
or null
+   * @param options the layout tuning options
+   * @param anchorToOriginal whether to anchor the result to the nodes' 
original top-left
+   */
+  public static void layoutPositioned(
+      List<? extends IGuiPosition> nodes,
+      List<int[]> edges,
+      List<? extends IGuiPosition> notes,
+      Options options,
+      boolean anchorToOriginal) {
+    if (nodes == null) {
+      return;
+    }
+    int n = nodes.size();
+    if (n == 0) {
+      return;
+    }
+    if (options == null) {
+      options = new Options();
+    }
+
+    Point origin = anchorToOriginal ? topLeftOfPositions(nodes) : null;
+    final Point[] computed = new Point[n];
+    layout(n, edges, options, (node, x, y) -> computed[node] = new Point(x, 
y));
+
+    // Translate so the arranged block lands where the nodes used to be.
+    int dx = 0;
+    int dy = 0;
+    if (anchorToOriginal) {
+      Point computedMin = topLeftOfPoints(computed);
+      dx = origin.x - computedMin.x;
+      dy = origin.y - computedMin.y;
+    }
+
+    // Capture node positions before/after so notes can follow the node 
they're closest to.
+    int[] beforeX = new int[n];
+    int[] beforeY = new int[n];
+    int[] afterX = new int[n];
+    int[] afterY = new int[n];
+    for (int i = 0; i < n; i++) {
+      IGuiPosition node = nodes.get(i);
+      Point before = node.getLocation();
+      beforeX[i] = before.x;
+      beforeY[i] = before.y;
+      if (computed[i] != null) {
+        afterX[i] = computed[i].x + dx;
+        afterY[i] = computed[i].y + dy;
+        node.setLocation(afterX[i], afterY[i]);
+      } else {
+        afterX[i] = before.x;
+        afterY[i] = before.y;
+      }
+    }
+
+    if (options.isMoveNotes() && notes != null) {
+      double threshold = Math.max(options.getLayerSpacing(), 
options.getNodeSpacing());
+      for (IGuiPosition note : notes) {
+        Point p = note.getLocation();
+        if (p == null) {
+          continue;
+        }
+        int nearest = nearestNode(p.x, p.y, beforeX, beforeY, threshold);
+        if (nearest >= 0) {
+          note.setLocation(
+              p.x + (afterX[nearest] - beforeX[nearest]),
+              p.y + (afterY[nearest] - beforeY[nearest]));
+        }
+      }
+    }
+  }
+
+  private static Point topLeftOfPositions(List<? extends IGuiPosition> 
positions) {
+    int minX = Integer.MAX_VALUE;
+    int minY = Integer.MAX_VALUE;
+    for (IGuiPosition position : positions) {
+      Point p = position.getLocation();
+      minX = Math.min(minX, p.x);
+      minY = Math.min(minY, p.y);
+    }
+    return new Point(minX, minY);
+  }
+
+  private static Point topLeftOfPoints(Point[] points) {
+    int minX = Integer.MAX_VALUE;
+    int minY = Integer.MAX_VALUE;
+    for (Point p : points) {
+      if (p != null) {
+        minX = Math.min(minX, p.x);
+        minY = Math.min(minY, p.y);
+      }
+    }
+    return new Point(minX, minY);
+  }
+
   /** Callback to receive the computed position for each node index. */
   public interface PositionSink {
     void setPosition(int nodeIndex, int x, int y);
diff --git 
a/engine/src/main/java/org/apache/hop/pipeline/PipelineMetaLayout.java 
b/engine/src/main/java/org/apache/hop/pipeline/PipelineMetaLayout.java
index 1a628bd552..f42941edc0 100644
--- a/engine/src/main/java/org/apache/hop/pipeline/PipelineMetaLayout.java
+++ b/engine/src/main/java/org/apache/hop/pipeline/PipelineMetaLayout.java
@@ -21,8 +21,6 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import org.apache.hop.core.NotePadMeta;
-import org.apache.hop.core.gui.Point;
 import org.apache.hop.core.layout.LayeredGraphLayout;
 import org.apache.hop.pipeline.transform.TransformMeta;
 
@@ -115,78 +113,7 @@ public final class PipelineMetaLayout {
     }
 
     boolean anchor = subset != null && !subset.isEmpty();
-    Point origin = anchor ? topLeftOfTransforms(transforms) : null;
-    final Point[] computed = new Point[n];
-    LayeredGraphLayout.layout(n, edges, options, (node, x, y) -> 
computed[node] = new Point(x, y));
-
-    // Translate so the arranged block lands where the selection used to be.
-    int dx = 0;
-    int dy = 0;
-    if (anchor) {
-      Point computedMin = topLeft(computed);
-      dx = origin.x - computedMin.x;
-      dy = origin.y - computedMin.y;
-    }
-
-    // Capture the node positions before/after so notes can follow the node 
they're closest to.
-    int[] nodeBeforeX = new int[n];
-    int[] nodeBeforeY = new int[n];
-    int[] nodeAfterX = new int[n];
-    int[] nodeAfterY = new int[n];
-    for (int i = 0; i < n; i++) {
-      Point before = transforms.get(i).getLocation();
-      nodeBeforeX[i] = before.x;
-      nodeBeforeY[i] = before.y;
-      if (computed[i] != null) {
-        nodeAfterX[i] = computed[i].x + dx;
-        nodeAfterY[i] = computed[i].y + dy;
-        transforms.get(i).setLocation(nodeAfterX[i], nodeAfterY[i]);
-      } else {
-        nodeAfterX[i] = before.x;
-        nodeAfterY[i] = before.y;
-      }
-    }
-
-    if (options.isMoveNotes()) {
-      double threshold = Math.max(options.getLayerSpacing(), 
options.getNodeSpacing());
-      for (int i = 0; i < pipelineMeta.nrNotes(); i++) {
-        NotePadMeta note = pipelineMeta.getNote(i);
-        Point p = note.getLocation();
-        if (p == null) {
-          continue;
-        }
-        int nearest = LayeredGraphLayout.nearestNode(p.x, p.y, nodeBeforeX, 
nodeBeforeY, threshold);
-        if (nearest >= 0) {
-          note.setLocation(
-              p.x + (nodeAfterX[nearest] - nodeBeforeX[nearest]),
-              p.y + (nodeAfterY[nearest] - nodeBeforeY[nearest]));
-        }
-      }
-    }
-  }
-
-  /** The minimum x and minimum y across the locations of the given transforms 
(as one Point). */
-  private static Point topLeftOfTransforms(List<TransformMeta> transforms) {
-    int minX = Integer.MAX_VALUE;
-    int minY = Integer.MAX_VALUE;
-    for (TransformMeta t : transforms) {
-      Point p = t.getLocation();
-      minX = Math.min(minX, p.x);
-      minY = Math.min(minY, p.y);
-    }
-    return new Point(minX, minY);
-  }
-
-  /** The minimum x and minimum y across the given points (as one Point). */
-  private static Point topLeft(Point[] points) {
-    int minX = Integer.MAX_VALUE;
-    int minY = Integer.MAX_VALUE;
-    for (Point p : points) {
-      if (p != null) {
-        minX = Math.min(minX, p.x);
-        minY = Math.min(minY, p.y);
-      }
-    }
-    return new Point(minX, minY);
+    LayeredGraphLayout.layoutPositioned(
+        transforms, edges, pipelineMeta.getNotes(), options, anchor);
   }
 }
diff --git 
a/engine/src/main/java/org/apache/hop/workflow/WorkflowMetaLayout.java 
b/engine/src/main/java/org/apache/hop/workflow/WorkflowMetaLayout.java
index 30dd180f7c..59c5e211a4 100644
--- a/engine/src/main/java/org/apache/hop/workflow/WorkflowMetaLayout.java
+++ b/engine/src/main/java/org/apache/hop/workflow/WorkflowMetaLayout.java
@@ -21,8 +21,6 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import org.apache.hop.core.NotePadMeta;
-import org.apache.hop.core.gui.Point;
 import org.apache.hop.core.layout.LayeredGraphLayout;
 import org.apache.hop.workflow.action.ActionMeta;
 
@@ -115,78 +113,6 @@ public final class WorkflowMetaLayout {
     }
 
     boolean anchor = subset != null && !subset.isEmpty();
-    Point origin = anchor ? topLeftOfActions(actions) : null;
-    final Point[] computed = new Point[n];
-    LayeredGraphLayout.layout(n, edges, options, (node, x, y) -> 
computed[node] = new Point(x, y));
-
-    // Translate so the arranged block lands where the selection used to be.
-    int dx = 0;
-    int dy = 0;
-    if (anchor) {
-      Point computedMin = topLeft(computed);
-      dx = origin.x - computedMin.x;
-      dy = origin.y - computedMin.y;
-    }
-
-    // Capture the node positions before/after so notes can follow the node 
they're closest to.
-    int[] nodeBeforeX = new int[n];
-    int[] nodeBeforeY = new int[n];
-    int[] nodeAfterX = new int[n];
-    int[] nodeAfterY = new int[n];
-    for (int i = 0; i < n; i++) {
-      Point before = actions.get(i).getLocation();
-      nodeBeforeX[i] = before.x;
-      nodeBeforeY[i] = before.y;
-      if (computed[i] != null) {
-        nodeAfterX[i] = computed[i].x + dx;
-        nodeAfterY[i] = computed[i].y + dy;
-        actions.get(i).setLocation(nodeAfterX[i], nodeAfterY[i]);
-      } else {
-        nodeAfterX[i] = before.x;
-        nodeAfterY[i] = before.y;
-      }
-    }
-
-    if (options.isMoveNotes()) {
-      double threshold = Math.max(options.getLayerSpacing(), 
options.getNodeSpacing());
-      for (int i = 0; i < workflowMeta.nrNotes(); i++) {
-        NotePadMeta note = workflowMeta.getNote(i);
-        Point p = note.getLocation();
-        if (p == null) {
-          continue;
-        }
-        int nearest = LayeredGraphLayout.nearestNode(p.x, p.y, nodeBeforeX, 
nodeBeforeY, threshold);
-        if (nearest >= 0) {
-          note.setLocation(
-              p.x + (nodeAfterX[nearest] - nodeBeforeX[nearest]),
-              p.y + (nodeAfterY[nearest] - nodeBeforeY[nearest]));
-        }
-      }
-    }
-  }
-
-  /** The minimum x and minimum y across the locations of the given actions 
(as one Point). */
-  private static Point topLeftOfActions(List<ActionMeta> actions) {
-    int minX = Integer.MAX_VALUE;
-    int minY = Integer.MAX_VALUE;
-    for (ActionMeta a : actions) {
-      Point p = a.getLocation();
-      minX = Math.min(minX, p.x);
-      minY = Math.min(minY, p.y);
-    }
-    return new Point(minX, minY);
-  }
-
-  /** The minimum x and minimum y across the given points (as one Point). */
-  private static Point topLeft(Point[] points) {
-    int minX = Integer.MAX_VALUE;
-    int minY = Integer.MAX_VALUE;
-    for (Point p : points) {
-      if (p != null) {
-        minX = Math.min(minX, p.x);
-        minY = Math.min(minY, p.y);
-      }
-    }
-    return new Point(minX, minY);
+    LayeredGraphLayout.layoutPositioned(actions, edges, 
workflowMeta.getNotes(), options, anchor);
   }
 }
diff --git a/ui/src/main/resources/ui/images/auto-layout.svg 
b/ui/src/main/resources/ui/images/auto-layout.svg
new file mode 100644
index 0000000000..9ef08337a0
--- /dev/null
+++ b/ui/src/main/resources/ui/images/auto-layout.svg
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg"; width="16" height="16">
+    <path d="M5 8 H8 V3 H10" fill="none" stroke="#0e3a5a" stroke-width="1"/>
+    <path d="M8 8 V13 H10" fill="none" stroke="#0e3a5a" stroke-width="1"/>
+    <rect x="1" y="6" width="4" height="4" fill="#0e3a5a"/>
+    <rect x="10" y="1" width="4" height="4" fill="#0e3a5a"/>
+    <rect x="10" y="11" width="4" height="4" fill="#0e3a5a"/>
+</svg>

Reply via email to