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>