// d2near applies near keywords when they're constants // Intended to be run as the last stage of layout after the diagram has already undergone layout
package d2near import ( ) const pad = 20 type set map[string]struct{} var HorizontalCenterNears = set{ "center-left": {}, "center-right": {}, } var VerticalCenterNears = set{ "top-center": {}, "bottom-center": {}, } var NonCenterNears = set{ "top-left": {}, "top-right": {}, "bottom-left": {}, "bottom-right": {}, } // Layout finds the shapes which are assigned constant near keywords and places them. func ( context.Context, *d2graph.Graph, []*d2graph.Graph) error { if len() == 0 { return nil } for , := range { .Root.ChildrenArray[0].Parent = .Root for , := range .Objects { .Graph = } } // 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 , := range []set{VerticalCenterNears, HorizontalCenterNears, NonCenterNears} { for , := range { := .Root.ChildrenArray[0] , := [d2graph.Key(.NearKey)[0]] if { , := .TopLeft.X, .TopLeft.Y .TopLeft = geo.NewPoint(place()) , := .TopLeft.X-, .TopLeft.Y- for , := range .Objects { // `obj` already been replaced above by `place(obj)` if == { continue } .TopLeft.X += .TopLeft.Y += } for , := range .Edges { for , := range .Route { .X += .Y += } } } } for , := range { := .Root.ChildrenArray[0] , := [d2graph.Key(.NearKey)[0]] if { // The z-index for constant nears does not matter, as it will not collide .Objects = append(.Objects, .Objects...) if .Parent.Children == nil { .Parent.Children = make(map[string]*d2graph.Object) } .Parent.Children[strings.ToLower(.ID)] = .Parent.ChildrenArray = append(.Parent.ChildrenArray, ) .Edges = append(.Edges, .Edges...) } } } return nil } // place returns the position of obj, taking into consideration its near value and the diagram func place( *d2graph.Object) (float64, float64) { , := boundingBox(.Graph) := .X - .X := .Y - .Y := d2graph.Key(.NearKey)[0] var , float64 switch { case "top-left": , = .X-.Width-pad, .Y-.Height-pad case "top-center": , = .X+/2-.Width/2, .Y-.Height-pad case "top-right": , = .X+pad, .Y-.Height-pad case "center-left": , = .X-.Width-pad, .Y+/2-.Height/2 case "center-right": , = .X+pad, .Y+/2-.Height/2 case "bottom-left": , = .X-.Width-pad, .Y+pad case "bottom-center": , = .X-/2-.Width/2, .Y+pad case "bottom-right": , = .X+pad, .Y+pad } if .LabelPosition != nil && !strings.Contains(*.LabelPosition, "INSIDE") { if strings.Contains(*.LabelPosition, "_TOP_") { // label is on the top, and container is placed on the bottom if strings.Contains(, "bottom") { += float64(.LabelDimensions.Height) } } else if strings.Contains(*.LabelPosition, "_LEFT_") { // label is on the left, and container is placed on the right if strings.Contains(, "right") { += float64(.LabelDimensions.Width) } } else if strings.Contains(*.LabelPosition, "_RIGHT_") { // label is on the right, and container is placed on the left if strings.Contains(, "left") { -= float64(.LabelDimensions.Width) } } else if strings.Contains(*.LabelPosition, "_BOTTOM_") { // label is on the bottom, and container is placed on the top if strings.Contains(, "top") { -= float64(.LabelDimensions.Height) } } } return , } // boundingBox gets the center of the graph as defined by shapes // The bounds taking into consideration only shapes gives more of a feeling of true center // It differs from d2target.BoundingBox which needs to include every visible thing func boundingBox( *d2graph.Graph) (, *geo.Point) { if len(.Objects) == 0 { return geo.NewPoint(0, 0), geo.NewPoint(0, 0) } := math.Inf(1) := math.Inf(1) := math.Inf(-1) := math.Inf(-1) for , := range .Objects { if .NearKey != nil { // Top left should not be MORE top than top-center // But it should go more left if top-center label extends beyond bounds of diagram switch d2graph.Key(.NearKey)[0] { case "top-center", "bottom-center": = math.Min(, .TopLeft.X) = math.Max(, .TopLeft.X+.Width) case "center-left", "center-right": = math.Min(, .TopLeft.Y) = math.Max(, .TopLeft.Y+.Height) } } else { if .OuterNearContainer() != nil { continue } = math.Min(, .TopLeft.X) = math.Min(, .TopLeft.Y) = math.Max(, .TopLeft.X+.Width) = math.Max(, .TopLeft.Y+.Height) if .Label.Value != "" && .LabelPosition != nil { := label.FromString(*.LabelPosition) if .IsOutside() { := .GetPointOnBox(.Box, label.PADDING, float64(.LabelDimensions.Width), float64(.LabelDimensions.Height)) = math.Min(, .X) = math.Min(, .Y) = math.Max(, .X+float64(.LabelDimensions.Width)) = math.Max(, .Y+float64(.LabelDimensions.Height)) } } } } for , := range .Edges { if .Src.OuterNearContainer() != nil || .Dst.OuterNearContainer() != nil { continue } if .Route != nil { for , := range .Route { = math.Min(, .X) = math.Min(, .Y) = math.Max(, .X) = math.Max(, .Y) } } } if math.IsInf(, 1) && math.IsInf(, -1) { = 0 = 0 } if math.IsInf(, 1) && math.IsInf(, -1) { = 0 = 0 } return geo.NewPoint(, ), geo.NewPoint(, ) }