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/go.mod b/go.mod index 2bd9411..d525b79 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,17 @@ module github.com/nulab/autog -go 1.22 +go 1.22.4 + +toolchain go1.22.7 require github.com/stretchr/testify v1.8.4 +replace github.com/vibridi/graphify => ../../vibridi/graphify + require ( + github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/vibridi/graphify v0.0.0-20240926092405-5791dfe773cb // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index fa4b6e6..050bcab 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,41 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= +github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= +github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw= +github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/vibridi/graphify v0.0.0-20240926092405-5791dfe773cb h1:vJaelmTCdGCWuUUmERFznS6843ewGv4MGKv8nOETyt4= +github.com/vibridi/graphify v0.0.0-20240926092405-5791dfe773cb/go.mod h1:ClQsJC5L+MO0eABQoEJBx8EXSxi++0VrUuIYXcYeViY= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= 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/alg_test.go b/internal/phase5/alg_test.go index 1fe1e39..f7fb9c1 100644 --- a/internal/phase5/alg_test.go +++ b/internal/phase5/alg_test.go @@ -7,9 +7,9 @@ import ( ) func TestAlg(t *testing.T) { - assert.EqualValues(t, 4, _endAlg) + assert.EqualValues(t, 5, _endAlg) - strs := []string{"noop", "straight", "piecewise", "ortho"} + strs := []string{"noop", "straight", "piecewise", "ortho", "splines"} for i := Alg(0); i < _endAlg; i++ { assert.Equal(t, 5, i.Phase()) 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}, } }