// d2elklayout is a wrapper around the Javascript port of ELK. // // Coordinates are relative to parents. // See https://www.eclipse.org/elk/documentation/tooldevelopers/graphdatastructure/coordinatesystem.html
package d2elklayout import ( _ ) //go:embed setup.js var setupJS string 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"` SelfLoopSpacing int `json:"elk.spacing.nodeSelfLoop"` } var DefaultOpts = ConfigurableOpts{ Algorithm: "layered", NodeSpacing: 70.0, Padding: "[top=50,left=50,bottom=50,right=50]", EdgeNodeSpacing: 40.0, SelfLoopSpacing: 50.0, } var port_spacing = 40. var edge_node_spacing = 40 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"` EdgeEdgeBetweenLayersSpacing int `json:"elk.layered.spacing.edgeEdgeBetweenLayers,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 ( context.Context, *d2graph.Graph) ( error) { return Layout(, , nil) } func ( context.Context, *d2graph.Graph, *ConfigurableOpts) ( error) { if == nil { = &DefaultOpts } defer xdefer.Errorf(&, "failed to ELK layout") := jsrunner.NewJSRunner() if .Engine() == jsrunner.Goja { := .NewObject() if := .Set("console", ); != nil { return } if , := .RunString(elkJS); != nil { return } if , := .RunString(setupJS); != nil { return } } := &ELKGraph{ ID: "", LayoutOptions: &elkOpts{ Thoroughness: 8, EdgeEdgeBetweenLayersSpacing: 50, 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, SelfLoopSpacing: .SelfLoopSpacing, }, }, } if .LayoutOptions.ConfigurableOpts.SelfLoopSpacing == DefaultOpts.SelfLoopSpacing { // +5 for a tiny bit of padding .LayoutOptions.ConfigurableOpts.SelfLoopSpacing = go2.Max(.LayoutOptions.ConfigurableOpts.SelfLoopSpacing, childrenMaxSelfLoop(.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) { := 0. := 0. for , := range .Edges { if .Src == { ++ } if .Dst == { ++ } } 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 = &elkOpts{ ForceNodeModelOrder: true, Thoroughness: 8, EdgeEdgeBetweenLayersSpacing: 50, HierarchyHandling: "INCLUDE_CHILDREN", FixedAlignment: "BALANCED", EdgeNode: edge_node_spacing, ConsiderModelOrder: "NODES_AND_EDGES", CycleBreakingStrategy: "GREEDY_MODEL_ORDER", NodeSizeConstraints: "MINIMUM_SIZE", ContentAlignment: "H_CENTER V_CENTER", ConfigurableOpts: ConfigurableOpts{ NodeSpacing: .NodeSpacing, EdgeNodeSpacing: .EdgeNodeSpacing, SelfLoopSpacing: .SelfLoopSpacing, Padding: .Padding, }, } if .LayoutOptions.ConfigurableOpts.SelfLoopSpacing == DefaultOpts.SelfLoopSpacing { .LayoutOptions.ConfigurableOpts.SelfLoopSpacing = go2.Max(.LayoutOptions.ConfigurableOpts.SelfLoopSpacing, childrenMaxSelfLoop(, .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 } var jsrunner.JSValue if .Engine() == jsrunner.Goja { := fmt.Sprintf(`var graph = %s`, ) if , := .RunString(); != nil { return } , = .RunString(`elk.layout(graph) .then(s => s) .catch(err => err.message) `) } else { , = .MustGet("elkResult") } if != nil { return } , := .WaitPromise(, ) if != nil { return fmt.Errorf("ELK layout error: %v", ) } var map[string]interface{} switch out := .(type) { case string: return fmt.Errorf("ELK layout error: %s", ) case map[string]interface{}: = default: return fmt.Errorf("ELK unexpected return: %v", ) } , := json.Marshal() if != 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], ) } } 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 .ShiftDescendants(.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 } } .ShiftDescendants(-.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 .ShiftDescendants(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 } } .ShiftDescendants(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() 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(), ) } // deleteBends is a shim for ELK to delete unnecessary bends // see https://github.com/terrastruct/d2/issues/1030 func deleteBends( *d2graph.Graph) { // 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], , ) } } } // 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:]..., ) break } } } func countObjectIntersects( *d2graph.Graph, , *d2graph.Object, geo.Segment) int { := 0 for , := range .Objects { if .Objects[] == || .Objects[] == { continue } if .Intersects(, float64(edge_node_spacing)-1) { ++ } } return } // countEdgeIntersects counts both crossings AND getting too close to a parallel segment func countEdgeIntersects( *d2graph.Graph, *d2graph.Edge, geo.Segment) (int, int, int, int) { := math.Ceil(.Start.Y) == math.Ceil(.End.Y) := 0 := 0 := 0 := 0 for , := range .Edges { if .Edges[] == { continue } for := 0; < len(.Route)-1; ++ { := geo.NewSegment(.Route[], .Route[+1]) := math.Ceil(.Start.Y) == math.Ceil(.End.Y) if == { if .Overlaps(*, !, 0.) { if { if math.Abs(.Start.Y-.Start.Y) < float64(edge_node_spacing)/2. { ++ if math.Abs(.Start.Y-.Start.Y) < float64(edge_node_spacing)/4. { ++ if math.Abs(.Start.Y-.Start.Y) < 1. { ++ } } } } else { if math.Abs(.Start.X-.Start.X) < float64(edge_node_spacing)/2. { ++ if math.Abs(.Start.X-.Start.X) < float64(edge_node_spacing)/4. { ++ if math.Abs(.Start.Y-.Start.Y) < 1. { ++ } } } } } } else { if .Intersects(*) { ++ } } } } return , , , } func childrenMaxSelfLoop( *d2graph.Object, bool) int { := 0 for , := range .Children { for , := range .Graph.Edges { if .Src == .Dst && .Src == && .Label.Value != "" { if { = go2.Max(, .LabelDimensions.Width) } else { = go2.Max(, .LabelDimensions.Height) } } } } return } 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()) } } } }