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

Near keys for container #1071

Merged
merged 45 commits into from
Apr 7, 2023
Merged
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
115f46f
feat: descendants now is allowed for container with near attribute
ShupingHe Mar 21, 2023
f62304d
chore: add e2e testcase
ShupingHe Mar 21, 2023
4df8864
chore: regenerate testcases
ShupingHe Mar 21, 2023
5820ba5
chore: regenerate testcases
ShupingHe Mar 21, 2023
597385e
fix: delete redundant testcases
ShupingHe Mar 21, 2023
1af6896
fix: revert near_bad_connected compile_test
ShupingHe Mar 22, 2023
3d32611
fix: cr delete extra blank line
ShupingHe Mar 22, 2023
72253e8
fix: compile_test testcase
ShupingHe Mar 22, 2023
22cce86
fix: cr, add validation for near connectioins
ShupingHe Mar 22, 2023
b9a94ca
fix: cr, more testcase
ShupingHe Mar 22, 2023
69532df
fix: cr, integrate logic of construct sub graph inside WithoutConstan…
ShupingHe Mar 22, 2023
17c4851
fix: calc labelDimension
ShupingHe Mar 23, 2023
df19311
fix: ignore objects inside near container when calc boundingBox
ShupingHe Mar 24, 2023
2ae1636
fix: regenerate testcases
ShupingHe Mar 24, 2023
7deb725
chore: delete redundant code
ShupingHe Mar 24, 2023
6844413
chore: new testcase
ShupingHe Mar 24, 2023
e09a85e
fix: recover testcases
ShupingHe Mar 24, 2023
4e4959f
fix: cr, delete redundant return value
ShupingHe Mar 24, 2023
ebab91b
fix: cr, delete redundant variable
ShupingHe Mar 24, 2023
7782231
fix: cr, use method instead of attribute
ShupingHe Mar 24, 2023
0df9abc
fix: delete redundant file
ShupingHe Mar 25, 2023
a21f5ec
fix: cr, calc labelPosition
ShupingHe Mar 27, 2023
d33efa2
chore: new testcase
ShupingHe Mar 27, 2023
54250d0
fix: nil labelPosition
ShupingHe Mar 27, 2023
4760888
chore: regenerate testcases
ShupingHe Mar 27, 2023
8e64ada
chore: regenerate testcase
ShupingHe Mar 27, 2023
66f620f
fix: typo
ShupingHe Mar 27, 2023
3d45f6a
fix: cr, return value
ShupingHe Mar 28, 2023
d85bb2d
fix: cr, redundant code
ShupingHe Mar 28, 2023
b9dd247
fix: cr, validateNear outside connection
ShupingHe Mar 28, 2023
6ee58d7
fix: cr, error info
ShupingHe Mar 30, 2023
ad515e6
chore: regenerate testcases
ShupingHe Mar 30, 2023
a3268f2
fix: cr, a clean spilit for temp graph
ShupingHe Mar 30, 2023
9089253
Merge branch 'master' into near-keys-for-container
ShupingHe Mar 31, 2023
ffa39a0
chore: regenerate testcases
ShupingHe Mar 31, 2023
cea4355
fix: cr, validation for near obj connect to outside
ShupingHe Mar 31, 2023
063d438
fix: find outer near contaienr
ShupingHe Mar 31, 2023
05a9498
chore: feature description doc
ShupingHe Apr 3, 2023
c77590e
fix: cr
ShupingHe Apr 4, 2023
4b677f8
Merge branch 'master' into near-keys-for-container
ShupingHe Apr 4, 2023
a006bf1
chore: regenerate testcases
ShupingHe Apr 4, 2023
7a395ec
fix: cr, OutNearContianer
ShupingHe Apr 5, 2023
e61629e
fix: cr
ShupingHe Apr 5, 2023
59ef4ad
fix: cr, attach objects of tempGraph
ShupingHe Apr 7, 2023
b2f905e
Merge branch 'master' into near-keys-for-container
ShupingHe Apr 7, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)

#### Improvements 🧹
Expand Down
34 changes: 19 additions & 15 deletions d2compiler/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -734,31 +734,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) validateBoardLinks(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 @@ -968,6 +968,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
164 changes: 134 additions & 30 deletions d2layouts/d2near/layout.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,79 +10,139 @@ 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
}
}

g.Edges = append(g.Edges, tempGraph.Edges...)
}
}
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)
obj.Parent.Children[obj.ID] = obj
obj.Parent.ChildrenArray = append(obj.Parent.ChildrenArray, obj)
attachChildren(g, obj)
}
}
}

// 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
}

func attachChildren(g *d2graph.Graph, obj *d2graph.Object) {
for _, child := range obj.ChildrenArray {
g.Objects = append(g.Objects, child)
attachChildren(g, child)
}
}

// place returns the position of obj, taking into consideration its near value and the diagram
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 +154,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 +176,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 +235,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 @@ -68,14 +68,21 @@ 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
}
}

err = d2sequence.Layout(ctx, g, coreLayout)
if err != nil {
return nil, err
}

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