diff --git a/d2graph/d2graph.go b/d2graph/d2graph.go index 57f135d57f..648188c0c8 100644 --- a/d2graph/d2graph.go +++ b/d2graph/d2graph.go @@ -52,6 +52,8 @@ type Graph struct { Steps []*Graph `json:"steps,omitempty"` Theme *d2themes.Theme `json:"theme,omitempty"` + + err LayoutError } func NewGraph() *Graph { @@ -1545,9 +1547,64 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler edge.LabelDimensions = *dims } + + g.validateTopLeft() + if len(g.err.Errors) > 0 { + return g.err + } + return nil } +func (g *Graph) validateTopLeft() { + g.Root.validateTopLeft() + g.Root.IterDescendants(func(_, obj *Object) { + if !obj.IsContainer() { + return + } + obj.validateTopLeft() + }) +} + +func (obj *Object) validateTopLeft() { + var fixedChildren []*Object + for _, child := range obj.ChildrenArray { + if child.Top == nil || child.Left == nil { + continue + } + fixedChildren = append(fixedChildren, child) + } + if len(fixedChildren) < 2 { + return + } + + for i, childI := range fixedChildren { + iTop, _ := strconv.Atoi(childI.Top.Value) + iLeft, _ := strconv.Atoi(childI.Left.Value) + iRight := iLeft + int(math.Ceil(childI.Width)) + iBottom := iTop + int(math.Ceil(childI.Height)) + + for j := i + 1; j < len(fixedChildren); j++ { + childJ := fixedChildren[j] + jTop, _ := strconv.Atoi(childJ.Top.Value) + jLeft, _ := strconv.Atoi(childJ.Left.Value) + jRight := jLeft + int(math.Ceil(childJ.Width)) + jBottom := jTop + int(math.Ceil(childJ.Height)) + + overlapsHorizontally := (iLeft <= jLeft && jLeft <= iRight) || + (iLeft <= jRight && jRight <= iRight) + overlapsVertically := (iTop <= jTop && jTop <= iBottom) || + (iTop <= jBottom && jBottom <= iBottom) + + if overlapsHorizontally && overlapsVertically { + obj.Graph.errorf(childI.Top.MapKey, "invalid top/left overlapping with %#v", childJ.ID) + obj.Graph.errorf(childJ.Top.MapKey, "invalid top/left overlapping with %#v", childI.ID) + continue + } + } + } +} + func (g *Graph) Texts() []*d2target.MText { var texts []*d2target.MText diff --git a/d2graph/layout.go b/d2graph/layout.go index 43342daa11..3dea6d8f49 100644 --- a/d2graph/layout.go +++ b/d2graph/layout.go @@ -3,6 +3,8 @@ package d2graph import ( "strings" + "oss.terrastruct.com/d2/d2ast" + "oss.terrastruct.com/d2/d2parser" "oss.terrastruct.com/d2/d2target" "oss.terrastruct.com/d2/lib/geo" "oss.terrastruct.com/d2/lib/label" @@ -189,3 +191,26 @@ func (obj *Object) GetLabelTopLeft() *geo.Point { ) return labelTL } + +type LayoutError struct { + Errors []d2ast.Error `json:"errs"` +} + +func (le LayoutError) Empty() bool { + return len(le.Errors) == 0 +} + +func (le LayoutError) Error() string { + var sb strings.Builder + for i, err := range le.Errors { + if i > 0 { + sb.WriteByte('\n') + } + sb.WriteString(err.Error()) + } + return sb.String() +} + +func (g *Graph) errorf(n d2ast.Node, f string, v ...interface{}) { + g.err.Errors = append(g.err.Errors, d2parser.Errorf(n, f, v...).(d2ast.Error)) +} diff --git a/d2lib/d2lib_test.go b/d2lib/d2lib_test.go new file mode 100644 index 0000000000..252a878b92 --- /dev/null +++ b/d2lib/d2lib_test.go @@ -0,0 +1,89 @@ +package d2lib_test + +import ( + "context" + "testing" + + "oss.terrastruct.com/d2/d2graph" + "oss.terrastruct.com/d2/d2layouts/d2dagrelayout" + "oss.terrastruct.com/d2/d2lib" + "oss.terrastruct.com/d2/lib/textmeasure" + "oss.terrastruct.com/util-go/assert" +) + +func TestValidateTopLeft(t *testing.T) { + assertCompile(t, ` +container: { + a: { + top: 100 + left: 100 + } + b: { + top: 100 + left: 100 + } +} +`, + `4:3: invalid top/left overlapping with "b" +8:3: invalid top/left overlapping with "a"`, + ) + + assertCompile(t, ` +container: { + a: { + top: 100 + left: 100 + } + b: { + top: 100 + left: 200 + } +} +`, + ``, + ) + + assertCompile(t, ` +a: { + top: 100 + left: 100 +} +b: { + top: 101 + left: 101 +} +`, + `3:2: invalid top/left overlapping with "b" +7:2: invalid top/left overlapping with "a"`, + ) + + assertCompile(t, ` +a: { + top: 100 + left: 100 +} +b: { + top: 99 + left: 99 +} +`, + `3:2: invalid top/left overlapping with "b" +7:2: invalid top/left overlapping with "a"`, + ) +} + +func assertCompile(t *testing.T, text string, expErr string) { + ruler, _ := textmeasure.NewRuler() + defaultLayout := func(ctx context.Context, g *d2graph.Graph) error { + return d2dagrelayout.Layout(ctx, g, nil) + } + _, _, err := d2lib.Compile(context.Background(), text, &d2lib.CompileOptions{ + Layout: defaultLayout, + Ruler: ruler, + }) + if expErr != "" { + assert.ErrorString(t, err, expErr) + } else { + assert.Success(t, err) + } +}