...

Source file src/oss.terrastruct.com/d2/d2layouts/d2elklayout/layout.go

Documentation: oss.terrastruct.com/d2/d2layouts/d2elklayout

     1  // d2elklayout is a wrapper around the Javascript port of ELK.
     2  //
     3  // Coordinates are relative to parents.
     4  // See https://www.eclipse.org/elk/documentation/tooldevelopers/graphdatastructure/coordinatesystem.html
     5  package d2elklayout
     6  
     7  import (
     8  	"context"
     9  	_ "embed"
    10  	"encoding/json"
    11  	"errors"
    12  	"fmt"
    13  	"math"
    14  	"regexp"
    15  	"strconv"
    16  	"strings"
    17  
    18  	"github.com/dop251/goja"
    19  
    20  	"oss.terrastruct.com/util-go/xdefer"
    21  
    22  	"oss.terrastruct.com/util-go/go2"
    23  
    24  	"oss.terrastruct.com/d2/d2graph"
    25  	"oss.terrastruct.com/d2/d2target"
    26  	"oss.terrastruct.com/d2/lib/geo"
    27  	"oss.terrastruct.com/d2/lib/label"
    28  	"oss.terrastruct.com/d2/lib/shape"
    29  )
    30  
    31  //go:embed elk.js
    32  var elkJS string
    33  
    34  //go:embed setup.js
    35  var setupJS string
    36  
    37  type ELKNode struct {
    38  	ID            string      `json:"id"`
    39  	X             float64     `json:"x"`
    40  	Y             float64     `json:"y"`
    41  	Width         float64     `json:"width"`
    42  	Height        float64     `json:"height"`
    43  	Children      []*ELKNode  `json:"children,omitempty"`
    44  	Ports         []*ELKPort  `json:"ports,omitempty"`
    45  	Labels        []*ELKLabel `json:"labels,omitempty"`
    46  	LayoutOptions *elkOpts    `json:"layoutOptions,omitempty"`
    47  }
    48  
    49  type PortSide string
    50  
    51  const (
    52  	South PortSide = "SOUTH"
    53  	North PortSide = "NORTH"
    54  	East  PortSide = "EAST"
    55  	West  PortSide = "WEST"
    56  )
    57  
    58  type Direction string
    59  
    60  const (
    61  	Down  Direction = "DOWN"
    62  	Up    Direction = "UP"
    63  	Right Direction = "RIGHT"
    64  	Left  Direction = "LEFT"
    65  )
    66  
    67  type ELKPort struct {
    68  	ID            string   `json:"id"`
    69  	X             float64  `json:"x"`
    70  	Y             float64  `json:"y"`
    71  	Width         float64  `json:"width"`
    72  	Height        float64  `json:"height"`
    73  	LayoutOptions *elkOpts `json:"layoutOptions,omitempty"`
    74  }
    75  
    76  type ELKLabel struct {
    77  	Text          string   `json:"text"`
    78  	X             float64  `json:"x"`
    79  	Y             float64  `json:"y"`
    80  	Width         float64  `json:"width"`
    81  	Height        float64  `json:"height"`
    82  	LayoutOptions *elkOpts `json:"layoutOptions,omitempty"`
    83  }
    84  
    85  type ELKPoint struct {
    86  	X float64 `json:"x"`
    87  	Y float64 `json:"y"`
    88  }
    89  
    90  type ELKEdgeSection struct {
    91  	Start      ELKPoint   `json:"startPoint"`
    92  	End        ELKPoint   `json:"endPoint"`
    93  	BendPoints []ELKPoint `json:"bendPoints,omitempty"`
    94  }
    95  
    96  type ELKEdge struct {
    97  	ID        string           `json:"id"`
    98  	Sources   []string         `json:"sources"`
    99  	Targets   []string         `json:"targets"`
   100  	Sections  []ELKEdgeSection `json:"sections,omitempty"`
   101  	Labels    []*ELKLabel      `json:"labels,omitempty"`
   102  	Container string           `json:"container"`
   103  }
   104  
   105  type ELKGraph struct {
   106  	ID            string     `json:"id"`
   107  	LayoutOptions *elkOpts   `json:"layoutOptions"`
   108  	Children      []*ELKNode `json:"children,omitempty"`
   109  	Edges         []*ELKEdge `json:"edges,omitempty"`
   110  }
   111  
   112  type ConfigurableOpts struct {
   113  	Algorithm       string `json:"elk.algorithm,omitempty"`
   114  	NodeSpacing     int    `json:"spacing.nodeNodeBetweenLayers,omitempty"`
   115  	Padding         string `json:"elk.padding,omitempty"`
   116  	EdgeNodeSpacing int    `json:"spacing.edgeNodeBetweenLayers,omitempty"`
   117  	SelfLoopSpacing int    `json:"elk.spacing.nodeSelfLoop"`
   118  }
   119  
   120  var DefaultOpts = ConfigurableOpts{
   121  	Algorithm:       "layered",
   122  	NodeSpacing:     70.0,
   123  	Padding:         "[top=50,left=50,bottom=50,right=50]",
   124  	EdgeNodeSpacing: 40.0,
   125  	SelfLoopSpacing: 50.0,
   126  }
   127  
   128  var port_spacing = 40.
   129  var edge_node_spacing = 40
   130  
   131  type elkOpts struct {
   132  	EdgeNode                     int       `json:"elk.spacing.edgeNode,omitempty"`
   133  	FixedAlignment               string    `json:"elk.layered.nodePlacement.bk.fixedAlignment,omitempty"`
   134  	Thoroughness                 int       `json:"elk.layered.thoroughness,omitempty"`
   135  	EdgeEdgeBetweenLayersSpacing int       `json:"elk.layered.spacing.edgeEdgeBetweenLayers,omitempty"`
   136  	Direction                    Direction `json:"elk.direction"`
   137  	HierarchyHandling            string    `json:"elk.hierarchyHandling,omitempty"`
   138  	InlineEdgeLabels             bool      `json:"elk.edgeLabels.inline,omitempty"`
   139  	ForceNodeModelOrder          bool      `json:"elk.layered.crossingMinimization.forceNodeModelOrder,omitempty"`
   140  	ConsiderModelOrder           string    `json:"elk.layered.considerModelOrder.strategy,omitempty"`
   141  	CycleBreakingStrategy        string    `json:"elk.layered.cycleBreaking.strategy,omitempty"`
   142  
   143  	SelfLoopDistribution string `json:"elk.layered.edgeRouting.selfLoopDistribution,omitempty"`
   144  
   145  	NodeSizeConstraints string `json:"elk.nodeSize.constraints,omitempty"`
   146  	ContentAlignment    string `json:"elk.contentAlignment,omitempty"`
   147  	NodeSizeMinimum     string `json:"elk.nodeSize.minimum,omitempty"`
   148  
   149  	PortSide        PortSide `json:"elk.port.side,omitempty"`
   150  	PortConstraints string   `json:"elk.portConstraints,omitempty"`
   151  
   152  	ConfigurableOpts
   153  }
   154  
   155  func DefaultLayout(ctx context.Context, g *d2graph.Graph) (err error) {
   156  	return Layout(ctx, g, nil)
   157  }
   158  
   159  func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err error) {
   160  	if opts == nil {
   161  		opts = &DefaultOpts
   162  	}
   163  	defer xdefer.Errorf(&err, "failed to ELK layout")
   164  
   165  	vm := goja.New()
   166  
   167  	console := vm.NewObject()
   168  	if err := vm.Set("console", console); err != nil {
   169  		return err
   170  	}
   171  
   172  	if _, err := vm.RunString(elkJS); err != nil {
   173  		return err
   174  	}
   175  	if _, err := vm.RunString(setupJS); err != nil {
   176  		return err
   177  	}
   178  
   179  	elkGraph := &ELKGraph{
   180  		ID: "",
   181  		LayoutOptions: &elkOpts{
   182  			Thoroughness:                 8,
   183  			EdgeEdgeBetweenLayersSpacing: 50,
   184  			EdgeNode:                     edge_node_spacing,
   185  			HierarchyHandling:            "INCLUDE_CHILDREN",
   186  			FixedAlignment:               "BALANCED",
   187  			ConsiderModelOrder:           "NODES_AND_EDGES",
   188  			CycleBreakingStrategy:        "GREEDY_MODEL_ORDER",
   189  			NodeSizeConstraints:          "MINIMUM_SIZE",
   190  			ContentAlignment:             "H_CENTER V_CENTER",
   191  			ConfigurableOpts: ConfigurableOpts{
   192  				Algorithm:       opts.Algorithm,
   193  				NodeSpacing:     opts.NodeSpacing,
   194  				EdgeNodeSpacing: opts.EdgeNodeSpacing,
   195  				SelfLoopSpacing: opts.SelfLoopSpacing,
   196  			},
   197  		},
   198  	}
   199  	if elkGraph.LayoutOptions.ConfigurableOpts.SelfLoopSpacing == DefaultOpts.SelfLoopSpacing {
   200  		// +5 for a tiny bit of padding
   201  		elkGraph.LayoutOptions.ConfigurableOpts.SelfLoopSpacing = go2.Max(elkGraph.LayoutOptions.ConfigurableOpts.SelfLoopSpacing, childrenMaxSelfLoop(g.Root, g.Root.Direction.Value == "down" || g.Root.Direction.Value == "" || g.Root.Direction.Value == "up")/2+5)
   202  	}
   203  	switch g.Root.Direction.Value {
   204  	case "down":
   205  		elkGraph.LayoutOptions.Direction = Down
   206  	case "up":
   207  		elkGraph.LayoutOptions.Direction = Up
   208  	case "right":
   209  		elkGraph.LayoutOptions.Direction = Right
   210  	case "left":
   211  		elkGraph.LayoutOptions.Direction = Left
   212  	default:
   213  		elkGraph.LayoutOptions.Direction = Down
   214  	}
   215  
   216  	// set label and icon positions for ELK
   217  	for _, obj := range g.Objects {
   218  		positionLabelsIcons(obj)
   219  	}
   220  
   221  	adjustments := make(map[*d2graph.Object]geo.Spacing)
   222  	elkNodes := make(map[*d2graph.Object]*ELKNode)
   223  	elkEdges := make(map[*d2graph.Edge]*ELKEdge)
   224  
   225  	// BFS
   226  	var walk func(*d2graph.Object, *d2graph.Object, func(*d2graph.Object, *d2graph.Object))
   227  	walk = func(obj, parent *d2graph.Object, fn func(*d2graph.Object, *d2graph.Object)) {
   228  		if obj.Parent != nil {
   229  			fn(obj, parent)
   230  		}
   231  		for _, ch := range obj.ChildrenArray {
   232  			walk(ch, obj, fn)
   233  		}
   234  	}
   235  
   236  	walk(g.Root, nil, func(obj, parent *d2graph.Object) {
   237  		incoming := 0.
   238  		outgoing := 0.
   239  		for _, e := range g.Edges {
   240  			if e.Src == obj {
   241  				outgoing++
   242  			}
   243  			if e.Dst == obj {
   244  				incoming++
   245  			}
   246  		}
   247  		if incoming >= 2 || outgoing >= 2 {
   248  			switch g.Root.Direction.Value {
   249  			case "right", "left":
   250  				if obj.Attributes.HeightAttr == nil {
   251  					obj.Height = math.Max(obj.Height, math.Max(incoming, outgoing)*port_spacing)
   252  				}
   253  			default:
   254  				if obj.Attributes.WidthAttr == nil {
   255  					obj.Width = math.Max(obj.Width, math.Max(incoming, outgoing)*port_spacing)
   256  				}
   257  			}
   258  		}
   259  
   260  		if obj.HasLabel() && obj.HasIcon() {
   261  			// this gives shapes extra height for their label if they also have an icon
   262  			obj.Height += float64(obj.LabelDimensions.Height + label.PADDING)
   263  		}
   264  
   265  		margin, _ := obj.SpacingOpt(label.PADDING, label.PADDING, false)
   266  		width := margin.Left + obj.Width + margin.Right
   267  		height := margin.Top + obj.Height + margin.Bottom
   268  		adjustments[obj] = margin
   269  
   270  		n := &ELKNode{
   271  			ID:     obj.AbsID(),
   272  			Width:  width,
   273  			Height: height,
   274  		}
   275  
   276  		if len(obj.ChildrenArray) > 0 {
   277  			n.LayoutOptions = &elkOpts{
   278  				ForceNodeModelOrder:          true,
   279  				Thoroughness:                 8,
   280  				EdgeEdgeBetweenLayersSpacing: 50,
   281  				HierarchyHandling:            "INCLUDE_CHILDREN",
   282  				FixedAlignment:               "BALANCED",
   283  				EdgeNode:                     edge_node_spacing,
   284  				ConsiderModelOrder:           "NODES_AND_EDGES",
   285  				CycleBreakingStrategy:        "GREEDY_MODEL_ORDER",
   286  				NodeSizeConstraints:          "MINIMUM_SIZE",
   287  				ContentAlignment:             "H_CENTER V_CENTER",
   288  				ConfigurableOpts: ConfigurableOpts{
   289  					NodeSpacing:     opts.NodeSpacing,
   290  					EdgeNodeSpacing: opts.EdgeNodeSpacing,
   291  					SelfLoopSpacing: opts.SelfLoopSpacing,
   292  					Padding:         opts.Padding,
   293  				},
   294  			}
   295  			if n.LayoutOptions.ConfigurableOpts.SelfLoopSpacing == DefaultOpts.SelfLoopSpacing {
   296  				n.LayoutOptions.ConfigurableOpts.SelfLoopSpacing = go2.Max(n.LayoutOptions.ConfigurableOpts.SelfLoopSpacing, childrenMaxSelfLoop(obj, g.Root.Direction.Value == "down" || g.Root.Direction.Value == "" || g.Root.Direction.Value == "up")/2+5)
   297  			}
   298  
   299  			switch elkGraph.LayoutOptions.Direction {
   300  			case Down, Up:
   301  				n.LayoutOptions.NodeSizeMinimum = fmt.Sprintf("(%d, %d)", int(math.Ceil(height)), int(math.Ceil(width)))
   302  			case Right, Left:
   303  				n.LayoutOptions.NodeSizeMinimum = fmt.Sprintf("(%d, %d)", int(math.Ceil(width)), int(math.Ceil(height)))
   304  			}
   305  		} else {
   306  			n.LayoutOptions = &elkOpts{
   307  				SelfLoopDistribution: "EQUALLY",
   308  			}
   309  		}
   310  
   311  		if obj.IsContainer() {
   312  			padding := parsePadding(opts.Padding)
   313  			padding = adjustPadding(obj, width, height, padding)
   314  			n.LayoutOptions.Padding = padding.String()
   315  		}
   316  
   317  		if obj.HasLabel() {
   318  			n.Labels = append(n.Labels, &ELKLabel{
   319  				Text:   obj.Label.Value,
   320  				Width:  float64(obj.LabelDimensions.Width),
   321  				Height: float64(obj.LabelDimensions.Height),
   322  			})
   323  		}
   324  
   325  		if parent == g.Root {
   326  			elkGraph.Children = append(elkGraph.Children, n)
   327  		} else {
   328  			elkNodes[parent].Children = append(elkNodes[parent].Children, n)
   329  		}
   330  
   331  		if obj.SQLTable != nil {
   332  			n.LayoutOptions.PortConstraints = "FIXED_POS"
   333  			columns := obj.SQLTable.Columns
   334  			colHeight := n.Height / float64(len(columns)+1)
   335  			n.Ports = make([]*ELKPort, 0, len(columns)*2)
   336  			var srcSide, dstSide PortSide
   337  			switch elkGraph.LayoutOptions.Direction {
   338  			case Left:
   339  				srcSide, dstSide = West, East
   340  			default:
   341  				srcSide, dstSide = East, West
   342  			}
   343  			for i, col := range columns {
   344  				n.Ports = append(n.Ports, &ELKPort{
   345  					ID:            srcPortID(obj, col.Name.Label),
   346  					Y:             float64(i+1)*colHeight + colHeight/2,
   347  					LayoutOptions: &elkOpts{PortSide: srcSide},
   348  				})
   349  				n.Ports = append(n.Ports, &ELKPort{
   350  					ID:            dstPortID(obj, col.Name.Label),
   351  					Y:             float64(i+1)*colHeight + colHeight/2,
   352  					LayoutOptions: &elkOpts{PortSide: dstSide},
   353  				})
   354  			}
   355  		}
   356  
   357  		elkNodes[obj] = n
   358  	})
   359  
   360  	var srcSide, dstSide PortSide
   361  	switch elkGraph.LayoutOptions.Direction {
   362  	case Up:
   363  		srcSide, dstSide = North, South
   364  	default:
   365  		srcSide, dstSide = South, North
   366  	}
   367  
   368  	ports := map[struct {
   369  		obj  *d2graph.Object
   370  		side PortSide
   371  	}][]*ELKPort{}
   372  
   373  	for ei, edge := range g.Edges {
   374  		var src, dst string
   375  
   376  		switch {
   377  		case edge.SrcTableColumnIndex != nil:
   378  			src = srcPortID(edge.Src, edge.Src.SQLTable.Columns[*edge.SrcTableColumnIndex].Name.Label)
   379  		case edge.Src.SQLTable != nil:
   380  			p := &ELKPort{
   381  				ID:            fmt.Sprintf("%s.%d", srcPortID(edge.Src, "__root__"), ei),
   382  				LayoutOptions: &elkOpts{PortSide: srcSide},
   383  			}
   384  			src = p.ID
   385  			elkNodes[edge.Src].Ports = append(elkNodes[edge.Src].Ports, p)
   386  			k := struct {
   387  				obj  *d2graph.Object
   388  				side PortSide
   389  			}{edge.Src, srcSide}
   390  			ports[k] = append(ports[k], p)
   391  		default:
   392  			src = edge.Src.AbsID()
   393  		}
   394  
   395  		switch {
   396  		case edge.DstTableColumnIndex != nil:
   397  			dst = dstPortID(edge.Dst, edge.Dst.SQLTable.Columns[*edge.DstTableColumnIndex].Name.Label)
   398  		case edge.Dst.SQLTable != nil:
   399  			p := &ELKPort{
   400  				ID:            fmt.Sprintf("%s.%d", dstPortID(edge.Dst, "__root__"), ei),
   401  				LayoutOptions: &elkOpts{PortSide: dstSide},
   402  			}
   403  			dst = p.ID
   404  			elkNodes[edge.Dst].Ports = append(elkNodes[edge.Dst].Ports, p)
   405  			k := struct {
   406  				obj  *d2graph.Object
   407  				side PortSide
   408  			}{edge.Dst, dstSide}
   409  			ports[k] = append(ports[k], p)
   410  		default:
   411  			dst = edge.Dst.AbsID()
   412  		}
   413  
   414  		e := &ELKEdge{
   415  			ID:      edge.AbsID(),
   416  			Sources: []string{src},
   417  			Targets: []string{dst},
   418  		}
   419  		if edge.Label.Value != "" {
   420  			e.Labels = append(e.Labels, &ELKLabel{
   421  				Text:   edge.Label.Value,
   422  				Width:  float64(edge.LabelDimensions.Width),
   423  				Height: float64(edge.LabelDimensions.Height),
   424  				LayoutOptions: &elkOpts{
   425  					InlineEdgeLabels: true,
   426  				},
   427  			})
   428  		}
   429  		elkGraph.Edges = append(elkGraph.Edges, e)
   430  		elkEdges[edge] = e
   431  	}
   432  
   433  	for k, ports := range ports {
   434  		width := elkNodes[k.obj].Width
   435  		spacing := width / float64(len(ports)+1)
   436  		for i, p := range ports {
   437  			p.X = float64(i+1) * spacing
   438  		}
   439  	}
   440  
   441  	raw, err := json.Marshal(elkGraph)
   442  	if err != nil {
   443  		return err
   444  	}
   445  
   446  	loadScript := fmt.Sprintf(`var graph = %s`, raw)
   447  
   448  	if _, err := vm.RunString(loadScript); err != nil {
   449  		return err
   450  	}
   451  
   452  	val, err := vm.RunString(`elk.layout(graph)
   453  .then(s => s)
   454  .catch(err => err.message)
   455  `)
   456  
   457  	if err != nil {
   458  		return err
   459  	}
   460  
   461  	p := val.Export()
   462  	if err != nil {
   463  		return err
   464  	}
   465  
   466  	promise := p.(*goja.Promise)
   467  
   468  	for promise.State() == goja.PromiseStatePending {
   469  		if err := ctx.Err(); err != nil {
   470  			return err
   471  		}
   472  		continue
   473  	}
   474  
   475  	if promise.State() == goja.PromiseStateRejected {
   476  		return errors.New("ELK: something went wrong")
   477  	}
   478  
   479  	result := promise.Result().Export()
   480  
   481  	var jsonOut map[string]interface{}
   482  	switch out := result.(type) {
   483  	case string:
   484  		return fmt.Errorf("ELK layout error: %s", out)
   485  	case map[string]interface{}:
   486  		jsonOut = out
   487  	default:
   488  		return fmt.Errorf("ELK unexpected return: %v", out)
   489  	}
   490  
   491  	jsonBytes, err := json.Marshal(jsonOut)
   492  	if err != nil {
   493  		return err
   494  	}
   495  
   496  	err = json.Unmarshal(jsonBytes, &elkGraph)
   497  	if err != nil {
   498  		return err
   499  	}
   500  
   501  	byID := make(map[string]*d2graph.Object)
   502  	walk(g.Root, nil, func(obj, parent *d2graph.Object) {
   503  		n := elkNodes[obj]
   504  
   505  		parentX := 0.0
   506  		parentY := 0.0
   507  		if parent != nil && parent != g.Root {
   508  			parentX = parent.TopLeft.X
   509  			parentY = parent.TopLeft.Y
   510  		}
   511  		obj.TopLeft = geo.NewPoint(parentX+n.X, parentY+n.Y)
   512  		obj.Width = math.Ceil(n.Width)
   513  		obj.Height = math.Ceil(n.Height)
   514  
   515  		byID[obj.AbsID()] = obj
   516  	})
   517  
   518  	for _, edge := range g.Edges {
   519  		e := elkEdges[edge]
   520  
   521  		parentX := 0.0
   522  		parentY := 0.0
   523  		if e.Container != "" {
   524  			parentX = byID[e.Container].TopLeft.X
   525  			parentY = byID[e.Container].TopLeft.Y
   526  		}
   527  
   528  		var points []*geo.Point
   529  		for _, s := range e.Sections {
   530  			points = append(points, &geo.Point{
   531  				X: parentX + s.Start.X,
   532  				Y: parentY + s.Start.Y,
   533  			})
   534  			for _, bp := range s.BendPoints {
   535  				points = append(points, &geo.Point{
   536  					X: parentX + bp.X,
   537  					Y: parentY + bp.Y,
   538  				})
   539  			}
   540  			points = append(points, &geo.Point{
   541  				X: parentX + s.End.X,
   542  				Y: parentY + s.End.Y,
   543  			})
   544  		}
   545  		edge.Route = points
   546  	}
   547  
   548  	objEdges := make(map[*d2graph.Object][]*d2graph.Edge)
   549  	for _, e := range g.Edges {
   550  		objEdges[e.Src] = append(objEdges[e.Src], e)
   551  		if e.Dst != e.Src {
   552  			objEdges[e.Dst] = append(objEdges[e.Dst], e)
   553  		}
   554  	}
   555  
   556  	for _, obj := range g.Objects {
   557  		if margin, has := adjustments[obj]; has {
   558  			edges := objEdges[obj]
   559  			// also move edges with the shrinking sides
   560  			if margin.Left > 0 {
   561  				for _, e := range edges {
   562  					l := len(e.Route)
   563  					if e.Src == obj && e.Route[0].X == obj.TopLeft.X {
   564  						e.Route[0].X += margin.Left
   565  					}
   566  					if e.Dst == obj && e.Route[l-1].X == obj.TopLeft.X {
   567  						e.Route[l-1].X += margin.Left
   568  					}
   569  				}
   570  				obj.TopLeft.X += margin.Left
   571  				obj.ShiftDescendants(margin.Left/2, 0)
   572  				obj.Width -= margin.Left
   573  			}
   574  			if margin.Right > 0 {
   575  				for _, e := range edges {
   576  					l := len(e.Route)
   577  					if e.Src == obj && e.Route[0].X == obj.TopLeft.X+obj.Width {
   578  						e.Route[0].X -= margin.Right
   579  					}
   580  					if e.Dst == obj && e.Route[l-1].X == obj.TopLeft.X+obj.Width {
   581  						e.Route[l-1].X -= margin.Right
   582  					}
   583  				}
   584  				obj.ShiftDescendants(-margin.Right/2, 0)
   585  				obj.Width -= margin.Right
   586  			}
   587  			if margin.Top > 0 {
   588  				for _, e := range edges {
   589  					l := len(e.Route)
   590  					if e.Src == obj && e.Route[0].Y == obj.TopLeft.Y {
   591  						e.Route[0].Y += margin.Top
   592  					}
   593  					if e.Dst == obj && e.Route[l-1].Y == obj.TopLeft.Y {
   594  						e.Route[l-1].Y += margin.Top
   595  					}
   596  				}
   597  				obj.TopLeft.Y += margin.Top
   598  				obj.ShiftDescendants(0, margin.Top/2)
   599  				obj.Height -= margin.Top
   600  			}
   601  			if margin.Bottom > 0 {
   602  				for _, e := range edges {
   603  					l := len(e.Route)
   604  					if e.Src == obj && e.Route[0].Y == obj.TopLeft.Y+obj.Height {
   605  						e.Route[0].Y -= margin.Bottom
   606  					}
   607  					if e.Dst == obj && e.Route[l-1].Y == obj.TopLeft.Y+obj.Height {
   608  						e.Route[l-1].Y -= margin.Bottom
   609  					}
   610  				}
   611  				obj.ShiftDescendants(0, -margin.Bottom/2)
   612  				obj.Height -= margin.Bottom
   613  			}
   614  		}
   615  	}
   616  
   617  	for _, edge := range g.Edges {
   618  		points := edge.Route
   619  
   620  		startIndex, endIndex := 0, len(points)-1
   621  		start := points[startIndex]
   622  		end := points[endIndex]
   623  
   624  		var originalSrcTL, originalDstTL *geo.Point
   625  		// if the edge passes through 3d/multiple, use the offset box for tracing to border
   626  		if srcDx, srcDy := edge.Src.GetModifierElementAdjustments(); srcDx != 0 || srcDy != 0 {
   627  			if start.X > edge.Src.TopLeft.X+srcDx &&
   628  				start.Y < edge.Src.TopLeft.Y+edge.Src.Height-srcDy {
   629  				originalSrcTL = edge.Src.TopLeft.Copy()
   630  				edge.Src.TopLeft.X += srcDx
   631  				edge.Src.TopLeft.Y -= srcDy
   632  			}
   633  		}
   634  		if dstDx, dstDy := edge.Dst.GetModifierElementAdjustments(); dstDx != 0 || dstDy != 0 {
   635  			if end.X > edge.Dst.TopLeft.X+dstDx &&
   636  				end.Y < edge.Dst.TopLeft.Y+edge.Dst.Height-dstDy {
   637  				originalDstTL = edge.Dst.TopLeft.Copy()
   638  				edge.Dst.TopLeft.X += dstDx
   639  				edge.Dst.TopLeft.Y -= dstDy
   640  			}
   641  		}
   642  
   643  		startIndex, endIndex = edge.TraceToShape(points, startIndex, endIndex)
   644  		points = points[startIndex : endIndex+1]
   645  
   646  		if edge.Label.Value != "" {
   647  			edge.LabelPosition = go2.Pointer(label.InsideMiddleCenter.String())
   648  		}
   649  
   650  		edge.Route = points
   651  
   652  		// undo 3d/multiple offset
   653  		if originalSrcTL != nil {
   654  			edge.Src.TopLeft.X = originalSrcTL.X
   655  			edge.Src.TopLeft.Y = originalSrcTL.Y
   656  		}
   657  		if originalDstTL != nil {
   658  			edge.Dst.TopLeft.X = originalDstTL.X
   659  			edge.Dst.TopLeft.Y = originalDstTL.Y
   660  		}
   661  	}
   662  
   663  	deleteBends(g)
   664  
   665  	return nil
   666  }
   667  
   668  func srcPortID(obj *d2graph.Object, column string) string {
   669  	return fmt.Sprintf("%s.%s.src", obj.AbsID(), column)
   670  }
   671  
   672  func dstPortID(obj *d2graph.Object, column string) string {
   673  	return fmt.Sprintf("%s.%s.dst", obj.AbsID(), column)
   674  }
   675  
   676  // deleteBends is a shim for ELK to delete unnecessary bends
   677  // see https://github.com/terrastruct/d2/issues/1030
   678  func deleteBends(g *d2graph.Graph) {
   679  	// Get rid of S-shapes at the source and the target
   680  	// TODO there might be value in repeating this. removal of an S shape introducing another S shape that can still be removed
   681  	for _, isSource := range []bool{true, false} {
   682  		for ei, e := range g.Edges {
   683  			if len(e.Route) < 4 {
   684  				continue
   685  			}
   686  			if e.Src == e.Dst {
   687  				continue
   688  			}
   689  			var endpoint *d2graph.Object
   690  			var start *geo.Point
   691  			var corner *geo.Point
   692  			var end *geo.Point
   693  
   694  			var columnIndex *int
   695  			if isSource {
   696  				start = e.Route[0]
   697  				corner = e.Route[1]
   698  				end = e.Route[2]
   699  				endpoint = e.Src
   700  				columnIndex = e.SrcTableColumnIndex
   701  			} else {
   702  				start = e.Route[len(e.Route)-1]
   703  				corner = e.Route[len(e.Route)-2]
   704  				end = e.Route[len(e.Route)-3]
   705  				endpoint = e.Dst
   706  				columnIndex = e.DstTableColumnIndex
   707  			}
   708  
   709  			isHorizontal := math.Ceil(start.Y) == math.Ceil(corner.Y)
   710  			dx, dy := endpoint.GetModifierElementAdjustments()
   711  
   712  			// Make sure it's still attached
   713  			switch {
   714  			case columnIndex != nil:
   715  				rowHeight := endpoint.Height / float64(len(endpoint.SQLTable.Columns)+1)
   716  				rowCenter := endpoint.TopLeft.Y + rowHeight*float64(*columnIndex+1) + rowHeight/2
   717  
   718  				// for row connections new Y coordinate should be within 1/3 row height from the row center
   719  				if math.Abs(end.Y-rowCenter) > rowHeight/3 {
   720  					continue
   721  				}
   722  			case isHorizontal:
   723  				if end.Y <= endpoint.TopLeft.Y+10-dy {
   724  					continue
   725  				}
   726  				if end.Y >= endpoint.TopLeft.Y+endpoint.Height-10 {
   727  					continue
   728  				}
   729  			default:
   730  				if end.X <= endpoint.TopLeft.X+10 {
   731  					continue
   732  				}
   733  				if end.X >= endpoint.TopLeft.X+endpoint.Width-10+dx {
   734  					continue
   735  				}
   736  			}
   737  
   738  			var newStart *geo.Point
   739  			if isHorizontal {
   740  				newStart = geo.NewPoint(start.X, end.Y)
   741  			} else {
   742  				newStart = geo.NewPoint(end.X, start.Y)
   743  			}
   744  
   745  			endpointShape := shape.NewShape(d2target.DSL_SHAPE_TO_SHAPE_TYPE[strings.ToLower(endpoint.Shape.Value)], endpoint.Box)
   746  			newStart = shape.TraceToShapeBorder(endpointShape, newStart, end)
   747  
   748  			// Check that the new segment doesn't collide with anything new
   749  
   750  			oldSegment := geo.NewSegment(start, corner)
   751  			newSegment := geo.NewSegment(newStart, end)
   752  
   753  			oldIntersects := countObjectIntersects(g, e.Src, e.Dst, *oldSegment)
   754  			newIntersects := countObjectIntersects(g, e.Src, e.Dst, *newSegment)
   755  
   756  			if newIntersects > oldIntersects {
   757  				continue
   758  			}
   759  
   760  			oldCrossingsCount, oldOverlapsCount, oldCloseOverlapsCount, oldTouchingCount := countEdgeIntersects(g, g.Edges[ei], *oldSegment)
   761  			newCrossingsCount, newOverlapsCount, newCloseOverlapsCount, newTouchingCount := countEdgeIntersects(g, g.Edges[ei], *newSegment)
   762  
   763  			if newCrossingsCount > oldCrossingsCount {
   764  				continue
   765  			}
   766  			if newOverlapsCount > oldOverlapsCount {
   767  				continue
   768  			}
   769  
   770  			if newCloseOverlapsCount > oldCloseOverlapsCount {
   771  				continue
   772  			}
   773  			if newTouchingCount > oldTouchingCount {
   774  				continue
   775  			}
   776  
   777  			// commit
   778  			if isSource {
   779  				g.Edges[ei].Route = append(
   780  					[]*geo.Point{newStart},
   781  					e.Route[3:]...,
   782  				)
   783  			} else {
   784  				g.Edges[ei].Route = append(
   785  					e.Route[:len(e.Route)-3],
   786  					newStart,
   787  				)
   788  			}
   789  		}
   790  	}
   791  
   792  	// Get rid of ladders
   793  	// ELK likes to do these for some reason
   794  	// .   ┌─
   795  	// . ┌─┘
   796  	// . │
   797  	// We want to transform these into L-shapes
   798  
   799  	points := map[geo.Point]int{}
   800  	for _, e := range g.Edges {
   801  		for _, p := range e.Route {
   802  			points[*p]++
   803  		}
   804  	}
   805  
   806  	for ei, e := range g.Edges {
   807  		if len(e.Route) < 6 {
   808  			continue
   809  		}
   810  		if e.Src == e.Dst {
   811  			continue
   812  		}
   813  
   814  		for i := 1; i < len(e.Route)-3; i++ {
   815  			before := e.Route[i-1]
   816  			start := e.Route[i]
   817  			corner := e.Route[i+1]
   818  			end := e.Route[i+2]
   819  			after := e.Route[i+3]
   820  
   821  			if c, _ := points[*corner]; c > 1 {
   822  				// If corner is shared with another edge, they merge
   823  				continue
   824  			}
   825  
   826  			// S-shape on sources only concerned one segment, since the other was just along the bound of endpoint
   827  			// These concern two segments
   828  
   829  			var newCorner *geo.Point
   830  			if math.Ceil(start.X) == math.Ceil(corner.X) {
   831  				newCorner = geo.NewPoint(end.X, start.Y)
   832  				// not ladder
   833  				if (end.X > start.X) != (start.X > before.X) {
   834  					continue
   835  				}
   836  				if (end.Y > start.Y) != (after.Y > end.Y) {
   837  					continue
   838  				}
   839  			} else {
   840  				newCorner = geo.NewPoint(start.X, end.Y)
   841  				if (end.Y > start.Y) != (start.Y > before.Y) {
   842  					continue
   843  				}
   844  				if (end.X > start.X) != (after.X > end.X) {
   845  					continue
   846  				}
   847  			}
   848  
   849  			oldS1 := geo.NewSegment(start, corner)
   850  			oldS2 := geo.NewSegment(corner, end)
   851  
   852  			newS1 := geo.NewSegment(start, newCorner)
   853  			newS2 := geo.NewSegment(newCorner, end)
   854  
   855  			// Check that the new segments doesn't collide with anything new
   856  			oldIntersects := countObjectIntersects(g, e.Src, e.Dst, *oldS1) + countObjectIntersects(g, e.Src, e.Dst, *oldS2)
   857  			newIntersects := countObjectIntersects(g, e.Src, e.Dst, *newS1) + countObjectIntersects(g, e.Src, e.Dst, *newS2)
   858  
   859  			if newIntersects > oldIntersects {
   860  				continue
   861  			}
   862  
   863  			oldCrossingsCount1, oldOverlapsCount1, oldCloseOverlapsCount1, oldTouchingCount1 := countEdgeIntersects(g, g.Edges[ei], *oldS1)
   864  			oldCrossingsCount2, oldOverlapsCount2, oldCloseOverlapsCount2, oldTouchingCount2 := countEdgeIntersects(g, g.Edges[ei], *oldS2)
   865  			oldCrossingsCount := oldCrossingsCount1 + oldCrossingsCount2
   866  			oldOverlapsCount := oldOverlapsCount1 + oldOverlapsCount2
   867  			oldCloseOverlapsCount := oldCloseOverlapsCount1 + oldCloseOverlapsCount2
   868  			oldTouchingCount := oldTouchingCount1 + oldTouchingCount2
   869  
   870  			newCrossingsCount1, newOverlapsCount1, newCloseOverlapsCount1, newTouchingCount1 := countEdgeIntersects(g, g.Edges[ei], *newS1)
   871  			newCrossingsCount2, newOverlapsCount2, newCloseOverlapsCount2, newTouchingCount2 := countEdgeIntersects(g, g.Edges[ei], *newS2)
   872  			newCrossingsCount := newCrossingsCount1 + newCrossingsCount2
   873  			newOverlapsCount := newOverlapsCount1 + newOverlapsCount2
   874  			newCloseOverlapsCount := newCloseOverlapsCount1 + newCloseOverlapsCount2
   875  			newTouchingCount := newTouchingCount1 + newTouchingCount2
   876  
   877  			if newCrossingsCount > oldCrossingsCount {
   878  				continue
   879  			}
   880  			if newOverlapsCount > oldOverlapsCount {
   881  				continue
   882  			}
   883  
   884  			if newCloseOverlapsCount > oldCloseOverlapsCount {
   885  				continue
   886  			}
   887  			if newTouchingCount > oldTouchingCount {
   888  				continue
   889  			}
   890  
   891  			// commit
   892  			g.Edges[ei].Route = append(append(
   893  				e.Route[:i],
   894  				newCorner,
   895  			),
   896  				e.Route[i+3:]...,
   897  			)
   898  			break
   899  		}
   900  	}
   901  }
   902  
   903  func countObjectIntersects(g *d2graph.Graph, src, dst *d2graph.Object, s geo.Segment) int {
   904  	count := 0
   905  	for i, o := range g.Objects {
   906  		if g.Objects[i] == src || g.Objects[i] == dst {
   907  			continue
   908  		}
   909  		if o.Intersects(s, float64(edge_node_spacing)-1) {
   910  			count++
   911  		}
   912  	}
   913  	return count
   914  }
   915  
   916  // countEdgeIntersects counts both crossings AND getting too close to a parallel segment
   917  func countEdgeIntersects(g *d2graph.Graph, sEdge *d2graph.Edge, s geo.Segment) (int, int, int, int) {
   918  	isHorizontal := math.Ceil(s.Start.Y) == math.Ceil(s.End.Y)
   919  	crossingsCount := 0
   920  	overlapsCount := 0
   921  	closeOverlapsCount := 0
   922  	touchingCount := 0
   923  	for i, e := range g.Edges {
   924  		if g.Edges[i] == sEdge {
   925  			continue
   926  		}
   927  
   928  		for i := 0; i < len(e.Route)-1; i++ {
   929  			otherS := geo.NewSegment(e.Route[i], e.Route[i+1])
   930  			otherIsHorizontal := math.Ceil(otherS.Start.Y) == math.Ceil(otherS.End.Y)
   931  			if isHorizontal == otherIsHorizontal {
   932  				if s.Overlaps(*otherS, !isHorizontal, 0.) {
   933  					if isHorizontal {
   934  						if math.Abs(s.Start.Y-otherS.Start.Y) < float64(edge_node_spacing)/2. {
   935  							overlapsCount++
   936  							if math.Abs(s.Start.Y-otherS.Start.Y) < float64(edge_node_spacing)/4. {
   937  								closeOverlapsCount++
   938  								if math.Abs(s.Start.Y-otherS.Start.Y) < 1. {
   939  									touchingCount++
   940  								}
   941  							}
   942  						}
   943  					} else {
   944  						if math.Abs(s.Start.X-otherS.Start.X) < float64(edge_node_spacing)/2. {
   945  							overlapsCount++
   946  							if math.Abs(s.Start.X-otherS.Start.X) < float64(edge_node_spacing)/4. {
   947  								closeOverlapsCount++
   948  								if math.Abs(s.Start.Y-otherS.Start.Y) < 1. {
   949  									touchingCount++
   950  								}
   951  							}
   952  						}
   953  					}
   954  				}
   955  			} else {
   956  				if s.Intersects(*otherS) {
   957  					crossingsCount++
   958  				}
   959  			}
   960  		}
   961  
   962  	}
   963  	return crossingsCount, overlapsCount, closeOverlapsCount, touchingCount
   964  }
   965  
   966  func childrenMaxSelfLoop(parent *d2graph.Object, isWidth bool) int {
   967  	max := 0
   968  	for _, ch := range parent.Children {
   969  		for _, e := range parent.Graph.Edges {
   970  			if e.Src == e.Dst && e.Src == ch && e.Label.Value != "" {
   971  				if isWidth {
   972  					max = go2.Max(max, e.LabelDimensions.Width)
   973  				} else {
   974  					max = go2.Max(max, e.LabelDimensions.Height)
   975  				}
   976  			}
   977  		}
   978  	}
   979  
   980  	return max
   981  }
   982  
   983  type shapePadding struct {
   984  	top, left, bottom, right int
   985  }
   986  
   987  // parse out values from elk padding string. e.g. "[top=50,left=50,bottom=50,right=50]"
   988  func parsePadding(in string) shapePadding {
   989  	reTop := regexp.MustCompile(`top=(\d+)`)
   990  	reLeft := regexp.MustCompile(`left=(\d+)`)
   991  	reBottom := regexp.MustCompile(`bottom=(\d+)`)
   992  	reRight := regexp.MustCompile(`right=(\d+)`)
   993  
   994  	padding := shapePadding{}
   995  
   996  	submatches := reTop.FindStringSubmatch(in)
   997  	if len(submatches) == 2 {
   998  		i, err := strconv.ParseInt(submatches[1], 10, 64)
   999  		if err == nil {
  1000  			padding.top = int(i)
  1001  		}
  1002  	}
  1003  
  1004  	submatches = reLeft.FindStringSubmatch(in)
  1005  	if len(submatches) == 2 {
  1006  		i, err := strconv.ParseInt(submatches[1], 10, 64)
  1007  		if err == nil {
  1008  			padding.left = int(i)
  1009  		}
  1010  	}
  1011  
  1012  	submatches = reBottom.FindStringSubmatch(in)
  1013  	if len(submatches) == 2 {
  1014  		i, err := strconv.ParseInt(submatches[1], 10, 64)
  1015  		if err == nil {
  1016  			padding.bottom = int(i)
  1017  		}
  1018  	}
  1019  
  1020  	submatches = reRight.FindStringSubmatch(in)
  1021  	i, err := strconv.ParseInt(submatches[1], 10, 64)
  1022  	if len(submatches) == 2 {
  1023  		if err == nil {
  1024  			padding.right = int(i)
  1025  		}
  1026  	}
  1027  
  1028  	return padding
  1029  }
  1030  
  1031  func (padding shapePadding) String() string {
  1032  	return fmt.Sprintf("[top=%d,left=%d,bottom=%d,right=%d]", padding.top, padding.left, padding.bottom, padding.right)
  1033  }
  1034  
  1035  func adjustPadding(obj *d2graph.Object, width, height float64, padding shapePadding) shapePadding {
  1036  	if !obj.IsContainer() {
  1037  		return padding
  1038  	}
  1039  
  1040  	// compute extra space padding for label/icon
  1041  	var extraTop, extraBottom, extraLeft, extraRight int
  1042  	if obj.HasLabel() && obj.LabelPosition != nil {
  1043  		labelHeight := obj.LabelDimensions.Height + 2*label.PADDING
  1044  		labelWidth := obj.LabelDimensions.Width + 2*label.PADDING
  1045  		switch label.FromString(*obj.LabelPosition) {
  1046  		case label.InsideTopLeft, label.InsideTopCenter, label.InsideTopRight:
  1047  			// Note: for corners we only add height
  1048  			extraTop = labelHeight
  1049  		case label.InsideBottomLeft, label.InsideBottomCenter, label.InsideBottomRight:
  1050  			extraBottom = labelHeight
  1051  		case label.InsideMiddleLeft:
  1052  			extraLeft = labelWidth
  1053  		case label.InsideMiddleRight:
  1054  			extraRight = labelWidth
  1055  		}
  1056  	}
  1057  	if obj.HasIcon() && obj.IconPosition != nil {
  1058  		iconSize := d2target.MAX_ICON_SIZE + 2*label.PADDING
  1059  		switch label.FromString(*obj.IconPosition) {
  1060  		case label.InsideTopLeft, label.InsideTopCenter, label.InsideTopRight:
  1061  			extraTop = go2.Max(extraTop, iconSize)
  1062  		case label.InsideBottomLeft, label.InsideBottomCenter, label.InsideBottomRight:
  1063  			extraBottom = go2.Max(extraBottom, iconSize)
  1064  		case label.InsideMiddleLeft:
  1065  			extraLeft = go2.Max(extraLeft, iconSize)
  1066  		case label.InsideMiddleRight:
  1067  			extraRight = go2.Max(extraRight, iconSize)
  1068  		}
  1069  	}
  1070  
  1071  	maxChildWidth, maxChildHeight := math.Inf(-1), math.Inf(-1)
  1072  	for _, c := range obj.ChildrenArray {
  1073  		if c.Width > maxChildWidth {
  1074  			maxChildWidth = c.Width
  1075  		}
  1076  		if c.Height > maxChildHeight {
  1077  			maxChildHeight = c.Height
  1078  		}
  1079  	}
  1080  	// We don't know exactly what the shape dimensions will be after layout, but for more accurate innerBox dimensions,
  1081  	// we add the maxChildWidth and maxChildHeight with computed additions for the innerBox calculation
  1082  	width += maxChildWidth + float64(extraLeft+extraRight)
  1083  	height += maxChildHeight + float64(extraTop+extraBottom)
  1084  	contentBox := geo.NewBox(geo.NewPoint(0, 0), width, height)
  1085  	shapeType := d2target.DSL_SHAPE_TO_SHAPE_TYPE[obj.Shape.Value]
  1086  	s := shape.NewShape(shapeType, contentBox)
  1087  	innerBox := s.GetInnerBox()
  1088  
  1089  	// If the shape inner box + label/icon height becomes greater than the default padding, we want to use that
  1090  	//
  1091  	// ┌OUTER───────────────────────────┬────────────────────────────────────────────┐
  1092  	// │                                │                                            │
  1093  	// │  ┌INNER──────── ┬ ─────────────│───────────────────────────────────────┐    │
  1094  	// │  │              │Label Padding │                                       │    │
  1095  	// │  │      ┌LABEL─ ┴ ─────────────│───────┐┬             ┌ICON── ┬ ────┐  │    │
  1096  	// │  │      │                      │       ││             │       │     │  │    │
  1097  	// │  │      │                      │       ││Label Height │   Icon│     │  │    │
  1098  	// │  │      │                      │       ││             │ Height│     │  │    │
  1099  	// │  │      └──────────────────────│───────┘┴             │       │     │  │    │
  1100  	// │  │                             │                      └────── ┴ ────┘  │    │
  1101  	// │  │                             │                                       │    │
  1102  	// │  │                             ┴Default ELK Padding                    │    │
  1103  	// │  │   ┌CHILD────────────────────────────────────────────────────────┐   │    │
  1104  	// │  │   │                                                             │   │    │
  1105  	// │  │   │                                                             │   │    │
  1106  	// │  │   │                                                             │   │    │
  1107  	// │  │   └─────────────────────────────────────────────────────────────┘   │    │
  1108  	// │  │                                                                     │    │
  1109  	// │  └─────────────────────────────────────────────────────────────────────┘    │
  1110  	// │                                                                             │
  1111  	// └─────────────────────────────────────────────────────────────────────────────┘
  1112  
  1113  	// estimated shape innerBox padding
  1114  	innerTop := int(math.Ceil(innerBox.TopLeft.Y))
  1115  	innerBottom := int(math.Ceil(height - (innerBox.TopLeft.Y + innerBox.Height)))
  1116  	innerLeft := int(math.Ceil(innerBox.TopLeft.X))
  1117  	innerRight := int(math.Ceil(width - (innerBox.TopLeft.X + innerBox.Width)))
  1118  
  1119  	padding.top = go2.Max(padding.top, innerTop+extraTop)
  1120  	padding.bottom = go2.Max(padding.bottom, innerBottom+extraBottom)
  1121  	padding.left = go2.Max(padding.left, innerLeft+extraLeft)
  1122  	padding.right = go2.Max(padding.right, innerRight+extraRight)
  1123  
  1124  	return padding
  1125  }
  1126  
  1127  func positionLabelsIcons(obj *d2graph.Object) {
  1128  	if obj.Icon != nil && obj.IconPosition == nil {
  1129  		if len(obj.ChildrenArray) > 0 {
  1130  			obj.IconPosition = go2.Pointer(label.InsideTopLeft.String())
  1131  			if obj.LabelPosition == nil {
  1132  				obj.LabelPosition = go2.Pointer(label.InsideTopRight.String())
  1133  				return
  1134  			}
  1135  		} else if obj.SQLTable != nil || obj.Class != nil || obj.Language != "" {
  1136  			obj.IconPosition = go2.Pointer(label.OutsideTopLeft.String())
  1137  		} else {
  1138  			obj.IconPosition = go2.Pointer(label.InsideMiddleCenter.String())
  1139  		}
  1140  	}
  1141  	if obj.HasLabel() && obj.LabelPosition == nil {
  1142  		if len(obj.ChildrenArray) > 0 {
  1143  			obj.LabelPosition = go2.Pointer(label.InsideTopCenter.String())
  1144  		} else if obj.HasOutsideBottomLabel() {
  1145  			obj.LabelPosition = go2.Pointer(label.OutsideBottomCenter.String())
  1146  		} else if obj.Icon != nil {
  1147  			obj.LabelPosition = go2.Pointer(label.InsideTopCenter.String())
  1148  		} else {
  1149  			obj.LabelPosition = go2.Pointer(label.InsideMiddleCenter.String())
  1150  		}
  1151  	}
  1152  }
  1153  

View as plain text