Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simple edges in grid #1586

Merged
merged 16 commits into from
Sep 15, 2023
1 change: 1 addition & 0 deletions ci/release/changelogs/next.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#### Features 🚀

- UTF-16 files are automatically detected and supported [#1525](https://github.com/terrastruct/d2/pull/1525)
- Grid diagrams can now have simple edges between cells [#1586](https://github.com/terrastruct/d2/pull/1586)

#### Improvements 🧹

Expand Down
34 changes: 27 additions & 7 deletions d2compiler/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -1072,13 +1072,33 @@ func (c *compiler) validateNear(g *d2graph.Graph) {

func (c *compiler) validateEdges(g *d2graph.Graph) {
for _, edge := range g.Edges {
if gd := edge.Src.Parent.ClosestGridDiagram(); gd != nil {
c.errorf(edge.GetAstEdge(), "edges in grid diagrams are not supported yet")
continue
}
if gd := edge.Dst.Parent.ClosestGridDiagram(); gd != nil {
c.errorf(edge.GetAstEdge(), "edges in grid diagrams are not supported yet")
continue
srcGrid := edge.Src.Parent.ClosestGridDiagram()
dstGrid := edge.Dst.Parent.ClosestGridDiagram()
if srcGrid != nil || dstGrid != nil {
if top := srcGrid.TopGridDiagram(); srcGrid != top {
// valid: grid.child1 -> grid.child2
// invalid: grid.childGrid.child1 -> grid.childGrid.child2
c.errorf(edge.GetAstEdge(), "edge must be on direct child of grid diagram %#v", top.AbsID())
continue
}
if top := dstGrid.TopGridDiagram(); dstGrid != top {
// valid: grid.child1 -> grid.child2
// invalid: grid.childGrid.child1 -> grid.childGrid.child2
c.errorf(edge.GetAstEdge(), "edge must be on direct child of grid diagram %#v", top.AbsID())
continue
}
if srcGrid != dstGrid {
// valid: a -> grid
// invalid: a -> grid.child
c.errorf(edge.GetAstEdge(), "edges into grid diagrams are not supported yet")
continue
}
if srcGrid != edge.Src.Parent || dstGrid != edge.Dst.Parent {
// valid: grid.child1 -> grid.child2
// invalid: grid.child1 -> grid.child2.child1
c.errorf(edge.GetAstEdge(), "grid diagrams can only have edges between children right now")
continue
}
}
}
}
Expand Down
26 changes: 22 additions & 4 deletions d2compiler/compile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2476,16 +2476,34 @@ d2/testdata/d2compiler/TestCompile/grid_gap_negative.d2:3:16: vertical-gap must
name: "grid_edge",
text: `hey: {
grid-rows: 1
a -> b
a -> b: ok
}
c -> hey.b
hey.a -> c
hey -> hey.a

hey -> c: ok
`,
expErr: `d2/testdata/d2compiler/TestCompile/grid_edge.d2:3:2: edges in grid diagrams are not supported yet
d2/testdata/d2compiler/TestCompile/grid_edge.d2:5:2: edges in grid diagrams are not supported yet
d2/testdata/d2compiler/TestCompile/grid_edge.d2:6:2: edges in grid diagrams are not supported yet`,
expErr: `d2/testdata/d2compiler/TestCompile/grid_edge.d2:5:2: edges into grid diagrams are not supported yet
d2/testdata/d2compiler/TestCompile/grid_edge.d2:6:2: edges into grid diagrams are not supported yet
d2/testdata/d2compiler/TestCompile/grid_edge.d2:7:2: edges into grid diagrams are not supported yet`,
},
{
name: "grid_deeper_edge",
text: `hey: {
grid-rows: 1
a -> b: ok
b: {
c -> d: not yet
}
a: {
grid-columns: 1
e -> f: also not yet
}
}
`,
expErr: `d2/testdata/d2compiler/TestCompile/grid_deeper_edge.d2:9:3: edge must be on direct child of grid diagram "hey"
d2/testdata/d2compiler/TestCompile/grid_deeper_edge.d2:5:3: grid diagrams can only have edges between children right now`,
},
{
name: "grid_nested",
Expand Down
7 changes: 7 additions & 0 deletions d2graph/d2graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -1169,6 +1169,13 @@ func (e *Edge) Text() *d2target.MText {
}
}

func (e *Edge) Move(dx, dy float64) {
for _, p := range e.Route {
p.X += dx
p.Y += dy
}
}

func (e *Edge) AbsID() string {
srcIDA := e.Src.AbsIDArray()
dstIDA := e.Dst.AbsIDArray()
Expand Down
19 changes: 19 additions & 0 deletions d2graph/grid_diagram.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,22 @@ func (obj *Object) ClosestGridDiagram() *Object {
}
return obj.Parent.ClosestGridDiagram()
}

// TopGridDiagram returns the least nested (outermost) grid diagram
func (obj *Object) TopGridDiagram() *Object {
if obj == nil {
return nil
}
var gd *Object
if obj.IsGridDiagram() {
gd = obj
}
curr := obj.Parent
for curr != nil {
if curr.IsGridDiagram() {
gd = curr
}
curr = curr.Parent
}
return gd
}
20 changes: 10 additions & 10 deletions d2graph/layout.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func (g *Graph) ExtractAsNestedGraph(obj *Object) *Graph {
func pluckObjAndEdges(g *Graph, obj *Object) (descendantsObjects []*Object, edges []*Edge) {
for i := 0; i < len(g.Edges); i++ {
edge := g.Edges[i]
if edge.Src == obj || edge.Dst == obj {
if edge.Src.IsDescendantOf(obj) && edge.Dst.IsDescendantOf(obj) {
edges = append(edges, edge)
g.Edges = append(g.Edges[:i], g.Edges[i+1:]...)
i--
Expand All @@ -69,15 +69,10 @@ func pluckObjAndEdges(g *Graph, obj *Object) (descendantsObjects []*Object, edge

for i := 0; i < len(g.Objects); i++ {
temp := g.Objects[i]
if temp.AbsID() == obj.AbsID() {
descendantsObjects = append(descendantsObjects, obj)
if temp.IsDescendantOf(obj) {
descendantsObjects = append(descendantsObjects, temp)
g.Objects = append(g.Objects[:i], g.Objects[i+1:]...)
for _, child := range obj.ChildrenArray {
subObjects, subEdges := pluckObjAndEdges(g, child)
descendantsObjects = append(descendantsObjects, subObjects...)
edges = append(edges, subEdges...)
}
break
i--
}
}

Expand All @@ -86,7 +81,12 @@ func pluckObjAndEdges(g *Graph, obj *Object) (descendantsObjects []*Object, edge

func (g *Graph) InjectNestedGraph(tempGraph *Graph, parent *Object) {
obj := tempGraph.Root.ChildrenArray[0]
obj.MoveWithDescendantsTo(0, 0)
dx := 0 - obj.TopLeft.X
dy := 0 - obj.TopLeft.Y
obj.MoveWithDescendants(dx, dy)
for _, e := range tempGraph.Edges {
e.Move(dx, dy)
}
obj.Parent = parent
for _, obj := range tempGraph.Objects {
obj.Graph = g
Expand Down
5 changes: 5 additions & 0 deletions d2layouts/d2grid/grid_diagram.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
type gridDiagram struct {
root *d2graph.Object
objects []*d2graph.Object
edges []*d2graph.Edge
rows int
columns int

Expand Down Expand Up @@ -107,6 +108,9 @@ func (gd *gridDiagram) shift(dx, dy float64) {
for _, obj := range gd.objects {
obj.MoveWithDescendants(dx, dy)
}
for _, e := range gd.edges {
e.Move(dx, dy)
}
}

func (gd *gridDiagram) cleanup(obj *d2graph.Object, graph *d2graph.Graph) {
Expand All @@ -122,4 +126,5 @@ func (gd *gridDiagram) cleanup(obj *d2graph.Object, graph *d2graph.Graph) {
restore(obj, child)
child.IterDescendants(restore)
}
graph.Edges = append(graph.Edges, gd.edges...)
}
52 changes: 46 additions & 6 deletions d2layouts/d2grid/layout.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const (
// 7. Put grid children back in correct location
func Layout(ctx context.Context, g *d2graph.Graph, layout d2graph.LayoutGraph) d2graph.LayoutGraph {
return func(ctx context.Context, g *d2graph.Graph) error {
gridDiagrams, objectOrder, err := withoutGridDiagrams(ctx, g, layout)
gridDiagrams, objectOrder, edgeOrder, err := withoutGridDiagrams(ctx, g, layout)
if err != nil {
return err
}
Expand All @@ -42,19 +42,24 @@ func Layout(ctx context.Context, g *d2graph.Graph, layout d2graph.LayoutGraph) d
return err
}

cleanup(g, gridDiagrams, objectOrder)
cleanup(g, gridDiagrams, objectOrder, edgeOrder)
return nil
}
}

func withoutGridDiagrams(ctx context.Context, g *d2graph.Graph, layout d2graph.LayoutGraph) (gridDiagrams map[string]*gridDiagram, objectOrder map[string]int, err error) {
func withoutGridDiagrams(ctx context.Context, g *d2graph.Graph, layout d2graph.LayoutGraph) (gridDiagrams map[string]*gridDiagram, objectOrder, edgeOrder map[string]int, err error) {
toRemove := make(map[*d2graph.Object]struct{})
edgeToRemove := make(map[*d2graph.Edge]struct{})
gridDiagrams = make(map[string]*gridDiagram)

objectOrder = make(map[string]int)
for i, obj := range g.Objects {
objectOrder[obj.AbsID()] = i
}
edgeOrder = make(map[string]int)
for i, edge := range g.Edges {
edgeOrder[edge.AbsID()] = i
}

var processGrid func(obj *d2graph.Object) error
processGrid = func(obj *d2graph.Object) error {
Expand All @@ -79,6 +84,9 @@ func withoutGridDiagrams(ctx context.Context, g *d2graph.Graph, layout d2graph.L
sort.SliceStable(obj.ChildrenArray, func(i, j int) bool {
return objectOrder[obj.ChildrenArray[i].AbsID()] < objectOrder[obj.ChildrenArray[j].AbsID()]
})
sort.SliceStable(g.Edges, func(i, j int) bool {
return edgeOrder[g.Edges[i].AbsID()] < edgeOrder[g.Edges[j].AbsID()]
})

for _, o := range tempGraph.Objects {
toRemove[o] = struct{}{}
Expand Down Expand Up @@ -200,6 +208,28 @@ func withoutGridDiagrams(ctx context.Context, g *d2graph.Graph, layout d2graph.L
for _, o := range gd.objects {
toRemove[o] = struct{}{}
}

// simple straight line edge routing between grid objects
for i, e := range g.Edges {
edgeOrder[e.AbsID()] = i
if !e.Src.Parent.IsDescendantOf(obj) && !e.Dst.Parent.IsDescendantOf(obj) {
continue
}
// if edge is within grid, remove it from outer layout
gd.edges = append(gd.edges, e)
edgeToRemove[e] = struct{}{}

if e.Src.Parent != obj || e.Dst.Parent != obj {
continue
}
// if edge is grid child, use simple routing
e.Route = []*geo.Point{e.Src.Center(), e.Dst.Center()}
e.TraceToShape(e.Route, 0, 1)
if e.Label.Value != "" {
e.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
}
}

return nil
}

Expand All @@ -218,7 +248,7 @@ func withoutGridDiagrams(ctx context.Context, g *d2graph.Graph, layout d2graph.L
}

if err := processGrid(obj); err != nil {
return nil, nil, err
return nil, nil, nil, err
}
}
}
Expand All @@ -230,8 +260,15 @@ func withoutGridDiagrams(ctx context.Context, g *d2graph.Graph, layout d2graph.L
}
}
g.Objects = layoutObjects
layoutEdges := make([]*d2graph.Edge, 0, len(edgeToRemove))
for _, e := range g.Edges {
if _, exists := edgeToRemove[e]; !exists {
layoutEdges = append(layoutEdges, e)
}
}
g.Edges = layoutEdges

return gridDiagrams, objectOrder, nil
return gridDiagrams, objectOrder, edgeOrder, nil
}

func layoutGrid(g *d2graph.Graph, obj *d2graph.Object) (*gridDiagram, error) {
Expand Down Expand Up @@ -940,11 +977,14 @@ func getDistToTarget(layout [][]*d2graph.Object, targetSize float64, horizontalG
// - translating the grid to its position placed by the core layout engine
// - restore the children of the grid
// - sorts objects to their original graph order
func cleanup(graph *d2graph.Graph, gridDiagrams map[string]*gridDiagram, objectsOrder map[string]int) {
func cleanup(graph *d2graph.Graph, gridDiagrams map[string]*gridDiagram, objectsOrder, edgeOrder map[string]int) {
defer func() {
sort.SliceStable(graph.Objects, func(i, j int) bool {
return objectsOrder[graph.Objects[i].AbsID()] < objectsOrder[graph.Objects[j].AbsID()]
})
sort.SliceStable(graph.Edges, func(i, j int) bool {
return edgeOrder[graph.Edges[i].AbsID()] < edgeOrder[graph.Edges[j].AbsID()]
})
}()

var restore func(obj *d2graph.Object)
Expand Down
2 changes: 2 additions & 0 deletions e2etests/stable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2833,6 +2833,8 @@ y: profits {
loadFromFile(t, "overlapping_child_label"),
loadFromFile(t, "dagre_spacing"),
loadFromFile(t, "dagre_spacing_right"),
loadFromFile(t, "simple_grid_edges"),
loadFromFile(t, "grid_nested_simple_edges"),
}

runa(t, tcs)
Expand Down
43 changes: 43 additions & 0 deletions e2etests/testdata/files/grid_nested_simple_edges.d2
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
direction: right
outer-grid -> outer-container

outer-grid: {
grid-columns: 1

inner-grid -> container -> etc

container: {
label.near: top-left
# edges not yet supported here since they must be direct grid children
a
b
c
}

inner-grid: {
grid-rows: 1
1
2
3
# edges here are not supported yet since this is inside another grid
}
}

outer-container: {
grid -> container

grid: {
grid-rows: 1
# direct child edges ok in least nested grid
1 -> 2 -> 3
}

container: {
# non grid edges ok
4 -> 5 -> 6
nested container: {
# nested non grid edges ok
7 -> 8
}
}
}
Loading