Skip to content

Commit 87fd1e2

Browse files
committed
[p5-splines] Complete implementation of spline routing (fixes #11)
1 parent 3e1cef5 commit 87fd1e2

10 files changed

+158
-60
lines changed

autolayout_options_algs.go

+4
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ const (
8888
// Dense graphs look tidier, but it's harder to understand where edges start and finish.
8989
// Suitable when there's few sets of edges with the same target node.
9090
EdgeRoutingOrtho = phase5.Ortho
91+
92+
// EdgeRoutingSplines outputs edges as piece-wise cubic Bézier curves. Edges that don't encounter obstacles
93+
// are drawn as straight lines.
94+
EdgeRoutingSplines = phase5.Splines
9195
)
9296

9397
func WithCycleBreaking(alg phase1.Alg) Option {

go.mod

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
module github.com/nulab/autog
22

3-
go 1.22
3+
go 1.22.4
4+
5+
toolchain go1.22.7
46

57
require github.com/stretchr/testify v1.8.4
68

9+
replace github.com/vibridi/graphify => ../../vibridi/graphify
10+
711
require (
12+
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b // indirect
813
github.com/davecgh/go-spew v1.1.1 // indirect
914
github.com/pmezard/go-difflib v1.0.0 // indirect
15+
github.com/vibridi/graphify v0.0.0-20240926092405-5791dfe773cb // indirect
1016
gopkg.in/yaml.v3 v3.0.1 // indirect
1117
)

go.sum

+31
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,41 @@
1+
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
2+
github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY=
3+
github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk=
4+
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw=
5+
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
16
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
27
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
8+
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
39
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
410
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
511
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
612
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
13+
github.com/vibridi/graphify v0.0.0-20240926092405-5791dfe773cb h1:vJaelmTCdGCWuUUmERFznS6843ewGv4MGKv8nOETyt4=
14+
github.com/vibridi/graphify v0.0.0-20240926092405-5791dfe773cb/go.mod h1:ClQsJC5L+MO0eABQoEJBx8EXSxi++0VrUuIYXcYeViY=
15+
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
16+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
17+
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
18+
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
19+
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
20+
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
21+
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
22+
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
23+
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
24+
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
25+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
26+
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
27+
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
28+
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
29+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
30+
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
31+
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
32+
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
33+
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
34+
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
35+
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
36+
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
737
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
838
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
939
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
1040
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
41+
honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=

internal/geom/point.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ type P struct {
1010
X, Y float64
1111
}
1212

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

internal/geom/shortest_test.go

-48
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
package geom
22

33
import (
4-
"fmt"
54
"slices"
6-
"strconv"
7-
"strings"
85
"testing"
96

107
"github.com/stretchr/testify/assert"
@@ -83,20 +80,6 @@ func TestShortest(t *testing.T) {
8380
})
8481
}
8582

86-
func TestShortestEdgeCases(t *testing.T) {
87-
rects := []Rect{
88-
{P{112, 90}, P{200, 140}},
89-
{P{80, 140}, P{150, 300}},
90-
{P{140, 300}, P{270, 380}},
91-
}
92-
start := P{190, 140 - 1}
93-
end := P{200, 300 + 1}
94-
95-
path := Shortest(start, end, rects)
96-
97-
printall(rects, start, end, path)
98-
}
99-
10083
func assertPath(t *testing.T, want, got []P) {
10184
require.Equal(t, len(want), len(got))
10285
for i := 0; i < len(got); i++ {
@@ -106,34 +89,3 @@ func assertPath(t *testing.T, want, got []P) {
10689
assert.Equal(t, want[i], got[i])
10790
}
10891
}
109-
110-
func printpath(path []P) {
111-
for i := 1; i < len(path); i++ {
112-
u, v := path[i-1], path[i]
113-
fmt.Printf(`<path d="M %.2f,%.2f %.2f,%.2f" stroke="black" stroke-width="3" />`+"\n", u.X, u.Y, v.X, v.Y)
114-
}
115-
}
116-
117-
func printall(rects []Rect, start, end P, path []P) {
118-
p := MergeRects(rects)
119-
120-
s := polyline(p.Points, "red")
121-
fmt.Println(s)
122-
123-
fmt.Println(start.String())
124-
fmt.Println(end.String())
125-
printpath(path)
126-
}
127-
128-
func polyline(points []P, color string) string {
129-
b := strings.Builder{}
130-
b.WriteString(`<polyline points="`)
131-
for _, p := range points {
132-
b.WriteString(strconv.FormatFloat(p.X, 'f', 2, 64))
133-
b.WriteRune(',')
134-
b.WriteString(strconv.FormatFloat(p.Y, 'f', 2, 64))
135-
b.WriteRune(' ')
136-
}
137-
b.WriteString(`" fill="none" stroke="` + color + `" />`)
138-
return b.String()
139-
}

internal/graph/node.go

+9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package graph
22

33
import (
4+
"fmt"
45
"sync"
56
)
67

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

26+
func (n *Node) SVG() string {
27+
return fmt.Sprintf(
28+
`<rect id="%s" x="%f" y="%f" width="%f" height="%f" style="fill: none; stroke: blue;" />`,
29+
"autog-node-"+n.ID,
30+
n.X, n.Y, n.W, n.H,
31+
)
32+
}
33+
2534
// Indeg returns the number of incoming edges
2635
func (n *Node) Indeg() int {
2736
return len(n.In)

internal/phase5/alg.go

+3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ func (alg Alg) String() (s string) {
1616
s = "piecewise"
1717
case Ortho:
1818
s = "ortho"
19+
case Splines:
20+
s = "splines"
1921
default:
2022
s = "<invalid>"
2123
}
@@ -27,5 +29,6 @@ const (
2729
Straight
2830
Polyline
2931
Ortho
32+
Splines
3033
_endAlg
3134
)

internal/phase5/alg_process.go

+2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ func (alg Alg) Process(g *graph.DGraph, params graph.Params) {
3030
execPolylineRouting(g, routableEdges)
3131
case Ortho:
3232
execOrthoRouting(g, routableEdges, params)
33+
case Splines:
34+
execSplines(g, routableEdges)
3335
default:
3436
panic("routing: unknown alg value")
3537
}

internal/phase5/alg_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import (
77
)
88

99
func TestAlg(t *testing.T) {
10-
assert.EqualValues(t, 4, _endAlg)
10+
assert.EqualValues(t, 5, _endAlg)
1111

12-
strs := []string{"noop", "straight", "piecewise", "ortho"}
12+
strs := []string{"noop", "straight", "piecewise", "ortho", "splines"}
1313

1414
for i := Alg(0); i < _endAlg; i++ {
1515
assert.Equal(t, 5, i.Phase())

internal/phase5/splines.go

+99-8
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,115 @@
11
package phase5
22

33
import (
4+
"slices"
5+
46
"github.com/nulab/autog/internal/geom"
57
"github.com/nulab/autog/internal/graph"
8+
imonitor "github.com/nulab/autog/internal/monitor"
69
)
710

8-
// todo: work in progress
9-
func execSplines(g *graph.DGraph, params graph.Params) {
10-
for _, e := range g.Edges {
11-
rects := []geom.Rect{
12-
// todo: build rects
13-
}
11+
func execSplines(g *graph.DGraph, routes []routableEdge) {
12+
for _, e := range routes {
13+
imonitor.Log("spline", e)
1414

15-
poly := geom.MergeRects(rects)
15+
rects := buildRects(g, e)
16+
17+
for _, r := range rects {
18+
imonitor.Log("rect", r)
19+
}
1620

1721
start := geom.P{e.From.X + e.From.W/2, e.From.Y + e.From.H}
1822
end := geom.P{e.To.X + e.To.W/2, e.To.Y}
1923

24+
imonitor.Log("shortest-start", start)
25+
imonitor.Log("shortest-end", end)
26+
for _, n := range e.ns {
27+
imonitor.Log("route-node", n)
28+
}
29+
2030
path := geom.Shortest(start, end, rects)
31+
32+
poly := geom.MergeRects(rects)
2133
ctrls := geom.FitSpline(path, geom.P{}, geom.P{}, poly.Sides())
22-
_ = ctrls
34+
slices.Reverse(ctrls)
35+
36+
e.Points = make([][2]float64, 0, len(ctrls)*4)
37+
for _, c := range ctrls {
38+
s := c.Float64Slice()
39+
e.Points = append(e.Points, [][2]float64{s[3], s[2], s[1], s[0]}...)
40+
}
41+
}
42+
}
43+
44+
func buildRects(g *graph.DGraph, r routableEdge) (rects []geom.Rect) {
45+
for i := 1; i < len(r.ns); i++ {
46+
top, btm := r.ns[i-1], r.ns[i]
47+
switch {
48+
49+
case !top.IsVirtual && !btm.IsVirtual:
50+
// add one rectangle that spans from the leftmost point to the rightmost point of the two nodes
51+
r := geom.Rect{
52+
TL: geom.P{min(top.X, btm.X), top.Y + top.H},
53+
BR: geom.P{max(top.X+top.W, btm.X+btm.W), btm.Y},
54+
}
55+
rects = append(rects, r)
56+
57+
case btm.IsVirtual:
58+
// add one rectangle that spans the entire space between the top and bottom layers
59+
// and one that spans the space around the virtual node
60+
tl := g.Layers[top.Layer]
61+
bl := g.Layers[btm.Layer]
62+
rects = append(rects, rectBetweenLayers(tl, bl))
63+
rects = append(rects, rectVirtualNode(btm, bl))
64+
65+
case top.IsVirtual:
66+
tl := g.Layers[top.Layer]
67+
bl := g.Layers[btm.Layer]
68+
rects = append(rects, rectBetweenLayers(tl, bl))
69+
}
70+
}
71+
72+
return
73+
}
74+
75+
func rectBetweenLayers(l1, l2 *graph.Layer) geom.Rect {
76+
h := l1.Head()
77+
t := l2.Tail()
78+
return geom.Rect{
79+
TL: geom.P{h.X, h.Y + h.H},
80+
BR: geom.P{t.X + t.W, t.Y},
81+
}
82+
}
83+
84+
func rectVirtualNode(vn *graph.Node, vl *graph.Layer) geom.Rect {
85+
switch p := vn.LayerPos; {
86+
case p == 0:
87+
// this p+1 access is safe: a layer cannot contain only one virtual node
88+
n := vl.Nodes[p+1]
89+
return geom.Rect{
90+
TL: geom.P{vn.X - 10, n.Y},
91+
BR: geom.P{n.X, n.Y + n.H},
92+
}
93+
94+
case p == vl.Len()-1:
95+
// this p-1 access is safe: a layer cannot contain only one virtual node
96+
n := vl.Nodes[p-1]
97+
return geom.Rect{
98+
TL: geom.P{n.X + n.W, n.Y},
99+
BR: geom.P{vn.X + 10, n.Y + n.H},
100+
}
101+
102+
default:
103+
n1 := vl.Nodes[p-1]
104+
n2 := vl.Nodes[p+1]
105+
return rectBetweenNodes(n1, n2)
106+
}
107+
}
108+
109+
func rectBetweenNodes(n1, n2 *graph.Node) geom.Rect {
110+
d := n2.X - (n1.X + n1.W)
111+
return geom.Rect{
112+
TL: geom.P{n1.X + n1.W + d/3, n1.Y},
113+
BR: geom.P{n2.X - d/3, n2.Y + n2.H},
23114
}
24115
}

0 commit comments

Comments
 (0)