Skip to content

Commit

Permalink
Complete implementation of spline routing (fixes #11)
Browse files Browse the repository at this point in the history
  • Loading branch information
vibridi committed Sep 27, 2024
1 parent 614fa4c commit fee567a
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 57 deletions.
4 changes: 4 additions & 0 deletions autolayout_options_algs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion internal/geom/point.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type P struct {
X, Y float64
}

func (p P) String() string {
func (p P) SVG() string {
return fmt.Sprintf(`<circle r="4" cx="%.02f" cy="%.02f" fill="black"/>`, p.X, p.Y)
}

Expand Down
48 changes: 0 additions & 48 deletions internal/geom/shortest_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
package geom

import (
"fmt"
"slices"
"strconv"
"strings"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -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++ {
Expand All @@ -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(`<path d="M %.2f,%.2f %.2f,%.2f" stroke="black" stroke-width="3" />`+"\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(`<polyline points="`)
for _, p := range points {
b.WriteString(strconv.FormatFloat(p.X, 'f', 2, 64))
b.WriteRune(',')
b.WriteString(strconv.FormatFloat(p.Y, 'f', 2, 64))
b.WriteRune(' ')
}
b.WriteString(`" fill="none" stroke="` + color + `" />`)
return b.String()
}
9 changes: 9 additions & 0 deletions internal/graph/node.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package graph

import (
"fmt"
"sync"
)

Expand All @@ -22,6 +23,14 @@ func (n *Node) String() string {
return n.ID
}

func (n *Node) SVG() string {
return fmt.Sprintf(
`<rect id="%s" x="%f" y="%f" width="%f" height="%f" style="fill: none; stroke: blue;" />`,
"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)
Expand Down
3 changes: 3 additions & 0 deletions internal/phase5/alg.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ func (alg Alg) String() (s string) {
s = "piecewise"
case Ortho:
s = "ortho"
case Splines:
s = "splines"
default:
s = "<invalid>"
}
Expand All @@ -27,5 +29,6 @@ const (
Straight
Polyline
Ortho
Splines
_endAlg
)
2 changes: 2 additions & 0 deletions internal/phase5/alg_process.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
107 changes: 99 additions & 8 deletions internal/phase5/splines.go
Original file line number Diff line number Diff line change
@@ -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},
}
}

0 comments on commit fee567a

Please sign in to comment.