package d2layouts
import (
"context"
"fmt"
"log/slog"
"math"
"sort"
"strings"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2layouts/d2grid"
"oss.terrastruct.com/d2/d2layouts/d2near"
"oss.terrastruct.com/d2/d2layouts/d2sequence"
"oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/label"
"oss.terrastruct.com/d2/lib/log"
"oss.terrastruct.com/util-go/go2"
)
type DiagramType string
const (
DefaultGraphType DiagramType = ""
ConstantNearGraph DiagramType = "constant-near"
GridDiagram DiagramType = "grid-diagram"
SequenceDiagram DiagramType = "sequence-diagram"
)
type GraphInfo struct {
IsConstantNear bool
DiagramType DiagramType
}
func (gi GraphInfo ) isDefault () bool {
return !gi .IsConstantNear && gi .DiagramType == DefaultGraphType
}
func SaveChildrenOrder (container *d2graph .Object ) (restoreOrder func ()) {
objectOrder := make (map [string ]int , len (container .ChildrenArray ))
for i , obj := range container .ChildrenArray {
objectOrder [obj .AbsID ()] = i
}
return func () {
sort .SliceStable (container .ChildrenArray , func (i , j int ) bool {
return objectOrder [container .ChildrenArray [i ].AbsID ()] < objectOrder [container .ChildrenArray [j ].AbsID ()]
})
}
}
func SaveOrder (g *d2graph .Graph ) (restoreOrder func ()) {
objectOrder := make (map [string ]int , len (g .Objects ))
for i , obj := range g .Objects {
objectOrder [obj .AbsID ()] = i
}
edgeOrder := make (map [string ]int , len (g .Edges ))
for i , edge := range g .Edges {
edgeOrder [edge .AbsID ()] = i
}
restoreRootOrder := SaveChildrenOrder (g .Root )
return func () {
sort .SliceStable (g .Objects , func (i , j int ) bool {
return objectOrder [g .Objects [i ].AbsID ()] < objectOrder [g .Objects [j ].AbsID ()]
})
sort .SliceStable (g .Edges , func (i , j int ) bool {
iIndex , iHas := edgeOrder [g .Edges [i ].AbsID ()]
jIndex , jHas := edgeOrder [g .Edges [j ].AbsID ()]
if iHas && jHas {
return iIndex < jIndex
}
return iHas
})
restoreRootOrder ()
}
}
func LayoutNested (ctx context .Context , g *d2graph .Graph , graphInfo GraphInfo , coreLayout d2graph .LayoutGraph , edgeRouter d2graph .RouteEdges ) error {
g .Root .Box = &geo .Box {}
extracted := make (map [string ]*d2graph .Graph )
var extractedOrder []string
var extractedEdges []*d2graph .Edge
var extractedEdgeIDs []edgeIDs
var constantNears []*d2graph .Graph
restoreOrder := SaveOrder (g )
defer restoreOrder ()
queue := make ([]*d2graph .Object , 0 , len (g .Root .ChildrenArray ))
queue = append (queue , g .Root .ChildrenArray ...)
for len (queue ) > 0 {
curr := queue [0 ]
queue = queue [1 :]
isGridCellContainer := graphInfo .DiagramType == GridDiagram &&
curr .IsContainer () && curr .Parent == g .Root
gi := NestedGraphInfo (curr )
if isGridCellContainer && gi .isDefault () {
nestedGraph , externalEdges , externalEdgeIDs := ExtractSubgraph (curr , true )
id := curr .AbsID ()
err := LayoutNested (ctx , nestedGraph , GraphInfo {}, coreLayout , edgeRouter )
if err != nil {
return err
}
InjectNested (g .Root , nestedGraph , false )
g .Edges = append (g .Edges , externalEdges ...)
restoreOrder ()
idToObj := make (map [string ]*d2graph .Object )
for _ , o := range g .Objects {
idToObj [o .AbsID ()] = o
}
lookup := func (idStr string ) (*d2graph .Object , error ) {
o , exists := idToObj [idStr ]
if !exists {
return nil , fmt .Errorf ("could not find object %#v after layout" , idStr )
}
return o , nil
}
curr , err = lookup (id )
if err != nil {
return err
}
for i , e := range externalEdges {
src , err := lookup (externalEdgeIDs [i ].srcID )
if err != nil {
return err
}
e .Src = src
dst , err := lookup (externalEdgeIDs [i ].dstID )
if err != nil {
return err
}
e .Dst = dst
}
dx := 0 - curr .TopLeft .X
dy := 0 - curr .TopLeft .Y
for _ , o := range nestedGraph .Objects {
if o == curr {
continue
}
o .TopLeft .X += dx
o .TopLeft .Y += dy
}
for _ , e := range nestedGraph .Edges {
e .Move (dx , dy )
}
nestedGraph , externalEdges , externalEdgeIDs = ExtractSubgraph (curr , false )
extractedEdges = append (extractedEdges , externalEdges ...)
extractedEdgeIDs = append (extractedEdgeIDs , externalEdgeIDs ...)
extracted [id ] = nestedGraph
extractedOrder = append (extractedOrder , id )
continue
}
if !gi .isDefault () {
if !gi .IsConstantNear && len (curr .Children ) == 0 {
continue
}
nestedGraph , externalEdges , externalEdgeIDs := ExtractSubgraph (curr , gi .IsConstantNear )
extractedEdges = append (extractedEdges , externalEdges ...)
extractedEdgeIDs = append (extractedEdgeIDs , externalEdgeIDs ...)
log .Debug (ctx , "layout nested" , slog .Any ("level" , curr .Level ()), slog .Any ("child" , curr .AbsID ()), slog .Any ("gi" , gi ))
nestedInfo := gi
nearKey := curr .NearKey
if gi .IsConstantNear {
nestedInfo = GraphInfo {}
curr .NearKey = nil
}
err := LayoutNested (ctx , nestedGraph , nestedInfo , coreLayout , edgeRouter )
if err != nil {
return err
}
if gi .IsConstantNear {
curr = nestedGraph .Root .ChildrenArray [0 ]
}
if gi .IsConstantNear {
curr .NearKey = nearKey
} else {
FitToGraph (curr , nestedGraph , geo .Spacing {})
curr .TopLeft = geo .NewPoint (0 , 0 )
}
if gi .IsConstantNear {
constantNears = append (constantNears , nestedGraph )
} else {
id := curr .AbsID ()
extracted [id ] = nestedGraph
extractedOrder = append (extractedOrder , id )
}
} else if len (curr .ChildrenArray ) > 0 {
queue = append (queue , curr .ChildrenArray ...)
}
}
var err error
if len (g .Objects ) > 0 {
switch graphInfo .DiagramType {
case GridDiagram :
log .Debug (ctx , "layout grid" , slog .Any ("rootlevel" , g .RootLevel ), slog .Any ("shapes" , g .PrintString ()))
if err = d2grid .Layout (ctx , g ); err != nil {
return err
}
case SequenceDiagram :
log .Debug (ctx , "layout sequence" , slog .Any ("rootlevel" , g .RootLevel ), slog .Any ("shapes" , g .PrintString ()))
err = d2sequence .Layout (ctx , g , coreLayout )
if err != nil {
return err
}
default :
log .Debug (ctx , "default layout" , slog .Any ("rootlevel" , g .RootLevel ), slog .Any ("shapes" , g .PrintString ()))
err := coreLayout (ctx , g )
if err != nil {
return err
}
}
}
if len (constantNears ) > 0 {
err = d2near .Layout (ctx , g , constantNears )
if err != nil {
return err
}
}
idToObj := make (map [string ]*d2graph .Object )
for _ , o := range g .Objects {
idToObj [o .AbsID ()] = o
}
for _ , id := range extractedOrder {
nestedGraph := extracted [id ]
obj , exists := idToObj [id ]
if !exists {
return fmt .Errorf ("could not find object %#v after layout" , id )
}
InjectNested (obj , nestedGraph , true )
PositionNested (obj , nestedGraph )
}
if len (extractedEdges ) > 0 {
for _ , o := range g .Objects {
idToObj [o .AbsID ()] = o
}
g .Edges = append (g .Edges , extractedEdges ...)
for i , e := range extractedEdges {
ids := extractedEdgeIDs [i ]
src , exists := idToObj [ids .srcID ]
if !exists {
return fmt .Errorf ("could not find object %#v after layout" , ids .srcID )
}
e .Src = src
dst , exists := idToObj [ids .dstID ]
if !exists {
return fmt .Errorf ("could not find object %#v after layout" , ids .dstID )
}
e .Dst = dst
}
err = edgeRouter (ctx , g , extractedEdges )
if err != nil {
return err
}
for i , e := range extractedEdges {
ids := extractedEdgeIDs [i ]
src , exists := idToObj [ids .srcID ]
if !exists {
return fmt .Errorf ("could not find object %#v after routing" , ids .srcID )
}
e .Src = src
dst , exists := idToObj [ids .dstID ]
if !exists {
return fmt .Errorf ("could not find object %#v after routing" , ids .dstID )
}
e .Dst = dst
}
}
log .Debug (ctx , "done" , slog .Any ("rootlevel" , g .RootLevel ), slog .Any ("shapes" , g .PrintString ()))
return err
}
func DefaultRouter (ctx context .Context , graph *d2graph .Graph , edges []*d2graph .Edge ) error {
for _ , e := range edges {
e .Route = []*geo .Point {e .Src .Center (), e .Dst .Center ()}
e .TraceToShape (e .Route , 0 , 1 )
if e .Label .Value != "" {
e .LabelPosition = go2 .Pointer (label .InsideMiddleCenter .String ())
}
}
return nil
}
func NestedGraphInfo (obj *d2graph .Object ) (gi GraphInfo ) {
if obj .Graph .RootLevel == 0 && obj .IsConstantNear () {
gi .IsConstantNear = true
}
if obj .IsSequenceDiagram () {
gi .DiagramType = SequenceDiagram
} else if obj .IsGridDiagram () {
gi .DiagramType = GridDiagram
}
return gi
}
type edgeIDs struct {
srcID, dstID string
}
func ExtractSubgraph (container *d2graph .Object , includeSelf bool ) (nestedGraph *d2graph .Graph , externalEdges []*d2graph .Edge , externalEdgeIDs []edgeIDs ) {
nestedGraph = d2graph .NewGraph ()
nestedGraph .RootLevel = int (container .Level ())
if includeSelf {
nestedGraph .RootLevel --
}
nestedGraph .Root .Attributes = container .Attributes
nestedGraph .Root .Box = &geo .Box {}
nestedGraph .Root .LabelPosition = container .LabelPosition
nestedGraph .Root .IconPosition = container .IconPosition
isNestedObject := func (obj *d2graph .Object ) bool {
if includeSelf {
return obj .IsDescendantOf (container )
}
return obj .Parent .IsDescendantOf (container )
}
g := container .Graph
remainingEdges := make ([]*d2graph .Edge , 0 , len (g .Edges ))
for _ , edge := range g .Edges {
srcIsNested := isNestedObject (edge .Src )
if d2sequence .IsLifelineEnd (edge .Dst ) {
if srcIsNested {
nestedGraph .Edges = append (nestedGraph .Edges , edge )
} else {
remainingEdges = append (remainingEdges , edge )
}
continue
}
dstIsNested := isNestedObject (edge .Dst )
if srcIsNested && dstIsNested {
nestedGraph .Edges = append (nestedGraph .Edges , edge )
} else if srcIsNested || dstIsNested {
externalEdges = append (externalEdges , edge )
externalEdgeIDs = append (externalEdgeIDs , edgeIDs {
srcID : edge .Src .AbsID (),
dstID : edge .Dst .AbsID (),
})
} else {
remainingEdges = append (remainingEdges , edge )
}
}
g .Edges = remainingEdges
remainingObjects := make ([]*d2graph .Object , 0 , len (g .Objects ))
for _ , obj := range g .Objects {
if isNestedObject (obj ) {
nestedGraph .Objects = append (nestedGraph .Objects , obj )
} else {
remainingObjects = append (remainingObjects , obj )
}
}
g .Objects = remainingObjects
for _ , o := range nestedGraph .Objects {
o .Graph = nestedGraph
}
if includeSelf {
if container .Parent != nil {
container .Parent .RemoveChild (container )
}
nestedGraph .Root .ChildrenArray = []*d2graph .Object {container }
container .Parent = nestedGraph .Root
nestedGraph .Root .Children [strings .ToLower (container .ID )] = container
} else {
nestedGraph .Root .ChildrenArray = append (nestedGraph .Root .ChildrenArray , container .ChildrenArray ...)
for _ , child := range container .ChildrenArray {
child .Parent = nestedGraph .Root
nestedGraph .Root .Children [strings .ToLower (child .ID )] = child
}
for k := range container .Children {
delete (container .Children , k )
}
container .ChildrenArray = nil
}
return nestedGraph , externalEdges , externalEdgeIDs
}
func InjectNested (container *d2graph .Object , nestedGraph *d2graph .Graph , isRoot bool ) {
g := container .Graph
for _ , obj := range nestedGraph .Root .ChildrenArray {
obj .Parent = container
if container .Children == nil {
container .Children = make (map [string ]*d2graph .Object )
}
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 ...)
if isRoot {
if nestedGraph .Root .LabelPosition != nil {
container .LabelPosition = nestedGraph .Root .LabelPosition
}
if nestedGraph .Root .IconPosition != nil {
container .IconPosition = nestedGraph .Root .IconPosition
}
container .Attributes = nestedGraph .Root .Attributes
}
}
func PositionNested (container *d2graph .Object , nestedGraph *d2graph .Graph ) {
dx := container .TopLeft .X
dy := container .TopLeft .Y
if dx == 0 && dy == 0 {
return
}
for _ , o := range nestedGraph .Objects {
o .TopLeft .X += dx
o .TopLeft .Y += dy
}
for _ , e := range nestedGraph .Edges {
e .Move (dx , dy )
}
}
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 {
if obj .TopLeft == nil {
panic (obj .AbsID ())
}
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 ) {
var width , height float64
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
}
The pages are generated with Golds v0.8.2 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .