// Package d2elklayout adapts D2 graphs to the native Go port of ELK. // // Coordinates are relative to parents. // See https://www.eclipse.org/elk/documentation/tooldevelopers/graphdatastructure/coordinatesystem.html
package d2elklayout import ( elk ) type ELKNode struct { ID string `json:"id"` X float64 `json:"x"` Y float64 `json:"y"` Width float64 `json:"width"` Height float64 `json:"height"` Children []*ELKNode `json:"children,omitempty"` Ports []*ELKPort `json:"ports,omitempty"` Labels []*ELKLabel `json:"labels,omitempty"` LayoutOptions *elkOpts `json:"layoutOptions,omitempty"` } type PortSide string const ( South PortSide = "SOUTH" North PortSide = "NORTH" East PortSide = "EAST" West PortSide = "WEST" ) type Direction string const ( Down Direction = "DOWN" Up Direction = "UP" Right Direction = "RIGHT" Left Direction = "LEFT" ) type ELKPort struct { ID string `json:"id"` X float64 `json:"x"` Y float64 `json:"y"` Width float64 `json:"width"` Height float64 `json:"height"` LayoutOptions *elkOpts `json:"layoutOptions,omitempty"` } type ELKLabel struct { Text string `json:"text"` X float64 `json:"x"` Y float64 `json:"y"` Width float64 `json:"width"` Height float64 `json:"height"` LayoutOptions *elkOpts `json:"layoutOptions,omitempty"` } type ELKPoint struct { X float64 `json:"x"` Y float64 `json:"y"` } type ELKEdgeSection struct { Start ELKPoint `json:"startPoint"` End ELKPoint `json:"endPoint"` BendPoints []ELKPoint `json:"bendPoints,omitempty"` } type ELKEdge struct { ID string `json:"id"` Sources []string `json:"sources"` Targets []string `json:"targets"` Sections []ELKEdgeSection `json:"sections,omitempty"` Labels []*ELKLabel `json:"labels,omitempty"` Container string `json:"container"` } type ELKGraph struct { ID string `json:"id"` LayoutOptions *elkOpts `json:"layoutOptions"` Children []*ELKNode `json:"children,omitempty"` Edges []*ELKEdge `json:"edges,omitempty"` } type ConfigurableOpts struct { Algorithm string `json:"elk.algorithm,omitempty"` NodeSpacing int `json:"spacing.nodeNodeBetweenLayers,omitempty"` Padding string `json:"elk.padding,omitempty"` EdgeNodeSpacing int `json:"spacing.edgeNodeBetweenLayers,omitempty"` EdgeEdgeSpacing *int `json:"spacing.edgeEdgeBetweenLayers,omitempty"` SelfLoopSpacing int `json:"elk.spacing.nodeSelfLoop"` } const default_edge_edge_spacing = 50 var DefaultOpts = ConfigurableOpts{ Algorithm: "layered", NodeSpacing: 70.0, Padding: "[top=50,left=50,bottom=50,right=50]", EdgeNodeSpacing: 40.0, EdgeEdgeSpacing: go2.Pointer(default_edge_edge_spacing), SelfLoopSpacing: 50.0, } var port_spacing = 40. var edge_node_spacing = 40 type objectDegree struct { incoming float64 outgoing float64 } type selfLoopLabelSize struct { width int height int } // elkGraphStats contains the D2-specific graph-wide measurements used while // adapting a graph to ELK. Keeping them here avoids rescanning every edge for // every object and container. type elkGraphStats struct { degrees map[*d2graph.Object]objectDegree selfLoopLabelMax map[*d2graph.Object]selfLoopLabelSize } func collectELKGraphStats( *d2graph.Graph) elkGraphStats { := elkGraphStats{ degrees: make(map[*d2graph.Object]objectDegree, len(.Objects)), selfLoopLabelMax: make(map[*d2graph.Object]selfLoopLabelSize), } for , := range .Edges { := .degrees[.Src] .outgoing++ .degrees[.Src] = := .degrees[.Dst] .incoming++ .degrees[.Dst] = if .Src != .Dst || .Label.Value == "" || .Src.Parent == nil { continue } := .Src.Parent := .selfLoopLabelMax[] .width = go2.Max(.width, .LabelDimensions.Width) .height = go2.Max(.height, .LabelDimensions.Height) .selfLoopLabelMax[] = } return } func ( elkGraphStats) ( *d2graph.Object, bool) int { := .selfLoopLabelMax[] if { return .width } return .height } type elkOpts struct { EdgeNode int `json:"elk.spacing.edgeNode,omitempty"` FixedAlignment string `json:"elk.layered.nodePlacement.bk.fixedAlignment,omitempty"` Thoroughness int `json:"elk.layered.thoroughness,omitempty"` Direction Direction `json:"elk.direction"` HierarchyHandling string `json:"elk.hierarchyHandling,omitempty"` InlineEdgeLabels bool `json:"elk.edgeLabels.inline,omitempty"` ForceNodeModelOrder bool `json:"elk.layered.crossingMinimization.forceNodeModelOrder,omitempty"` ConsiderModelOrder string `json:"elk.layered.considerModelOrder.strategy,omitempty"` CycleBreakingStrategy string `json:"elk.layered.cycleBreaking.strategy,omitempty"` SelfLoopDistribution string `json:"elk.layered.edgeRouting.selfLoopDistribution,omitempty"` NodeSizeConstraints string `json:"elk.nodeSize.constraints,omitempty"` ContentAlignment string `json:"elk.contentAlignment,omitempty"` NodeSizeMinimum string `json:"elk.nodeSize.minimum,omitempty"` PortSide PortSide `json:"elk.port.side,omitempty"` PortConstraints string `json:"elk.portConstraints,omitempty"` ConfigurableOpts } func edgeEdgeSpacingOrDefault( *ConfigurableOpts) *int { // Callers predating this option leave the field nil, and D2 sent 50 before // it existed. Nil keeps that rather than dropping to ELK's default. if .EdgeEdgeSpacing == nil { return go2.Pointer(default_edge_edge_spacing) } return go2.Pointer(*.EdgeEdgeSpacing) } func newRootLayoutOptions( *ConfigurableOpts) *elkOpts { return &elkOpts{ Thoroughness: 8, EdgeNode: edge_node_spacing, HierarchyHandling: "INCLUDE_CHILDREN", FixedAlignment: "BALANCED", ConsiderModelOrder: "NODES_AND_EDGES", CycleBreakingStrategy: "GREEDY_MODEL_ORDER", NodeSizeConstraints: "MINIMUM_SIZE", ContentAlignment: "H_CENTER V_CENTER", ConfigurableOpts: ConfigurableOpts{ Algorithm: .Algorithm, NodeSpacing: .NodeSpacing, EdgeNodeSpacing: .EdgeNodeSpacing, EdgeEdgeSpacing: edgeEdgeSpacingOrDefault(), SelfLoopSpacing: .SelfLoopSpacing, }, } } func newContainerLayoutOptions( *ConfigurableOpts) *elkOpts { return &elkOpts{ Thoroughness: 8, HierarchyHandling: "INCLUDE_CHILDREN", FixedAlignment: "BALANCED", EdgeNode: edge_node_spacing, // ELK 0.12 crashes on compound subgraphs when model-order // processing is enabled below the root. Keep it disabled there. ConsiderModelOrder: "NONE", CycleBreakingStrategy: "GREEDY", NodeSizeConstraints: "MINIMUM_SIZE", ContentAlignment: "H_CENTER V_CENTER", ConfigurableOpts: ConfigurableOpts{ NodeSpacing: .NodeSpacing, EdgeNodeSpacing: .EdgeNodeSpacing, EdgeEdgeSpacing: edgeEdgeSpacingOrDefault(), SelfLoopSpacing: .SelfLoopSpacing, Padding: .Padding, }, } } func ( context.Context, *d2graph.Graph) ( error) { return Layout(, , nil) } func ( context.Context, *d2graph.Graph, *ConfigurableOpts) ( error) { if == nil { = context.Background() } if == nil { = &DefaultOpts } defer xdefer.Errorf(&, "failed to ELK layout") if := layoutguard.CheckGraph(, "ELK", ); != nil { return } := collectELKGraphStats() := &ELKGraph{ ID: "", LayoutOptions: newRootLayoutOptions(), } if .LayoutOptions.ConfigurableOpts.SelfLoopSpacing == DefaultOpts.SelfLoopSpacing { // +5 for a tiny bit of padding .LayoutOptions.ConfigurableOpts.SelfLoopSpacing = go2.Max(.LayoutOptions.ConfigurableOpts.SelfLoopSpacing, .maxSelfLoopLabel(.Root, .Root.Direction.Value == "down" || .Root.Direction.Value == "" || .Root.Direction.Value == "up")/2+5) } switch .Root.Direction.Value { case "down": .LayoutOptions.Direction = Down case "up": .LayoutOptions.Direction = Up case "right": .LayoutOptions.Direction = Right case "left": .LayoutOptions.Direction = Left default: .LayoutOptions.Direction = Down } // set label and icon positions for ELK for , := range .Objects { positionLabelsIcons() } := make(map[*d2graph.Object]geo.Spacing) := make(map[*d2graph.Object]*ELKNode) := make(map[*d2graph.Edge]*ELKEdge) // BFS var func(*d2graph.Object, *d2graph.Object, func(*d2graph.Object, *d2graph.Object)) = func(, *d2graph.Object, func(*d2graph.Object, *d2graph.Object)) { if .Parent != nil { (, ) } for , := range .ChildrenArray { (, , ) } } (.Root, nil, func(, *d2graph.Object) { := .degrees[] := .incoming := .outgoing if >= 2 || >= 2 { switch .Root.Direction.Value { case "right", "left": if .Attributes.HeightAttr == nil { .Height = math.Max(.Height, math.Max(, )*port_spacing) } default: if .Attributes.WidthAttr == nil { .Width = math.Max(.Width, math.Max(, )*port_spacing) } } } if .HasLabel() && .HasIcon() { // this gives shapes extra height for their label if they also have an icon .Height += float64(.LabelDimensions.Height + label.PADDING) } , := .SpacingOpt(label.PADDING, label.PADDING, false) := .Left + .Width + .Right := .Top + .Height + .Bottom [] = := &ELKNode{ ID: .AbsID(), Width: , Height: , } if len(.ChildrenArray) > 0 { .LayoutOptions = newContainerLayoutOptions() if .LayoutOptions.ConfigurableOpts.SelfLoopSpacing == DefaultOpts.SelfLoopSpacing { .LayoutOptions.ConfigurableOpts.SelfLoopSpacing = go2.Max(.LayoutOptions.ConfigurableOpts.SelfLoopSpacing, .maxSelfLoopLabel(, .Root.Direction.Value == "down" || .Root.Direction.Value == "" || .Root.Direction.Value == "up")/2+5) } switch .LayoutOptions.Direction { case Down, Up: .LayoutOptions.NodeSizeMinimum = fmt.Sprintf("(%d, %d)", int(math.Ceil()), int(math.Ceil())) case Right, Left: .LayoutOptions.NodeSizeMinimum = fmt.Sprintf("(%d, %d)", int(math.Ceil()), int(math.Ceil())) } } else { .LayoutOptions = &elkOpts{ SelfLoopDistribution: "EQUALLY", } } if .IsContainer() { := parsePadding(.Padding) = adjustPadding(, , , ) .LayoutOptions.Padding = .String() } if .HasLabel() { .Labels = append(.Labels, &ELKLabel{ Text: .Label.Value, Width: float64(.LabelDimensions.Width), Height: float64(.LabelDimensions.Height), }) } if == .Root { .Children = append(.Children, ) } else { [].Children = append([].Children, ) } if .SQLTable != nil { .LayoutOptions.PortConstraints = "FIXED_POS" := .SQLTable.Columns := .Height / float64(len()+1) .Ports = make([]*ELKPort, 0, len()*2) var , PortSide switch .LayoutOptions.Direction { case Left: , = West, East default: , = East, West } for , := range { .Ports = append(.Ports, &ELKPort{ ID: srcPortID(, .Name.Label), Y: float64(+1)* + /2, LayoutOptions: &elkOpts{PortSide: }, }) .Ports = append(.Ports, &ELKPort{ ID: dstPortID(, .Name.Label), Y: float64(+1)* + /2, LayoutOptions: &elkOpts{PortSide: }, }) } } [] = }) var , PortSide switch .LayoutOptions.Direction { case Up: , = North, South default: , = South, North } := map[struct { *d2graph.Object PortSide }][]*ELKPort{} for , := range .Edges { var , string switch { case .SrcTableColumnIndex != nil: = srcPortID(.Src, .Src.SQLTable.Columns[*.SrcTableColumnIndex].Name.Label) case .Src.SQLTable != nil: := &ELKPort{ ID: fmt.Sprintf("%s.%d", srcPortID(.Src, "__root__"), ), LayoutOptions: &elkOpts{PortSide: }, } = .ID [.Src].Ports = append([.Src].Ports, ) := struct { *d2graph.Object PortSide }{.Src, } [] = append([], ) default: = .Src.AbsID() } switch { case .DstTableColumnIndex != nil: = dstPortID(.Dst, .Dst.SQLTable.Columns[*.DstTableColumnIndex].Name.Label) case .Dst.SQLTable != nil: := &ELKPort{ ID: fmt.Sprintf("%s.%d", dstPortID(.Dst, "__root__"), ), LayoutOptions: &elkOpts{PortSide: }, } = .ID [.Dst].Ports = append([.Dst].Ports, ) := struct { *d2graph.Object PortSide }{.Dst, } [] = append([], ) default: = .Dst.AbsID() } := &ELKEdge{ ID: .AbsID(), Sources: []string{}, Targets: []string{}, } if .Label.Value != "" { .Labels = append(.Labels, &ELKLabel{ Text: .Label.Value, Width: float64(.LabelDimensions.Width), Height: float64(.LabelDimensions.Height), LayoutOptions: &elkOpts{ InlineEdgeLabels: true, }, }) } .Edges = append(.Edges, ) [] = } for , := range { := [.].Width := / float64(len()+1) for , := range { .X = float64(+1) * } } , := json.Marshal() if != nil { return } if := .Err(); != nil { return } // ELK does not accept a context. Check immediately around the call; the // topology preflight above bounds inputs before entering this section. , := elk.LayoutJSON() if != nil { return fmt.Errorf("native ELK layout failed: %w", ) } if := .Err(); != nil { return } = json.Unmarshal(, &) if != nil { return } := make(map[string]*d2graph.Object) (.Root, nil, func(, *d2graph.Object) { := [] := 0.0 := 0.0 if != nil && != .Root { = .TopLeft.X = .TopLeft.Y } .TopLeft = geo.NewPoint(+.X, +.Y) .Width = math.Ceil(.Width) .Height = math.Ceil(.Height) [.AbsID()] = }) for , := range .Edges { := [] := 0.0 := 0.0 if .Container != "" { = [.Container].TopLeft.X = [.Container].TopLeft.Y } var []*geo.Point for , := range .Sections { = append(, &geo.Point{ X: + .Start.X, Y: + .Start.Y, }) for , := range .BendPoints { = append(, &geo.Point{ X: + .X, Y: + .Y, }) } = append(, &geo.Point{ X: + .End.X, Y: + .End.Y, }) } .Route = } := make(map[*d2graph.Object][]*d2graph.Edge) for , := range .Edges { [.Src] = append([.Src], ) if .Dst != .Src { [.Dst] = append([.Dst], ) } } := newDescendantShifter() for , := range .Objects { if , := []; { := [] // also move edges with the shrinking sides if .Left > 0 { for , := range { := len(.Route) if .Src == && .Route[0].X == .TopLeft.X { .Route[0].X += .Left } if .Dst == && .Route[-1].X == .TopLeft.X { .Route[-1].X += .Left } } .TopLeft.X += .Left .shift(, .Left/2, 0) .Width -= .Left } if .Right > 0 { for , := range { := len(.Route) if .Src == && .Route[0].X == .TopLeft.X+.Width { .Route[0].X -= .Right } if .Dst == && .Route[-1].X == .TopLeft.X+.Width { .Route[-1].X -= .Right } } .shift(, -.Right/2, 0) .Width -= .Right } if .Top > 0 { for , := range { := len(.Route) if .Src == && .Route[0].Y == .TopLeft.Y { .Route[0].Y += .Top } if .Dst == && .Route[-1].Y == .TopLeft.Y { .Route[-1].Y += .Top } } .TopLeft.Y += .Top .shift(, 0, .Top/2) .Height -= .Top } if .Bottom > 0 { for , := range { := len(.Route) if .Src == && .Route[0].Y == .TopLeft.Y+.Height { .Route[0].Y -= .Bottom } if .Dst == && .Route[-1].Y == .TopLeft.Y+.Height { .Route[-1].Y -= .Bottom } } .shift(, 0, -.Bottom/2) .Height -= .Bottom } } } for , := range .Edges { := .Route , := 0, len()-1 := [] := [] var , *geo.Point // if the edge passes through 3d/multiple, use the offset box for tracing to border if , := .Src.GetModifierElementAdjustments(); != 0 || != 0 { if .X > .Src.TopLeft.X+ && .Y < .Src.TopLeft.Y+.Src.Height- { = .Src.TopLeft.Copy() .Src.TopLeft.X += .Src.TopLeft.Y -= } } if , := .Dst.GetModifierElementAdjustments(); != 0 || != 0 { if .X > .Dst.TopLeft.X+ && .Y < .Dst.TopLeft.Y+.Dst.Height- { = .Dst.TopLeft.Copy() .Dst.TopLeft.X += .Dst.TopLeft.Y -= } } , = .TraceToShape(, , ) = [ : +1] if .Label.Value != "" { .LabelPosition = go2.Pointer(label.InsideMiddleCenter.String()) } .Route = // undo 3d/multiple offset if != nil { .Src.TopLeft.X = .X .Src.TopLeft.Y = .Y } if != nil { .Dst.TopLeft.X = .X .Dst.TopLeft.Y = .Y } } deleteBends() deconflictSelfLoopLabels() return nil } func srcPortID( *d2graph.Object, string) string { return fmt.Sprintf("%s.%s.src", .AbsID(), ) } func dstPortID( *d2graph.Object, string) string { return fmt.Sprintf("%s.%s.dst", .AbsID(), ) } // deconflictSelfLoopLabels keeps the usual centered label position unless two // labels on self loops around the same object materially overlap. Newer ELK // versions can place multiple fixed-port loops on the same side of an object, // making both route midpoints coincide. Moving only the later label preserves // the routed geometry and existing output for non-overlapping loops. func deconflictSelfLoopLabels( *d2graph.Graph) { const = float64(label.PADDING) := [...]float64{0.4, 0.6, 0.3, 0.7, 0.2, 0.8, 0.1, 0.9} := make(map[*d2graph.Object][]*geo.Box) for , := range .Edges { if .Src != .Dst || .Label.Value == "" || .LabelPosition == nil || len(.Route) < 2 { continue } := label.FromString(*.LabelPosition) := 0.0 if .LabelPercentage != nil { = *.LabelPercentage } := edgeLabelBox(, , ) if == nil { continue } := [.Src] if materiallyOverlapsAny(, , ) { for , := range { := edgeLabelBox(, label.UnlockedMiddle, ) if == nil || overlapsAny(, ) { continue } .LabelPosition = go2.Pointer(label.UnlockedMiddle.String()) .LabelPercentage = go2.Pointer() = break } } [.Src] = append(, ) } } func edgeLabelBox( *d2graph.Edge, label.Position, float64) *geo.Box { , := .GetPointOnRoute( geo.Route(.Route), 0, , float64(.LabelDimensions.Width), float64(.LabelDimensions.Height), ) if == nil { return nil } return geo.NewBox(, float64(.LabelDimensions.Width), float64(.LabelDimensions.Height)) } func materiallyOverlapsAny( *geo.Box, []*geo.Box, float64) bool { for , := range { := math.Min(.TopLeft.X+.Width, .TopLeft.X+.Width) - math.Max(.TopLeft.X, .TopLeft.X) := math.Min(.TopLeft.Y+.Height, .TopLeft.Y+.Height) - math.Max(.TopLeft.Y, .TopLeft.Y) if > && > { return true } } return false } func overlapsAny( *geo.Box, []*geo.Box) bool { for , := range { if .Overlaps(*) { return true } } return false } // deleteBends is a shim for ELK to delete unnecessary bends // see https://github.com/terrastruct/d2/issues/1030 func deleteBends( *d2graph.Graph) { := false for , := range .Edges { if .Src != .Dst && len(.Route) >= 4 { = true break } } if ! { return } := newBendCollisionIndex() // Get rid of S-shapes at the source and the target // TODO there might be value in repeating this. removal of an S shape introducing another S shape that can still be removed for , := range []bool{true, false} { for , := range .Edges { if len(.Route) < 4 { continue } if .Src == .Dst { continue } var *d2graph.Object var *geo.Point var *geo.Point var *geo.Point var *int if { = .Route[0] = .Route[1] = .Route[2] = .Src = .SrcTableColumnIndex } else { = .Route[len(.Route)-1] = .Route[len(.Route)-2] = .Route[len(.Route)-3] = .Dst = .DstTableColumnIndex } := math.Ceil(.Y) == math.Ceil(.Y) , := .GetModifierElementAdjustments() // Make sure it's still attached switch { case != nil: := .Height / float64(len(.SQLTable.Columns)+1) := .TopLeft.Y + *float64(*+1) + /2 // for row connections new Y coordinate should be within 1/3 row height from the row center if math.Abs(.Y-) > /3 { continue } case : if .Y <= .TopLeft.Y+10- { continue } if .Y >= .TopLeft.Y+.Height-10 { continue } default: if .X <= .TopLeft.X+10 { continue } if .X >= .TopLeft.X+.Width-10+ { continue } } var *geo.Point if { = geo.NewPoint(.X, .Y) } else { = geo.NewPoint(.X, .Y) } := shape.NewShape(d2target.DSL_SHAPE_TO_SHAPE_TYPE[strings.ToLower(.Shape.Value)], .Box) = shape.TraceToShapeBorder(, , ) // Check that the new segment doesn't collide with anything new := geo.NewSegment(, ) := geo.NewSegment(, ) := .countObjectIntersects(.Src, .Dst, *) := .countObjectIntersects(.Src, .Dst, *) if > { continue } , , , := .countEdgeIntersects(.Edges[], *) , , , := .countEdgeIntersects(.Edges[], *) if > { continue } if > { continue } if > { continue } if > { continue } // commit if { .Edges[].Route = append( []*geo.Point{}, .Route[3:]..., ) } else { .Edges[].Route = append( .Route[:len(.Route)-3], , ) } .updateEdge(.Edges[]) } } // Get rid of ladders // ELK likes to do these for some reason // . ┌─ // . ┌─┘ // . │ // We want to transform these into L-shapes := map[geo.Point]int{} for , := range .Edges { for , := range .Route { [*]++ } } for , := range .Edges { if len(.Route) < 6 { continue } if .Src == .Dst { continue } for := 1; < len(.Route)-3; ++ { := .Route[-1] := .Route[] := .Route[+1] := .Route[+2] := .Route[+3] if , := [*]; > 1 { // If corner is shared with another edge, they merge continue } // S-shape on sources only concerned one segment, since the other was just along the bound of endpoint // These concern two segments var *geo.Point if math.Ceil(.X) == math.Ceil(.X) { = geo.NewPoint(.X, .Y) // not ladder if (.X > .X) != (.X > .X) { continue } if (.Y > .Y) != (.Y > .Y) { continue } } else { = geo.NewPoint(.X, .Y) if (.Y > .Y) != (.Y > .Y) { continue } if (.X > .X) != (.X > .X) { continue } } := geo.NewSegment(, ) := geo.NewSegment(, ) := geo.NewSegment(, ) := geo.NewSegment(, ) // Check that the new segments doesn't collide with anything new := .countObjectIntersects(.Src, .Dst, *) + .countObjectIntersects(.Src, .Dst, *) := .countObjectIntersects(.Src, .Dst, *) + .countObjectIntersects(.Src, .Dst, *) if > { continue } , , , := .countEdgeIntersects(.Edges[], *) , , , := .countEdgeIntersects(.Edges[], *) := + := + := + := + , , , := .countEdgeIntersects(.Edges[], *) , , , := .countEdgeIntersects(.Edges[], *) := + := + := + := + if > { continue } if > { continue } if > { continue } if > { continue } // commit .Edges[].Route = append(append( .Route[:], , ), .Route[+3:]..., ) .updateEdge(.Edges[]) break } } } type shapePadding struct { top, left, bottom, right int } // parse out values from elk padding string. e.g. "[top=50,left=50,bottom=50,right=50]" func parsePadding( string) shapePadding { := regexp.MustCompile(`top=(\d+)`) := regexp.MustCompile(`left=(\d+)`) := regexp.MustCompile(`bottom=(\d+)`) := regexp.MustCompile(`right=(\d+)`) := shapePadding{} := .FindStringSubmatch() if len() == 2 { , := strconv.ParseInt([1], 10, 64) if == nil { .top = int() } } = .FindStringSubmatch() if len() == 2 { , := strconv.ParseInt([1], 10, 64) if == nil { .left = int() } } = .FindStringSubmatch() if len() == 2 { , := strconv.ParseInt([1], 10, 64) if == nil { .bottom = int() } } = .FindStringSubmatch() , := strconv.ParseInt([1], 10, 64) if len() == 2 { if == nil { .right = int() } } return } func ( shapePadding) () string { return fmt.Sprintf("[top=%d,left=%d,bottom=%d,right=%d]", .top, .left, .bottom, .right) } func adjustPadding( *d2graph.Object, , float64, shapePadding) shapePadding { if !.IsContainer() { return } // compute extra space padding for label/icon var , , , int if .HasLabel() && .LabelPosition != nil { := .LabelDimensions.Height + 2*label.PADDING := .LabelDimensions.Width + 2*label.PADDING switch label.FromString(*.LabelPosition) { case label.InsideTopLeft, label.InsideTopCenter, label.InsideTopRight: // Note: for corners we only add height = case label.InsideBottomLeft, label.InsideBottomCenter, label.InsideBottomRight: = case label.InsideMiddleLeft: = case label.InsideMiddleRight: = } } if .HasIcon() && .IconPosition != nil { := d2target.MAX_ICON_SIZE + 2*label.PADDING switch label.FromString(*.IconPosition) { case label.InsideTopLeft, label.InsideTopCenter, label.InsideTopRight: = go2.Max(, ) case label.InsideBottomLeft, label.InsideBottomCenter, label.InsideBottomRight: = go2.Max(, ) case label.InsideMiddleLeft: = go2.Max(, ) case label.InsideMiddleRight: = go2.Max(, ) } } , := math.Inf(-1), math.Inf(-1) for , := range .ChildrenArray { if .Width > { = .Width } if .Height > { = .Height } } // We don't know exactly what the shape dimensions will be after layout, but for more accurate innerBox dimensions, // we add the maxChildWidth and maxChildHeight with computed additions for the innerBox calculation += + float64(+) += + float64(+) := geo.NewBox(geo.NewPoint(0, 0), , ) := d2target.DSL_SHAPE_TO_SHAPE_TYPE[.Shape.Value] := shape.NewShape(, ) := .GetInnerBox() // If the shape inner box + label/icon height becomes greater than the default padding, we want to use that // // ┌OUTER───────────────────────────┬────────────────────────────────────────────┐ // │ │ │ // │ ┌INNER──────── ┬ ─────────────│───────────────────────────────────────┐ │ // │ │ │Label Padding │ │ │ // │ │ ┌LABEL─ ┴ ─────────────│───────┐┬ ┌ICON── ┬ ────┐ │ │ // │ │ │ │ ││ │ │ │ │ │ // │ │ │ │ ││Label Height │ Icon│ │ │ │ // │ │ │ │ ││ │ Height│ │ │ │ // │ │ └──────────────────────│───────┘┴ │ │ │ │ │ // │ │ │ └────── ┴ ────┘ │ │ // │ │ │ │ │ // │ │ ┴Default ELK Padding │ │ // │ │ ┌CHILD────────────────────────────────────────────────────────┐ │ │ // │ │ │ │ │ │ // │ │ │ │ │ │ // │ │ │ │ │ │ // │ │ └─────────────────────────────────────────────────────────────┘ │ │ // │ │ │ │ // │ └─────────────────────────────────────────────────────────────────────┘ │ // │ │ // └─────────────────────────────────────────────────────────────────────────────┘ // estimated shape innerBox padding := int(math.Ceil(.TopLeft.Y)) := int(math.Ceil( - (.TopLeft.Y + .Height))) := int(math.Ceil(.TopLeft.X)) := int(math.Ceil( - (.TopLeft.X + .Width))) .top = go2.Max(.top, +) .bottom = go2.Max(.bottom, +) .left = go2.Max(.left, +) .right = go2.Max(.right, +) return } func positionLabelsIcons( *d2graph.Object) { if .Icon != nil && .IconPosition == nil { if len(.ChildrenArray) > 0 { .IconPosition = go2.Pointer(label.InsideTopLeft.String()) if .LabelPosition == nil { .LabelPosition = go2.Pointer(label.InsideTopRight.String()) return } } else if .SQLTable != nil || .Class != nil || .Language != "" { .IconPosition = go2.Pointer(label.OutsideTopLeft.String()) } else { .IconPosition = go2.Pointer(label.InsideMiddleCenter.String()) } } if .HasLabel() && .LabelPosition == nil { if len(.ChildrenArray) > 0 { .LabelPosition = go2.Pointer(label.InsideTopCenter.String()) } else if .HasOutsideBottomLabel() { .LabelPosition = go2.Pointer(label.OutsideBottomCenter.String()) } else if .Icon != nil { .LabelPosition = go2.Pointer(label.InsideTopCenter.String()) } else { .LabelPosition = go2.Pointer(label.InsideMiddleCenter.String()) } if float64(.LabelDimensions.Width) > .Width || float64(.LabelDimensions.Height) > .Height { if len(.ChildrenArray) > 0 { .LabelPosition = go2.Pointer(label.OutsideTopCenter.String()) } else { .LabelPosition = go2.Pointer(label.OutsideBottomCenter.String()) } } } }