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}, } }