2023-09-14 20:38:59 +00:00
|
|
|
package d2layouts
|
|
|
|
|
|
2023-09-14 21:17:18 +00:00
|
|
|
import (
|
2023-09-15 21:44:20 +00:00
|
|
|
"context"
|
2023-09-15 00:00:44 +00:00
|
|
|
"math"
|
2023-09-14 22:38:52 +00:00
|
|
|
"strings"
|
|
|
|
|
|
2023-09-16 05:07:06 +00:00
|
|
|
"cdr.dev/slog"
|
2023-09-14 21:17:18 +00:00
|
|
|
"oss.terrastruct.com/d2/d2graph"
|
2023-09-15 21:44:20 +00:00
|
|
|
"oss.terrastruct.com/d2/d2layouts/d2grid"
|
|
|
|
|
"oss.terrastruct.com/d2/d2layouts/d2near"
|
|
|
|
|
"oss.terrastruct.com/d2/d2layouts/d2sequence"
|
2023-09-15 00:00:44 +00:00
|
|
|
"oss.terrastruct.com/d2/lib/geo"
|
2023-09-16 05:07:06 +00:00
|
|
|
"oss.terrastruct.com/d2/lib/log"
|
2023-09-14 21:17:18 +00:00
|
|
|
)
|
2023-09-14 20:38:59 +00:00
|
|
|
|
2023-09-15 21:44:20 +00:00
|
|
|
type DiagramType string
|
2023-09-14 21:17:18 +00:00
|
|
|
|
2023-09-15 21:44:20 +00:00
|
|
|
// a grid diagram at a constant near is
|
2023-09-14 21:17:18 +00:00
|
|
|
const (
|
2023-09-15 21:44:20 +00:00
|
|
|
DefaultGraphType DiagramType = ""
|
|
|
|
|
ConstantNearGraph DiagramType = "constant-near"
|
|
|
|
|
GridDiagram DiagramType = "grid-diagram"
|
|
|
|
|
SequenceDiagram DiagramType = "sequence-diagram"
|
2023-09-14 21:17:18 +00:00
|
|
|
)
|
|
|
|
|
|
2023-09-15 21:44:20 +00:00
|
|
|
type GraphInfo struct {
|
|
|
|
|
IsConstantNear bool
|
|
|
|
|
DiagramType DiagramType
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (gi GraphInfo) isDefault() bool {
|
|
|
|
|
return !gi.IsConstantNear && gi.DiagramType == DefaultGraphType
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func LayoutNested(ctx context.Context, g *d2graph.Graph, graphInfo GraphInfo, coreLayout d2graph.LayoutGraph) geo.Spacing {
|
|
|
|
|
|
2023-09-16 05:07:06 +00:00
|
|
|
log.Warn(ctx, "ln info", slog.F("gi", graphInfo))
|
2023-09-14 20:38:59 +00:00
|
|
|
// Before we can layout these nodes, we need to handle all nested diagrams first.
|
|
|
|
|
extracted := make(map[*d2graph.Object]*d2graph.Graph)
|
2023-09-15 21:44:20 +00:00
|
|
|
extractedInfo := make(map[*d2graph.Object]GraphInfo)
|
|
|
|
|
|
|
|
|
|
var constantNears []*d2graph.Graph
|
2023-09-14 20:38:59 +00:00
|
|
|
|
|
|
|
|
// Iterate top-down from Root so all nested diagrams can process their own contents
|
|
|
|
|
queue := make([]*d2graph.Object, 0, len(g.Root.ChildrenArray))
|
2023-09-15 21:44:20 +00:00
|
|
|
if graphInfo.IsConstantNear {
|
|
|
|
|
near := g.Root.ChildrenArray[0]
|
|
|
|
|
if len(near.Children) > 0 {
|
|
|
|
|
queue = append(queue, near.ChildrenArray...)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
queue = append(queue, g.Root.ChildrenArray...)
|
|
|
|
|
}
|
2023-09-14 20:38:59 +00:00
|
|
|
|
|
|
|
|
for _, child := range queue {
|
2023-09-15 21:44:20 +00:00
|
|
|
if gi := NestedGraphInfo(child); !gi.isDefault() {
|
|
|
|
|
extractedInfo[child] = gi
|
2023-09-16 05:07:06 +00:00
|
|
|
// log.Warn(ctx, "nested", slog.F("child", child.AbsID()), slog.F("gi", gi))
|
2023-09-14 20:38:59 +00:00
|
|
|
|
2023-09-16 05:07:06 +00:00
|
|
|
// There is a nested diagram here, so extract its contents and process in the same way
|
|
|
|
|
nestedGraph := ExtractDescendants(child)
|
2023-09-14 20:38:59 +00:00
|
|
|
|
2023-09-15 21:44:20 +00:00
|
|
|
// Layout of nestedGraph is completed
|
2023-09-16 05:07:06 +00:00
|
|
|
// log.Error(ctx, "recurse", slog.F("child", child.AbsID()), slog.F("level", child.Level()))
|
2023-09-15 21:44:20 +00:00
|
|
|
spacing := LayoutNested(ctx, nestedGraph, gi, coreLayout)
|
2023-09-16 05:07:06 +00:00
|
|
|
log.Warn(ctx, "fitting child", slog.F("child", child.AbsID()))
|
|
|
|
|
// Fit child to size of nested layout
|
|
|
|
|
FitToGraph(child, nestedGraph, spacing)
|
|
|
|
|
|
|
|
|
|
var nearGraph *d2graph.Graph
|
|
|
|
|
if gi.IsConstantNear {
|
|
|
|
|
nearGraph = ExtractSelf(child)
|
|
|
|
|
child.TopLeft = geo.NewPoint(0, 0)
|
|
|
|
|
child.Width = nestedGraph.Root.Width
|
|
|
|
|
child.Width = nestedGraph.Root.Height
|
2023-09-15 21:44:20 +00:00
|
|
|
}
|
2023-09-14 20:38:59 +00:00
|
|
|
|
2023-09-16 05:07:06 +00:00
|
|
|
// if gi.IsConstantNear {
|
|
|
|
|
// // FitToGraph(child, nestedGraph, spacing)
|
|
|
|
|
// if nestedGraph.Root.Box != nil {
|
|
|
|
|
// child.Width = nestedGraph.Root.Width
|
|
|
|
|
// child.Height = nestedGraph.Root.Height
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
|
2023-09-14 20:38:59 +00:00
|
|
|
// We will restore the contents after running layout with child as the placeholder
|
2023-09-15 21:44:20 +00:00
|
|
|
if gi.IsConstantNear {
|
2023-09-16 05:07:06 +00:00
|
|
|
constantNears = append(constantNears, nearGraph)
|
2023-09-15 21:44:20 +00:00
|
|
|
}
|
2023-09-16 05:07:06 +00:00
|
|
|
extracted[child] = nestedGraph
|
2023-09-14 20:38:59 +00:00
|
|
|
} else if len(child.Children) > 0 {
|
|
|
|
|
queue = append(queue, child.ChildrenArray...)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We can now run layout with accurate sizes of nested layout containers
|
|
|
|
|
// Layout according to the type of diagram
|
2023-09-15 21:44:20 +00:00
|
|
|
LayoutDiagram := func(ctx context.Context, g *d2graph.Graph, graphInfo GraphInfo, coreLayout d2graph.LayoutGraph) geo.Spacing {
|
|
|
|
|
spacing := geo.Spacing{}
|
|
|
|
|
var err error
|
|
|
|
|
// TODO
|
|
|
|
|
|
|
|
|
|
switch graphInfo.DiagramType {
|
|
|
|
|
case GridDiagram:
|
2023-09-16 05:07:06 +00:00
|
|
|
log.Warn(ctx, "layout grid", slog.F("rootlevel", g.RootLevel), slog.F("shapes", g.PrintString()))
|
|
|
|
|
// layoutWithGrids := d2grid.Layout2(ctx, g, coreLayout)
|
|
|
|
|
// layoutWithGrids(ctx, g)
|
|
|
|
|
if err = d2grid.Layout2(ctx, g); err != nil {
|
2023-09-15 21:44:20 +00:00
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case SequenceDiagram:
|
2023-09-16 05:07:06 +00:00
|
|
|
log.Warn(ctx, "layout sequence", slog.F("rootlevel", g.RootLevel), slog.F("shapes", g.PrintString()))
|
2023-09-15 23:50:27 +00:00
|
|
|
err = d2sequence.Layout2(ctx, g, coreLayout)
|
2023-09-15 21:44:20 +00:00
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
default:
|
2023-09-16 05:07:06 +00:00
|
|
|
log.Warn(ctx, "default layout", slog.F("rootlevel", g.RootLevel), slog.F("shapes", g.PrintString()))
|
2023-09-15 21:44:20 +00:00
|
|
|
err := coreLayout(ctx, g)
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return spacing
|
|
|
|
|
}
|
|
|
|
|
spacing := LayoutDiagram(ctx, g, graphInfo, coreLayout)
|
2023-09-14 20:38:59 +00:00
|
|
|
|
2023-09-15 21:44:20 +00:00
|
|
|
// if there are
|
|
|
|
|
if len(constantNears) > 0 {
|
|
|
|
|
err := d2near.Layout(ctx, g, constantNears)
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
2023-09-14 20:38:59 +00:00
|
|
|
}
|
2023-09-15 00:00:44 +00:00
|
|
|
|
2023-09-16 05:07:06 +00:00
|
|
|
// With the layout set, inject all the extracted graphs
|
|
|
|
|
for n, nestedGraph := range extracted {
|
|
|
|
|
// if !extractedInfo[n].IsConstantNear {
|
|
|
|
|
InjectNested(n, nestedGraph)
|
|
|
|
|
PositionNested(n, nestedGraph)
|
|
|
|
|
// }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log.Warn(ctx, "done", slog.F("rootlevel", g.RootLevel))
|
2023-09-15 00:00:44 +00:00
|
|
|
return spacing
|
2023-09-14 20:38:59 +00:00
|
|
|
}
|
|
|
|
|
|
2023-09-15 21:44:20 +00:00
|
|
|
// TODO multiple types at same (e.g. constant nears with grid at root level)
|
|
|
|
|
// e.g. constant nears with sequence diagram at root level
|
|
|
|
|
func NestedGraphInfo(obj *d2graph.Object) (gi GraphInfo) {
|
2023-09-14 21:55:52 +00:00
|
|
|
if obj.Graph.RootLevel == 0 && obj.IsConstantNear() {
|
2023-09-15 21:44:20 +00:00
|
|
|
gi.IsConstantNear = true
|
2023-09-14 21:17:18 +00:00
|
|
|
}
|
2023-09-15 21:44:20 +00:00
|
|
|
// if obj.Graph.RootLevel == -1 {
|
|
|
|
|
// for _, obj := range obj.Graph.Root.ChildrenArray {
|
|
|
|
|
// if obj.IsConstantNear() {
|
|
|
|
|
// return ConstantNearGraph
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
// }
|
2023-09-14 21:55:52 +00:00
|
|
|
if obj.IsSequenceDiagram() {
|
2023-09-15 21:44:20 +00:00
|
|
|
gi.DiagramType = SequenceDiagram
|
|
|
|
|
} else if obj.IsGridDiagram() {
|
|
|
|
|
gi.DiagramType = GridDiagram
|
|
|
|
|
}
|
|
|
|
|
return gi
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func ExtractSelf(container *d2graph.Object) *d2graph.Graph {
|
|
|
|
|
nestedGraph := d2graph.NewGraph()
|
|
|
|
|
nestedGraph.RootLevel = int(container.Level()) - 1
|
2023-09-18 23:41:46 +00:00
|
|
|
nestedGraph.Root.Box = &geo.Box{}
|
2023-09-15 21:44:20 +00:00
|
|
|
|
|
|
|
|
// separate out nested edges
|
|
|
|
|
g := container.Graph
|
|
|
|
|
remainingEdges := make([]*d2graph.Edge, 0, len(g.Edges))
|
|
|
|
|
for _, edge := range g.Edges {
|
|
|
|
|
if edge.Src.IsDescendantOf(container) && edge.Dst.IsDescendantOf(container) {
|
|
|
|
|
nestedGraph.Edges = append(nestedGraph.Edges, edge)
|
|
|
|
|
} else {
|
|
|
|
|
remainingEdges = append(remainingEdges, edge)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
g.Edges = remainingEdges
|
|
|
|
|
|
|
|
|
|
// separate out nested objects
|
|
|
|
|
remainingObjects := make([]*d2graph.Object, 0, len(g.Objects))
|
|
|
|
|
for _, obj := range g.Objects {
|
|
|
|
|
if obj.IsDescendantOf(container) {
|
|
|
|
|
nestedGraph.Objects = append(nestedGraph.Objects, obj)
|
|
|
|
|
} else {
|
|
|
|
|
remainingObjects = append(remainingObjects, obj)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
g.Objects = remainingObjects
|
|
|
|
|
|
|
|
|
|
// update object and new root references
|
|
|
|
|
for _, o := range nestedGraph.Objects {
|
|
|
|
|
o.Graph = nestedGraph
|
2023-09-14 21:17:18 +00:00
|
|
|
}
|
2023-09-15 21:44:20 +00:00
|
|
|
|
|
|
|
|
// remove container parent's references
|
|
|
|
|
if container.Parent != nil {
|
|
|
|
|
container.Parent.RemoveChild(container)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// set root references
|
|
|
|
|
nestedGraph.Root.ChildrenArray = []*d2graph.Object{container}
|
|
|
|
|
container.Parent = nestedGraph.Root
|
|
|
|
|
nestedGraph.Root.Children[strings.ToLower(container.ID)] = container
|
|
|
|
|
|
|
|
|
|
return nestedGraph
|
2023-09-14 20:38:59 +00:00
|
|
|
}
|
|
|
|
|
|
2023-09-15 21:44:20 +00:00
|
|
|
func ExtractDescendants(container *d2graph.Object) *d2graph.Graph {
|
2023-09-15 00:00:44 +00:00
|
|
|
nestedGraph := d2graph.NewGraph()
|
|
|
|
|
nestedGraph.RootLevel = int(container.Level())
|
2023-09-18 23:41:46 +00:00
|
|
|
nestedGraph.Root.Attributes = container.Attributes
|
2023-09-15 23:50:27 +00:00
|
|
|
nestedGraph.Root.Box = &geo.Box{}
|
2023-09-14 22:38:52 +00:00
|
|
|
|
|
|
|
|
// separate out nested edges
|
|
|
|
|
g := container.Graph
|
|
|
|
|
remainingEdges := make([]*d2graph.Edge, 0, len(g.Edges))
|
|
|
|
|
for _, edge := range g.Edges {
|
|
|
|
|
if edge.Src.Parent.IsDescendantOf(container) && edge.Dst.Parent.IsDescendantOf(container) {
|
2023-09-15 00:00:44 +00:00
|
|
|
nestedGraph.Edges = append(nestedGraph.Edges, edge)
|
2023-09-14 22:38:52 +00:00
|
|
|
} else {
|
|
|
|
|
remainingEdges = append(remainingEdges, edge)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
g.Edges = remainingEdges
|
|
|
|
|
|
|
|
|
|
// separate out nested objects
|
|
|
|
|
remainingObjects := make([]*d2graph.Object, 0, len(g.Objects))
|
|
|
|
|
for _, obj := range g.Objects {
|
2023-09-15 21:44:20 +00:00
|
|
|
if obj.Parent.IsDescendantOf(container) {
|
2023-09-15 00:00:44 +00:00
|
|
|
nestedGraph.Objects = append(nestedGraph.Objects, obj)
|
2023-09-14 22:38:52 +00:00
|
|
|
} else {
|
|
|
|
|
remainingObjects = append(remainingObjects, obj)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
g.Objects = remainingObjects
|
|
|
|
|
|
|
|
|
|
// update object and new root references
|
2023-09-15 00:00:44 +00:00
|
|
|
for _, o := range nestedGraph.Objects {
|
|
|
|
|
o.Graph = nestedGraph
|
2023-09-14 22:38:52 +00:00
|
|
|
}
|
|
|
|
|
// set root references
|
2023-09-15 00:00:44 +00:00
|
|
|
nestedGraph.Root.ChildrenArray = append(nestedGraph.Root.ChildrenArray, container.ChildrenArray...)
|
2023-09-14 22:38:52 +00:00
|
|
|
for _, child := range container.ChildrenArray {
|
2023-09-15 00:00:44 +00:00
|
|
|
child.Parent = nestedGraph.Root
|
|
|
|
|
nestedGraph.Root.Children[strings.ToLower(child.ID)] = child
|
2023-09-14 22:38:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// remove container's references
|
|
|
|
|
for k := range container.Children {
|
|
|
|
|
delete(container.Children, k)
|
|
|
|
|
}
|
|
|
|
|
container.ChildrenArray = nil
|
|
|
|
|
|
2023-09-15 00:00:44 +00:00
|
|
|
return nestedGraph
|
2023-09-14 20:38:59 +00:00
|
|
|
}
|
|
|
|
|
|
2023-09-15 00:00:44 +00:00
|
|
|
func InjectNested(container *d2graph.Object, nestedGraph *d2graph.Graph) {
|
|
|
|
|
g := container.Graph
|
|
|
|
|
for _, obj := range nestedGraph.Root.ChildrenArray {
|
|
|
|
|
obj.Parent = container
|
|
|
|
|
container.Children[strings.ToLower(obj.ID)] = obj
|
|
|
|
|
container.ChildrenArray = append(container.ChildrenArray, obj)
|
|
|
|
|
}
|
|
|
|
|
for _, obj := range nestedGraph.Objects {
|
|
|
|
|
obj.Graph = g
|
|
|
|
|
}
|
|
|
|
|
g.Objects = append(g.Objects, nestedGraph.Objects...)
|
|
|
|
|
g.Edges = append(g.Edges, nestedGraph.Edges...)
|
|
|
|
|
|
2023-09-16 05:07:06 +00:00
|
|
|
if g.Root.LabelPosition != nil {
|
|
|
|
|
container.LabelPosition = g.Root.LabelPosition
|
|
|
|
|
}
|
2023-09-15 21:44:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func PositionNested(container *d2graph.Object, nestedGraph *d2graph.Graph) {
|
2023-09-16 05:07:06 +00:00
|
|
|
// _, _ := boundingBox(nestedGraph)
|
2023-09-15 00:00:44 +00:00
|
|
|
// Note: assumes nestedGraph's layout has contents positioned relative to 0,0
|
2023-09-16 05:07:06 +00:00
|
|
|
dx := container.TopLeft.X //- tl.X
|
|
|
|
|
dy := container.TopLeft.Y //- tl.Y
|
2023-09-15 00:00:44 +00:00
|
|
|
for _, o := range nestedGraph.Objects {
|
|
|
|
|
o.TopLeft.X += dx
|
|
|
|
|
o.TopLeft.Y += dy
|
|
|
|
|
}
|
|
|
|
|
for _, e := range nestedGraph.Edges {
|
|
|
|
|
e.Move(dx, dy)
|
|
|
|
|
}
|
2023-09-14 20:38:59 +00:00
|
|
|
}
|
|
|
|
|
|
2023-09-15 00:00:44 +00:00
|
|
|
func boundingBox(g *d2graph.Graph) (tl, br *geo.Point) {
|
|
|
|
|
if len(g.Objects) == 0 {
|
|
|
|
|
return geo.NewPoint(0, 0), geo.NewPoint(0, 0)
|
|
|
|
|
}
|
|
|
|
|
tl = geo.NewPoint(math.Inf(1), math.Inf(1))
|
|
|
|
|
br = geo.NewPoint(math.Inf(-1), math.Inf(-1))
|
|
|
|
|
|
|
|
|
|
for _, obj := range g.Objects {
|
2023-09-16 05:07:06 +00:00
|
|
|
if obj.TopLeft == nil {
|
|
|
|
|
panic(obj.AbsID())
|
|
|
|
|
}
|
2023-09-15 00:00:44 +00:00
|
|
|
tl.X = math.Min(tl.X, obj.TopLeft.X)
|
|
|
|
|
tl.Y = math.Min(tl.Y, obj.TopLeft.Y)
|
|
|
|
|
br.X = math.Max(br.X, obj.TopLeft.X+obj.Width)
|
|
|
|
|
br.Y = math.Max(br.Y, obj.TopLeft.Y+obj.Height)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return tl, br
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func FitToGraph(container *d2graph.Object, nestedGraph *d2graph.Graph, padding geo.Spacing) {
|
2023-09-16 05:07:06 +00:00
|
|
|
var width, height float64
|
|
|
|
|
// if nestedGraph.Root.Box != nil {
|
|
|
|
|
width = nestedGraph.Root.Width
|
|
|
|
|
height = nestedGraph.Root.Height
|
|
|
|
|
if width == 0 || height == 0 {
|
|
|
|
|
tl, br := boundingBox(nestedGraph)
|
|
|
|
|
width = br.X - tl.X
|
|
|
|
|
height = br.Y - tl.Y
|
|
|
|
|
}
|
|
|
|
|
container.Width = padding.Left + width + padding.Right
|
|
|
|
|
container.Height = padding.Top + height + padding.Bottom
|
2023-09-14 20:38:59 +00:00
|
|
|
}
|
|
|
|
|
|
2023-09-15 21:44:20 +00:00
|
|
|
// func LayoutDiagram(ctx context.Context, g *d2graph.Graph, graphInfo GraphInfo, coreLayout d2graph.LayoutGraph) geo.Spacing {
|
|
|
|
|
// spacing := geo.Spacing{}
|
|
|
|
|
// var err error
|
|
|
|
|
// // TODO
|
|
|
|
|
|
|
|
|
|
// // Need subgraphs?
|
|
|
|
|
|
|
|
|
|
// // if graphInfo.IsConstantNear
|
|
|
|
|
// // case ConstantNearGraph:
|
|
|
|
|
// // // constantNearGraphs := d2near.WithoutConstantNears(ctx, g)
|
|
|
|
|
// // constantNearGraphs := d2near.WithoutConstantNears(ctx, g)
|
|
|
|
|
|
|
|
|
|
// // err = d2near.Layout(ctx, g, constantNearGraphs)
|
|
|
|
|
// // if err != nil {
|
|
|
|
|
// // panic(err)
|
|
|
|
|
// // }
|
|
|
|
|
|
|
|
|
|
// switch graphInfo.DiagramType {
|
|
|
|
|
// case GridDiagram:
|
|
|
|
|
// layoutWithGrids := d2grid.Layout(ctx, g, coreLayout)
|
|
|
|
|
// if err = layoutWithGrids(ctx, g); err != nil {
|
|
|
|
|
// panic(err)
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// case SequenceDiagram:
|
|
|
|
|
// err = d2sequence.Layout(ctx, g, coreLayout)
|
|
|
|
|
// if err != nil {
|
|
|
|
|
// panic(err)
|
|
|
|
|
// }
|
|
|
|
|
// default:
|
|
|
|
|
// err := coreLayout(ctx, g)
|
|
|
|
|
// if err != nil {
|
|
|
|
|
// panic(err)
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// return spacing
|
|
|
|
|
// }
|