From d1f7e23affff98703c68f98f902a0bbe9ac558c2 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Thu, 8 Jun 2023 14:25:13 -0700 Subject: [PATCH] done --- d2layouts/d2dagrelayout/layout.go | 98 +++- e2etests/stable_test.go | 17 + .../dagre-container/dagre/board.exp.json | 424 ++++++++++++++++++ .../dagre-container/dagre/sketch.exp.svg | 102 +++++ .../stable/dagre-container/elk/board.exp.json | 415 +++++++++++++++++ .../stable/dagre-container/elk/sketch.exp.svg | 102 +++++ 6 files changed, 1137 insertions(+), 21 deletions(-) create mode 100644 e2etests/testdata/stable/dagre-container/dagre/board.exp.json create mode 100644 e2etests/testdata/stable/dagre-container/dagre/sketch.exp.svg create mode 100644 e2etests/testdata/stable/dagre-container/elk/board.exp.json create mode 100644 e2etests/testdata/stable/dagre-container/elk/sketch.exp.svg diff --git a/d2layouts/d2dagrelayout/layout.go b/d2layouts/d2dagrelayout/layout.go index 6ee2c57ad..da29d818a 100644 --- a/d2layouts/d2dagrelayout/layout.go +++ b/d2layouts/d2dagrelayout/layout.go @@ -544,22 +544,7 @@ func getEdgeEndpoints(g *d2graph.Graph, edge *d2graph.Edge) (*d2graph.Object, *d } dst := edge.Dst for len(dst.Children) > 0 && dst.Class == nil && dst.SQLTable == nil { - dst = dst.ChildrenArray[0] - - // We want to get the top node of destinations - for _, child := range dst.ChildrenArray { - isHead := true - for _, e := range g.Edges { - if inContainer(e.Src, child) != nil && inContainer(e.Dst, dst) != nil { - isHead = false - break - } - } - if isHead { - dst = child - break - } - } + dst = getLongestEdgeChainHead(g, dst) } if edge.SrcArrow && !edge.DstArrow { // for `b <- a`, edge.Edge is `a -> b` and we expect this routing result @@ -607,8 +592,73 @@ func generateAddEdgeLine(fromID, toID, edgeID string, width, height int) string return fmt.Sprintf("g.setEdge({v:`%s`, w:`%s`, name:`%s`}, { width:%d, height:%d, labelpos: `c` });\n", escapeID(fromID), escapeID(toID), escapeID(edgeID), width, height) } +// getLongestEdgeChainHead finds the longest chain in a container and gets its head +// If there are multiple chains of the same length, get the head closest to the center +func getLongestEdgeChainHead(g *d2graph.Graph, container *d2graph.Object) *d2graph.Object { + rank := make(map[*d2graph.Object]int) + chainLength := make(map[*d2graph.Object]int) + + for _, obj := range container.ChildrenArray { + isHead := true + for _, e := range g.Edges { + if inContainer(e.Src, container) != nil && inContainer(e.Dst, obj) != nil { + isHead = false + break + } + } + if !isHead { + continue + } + rank[obj] = 1 + chainLength[obj] = 1 + // BFS + queue := []*d2graph.Object{obj} + visited := make(map[*d2graph.Object]struct{}) + for len(queue) > 0 { + curr := queue[0] + queue = queue[1:] + if _, ok := visited[curr]; ok { + continue + } + visited[curr] = struct{}{} + for _, e := range g.Edges { + child := inContainer(e.Dst, container) + if child == curr { + continue + } + if child != nil && inContainer(e.Src, curr) != nil { + if rank[curr]+1 > rank[child] { + rank[child] = rank[curr] + 1 + chainLength[obj] = go2.Max(chainLength[obj], rank[child]) + } + queue = append(queue, child) + } + } + } + } + max := int(math.MinInt32) + for _, obj := range container.ChildrenArray { + if chainLength[obj] > max { + max = chainLength[obj] + } + } + + var heads []*d2graph.Object + for i, obj := range container.ChildrenArray { + if rank[obj] == 1 && chainLength[obj] == max { + heads = append(heads, container.ChildrenArray[i]) + } + } + + if len(heads) > 0 { + return heads[int(math.Floor(float64(len(heads))/2.0))] + } + return container.ChildrenArray[0] +} + // getLongestEdgeChainTail gets the node at the end of the longest edge chain, because that will be the end of the container -// and is what external connections should connect with +// and is what external connections should connect with. +// If there are multiple of same length, get the one closest to the middle func getLongestEdgeChainTail(g *d2graph.Graph, container *d2graph.Object) *d2graph.Object { rank := make(map[*d2graph.Object]int) @@ -647,14 +697,20 @@ func getLongestEdgeChainTail(g *d2graph.Graph, container *d2graph.Object) *d2gra } } max := int(math.MinInt32) - var tail *d2graph.Object for _, obj := range container.ChildrenArray { - if rank[obj] >= max { + if rank[obj] > max { max = rank[obj] - tail = obj } } - return tail + + var tails []*d2graph.Object + for i, obj := range container.ChildrenArray { + if rank[obj] == max { + tails = append(tails, container.ChildrenArray[i]) + } + } + + return tails[int(math.Floor(float64(len(tails))/2.0))] } func inContainer(obj, container *d2graph.Object) *d2graph.Object { diff --git a/e2etests/stable_test.go b/e2etests/stable_test.go index dcdc0f8e7..809e791d5 100644 --- a/e2etests/stable_test.go +++ b/e2etests/stable_test.go @@ -2316,6 +2316,23 @@ Listen <-> Talk: { target-arrowhead.shape: diamond label: hear } +`, + }, + { + name: "dagre-container", + script: `a: { + a + b + c +} + +b: { + a + b + c +} + +a -> b `, }, { diff --git a/e2etests/testdata/stable/dagre-container/dagre/board.exp.json b/e2etests/testdata/stable/dagre-container/dagre/board.exp.json new file mode 100644 index 000000000..b7b6176ff --- /dev/null +++ b/e2etests/testdata/stable/dagre-container/dagre/board.exp.json @@ -0,0 +1,424 @@ +{ + "name": "", + "isFolderOnly": false, + "fontFamily": "SourceSansPro", + "shapes": [ + { + "id": "a", + "type": "rectangle", + "pos": { + "x": 0, + "y": 41 + }, + "width": 359, + "height": 125, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B4", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "a", + "fontSize": 28, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 12, + "labelHeight": 36, + "labelPosition": "OUTSIDE_TOP_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "a.a", + "type": "rectangle", + "pos": { + "x": 40, + "y": 70 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "a", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "a.b", + "type": "rectangle", + "pos": { + "x": 153, + "y": 70 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "b", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "a.c", + "type": "rectangle", + "pos": { + "x": 266, + "y": 70 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "c", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "b", + "type": "rectangle", + "pos": { + "x": 113, + "y": 307 + }, + "width": 359, + "height": 125, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B4", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "b", + "fontSize": 28, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 13, + "labelHeight": 36, + "labelPosition": "OUTSIDE_TOP_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "b.a", + "type": "rectangle", + "pos": { + "x": 153, + "y": 336 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "a", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "b.b", + "type": "rectangle", + "pos": { + "x": 266, + "y": 336 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "b", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "b.c", + "type": "rectangle", + "pos": { + "x": 379, + "y": 336 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "c", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + } + ], + "connections": [ + { + "id": "(a -> b)[0]", + "src": "a", + "srcArrow": "none", + "dst": "b", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 179.5, + "y": 166 + }, + { + "x": 179.5, + "y": 206 + }, + { + "x": 179.5, + "y": 234.1999969482422 + }, + { + "x": 179.5, + "y": 307 + } + ], + "isCurve": true, + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + } + ], + "root": { + "id": "", + "type": "", + "pos": { + "x": 0, + "y": 0 + }, + "width": 0, + "height": 0, + "opacity": 0, + "strokeDash": 0, + "strokeWidth": 0, + "borderRadius": 0, + "fill": "N7", + "stroke": "", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "zIndex": 0, + "level": 0 + } +} diff --git a/e2etests/testdata/stable/dagre-container/dagre/sketch.exp.svg b/e2etests/testdata/stable/dagre-container/dagre/sketch.exp.svg new file mode 100644 index 000000000..d54a973e9 --- /dev/null +++ b/e2etests/testdata/stable/dagre-container/dagre/sketch.exp.svg @@ -0,0 +1,102 @@ +ababcabc + + + \ No newline at end of file diff --git a/e2etests/testdata/stable/dagre-container/elk/board.exp.json b/e2etests/testdata/stable/dagre-container/elk/board.exp.json new file mode 100644 index 000000000..09e17e037 --- /dev/null +++ b/e2etests/testdata/stable/dagre-container/elk/board.exp.json @@ -0,0 +1,415 @@ +{ + "name": "", + "isFolderOnly": false, + "fontFamily": "SourceSansPro", + "shapes": [ + { + "id": "a", + "type": "rectangle", + "pos": { + "x": 12, + "y": 12 + }, + "width": 299, + "height": 166, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B4", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "a", + "fontSize": 28, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 12, + "labelHeight": 36, + "labelPosition": "INSIDE_TOP_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "a.a", + "type": "rectangle", + "pos": { + "x": 62, + "y": 62 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "a", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "a.b", + "type": "rectangle", + "pos": { + "x": 135, + "y": 62 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "b", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "a.c", + "type": "rectangle", + "pos": { + "x": 208, + "y": 62 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "c", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "b", + "type": "rectangle", + "pos": { + "x": 12, + "y": 248 + }, + "width": 299, + "height": 166, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B4", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "b", + "fontSize": 28, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 13, + "labelHeight": 36, + "labelPosition": "INSIDE_TOP_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "b.a", + "type": "rectangle", + "pos": { + "x": 62, + "y": 298 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "a", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "b.b", + "type": "rectangle", + "pos": { + "x": 135, + "y": 298 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "b", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "b.c", + "type": "rectangle", + "pos": { + "x": 208, + "y": 298 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "c", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + } + ], + "connections": [ + { + "id": "(a -> b)[0]", + "src": "a", + "srcArrow": "none", + "dst": "b", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 161.5, + "y": 178 + }, + { + "x": 161.5, + "y": 248 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + } + ], + "root": { + "id": "", + "type": "", + "pos": { + "x": 0, + "y": 0 + }, + "width": 0, + "height": 0, + "opacity": 0, + "strokeDash": 0, + "strokeWidth": 0, + "borderRadius": 0, + "fill": "N7", + "stroke": "", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "zIndex": 0, + "level": 0 + } +} diff --git a/e2etests/testdata/stable/dagre-container/elk/sketch.exp.svg b/e2etests/testdata/stable/dagre-container/elk/sketch.exp.svg new file mode 100644 index 000000000..899a5fac2 --- /dev/null +++ b/e2etests/testdata/stable/dagre-container/elk/sketch.exp.svg @@ -0,0 +1,102 @@ +ababcabc + + + \ No newline at end of file