layout nested grids
This commit is contained in:
parent
939eb500da
commit
aff2c5c68e
11 changed files with 4517 additions and 80 deletions
|
|
@ -841,13 +841,6 @@ func (c *compiler) validateKey(obj *d2graph.Object, f *d2ir.Field) {
|
|||
if !in && arrowheadIn {
|
||||
c.errorf(f.LastPrimaryKey(), fmt.Sprintf(`invalid shape, can only set "%s" for arrowheads`, obj.Shape.Value))
|
||||
}
|
||||
case "grid-rows", "grid-columns", "grid-gap", "vertical-gap", "horizontal-gap":
|
||||
for _, child := range obj.ChildrenArray {
|
||||
if child.IsContainer() {
|
||||
c.errorf(f.LastPrimaryKey(),
|
||||
fmt.Sprintf(`%#v can only be used on containers with one level of nesting right now. (%#v has nested %#v)`, keyword, child.AbsID(), child.ChildrenArray[0].ID))
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package d2graph
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
|
@ -1823,3 +1824,96 @@ func (obj *Object) MoveWithDescendants(dx, dy float64) {
|
|||
child.MoveWithDescendants(dx, dy)
|
||||
}
|
||||
}
|
||||
|
||||
func (obj *Object) MoveWithDescendantsTo(x, y float64) {
|
||||
dx := x - obj.TopLeft.X
|
||||
dy := y - obj.TopLeft.Y
|
||||
obj.MoveWithDescendants(dx, dy)
|
||||
}
|
||||
|
||||
func (parent *Object) removeChild(child *Object) {
|
||||
delete(parent.Children, strings.ToLower(child.ID))
|
||||
for i := 0; i < len(parent.ChildrenArray); i++ {
|
||||
if parent.ChildrenArray[i] == child {
|
||||
parent.ChildrenArray = append(parent.ChildrenArray[:i], parent.ChildrenArray[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove obj and all descendants from graph, as a new Graph
|
||||
func (g *Graph) ExtractAsNestedGraph(obj *Object) *Graph {
|
||||
descendantObjects, edges := pluckObjAndEdges(g, obj)
|
||||
|
||||
tempGraph := NewGraph()
|
||||
tempGraph.Root.ChildrenArray = []*Object{obj}
|
||||
tempGraph.Root.Children[strings.ToLower(obj.ID)] = obj
|
||||
|
||||
for _, descendantObj := range descendantObjects {
|
||||
descendantObj.Graph = tempGraph
|
||||
}
|
||||
tempGraph.Objects = descendantObjects
|
||||
tempGraph.Edges = edges
|
||||
|
||||
obj.Parent.removeChild(obj)
|
||||
obj.Parent = tempGraph.Root
|
||||
|
||||
return tempGraph
|
||||
}
|
||||
|
||||
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 {
|
||||
edges = append(edges, edge)
|
||||
g.Edges = append(g.Edges[:i], g.Edges[i+1:]...)
|
||||
i--
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < len(g.Objects); i++ {
|
||||
temp := g.Objects[i]
|
||||
if temp.AbsID() == obj.AbsID() {
|
||||
descendantsObjects = append(descendantsObjects, obj)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
return descendantsObjects, edges
|
||||
}
|
||||
|
||||
func (g *Graph) InjectNestedGraph(tempGraph *Graph, parent *Object) {
|
||||
obj := tempGraph.Root.ChildrenArray[0]
|
||||
obj.MoveWithDescendantsTo(0, 0)
|
||||
obj.Parent = parent
|
||||
for _, obj := range tempGraph.Objects {
|
||||
obj.Graph = g
|
||||
}
|
||||
g.Objects = append(g.Objects, tempGraph.Objects...)
|
||||
parent.Children[strings.ToLower(obj.ID)] = obj
|
||||
parent.ChildrenArray = append(parent.ChildrenArray, obj)
|
||||
g.Edges = append(g.Edges, tempGraph.Edges...)
|
||||
}
|
||||
|
||||
func (g *Graph) PrintString() string {
|
||||
buf := &bytes.Buffer{}
|
||||
fmt.Fprint(buf, "Objects: [")
|
||||
for _, obj := range g.Objects {
|
||||
fmt.Fprintf(buf, "%#v @(%v)", obj.AbsID(), obj.TopLeft.ToString())
|
||||
}
|
||||
fmt.Fprint(buf, "]")
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (obj *Object) IterDescendants(apply func(parent, child *Object)) {
|
||||
for _, c := range obj.ChildrenArray {
|
||||
apply(obj, c)
|
||||
c.IterDescendants(apply)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"oss.terrastruct.com/d2/d2graph"
|
||||
"oss.terrastruct.com/d2/lib/geo"
|
||||
)
|
||||
|
||||
type gridDiagram struct {
|
||||
|
|
@ -95,6 +96,10 @@ func newGridDiagram(root *d2graph.Object) *gridDiagram {
|
|||
gd.horizontalGap, _ = strconv.Atoi(root.HorizontalGap.Value)
|
||||
}
|
||||
|
||||
for _, o := range gd.objects {
|
||||
o.TopLeft = geo.NewPoint(0, 0)
|
||||
}
|
||||
|
||||
return &gd
|
||||
}
|
||||
|
||||
|
|
@ -107,9 +112,15 @@ func (gd *gridDiagram) shift(dx, dy float64) {
|
|||
func (gd *gridDiagram) cleanup(obj *d2graph.Object, graph *d2graph.Graph) {
|
||||
obj.Children = make(map[string]*d2graph.Object)
|
||||
obj.ChildrenArray = make([]*d2graph.Object, 0)
|
||||
for _, child := range gd.objects {
|
||||
obj.Children[strings.ToLower(child.ID)] = child
|
||||
obj.ChildrenArray = append(obj.ChildrenArray, child)
|
||||
|
||||
restore := func(parent, child *d2graph.Object) {
|
||||
// fmt.Printf("restore %v's %v\n", parent.AbsID(), child.AbsID())
|
||||
parent.Children[strings.ToLower(child.ID)] = child
|
||||
parent.ChildrenArray = append(parent.ChildrenArray, child)
|
||||
graph.Objects = append(graph.Objects, child)
|
||||
}
|
||||
for _, child := range gd.objects {
|
||||
restore(obj, child)
|
||||
child.IterDescendants(restore)
|
||||
}
|
||||
graph.Objects = append(graph.Objects, gd.objects...)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,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)
|
||||
gridDiagrams, objectOrder, err := withoutGridDiagrams(ctx, g, layout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -49,18 +49,42 @@ func Layout(ctx context.Context, g *d2graph.Graph, layout d2graph.LayoutGraph) d
|
|||
}
|
||||
}
|
||||
|
||||
func withoutGridDiagrams(ctx context.Context, g *d2graph.Graph) (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 map[string]int, err error) {
|
||||
toRemove := make(map[*d2graph.Object]struct{})
|
||||
gridDiagrams = make(map[string]*gridDiagram)
|
||||
|
||||
objectOrder = make(map[string]int)
|
||||
for i, obj := range g.Objects {
|
||||
objectOrder[obj.AbsID()] = i
|
||||
}
|
||||
|
||||
var processGrid func(obj *d2graph.Object) error
|
||||
processGrid = func(obj *d2graph.Object) error {
|
||||
// layout any nested grids first
|
||||
for _, child := range obj.ChildrenArray {
|
||||
if child.IsGridDiagram() {
|
||||
if err := processGrid(child); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if len(child.ChildrenArray) > 0 {
|
||||
tempGraph := g.ExtractAsNestedGraph(child)
|
||||
if err := layout(ctx, tempGraph); err != nil {
|
||||
return err
|
||||
}
|
||||
g.InjectNestedGraph(tempGraph, obj)
|
||||
|
||||
sort.SliceStable(g.Objects, func(i, j int) bool {
|
||||
return objectOrder[g.Objects[i].AbsID()] < objectOrder[g.Objects[j].AbsID()]
|
||||
})
|
||||
sort.SliceStable(child.ChildrenArray, func(i, j int) bool {
|
||||
return objectOrder[child.ChildrenArray[i].AbsID()] < objectOrder[child.ChildrenArray[j].AbsID()]
|
||||
})
|
||||
sort.SliceStable(obj.ChildrenArray, func(i, j int) bool {
|
||||
return objectOrder[obj.ChildrenArray[i].AbsID()] < objectOrder[obj.ChildrenArray[j].AbsID()]
|
||||
})
|
||||
|
||||
for _, o := range tempGraph.Objects {
|
||||
toRemove[o] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -132,10 +156,8 @@ func withoutGridDiagrams(ctx context.Context, g *d2graph.Graph) (gridDiagrams ma
|
|||
}
|
||||
}
|
||||
|
||||
objectOrder = make(map[string]int)
|
||||
layoutObjects := make([]*d2graph.Object, 0, len(toRemove))
|
||||
for i, obj := range g.Objects {
|
||||
objectOrder[obj.AbsID()] = i
|
||||
for _, obj := range g.Objects {
|
||||
if _, exists := toRemove[obj]; !exists {
|
||||
layoutObjects = append(layoutObjects, obj)
|
||||
}
|
||||
|
|
@ -223,7 +245,7 @@ func (gd *gridDiagram) layoutEvenly(g *d2graph.Graph, obj *d2graph.Object) {
|
|||
}
|
||||
o.Width = colWidths[j]
|
||||
o.Height = rowHeights[i]
|
||||
o.TopLeft = cursor.Copy()
|
||||
o.MoveWithDescendantsTo(cursor.X, cursor.Y)
|
||||
cursor.X += o.Width + horizontalGap
|
||||
}
|
||||
cursor.X = 0
|
||||
|
|
@ -238,7 +260,7 @@ func (gd *gridDiagram) layoutEvenly(g *d2graph.Graph, obj *d2graph.Object) {
|
|||
}
|
||||
o.Width = colWidths[j]
|
||||
o.Height = rowHeights[i]
|
||||
o.TopLeft = cursor.Copy()
|
||||
o.MoveWithDescendantsTo(cursor.X, cursor.Y)
|
||||
cursor.Y += o.Height + verticalGap
|
||||
}
|
||||
cursor.X += colWidths[j] + horizontalGap
|
||||
|
|
@ -374,7 +396,7 @@ func (gd *gridDiagram) layoutDynamic(g *d2graph.Graph, obj *d2graph.Object) {
|
|||
for _, row := range layout {
|
||||
rowHeight := 0.
|
||||
for _, o := range row {
|
||||
o.TopLeft = cursor.Copy()
|
||||
o.MoveWithDescendantsTo(cursor.X, cursor.Y)
|
||||
cursor.X += o.Width + horizontalGap
|
||||
rowHeight = math.Max(rowHeight, o.Height)
|
||||
}
|
||||
|
|
@ -462,7 +484,7 @@ func (gd *gridDiagram) layoutDynamic(g *d2graph.Graph, obj *d2graph.Object) {
|
|||
for _, column := range layout {
|
||||
colWidth := 0.
|
||||
for _, o := range column {
|
||||
o.TopLeft = cursor.Copy()
|
||||
o.MoveWithDescendantsTo(cursor.X, cursor.Y)
|
||||
cursor.Y += o.Height + verticalGap
|
||||
colWidth = math.Max(colWidth, o.Width)
|
||||
}
|
||||
|
|
@ -834,18 +856,11 @@ func cleanup(graph *d2graph.Graph, gridDiagrams map[string]*gridDiagram, objects
|
|||
})
|
||||
}()
|
||||
|
||||
if graph.Root.IsGridDiagram() {
|
||||
gd, exists := gridDiagrams[graph.Root.AbsID()]
|
||||
if exists {
|
||||
gd.cleanup(graph.Root, graph)
|
||||
// return
|
||||
}
|
||||
}
|
||||
|
||||
for _, obj := range graph.Objects {
|
||||
var restore func(obj *d2graph.Object)
|
||||
restore = func(obj *d2graph.Object) {
|
||||
gd, exists := gridDiagrams[obj.AbsID()]
|
||||
if !exists {
|
||||
continue
|
||||
return
|
||||
}
|
||||
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
|
||||
// shift the grid from (0, 0)
|
||||
|
|
@ -854,5 +869,20 @@ func cleanup(graph *d2graph.Graph, gridDiagrams map[string]*gridDiagram, objects
|
|||
obj.TopLeft.Y+CONTAINER_PADDING,
|
||||
)
|
||||
gd.cleanup(obj, graph)
|
||||
|
||||
for _, child := range obj.ChildrenArray {
|
||||
restore(child)
|
||||
}
|
||||
}
|
||||
|
||||
if graph.Root.IsGridDiagram() {
|
||||
gd, exists := gridDiagrams[graph.Root.AbsID()]
|
||||
if exists {
|
||||
gd.cleanup(graph.Root, graph)
|
||||
}
|
||||
}
|
||||
|
||||
for _, obj := range graph.Objects {
|
||||
restore(obj)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -148,62 +148,14 @@ func WithoutConstantNears(ctx context.Context, g *d2graph.Graph) (constantNearGr
|
|||
}
|
||||
_, isConst := d2graph.NearConstants[d2graph.Key(obj.NearKey)[0]]
|
||||
if isConst {
|
||||
descendantObjects, edges := pluckObjAndEdges(g, obj)
|
||||
|
||||
tempGraph := d2graph.NewGraph()
|
||||
tempGraph.Root.ChildrenArray = []*d2graph.Object{obj}
|
||||
tempGraph.Root.Children[strings.ToLower(obj.ID)] = obj
|
||||
|
||||
for _, descendantObj := range descendantObjects {
|
||||
descendantObj.Graph = tempGraph
|
||||
}
|
||||
tempGraph.Objects = descendantObjects
|
||||
tempGraph.Edges = edges
|
||||
|
||||
tempGraph := g.ExtractAsNestedGraph(obj)
|
||||
constantNearGraphs = append(constantNearGraphs, tempGraph)
|
||||
|
||||
i--
|
||||
delete(obj.Parent.Children, strings.ToLower(obj.ID))
|
||||
for i := 0; i < len(obj.Parent.ChildrenArray); i++ {
|
||||
if obj.Parent.ChildrenArray[i] == obj {
|
||||
obj.Parent.ChildrenArray = append(obj.Parent.ChildrenArray[:i], obj.Parent.ChildrenArray[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
obj.Parent = tempGraph.Root
|
||||
}
|
||||
}
|
||||
return constantNearGraphs
|
||||
}
|
||||
|
||||
func pluckObjAndEdges(g *d2graph.Graph, obj *d2graph.Object) (descendantsObjects []*d2graph.Object, edges []*d2graph.Edge) {
|
||||
for i := 0; i < len(g.Edges); i++ {
|
||||
edge := g.Edges[i]
|
||||
if edge.Src == obj || edge.Dst == obj {
|
||||
edges = append(edges, edge)
|
||||
g.Edges = append(g.Edges[:i], g.Edges[i+1:]...)
|
||||
i--
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < len(g.Objects); i++ {
|
||||
temp := g.Objects[i]
|
||||
if temp.AbsID() == obj.AbsID() {
|
||||
descendantsObjects = append(descendantsObjects, obj)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
return descendantsObjects, edges
|
||||
}
|
||||
|
||||
// boundingBox gets the center of the graph as defined by shapes
|
||||
// The bounds taking into consideration only shapes gives more of a feeling of true center
|
||||
// It differs from d2target.BoundingBox which needs to include every visible thing
|
||||
|
|
|
|||
|
|
@ -2721,6 +2721,7 @@ scenarios: {
|
|||
loadFromFile(t, "ent2d2_basic"),
|
||||
loadFromFile(t, "ent2d2_right"),
|
||||
loadFromFile(t, "grid_large_checkered"),
|
||||
loadFromFile(t, "grid_nested"),
|
||||
}
|
||||
|
||||
runa(t, tcs)
|
||||
|
|
|
|||
96
e2etests/testdata/files/grid_nested.d2
vendored
Normal file
96
e2etests/testdata/files/grid_nested.d2
vendored
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
classes: {
|
||||
2x2: {
|
||||
grid-rows: 2
|
||||
grid-columns: 2
|
||||
}
|
||||
gap0: {
|
||||
vertical-gap: 0
|
||||
horizontal-gap: 0
|
||||
}
|
||||
}
|
||||
|
||||
grid w/ container: {
|
||||
class: 2x2
|
||||
|
||||
a
|
||||
b: {
|
||||
b child
|
||||
}
|
||||
c
|
||||
d
|
||||
}
|
||||
|
||||
grid w/ nested containers: {
|
||||
class: 2x2
|
||||
|
||||
a
|
||||
b: {
|
||||
b 1: {
|
||||
b 2: {
|
||||
b 3: {
|
||||
b 4
|
||||
}
|
||||
}
|
||||
b 2a: {
|
||||
b 3a: {
|
||||
b 3a
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
c
|
||||
d
|
||||
}
|
||||
|
||||
grid in grid: {
|
||||
class: 2x2
|
||||
|
||||
a
|
||||
b: {
|
||||
class: 2x2
|
||||
|
||||
a
|
||||
b
|
||||
c
|
||||
d
|
||||
}
|
||||
c
|
||||
d
|
||||
}
|
||||
|
||||
grid w/ grid w/ grid: {
|
||||
class: [2x2; gap0]
|
||||
|
||||
a
|
||||
b: {
|
||||
class: [2x2; gap0]
|
||||
|
||||
a
|
||||
b
|
||||
c: {
|
||||
class: [2x2; gap0]
|
||||
|
||||
a
|
||||
b
|
||||
c
|
||||
d: {
|
||||
class: [2x2; gap0]
|
||||
|
||||
a: {
|
||||
class: [2x2; gap0]
|
||||
|
||||
a
|
||||
b
|
||||
c
|
||||
d
|
||||
}
|
||||
b
|
||||
c
|
||||
d
|
||||
}
|
||||
}
|
||||
d
|
||||
}
|
||||
c
|
||||
d
|
||||
}
|
||||
2028
e2etests/testdata/stable/grid_nested/dagre/board.exp.json
generated
vendored
Normal file
2028
e2etests/testdata/stable/grid_nested/dagre/board.exp.json
generated
vendored
Normal file
File diff suppressed because it is too large
Load diff
102
e2etests/testdata/stable/grid_nested/dagre/sketch.exp.svg
vendored
Normal file
102
e2etests/testdata/stable/grid_nested/dagre/sketch.exp.svg
vendored
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 28 KiB |
2028
e2etests/testdata/stable/grid_nested/elk/board.exp.json
generated
vendored
Normal file
2028
e2etests/testdata/stable/grid_nested/elk/board.exp.json
generated
vendored
Normal file
File diff suppressed because it is too large
Load diff
102
e2etests/testdata/stable/grid_nested/elk/sketch.exp.svg
vendored
Normal file
102
e2etests/testdata/stable/grid_nested/elk/sketch.exp.svg
vendored
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 28 KiB |
Loading…
Reference in a new issue