...

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

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

     1  // d2near applies near keywords when they're constants
     2  // Intended to be run as the last stage of layout after the diagram has already undergone layout
     3  package d2near
     4  
     5  import (
     6  	"context"
     7  	"math"
     8  	"strings"
     9  
    10  	"oss.terrastruct.com/d2/d2graph"
    11  	"oss.terrastruct.com/d2/lib/geo"
    12  	"oss.terrastruct.com/d2/lib/label"
    13  )
    14  
    15  const pad = 20
    16  
    17  type set map[string]struct{}
    18  
    19  var HorizontalCenterNears = set{
    20  	"center-left":  {},
    21  	"center-right": {},
    22  }
    23  var VerticalCenterNears = set{
    24  	"top-center":    {},
    25  	"bottom-center": {},
    26  }
    27  var NonCenterNears = set{
    28  	"top-left":     {},
    29  	"top-right":    {},
    30  	"bottom-left":  {},
    31  	"bottom-right": {},
    32  }
    33  
    34  // Layout finds the shapes which are assigned constant near keywords and places them.
    35  func Layout(ctx context.Context, g *d2graph.Graph, constantNearGraphs []*d2graph.Graph) error {
    36  	if len(constantNearGraphs) == 0 {
    37  		return nil
    38  	}
    39  
    40  	for _, tempGraph := range constantNearGraphs {
    41  		tempGraph.Root.ChildrenArray[0].Parent = g.Root
    42  		for _, obj := range tempGraph.Objects {
    43  			obj.Graph = g
    44  		}
    45  	}
    46  
    47  	// Imagine the graph has two long texts, one at top center and one at top left.
    48  	// Top left should go left enough to not collide with center.
    49  	// So place the center ones first, then the later ones will consider them for bounding box
    50  	for _, currentSet := range []set{VerticalCenterNears, HorizontalCenterNears, NonCenterNears} {
    51  		for _, tempGraph := range constantNearGraphs {
    52  			obj := tempGraph.Root.ChildrenArray[0]
    53  			_, in := currentSet[d2graph.Key(obj.NearKey)[0]]
    54  			if in {
    55  				prevX, prevY := obj.TopLeft.X, obj.TopLeft.Y
    56  				obj.TopLeft = geo.NewPoint(place(obj))
    57  				dx, dy := obj.TopLeft.X-prevX, obj.TopLeft.Y-prevY
    58  
    59  				for _, subObject := range tempGraph.Objects {
    60  					// `obj` already been replaced above by `place(obj)`
    61  					if subObject == obj {
    62  						continue
    63  					}
    64  					subObject.TopLeft.X += dx
    65  					subObject.TopLeft.Y += dy
    66  				}
    67  				for _, subEdge := range tempGraph.Edges {
    68  					for _, point := range subEdge.Route {
    69  						point.X += dx
    70  						point.Y += dy
    71  					}
    72  				}
    73  			}
    74  		}
    75  		for _, tempGraph := range constantNearGraphs {
    76  			obj := tempGraph.Root.ChildrenArray[0]
    77  			_, in := currentSet[d2graph.Key(obj.NearKey)[0]]
    78  			if in {
    79  				// The z-index for constant nears does not matter, as it will not collide
    80  				g.Objects = append(g.Objects, tempGraph.Objects...)
    81  				if obj.Parent.Children == nil {
    82  					obj.Parent.Children = make(map[string]*d2graph.Object)
    83  				}
    84  				obj.Parent.Children[strings.ToLower(obj.ID)] = obj
    85  				obj.Parent.ChildrenArray = append(obj.Parent.ChildrenArray, obj)
    86  				g.Edges = append(g.Edges, tempGraph.Edges...)
    87  			}
    88  		}
    89  	}
    90  
    91  	return nil
    92  }
    93  
    94  // place returns the position of obj, taking into consideration its near value and the diagram
    95  func place(obj *d2graph.Object) (float64, float64) {
    96  	tl, br := boundingBox(obj.Graph)
    97  	w := br.X - tl.X
    98  	h := br.Y - tl.Y
    99  
   100  	nearKeyStr := d2graph.Key(obj.NearKey)[0]
   101  	var x, y float64
   102  	switch nearKeyStr {
   103  	case "top-left":
   104  		x, y = tl.X-obj.Width-pad, tl.Y-obj.Height-pad
   105  	case "top-center":
   106  		x, y = tl.X+w/2-obj.Width/2, tl.Y-obj.Height-pad
   107  	case "top-right":
   108  		x, y = br.X+pad, tl.Y-obj.Height-pad
   109  	case "center-left":
   110  		x, y = tl.X-obj.Width-pad, tl.Y+h/2-obj.Height/2
   111  	case "center-right":
   112  		x, y = br.X+pad, tl.Y+h/2-obj.Height/2
   113  	case "bottom-left":
   114  		x, y = tl.X-obj.Width-pad, br.Y+pad
   115  	case "bottom-center":
   116  		x, y = br.X-w/2-obj.Width/2, br.Y+pad
   117  	case "bottom-right":
   118  		x, y = br.X+pad, br.Y+pad
   119  	}
   120  
   121  	if obj.LabelPosition != nil && !strings.Contains(*obj.LabelPosition, "INSIDE") {
   122  		if strings.Contains(*obj.LabelPosition, "_TOP_") {
   123  			// label is on the top, and container is placed on the bottom
   124  			if strings.Contains(nearKeyStr, "bottom") {
   125  				y += float64(obj.LabelDimensions.Height)
   126  			}
   127  		} else if strings.Contains(*obj.LabelPosition, "_LEFT_") {
   128  			// label is on the left, and container is placed on the right
   129  			if strings.Contains(nearKeyStr, "right") {
   130  				x += float64(obj.LabelDimensions.Width)
   131  			}
   132  		} else if strings.Contains(*obj.LabelPosition, "_RIGHT_") {
   133  			// label is on the right, and container is placed on the left
   134  			if strings.Contains(nearKeyStr, "left") {
   135  				x -= float64(obj.LabelDimensions.Width)
   136  			}
   137  		} else if strings.Contains(*obj.LabelPosition, "_BOTTOM_") {
   138  			// label is on the bottom, and container is placed on the top
   139  			if strings.Contains(nearKeyStr, "top") {
   140  				y -= float64(obj.LabelDimensions.Height)
   141  			}
   142  		}
   143  	}
   144  
   145  	return x, y
   146  }
   147  
   148  // boundingBox gets the center of the graph as defined by shapes
   149  // The bounds taking into consideration only shapes gives more of a feeling of true center
   150  // It differs from d2target.BoundingBox which needs to include every visible thing
   151  func boundingBox(g *d2graph.Graph) (tl, br *geo.Point) {
   152  	if len(g.Objects) == 0 {
   153  		return geo.NewPoint(0, 0), geo.NewPoint(0, 0)
   154  	}
   155  	x1 := math.Inf(1)
   156  	y1 := math.Inf(1)
   157  	x2 := math.Inf(-1)
   158  	y2 := math.Inf(-1)
   159  
   160  	for _, obj := range g.Objects {
   161  		if obj.NearKey != nil {
   162  			// Top left should not be MORE top than top-center
   163  			// But it should go more left if top-center label extends beyond bounds of diagram
   164  			switch d2graph.Key(obj.NearKey)[0] {
   165  			case "top-center", "bottom-center":
   166  				x1 = math.Min(x1, obj.TopLeft.X)
   167  				x2 = math.Max(x2, obj.TopLeft.X+obj.Width)
   168  			case "center-left", "center-right":
   169  				y1 = math.Min(y1, obj.TopLeft.Y)
   170  				y2 = math.Max(y2, obj.TopLeft.Y+obj.Height)
   171  			}
   172  		} else {
   173  			if obj.OuterNearContainer() != nil {
   174  				continue
   175  			}
   176  			x1 = math.Min(x1, obj.TopLeft.X)
   177  			y1 = math.Min(y1, obj.TopLeft.Y)
   178  			x2 = math.Max(x2, obj.TopLeft.X+obj.Width)
   179  			y2 = math.Max(y2, obj.TopLeft.Y+obj.Height)
   180  			if obj.Label.Value != "" && obj.LabelPosition != nil {
   181  				labelPosition := label.FromString(*obj.LabelPosition)
   182  				if labelPosition.IsOutside() {
   183  					labelTL := labelPosition.GetPointOnBox(obj.Box, label.PADDING, float64(obj.LabelDimensions.Width), float64(obj.LabelDimensions.Height))
   184  					x1 = math.Min(x1, labelTL.X)
   185  					y1 = math.Min(y1, labelTL.Y)
   186  					x2 = math.Max(x2, labelTL.X+float64(obj.LabelDimensions.Width))
   187  					y2 = math.Max(y2, labelTL.Y+float64(obj.LabelDimensions.Height))
   188  				}
   189  			}
   190  		}
   191  	}
   192  
   193  	if math.IsInf(x1, 1) && math.IsInf(x2, -1) {
   194  		x1 = 0
   195  		x2 = 0
   196  	}
   197  	if math.IsInf(y1, 1) && math.IsInf(y2, -1) {
   198  		y1 = 0
   199  		y2 = 0
   200  	}
   201  
   202  	return geo.NewPoint(x1, y1), geo.NewPoint(x2, y2)
   203  }
   204  

View as plain text