From fee567ab44595f0ecd26f3618806e1ca5882c669 Mon Sep 17 00:00:00 2001
From: Gabriele <10689839+vibridi@users.noreply.github.com>
Date: Fri, 27 Sep 2024 10:16:11 +0200
Subject: [PATCH] Complete implementation of spline routing (fixes #11)
---
autolayout_options_algs.go | 4 ++
internal/geom/point.go | 2 +-
internal/geom/shortest_test.go | 48 ---------------
internal/graph/node.go | 9 +++
internal/phase5/alg.go | 3 +
internal/phase5/alg_process.go | 2 +
internal/phase5/splines.go | 107 ++++++++++++++++++++++++++++++---
7 files changed, 118 insertions(+), 57 deletions(-)
diff --git a/autolayout_options_algs.go b/autolayout_options_algs.go
index b8601d3..43d4c17 100644
--- a/autolayout_options_algs.go
+++ b/autolayout_options_algs.go
@@ -88,6 +88,10 @@ const (
// Dense graphs look tidier, but it's harder to understand where edges start and finish.
// Suitable when there's few sets of edges with the same target node.
EdgeRoutingOrtho = phase5.Ortho
+
+ // EdgeRoutingSplines outputs edges as piece-wise cubic Bézier curves. Edges that don't encounter obstacles
+ // are drawn as straight lines.
+ EdgeRoutingSplines = phase5.Splines
)
func WithCycleBreaking(alg phase1.Alg) Option {
diff --git a/internal/geom/point.go b/internal/geom/point.go
index ba6e92c..286eb4e 100644
--- a/internal/geom/point.go
+++ b/internal/geom/point.go
@@ -10,7 +10,7 @@ type P struct {
X, Y float64
}
-func (p P) String() string {
+func (p P) SVG() string {
return fmt.Sprintf(``, p.X, p.Y)
}
diff --git a/internal/geom/shortest_test.go b/internal/geom/shortest_test.go
index 1c1f7f0..883638b 100644
--- a/internal/geom/shortest_test.go
+++ b/internal/geom/shortest_test.go
@@ -1,10 +1,7 @@
package geom
import (
- "fmt"
"slices"
- "strconv"
- "strings"
"testing"
"github.com/stretchr/testify/assert"
@@ -83,20 +80,6 @@ func TestShortest(t *testing.T) {
})
}
-func TestShortestEdgeCases(t *testing.T) {
- rects := []Rect{
- {P{112, 90}, P{200, 140}},
- {P{80, 140}, P{150, 300}},
- {P{140, 300}, P{270, 380}},
- }
- start := P{190, 140 - 1}
- end := P{200, 300 + 1}
-
- path := Shortest(start, end, rects)
-
- printall(rects, start, end, path)
-}
-
func assertPath(t *testing.T, want, got []P) {
require.Equal(t, len(want), len(got))
for i := 0; i < len(got); i++ {
@@ -106,34 +89,3 @@ func assertPath(t *testing.T, want, got []P) {
assert.Equal(t, want[i], got[i])
}
}
-
-func printpath(path []P) {
- for i := 1; i < len(path); i++ {
- u, v := path[i-1], path[i]
- fmt.Printf(``+"\n", u.X, u.Y, v.X, v.Y)
- }
-}
-
-func printall(rects []Rect, start, end P, path []P) {
- p := MergeRects(rects)
-
- s := polyline(p.Points, "red")
- fmt.Println(s)
-
- fmt.Println(start.String())
- fmt.Println(end.String())
- printpath(path)
-}
-
-func polyline(points []P, color string) string {
- b := strings.Builder{}
- b.WriteString(``)
- return b.String()
-}
diff --git a/internal/graph/node.go b/internal/graph/node.go
index 275be95..88d76b0 100644
--- a/internal/graph/node.go
+++ b/internal/graph/node.go
@@ -1,6 +1,7 @@
package graph
import (
+ "fmt"
"sync"
)
@@ -22,6 +23,14 @@ func (n *Node) String() string {
return n.ID
}
+func (n *Node) SVG() string {
+ return fmt.Sprintf(
+ ``,
+ "autog-node-"+n.ID,
+ n.X, n.Y, n.W, n.H,
+ )
+}
+
// Indeg returns the number of incoming edges
func (n *Node) Indeg() int {
return len(n.In)
diff --git a/internal/phase5/alg.go b/internal/phase5/alg.go
index 2891b3b..c2c81b8 100644
--- a/internal/phase5/alg.go
+++ b/internal/phase5/alg.go
@@ -16,6 +16,8 @@ func (alg Alg) String() (s string) {
s = "piecewise"
case Ortho:
s = "ortho"
+ case Splines:
+ s = "splines"
default:
s = ""
}
@@ -27,5 +29,6 @@ const (
Straight
Polyline
Ortho
+ Splines
_endAlg
)
diff --git a/internal/phase5/alg_process.go b/internal/phase5/alg_process.go
index a88af3c..5d46f7e 100644
--- a/internal/phase5/alg_process.go
+++ b/internal/phase5/alg_process.go
@@ -30,6 +30,8 @@ func (alg Alg) Process(g *graph.DGraph, params graph.Params) {
execPolylineRouting(g, routableEdges)
case Ortho:
execOrthoRouting(g, routableEdges, params)
+ case Splines:
+ execSplines(g, routableEdges)
default:
panic("routing: unknown alg value")
}
diff --git a/internal/phase5/splines.go b/internal/phase5/splines.go
index 4e69bcf..6bc2aff 100644
--- a/internal/phase5/splines.go
+++ b/internal/phase5/splines.go
@@ -1,24 +1,115 @@
package phase5
import (
+ "slices"
+
"github.com/nulab/autog/internal/geom"
"github.com/nulab/autog/internal/graph"
+ imonitor "github.com/nulab/autog/internal/monitor"
)
-// todo: work in progress
-func execSplines(g *graph.DGraph, params graph.Params) {
- for _, e := range g.Edges {
- rects := []geom.Rect{
- // todo: build rects
- }
+func execSplines(g *graph.DGraph, routes []routableEdge) {
+ for _, e := range routes {
+ imonitor.Log("spline", e)
- poly := geom.MergeRects(rects)
+ rects := buildRects(g, e)
+
+ for _, r := range rects {
+ imonitor.Log("rect", r)
+ }
start := geom.P{e.From.X + e.From.W/2, e.From.Y + e.From.H}
end := geom.P{e.To.X + e.To.W/2, e.To.Y}
+ imonitor.Log("shortest-start", start)
+ imonitor.Log("shortest-end", end)
+ for _, n := range e.ns {
+ imonitor.Log("route-node", n)
+ }
+
path := geom.Shortest(start, end, rects)
+
+ poly := geom.MergeRects(rects)
ctrls := geom.FitSpline(path, geom.P{}, geom.P{}, poly.Sides())
- _ = ctrls
+ slices.Reverse(ctrls)
+
+ e.Points = make([][2]float64, 0, len(ctrls)*4)
+ for _, c := range ctrls {
+ s := c.Float64Slice()
+ e.Points = append(e.Points, [][2]float64{s[3], s[2], s[1], s[0]}...)
+ }
+ }
+}
+
+func buildRects(g *graph.DGraph, r routableEdge) (rects []geom.Rect) {
+ for i := 1; i < len(r.ns); i++ {
+ top, btm := r.ns[i-1], r.ns[i]
+ switch {
+
+ case !top.IsVirtual && !btm.IsVirtual:
+ // add one rectangle that spans from the leftmost point to the rightmost point of the two nodes
+ r := geom.Rect{
+ TL: geom.P{min(top.X, btm.X), top.Y + top.H},
+ BR: geom.P{max(top.X+top.W, btm.X+btm.W), btm.Y},
+ }
+ rects = append(rects, r)
+
+ case btm.IsVirtual:
+ // add one rectangle that spans the entire space between the top and bottom layers
+ // and one that spans the space around the virtual node
+ tl := g.Layers[top.Layer]
+ bl := g.Layers[btm.Layer]
+ rects = append(rects, rectBetweenLayers(tl, bl))
+ rects = append(rects, rectVirtualNode(btm, bl))
+
+ case top.IsVirtual:
+ tl := g.Layers[top.Layer]
+ bl := g.Layers[btm.Layer]
+ rects = append(rects, rectBetweenLayers(tl, bl))
+ }
+ }
+
+ return
+}
+
+func rectBetweenLayers(l1, l2 *graph.Layer) geom.Rect {
+ h := l1.Head()
+ t := l2.Tail()
+ return geom.Rect{
+ TL: geom.P{h.X, h.Y + h.H},
+ BR: geom.P{t.X + t.W, t.Y},
+ }
+}
+
+func rectVirtualNode(vn *graph.Node, vl *graph.Layer) geom.Rect {
+ switch p := vn.LayerPos; {
+ case p == 0:
+ // this p+1 access is safe: a layer cannot contain only one virtual node
+ n := vl.Nodes[p+1]
+ return geom.Rect{
+ TL: geom.P{vn.X - 10, n.Y},
+ BR: geom.P{n.X, n.Y + n.H},
+ }
+
+ case p == vl.Len()-1:
+ // this p-1 access is safe: a layer cannot contain only one virtual node
+ n := vl.Nodes[p-1]
+ return geom.Rect{
+ TL: geom.P{n.X + n.W, n.Y},
+ BR: geom.P{vn.X + 10, n.Y + n.H},
+ }
+
+ default:
+ n1 := vl.Nodes[p-1]
+ n2 := vl.Nodes[p+1]
+ return rectBetweenNodes(n1, n2)
+ }
+}
+
+func rectBetweenNodes(n1, n2 *graph.Node) geom.Rect {
+ d := n2.X - (n1.X + n1.W)
+ return geom.Rect{
+ TL: geom.P{n1.X + n1.W + d/3, n1.Y},
+ BR: geom.P{n2.X - d/3, n2.Y + n2.H},
}
}