Skip to content

Commit

Permalink
Merge pull request #1071 from donglixiaoche/near-keys-for-container
Browse files Browse the repository at this point in the history
Near keys for container
  • Loading branch information
alixander authored Apr 7, 2023
2 parents b3511d1 + b2f905e commit 2d7cdc5
Show file tree
Hide file tree
Showing 32 changed files with 6,036 additions and 779 deletions.
1 change: 1 addition & 0 deletions ci/release/changelogs/next.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#### Features 🚀

- Container with constant key near attribute now can have descendant objects and connections [#1071](https://github.com/terrastruct/d2/pull/1071)
- Multi-board SVG outputs with internal links go to their output paths [#1116](https://github.com/terrastruct/d2/pull/1116)
- New grid layout to place nodes in rows and columns [#1122](https://github.com/terrastruct/d2/pull/1122)

Expand Down
34 changes: 19 additions & 15 deletions d2compiler/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -768,31 +768,35 @@ func (c *compiler) validateNear(g *d2graph.Graph) {
}
}
} else if isConst {
is := false
for _, e := range g.Edges {
if e.Src == obj || e.Dst == obj {
is = true
break
}
}
if is {
c.errorf(obj.Attributes.NearKey, "constant near keys cannot be set on connected shapes")
continue
}
if obj.Parent != g.Root {
c.errorf(obj.Attributes.NearKey, "constant near keys can only be set on root level shapes")
continue
}
if len(obj.ChildrenArray) > 0 {
c.errorf(obj.Attributes.NearKey, "constant near keys cannot be set on shapes with children")
continue
}
} else {
c.errorf(obj.Attributes.NearKey, "near key %#v must be the absolute path to a shape or one of the following constants: %s", d2format.Format(obj.Attributes.NearKey), strings.Join(d2graph.NearConstantsArray, ", "))
continue
}
}
}

for _, edge := range g.Edges {
srcNearContainer := edge.Src.OuterNearContainer()
dstNearContainer := edge.Dst.OuterNearContainer()

var isSrcNearConst, isDstNearConst bool

if srcNearContainer != nil {
_, isSrcNearConst = d2graph.NearConstants[d2graph.Key(srcNearContainer.Attributes.NearKey)[0]]
}
if dstNearContainer != nil {
_, isDstNearConst = d2graph.NearConstants[d2graph.Key(dstNearContainer.Attributes.NearKey)[0]]
}

if (isSrcNearConst || isDstNearConst) && srcNearContainer != dstNearContainer {
c.errorf(edge.References[0].Edge, "cannot connect objects from within a container, that has near constant set, to objects outside that container")
}
}

}

func (c *compiler) validateEdges(g *d2graph.Graph) {
Expand Down
32 changes: 17 additions & 15 deletions d2compiler/compile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1559,24 +1559,26 @@ d2/testdata/d2compiler/TestCompile/near-invalid.d2:14:9: near keys cannot be set
expErr: `d2/testdata/d2compiler/TestCompile/near_bad_constant.d2:1:9: near key "txop-center" must be the absolute path to a shape or one of the following constants: top-left, top-center, top-right, center-left, center-right, bottom-left, bottom-center, bottom-right`,
},
{
name: "near_bad_container",
name: "near_bad_connected",

text: `x: {
near: top-center
y
}
`,
expErr: `d2/testdata/d2compiler/TestCompile/near_bad_container.d2:2:9: constant near keys cannot be set on shapes with children`,
text: `
x: {
near: top-center
}
x -> y
`,
expErr: `d2/testdata/d2compiler/TestCompile/near_bad_connected.d2:5:5: cannot connect objects from within a container, that has near constant set, to objects outside that container`,
},
{
name: "near_bad_connected",

text: `x: {
near: top-center
}
x -> y
`,
expErr: `d2/testdata/d2compiler/TestCompile/near_bad_connected.d2:2:9: constant near keys cannot be set on connected shapes`,
name: "near_descendant_connect_to_outside",
text: `
x: {
near: top-left
y
}
x.y -> z
`,
expErr: "d2/testdata/d2compiler/TestCompile/near_descendant_connect_to_outside.d2:6:5: cannot connect objects from within a container, that has near constant set, to objects outside that container",
},
{
name: "nested_near_constant",
Expand Down
10 changes: 10 additions & 0 deletions d2graph/d2graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -974,6 +974,16 @@ func (obj *Object) GetDefaultSize(mtexts []*d2target.MText, ruler *textmeasure.R
return &dims, nil
}

func (obj *Object) OuterNearContainer() *Object {
for obj != nil {
if obj.Attributes.NearKey != nil {
return obj
}
obj = obj.Parent
}
return nil
}

type Edge struct {
Index int `json:"index"`

Expand Down
157 changes: 126 additions & 31 deletions d2layouts/d2near/layout.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,47 +10,62 @@ import (
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/label"
"oss.terrastruct.com/util-go/go2"
)

const pad = 20

// Layout finds the shapes which are assigned constant near keywords and places them.
func Layout(ctx context.Context, g *d2graph.Graph, constantNears []*d2graph.Object) error {
if len(constantNears) == 0 {
func Layout(ctx context.Context, g *d2graph.Graph, constantNearGraphs []*d2graph.Graph) error {
if len(constantNearGraphs) == 0 {
return nil
}

for _, tempGraph := range constantNearGraphs {
tempGraph.Root.ChildrenArray[0].Parent = g.Root
for _, obj := range tempGraph.Objects {
obj.Graph = g
}
}

// Imagine the graph has two long texts, one at top center and one at top left.
// Top left should go left enough to not collide with center.
// So place the center ones first, then the later ones will consider them for bounding box
for _, processCenters := range []bool{true, false} {
for _, obj := range constantNears {
for _, tempGraph := range constantNearGraphs {
obj := tempGraph.Root.ChildrenArray[0]
if processCenters == strings.Contains(d2graph.Key(obj.Attributes.NearKey)[0], "-center") {
prevX, prevY := obj.TopLeft.X, obj.TopLeft.Y
obj.TopLeft = geo.NewPoint(place(obj))
dx, dy := obj.TopLeft.X-prevX, obj.TopLeft.Y-prevY

for _, subObject := range tempGraph.Objects {
// `obj` already been replaced above by `place(obj)`
if subObject == obj {
continue
}
subObject.TopLeft.X += dx
subObject.TopLeft.Y += dy
}
for _, subEdge := range tempGraph.Edges {
for _, point := range subEdge.Route {
point.X += dx
point.Y += dy
}
}
}
}
for _, obj := range constantNears {
for _, tempGraph := range constantNearGraphs {
obj := tempGraph.Root.ChildrenArray[0]
if processCenters == strings.Contains(d2graph.Key(obj.Attributes.NearKey)[0], "-center") {
// The z-index for constant nears does not matter, as it will not collide
g.Objects = append(g.Objects, obj)
g.Objects = append(g.Objects, tempGraph.Objects...)
obj.Parent.Children[strings.ToLower(obj.ID)] = obj
obj.Parent.ChildrenArray = append(obj.Parent.ChildrenArray, obj)
g.Edges = append(g.Edges, tempGraph.Edges...)
}
}
}

// These shapes skipped core layout, which means they also skipped label placements
for _, obj := range constantNears {
if obj.HasOutsideBottomLabel() {
obj.LabelPosition = go2.Pointer(string(label.OutsideBottomCenter))
} else if obj.Attributes.Icon != nil {
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
} else {
obj.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
}
}

return nil
}

Expand All @@ -59,30 +74,66 @@ func place(obj *d2graph.Object) (float64, float64) {
tl, br := boundingBox(obj.Graph)
w := br.X - tl.X
h := br.Y - tl.Y
switch d2graph.Key(obj.Attributes.NearKey)[0] {

nearKeyStr := d2graph.Key(obj.Attributes.NearKey)[0]
var x, y float64
switch nearKeyStr {
case "top-left":
return tl.X - obj.Width - pad, tl.Y - obj.Height - pad
x, y = tl.X-obj.Width-pad, tl.Y-obj.Height-pad
break
case "top-center":
return tl.X + w/2 - obj.Width/2, tl.Y - obj.Height - pad
x, y = tl.X+w/2-obj.Width/2, tl.Y-obj.Height-pad
break
case "top-right":
return br.X + pad, tl.Y - obj.Height - pad
x, y = br.X+pad, tl.Y-obj.Height-pad
break
case "center-left":
return tl.X - obj.Width - pad, tl.Y + h/2 - obj.Height/2
x, y = tl.X-obj.Width-pad, tl.Y+h/2-obj.Height/2
break
case "center-right":
return br.X + pad, tl.Y + h/2 - obj.Height/2
x, y = br.X+pad, tl.Y+h/2-obj.Height/2
break
case "bottom-left":
return tl.X - obj.Width - pad, br.Y + pad
x, y = tl.X-obj.Width-pad, br.Y+pad
break
case "bottom-center":
return br.X - w/2 - obj.Width/2, br.Y + pad
x, y = br.X-w/2-obj.Width/2, br.Y+pad
break
case "bottom-right":
return br.X + pad, br.Y + pad
x, y = br.X+pad, br.Y+pad
break
}

if obj.LabelPosition != nil && !strings.Contains(*obj.LabelPosition, "INSIDE") {
if strings.Contains(*obj.LabelPosition, "_TOP_") {
// label is on the top, and container is placed on the bottom
if strings.Contains(nearKeyStr, "bottom") {
y += float64(*obj.LabelHeight)
}
} else if strings.Contains(*obj.LabelPosition, "_LEFT_") {
// label is on the left, and container is placed on the right
if strings.Contains(nearKeyStr, "right") {
x += float64(*obj.LabelWidth)
}
} else if strings.Contains(*obj.LabelPosition, "_RIGHT_") {
// label is on the right, and container is placed on the left
if strings.Contains(nearKeyStr, "left") {
x -= float64(*obj.LabelWidth)
}
} else if strings.Contains(*obj.LabelPosition, "_BOTTOM_") {
// label is on the bottom, and container is placed on the top
if strings.Contains(nearKeyStr, "top") {
y -= float64(*obj.LabelHeight)
}
}
}
return 0, 0

return x, y
}

// WithoutConstantNears plucks out the graph objects which have "near" set to a constant value
// This is to be called before layout engines so they don't take part in regular positioning
func WithoutConstantNears(ctx context.Context, g *d2graph.Graph) (nears []*d2graph.Object) {
func WithoutConstantNears(ctx context.Context, g *d2graph.Graph) (constantNearGraphs []*d2graph.Graph) {
for i := 0; i < len(g.Objects); i++ {
obj := g.Objects[i]
if obj.Attributes.NearKey == nil {
Expand All @@ -94,8 +145,20 @@ func WithoutConstantNears(ctx context.Context, g *d2graph.Graph) (nears []*d2gra
}
_, isConst := d2graph.NearConstants[d2graph.Key(obj.Attributes.NearKey)[0]]
if isConst {
nears = append(nears, obj)
g.Objects = append(g.Objects[:i], g.Objects[i+1:]...)
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

constantNearGraphs = append(constantNearGraphs, tempGraph)

i--
delete(obj.Parent.Children, strings.ToLower(obj.ID))
for i := 0; i < len(obj.Parent.ChildrenArray); i++ {
Expand All @@ -104,9 +167,38 @@ func WithoutConstantNears(ctx context.Context, g *d2graph.Graph) (nears []*d2gra
break
}
}

obj.Parent = tempGraph.Root
}
}
return nears
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
Expand Down Expand Up @@ -134,6 +226,9 @@ func boundingBox(g *d2graph.Graph) (tl, br *geo.Point) {
y2 = math.Max(y2, obj.TopLeft.Y+obj.Height)
}
} else {
if obj.OuterNearContainer() != nil {
continue
}
x1 = math.Min(x1, obj.TopLeft.X)
y1 = math.Min(y1, obj.TopLeft.Y)
x2 = math.Max(x2, obj.TopLeft.X+obj.Width)
Expand Down
11 changes: 9 additions & 2 deletions d2lib/d2.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,14 @@ func compile(ctx context.Context, g *d2graph.Graph, opts *CompileOptions) (*d2ta
return nil, err
}

constantNears := d2near.WithoutConstantNears(ctx, g)
constantNearGraphs := d2near.WithoutConstantNears(ctx, g)

// run core layout for constantNears
for _, tempGraph := range constantNearGraphs {
if err = coreLayout(ctx, tempGraph); err != nil {
return nil, err
}
}

layoutWithGrids := d2grid.Layout(ctx, g, coreLayout)

Expand All @@ -78,7 +85,7 @@ func compile(ctx context.Context, g *d2graph.Graph, opts *CompileOptions) (*d2ta
return nil, err
}

err = d2near.Layout(ctx, g, constantNears)
err = d2near.Layout(ctx, g, constantNearGraphs)
if err != nil {
return nil, err
}
Expand Down
Loading

0 comments on commit 2d7cdc5

Please sign in to comment.