package d2elklayout
import (
"context"
_ "embed"
"encoding/json"
"fmt"
"math"
"regexp"
"strconv"
"strings"
"oss.terrastruct.com/util-go/xdefer"
"oss.terrastruct.com/util-go/go2"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/jsrunner"
"oss.terrastruct.com/d2/lib/label"
"oss.terrastruct.com/d2/lib/shape"
)
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 DefaultLayout (ctx context .Context , g *d2graph .Graph ) (err error ) {
return Layout (ctx , g , nil )
}
func Layout (ctx context .Context , g *d2graph .Graph , opts *ConfigurableOpts ) (err error ) {
if opts == nil {
opts = &DefaultOpts
}
defer xdefer .Errorf (&err , "failed to ELK layout" )
runner := jsrunner .NewJSRunner ()
if runner .Engine () == jsrunner .Goja {
console := runner .NewObject ()
if err := runner .Set ("console" , console ); err != nil {
return err
}
if _ , err := runner .RunString (elkJS ); err != nil {
return err
}
if _ , err := runner .RunString (setupJS ); err != nil {
return err
}
}
elkGraph := &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 : opts .Algorithm ,
NodeSpacing : opts .NodeSpacing ,
EdgeNodeSpacing : opts .EdgeNodeSpacing ,
SelfLoopSpacing : opts .SelfLoopSpacing ,
},
},
}
if elkGraph .LayoutOptions .ConfigurableOpts .SelfLoopSpacing == DefaultOpts .SelfLoopSpacing {
elkGraph .LayoutOptions .ConfigurableOpts .SelfLoopSpacing = go2 .Max (elkGraph .LayoutOptions .ConfigurableOpts .SelfLoopSpacing , childrenMaxSelfLoop (g .Root , g .Root .Direction .Value == "down" || g .Root .Direction .Value == "" || g .Root .Direction .Value == "up" )/2 +5 )
}
switch g .Root .Direction .Value {
case "down" :
elkGraph .LayoutOptions .Direction = Down
case "up" :
elkGraph .LayoutOptions .Direction = Up
case "right" :
elkGraph .LayoutOptions .Direction = Right
case "left" :
elkGraph .LayoutOptions .Direction = Left
default :
elkGraph .LayoutOptions .Direction = Down
}
for _ , obj := range g .Objects {
positionLabelsIcons (obj )
}
adjustments := make (map [*d2graph .Object ]geo .Spacing )
elkNodes := make (map [*d2graph .Object ]*ELKNode )
elkEdges := make (map [*d2graph .Edge ]*ELKEdge )
var walk func (*d2graph .Object , *d2graph .Object , func (*d2graph .Object , *d2graph .Object ))
walk = func (obj , parent *d2graph .Object , fn func (*d2graph .Object , *d2graph .Object )) {
if obj .Parent != nil {
fn (obj , parent )
}
for _ , ch := range obj .ChildrenArray {
walk (ch , obj , fn )
}
}
walk (g .Root , nil , func (obj , parent *d2graph .Object ) {
incoming := 0.
outgoing := 0.
for _ , e := range g .Edges {
if e .Src == obj {
outgoing ++
}
if e .Dst == obj {
incoming ++
}
}
if incoming >= 2 || outgoing >= 2 {
switch g .Root .Direction .Value {
case "right" , "left" :
if obj .Attributes .HeightAttr == nil {
obj .Height = math .Max (obj .Height , math .Max (incoming , outgoing )*port_spacing )
}
default :
if obj .Attributes .WidthAttr == nil {
obj .Width = math .Max (obj .Width , math .Max (incoming , outgoing )*port_spacing )
}
}
}
if obj .HasLabel () && obj .HasIcon () {
obj .Height += float64 (obj .LabelDimensions .Height + label .PADDING )
}
margin , _ := obj .SpacingOpt (label .PADDING , label .PADDING , false )
width := margin .Left + obj .Width + margin .Right
height := margin .Top + obj .Height + margin .Bottom
adjustments [obj ] = margin
n := &ELKNode {
ID : obj .AbsID (),
Width : width ,
Height : height ,
}
if len (obj .ChildrenArray ) > 0 {
n .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 : opts .NodeSpacing ,
EdgeNodeSpacing : opts .EdgeNodeSpacing ,
SelfLoopSpacing : opts .SelfLoopSpacing ,
Padding : opts .Padding ,
},
}
if n .LayoutOptions .ConfigurableOpts .SelfLoopSpacing == DefaultOpts .SelfLoopSpacing {
n .LayoutOptions .ConfigurableOpts .SelfLoopSpacing = go2 .Max (n .LayoutOptions .ConfigurableOpts .SelfLoopSpacing , childrenMaxSelfLoop (obj , g .Root .Direction .Value == "down" || g .Root .Direction .Value == "" || g .Root .Direction .Value == "up" )/2 +5 )
}
switch elkGraph .LayoutOptions .Direction {
case Down , Up :
n .LayoutOptions .NodeSizeMinimum = fmt .Sprintf ("(%d, %d)" , int (math .Ceil (height )), int (math .Ceil (width )))
case Right , Left :
n .LayoutOptions .NodeSizeMinimum = fmt .Sprintf ("(%d, %d)" , int (math .Ceil (width )), int (math .Ceil (height )))
}
} else {
n .LayoutOptions = &elkOpts {
SelfLoopDistribution : "EQUALLY" ,
}
}
if obj .IsContainer () {
padding := parsePadding (opts .Padding )
padding = adjustPadding (obj , width , height , padding )
n .LayoutOptions .Padding = padding .String ()
}
if obj .HasLabel () {
n .Labels = append (n .Labels , &ELKLabel {
Text : obj .Label .Value ,
Width : float64 (obj .LabelDimensions .Width ),
Height : float64 (obj .LabelDimensions .Height ),
})
}
if parent == g .Root {
elkGraph .Children = append (elkGraph .Children , n )
} else {
elkNodes [parent ].Children = append (elkNodes [parent ].Children , n )
}
if obj .SQLTable != nil {
n .LayoutOptions .PortConstraints = "FIXED_POS"
columns := obj .SQLTable .Columns
colHeight := n .Height / float64 (len (columns )+1 )
n .Ports = make ([]*ELKPort , 0 , len (columns )*2 )
var srcSide , dstSide PortSide
switch elkGraph .LayoutOptions .Direction {
case Left :
srcSide , dstSide = West , East
default :
srcSide , dstSide = East , West
}
for i , col := range columns {
n .Ports = append (n .Ports , &ELKPort {
ID : srcPortID (obj , col .Name .Label ),
Y : float64 (i +1 )*colHeight + colHeight /2 ,
LayoutOptions : &elkOpts {PortSide : srcSide },
})
n .Ports = append (n .Ports , &ELKPort {
ID : dstPortID (obj , col .Name .Label ),
Y : float64 (i +1 )*colHeight + colHeight /2 ,
LayoutOptions : &elkOpts {PortSide : dstSide },
})
}
}
elkNodes [obj ] = n
})
var srcSide , dstSide PortSide
switch elkGraph .LayoutOptions .Direction {
case Up :
srcSide , dstSide = North , South
default :
srcSide , dstSide = South , North
}
ports := map [struct {
obj *d2graph .Object
side PortSide
}][]*ELKPort {}
for ei , edge := range g .Edges {
var src , dst string
switch {
case edge .SrcTableColumnIndex != nil :
src = srcPortID (edge .Src , edge .Src .SQLTable .Columns [*edge .SrcTableColumnIndex ].Name .Label )
case edge .Src .SQLTable != nil :
p := &ELKPort {
ID : fmt .Sprintf ("%s.%d" , srcPortID (edge .Src , "__root__" ), ei ),
LayoutOptions : &elkOpts {PortSide : srcSide },
}
src = p .ID
elkNodes [edge .Src ].Ports = append (elkNodes [edge .Src ].Ports , p )
k := struct {
obj *d2graph .Object
side PortSide
}{edge .Src , srcSide }
ports [k ] = append (ports [k ], p )
default :
src = edge .Src .AbsID ()
}
switch {
case edge .DstTableColumnIndex != nil :
dst = dstPortID (edge .Dst , edge .Dst .SQLTable .Columns [*edge .DstTableColumnIndex ].Name .Label )
case edge .Dst .SQLTable != nil :
p := &ELKPort {
ID : fmt .Sprintf ("%s.%d" , dstPortID (edge .Dst , "__root__" ), ei ),
LayoutOptions : &elkOpts {PortSide : dstSide },
}
dst = p .ID
elkNodes [edge .Dst ].Ports = append (elkNodes [edge .Dst ].Ports , p )
k := struct {
obj *d2graph .Object
side PortSide
}{edge .Dst , dstSide }
ports [k ] = append (ports [k ], p )
default :
dst = edge .Dst .AbsID ()
}
e := &ELKEdge {
ID : edge .AbsID (),
Sources : []string {src },
Targets : []string {dst },
}
if edge .Label .Value != "" {
e .Labels = append (e .Labels , &ELKLabel {
Text : edge .Label .Value ,
Width : float64 (edge .LabelDimensions .Width ),
Height : float64 (edge .LabelDimensions .Height ),
LayoutOptions : &elkOpts {
InlineEdgeLabels : true ,
},
})
}
elkGraph .Edges = append (elkGraph .Edges , e )
elkEdges [edge ] = e
}
for k , ports := range ports {
width := elkNodes [k .obj ].Width
spacing := width / float64 (len (ports )+1 )
for i , p := range ports {
p .X = float64 (i +1 ) * spacing
}
}
raw , err := json .Marshal (elkGraph )
if err != nil {
return err
}
var val jsrunner .JSValue
if runner .Engine () == jsrunner .Goja {
loadScript := fmt .Sprintf (`var graph = %s` , raw )
if _ , err := runner .RunString (loadScript ); err != nil {
return err
}
val , err = runner .RunString (`elk.layout(graph)
.then(s => s)
.catch(err => err.message)
` )
} else {
val , err = runner .MustGet ("elkResult" )
}
if err != nil {
return err
}
result , err := runner .WaitPromise (ctx , val )
if err != nil {
return fmt .Errorf ("ELK layout error: %v" , err )
}
var jsonOut map [string ]interface {}
switch out := result .(type ) {
case string :
return fmt .Errorf ("ELK layout error: %s" , out )
case map [string ]interface {}:
jsonOut = out
default :
return fmt .Errorf ("ELK unexpected return: %v" , out )
}
jsonBytes , err := json .Marshal (jsonOut )
if err != nil {
return err
}
err = json .Unmarshal (jsonBytes , &elkGraph )
if err != nil {
return err
}
byID := make (map [string ]*d2graph .Object )
walk (g .Root , nil , func (obj , parent *d2graph .Object ) {
n := elkNodes [obj ]
parentX := 0.0
parentY := 0.0
if parent != nil && parent != g .Root {
parentX = parent .TopLeft .X
parentY = parent .TopLeft .Y
}
obj .TopLeft = geo .NewPoint (parentX +n .X , parentY +n .Y )
obj .Width = math .Ceil (n .Width )
obj .Height = math .Ceil (n .Height )
byID [obj .AbsID ()] = obj
})
for _ , edge := range g .Edges {
e := elkEdges [edge ]
parentX := 0.0
parentY := 0.0
if e .Container != "" {
parentX = byID [e .Container ].TopLeft .X
parentY = byID [e .Container ].TopLeft .Y
}
var points []*geo .Point
for _ , s := range e .Sections {
points = append (points , &geo .Point {
X : parentX + s .Start .X ,
Y : parentY + s .Start .Y ,
})
for _ , bp := range s .BendPoints {
points = append (points , &geo .Point {
X : parentX + bp .X ,
Y : parentY + bp .Y ,
})
}
points = append (points , &geo .Point {
X : parentX + s .End .X ,
Y : parentY + s .End .Y ,
})
}
edge .Route = points
}
objEdges := make (map [*d2graph .Object ][]*d2graph .Edge )
for _ , e := range g .Edges {
objEdges [e .Src ] = append (objEdges [e .Src ], e )
if e .Dst != e .Src {
objEdges [e .Dst ] = append (objEdges [e .Dst ], e )
}
}
for _ , obj := range g .Objects {
if margin , has := adjustments [obj ]; has {
edges := objEdges [obj ]
if margin .Left > 0 {
for _ , e := range edges {
l := len (e .Route )
if e .Src == obj && e .Route [0 ].X == obj .TopLeft .X {
e .Route [0 ].X += margin .Left
}
if e .Dst == obj && e .Route [l -1 ].X == obj .TopLeft .X {
e .Route [l -1 ].X += margin .Left
}
}
obj .TopLeft .X += margin .Left
obj .ShiftDescendants (margin .Left /2 , 0 )
obj .Width -= margin .Left
}
if margin .Right > 0 {
for _ , e := range edges {
l := len (e .Route )
if e .Src == obj && e .Route [0 ].X == obj .TopLeft .X +obj .Width {
e .Route [0 ].X -= margin .Right
}
if e .Dst == obj && e .Route [l -1 ].X == obj .TopLeft .X +obj .Width {
e .Route [l -1 ].X -= margin .Right
}
}
obj .ShiftDescendants (-margin .Right /2 , 0 )
obj .Width -= margin .Right
}
if margin .Top > 0 {
for _ , e := range edges {
l := len (e .Route )
if e .Src == obj && e .Route [0 ].Y == obj .TopLeft .Y {
e .Route [0 ].Y += margin .Top
}
if e .Dst == obj && e .Route [l -1 ].Y == obj .TopLeft .Y {
e .Route [l -1 ].Y += margin .Top
}
}
obj .TopLeft .Y += margin .Top
obj .ShiftDescendants (0 , margin .Top /2 )
obj .Height -= margin .Top
}
if margin .Bottom > 0 {
for _ , e := range edges {
l := len (e .Route )
if e .Src == obj && e .Route [0 ].Y == obj .TopLeft .Y +obj .Height {
e .Route [0 ].Y -= margin .Bottom
}
if e .Dst == obj && e .Route [l -1 ].Y == obj .TopLeft .Y +obj .Height {
e .Route [l -1 ].Y -= margin .Bottom
}
}
obj .ShiftDescendants (0 , -margin .Bottom /2 )
obj .Height -= margin .Bottom
}
}
}
for _ , edge := range g .Edges {
points := edge .Route
startIndex , endIndex := 0 , len (points )-1
start := points [startIndex ]
end := points [endIndex ]
var originalSrcTL , originalDstTL *geo .Point
if srcDx , srcDy := edge .Src .GetModifierElementAdjustments (); srcDx != 0 || srcDy != 0 {
if start .X > edge .Src .TopLeft .X +srcDx &&
start .Y < edge .Src .TopLeft .Y +edge .Src .Height -srcDy {
originalSrcTL = edge .Src .TopLeft .Copy ()
edge .Src .TopLeft .X += srcDx
edge .Src .TopLeft .Y -= srcDy
}
}
if dstDx , dstDy := edge .Dst .GetModifierElementAdjustments (); dstDx != 0 || dstDy != 0 {
if end .X > edge .Dst .TopLeft .X +dstDx &&
end .Y < edge .Dst .TopLeft .Y +edge .Dst .Height -dstDy {
originalDstTL = edge .Dst .TopLeft .Copy ()
edge .Dst .TopLeft .X += dstDx
edge .Dst .TopLeft .Y -= dstDy
}
}
startIndex , endIndex = edge .TraceToShape (points , startIndex , endIndex )
points = points [startIndex : endIndex +1 ]
if edge .Label .Value != "" {
edge .LabelPosition = go2 .Pointer (label .InsideMiddleCenter .String ())
}
edge .Route = points
if originalSrcTL != nil {
edge .Src .TopLeft .X = originalSrcTL .X
edge .Src .TopLeft .Y = originalSrcTL .Y
}
if originalDstTL != nil {
edge .Dst .TopLeft .X = originalDstTL .X
edge .Dst .TopLeft .Y = originalDstTL .Y
}
}
deleteBends (g )
return nil
}
func srcPortID(obj *d2graph .Object , column string ) string {
return fmt .Sprintf ("%s.%s.src" , obj .AbsID (), column )
}
func dstPortID(obj *d2graph .Object , column string ) string {
return fmt .Sprintf ("%s.%s.dst" , obj .AbsID (), column )
}
func deleteBends(g *d2graph .Graph ) {
for _ , isSource := range []bool {true , false } {
for ei , e := range g .Edges {
if len (e .Route ) < 4 {
continue
}
if e .Src == e .Dst {
continue
}
var endpoint *d2graph .Object
var start *geo .Point
var corner *geo .Point
var end *geo .Point
var columnIndex *int
if isSource {
start = e .Route [0 ]
corner = e .Route [1 ]
end = e .Route [2 ]
endpoint = e .Src
columnIndex = e .SrcTableColumnIndex
} else {
start = e .Route [len (e .Route )-1 ]
corner = e .Route [len (e .Route )-2 ]
end = e .Route [len (e .Route )-3 ]
endpoint = e .Dst
columnIndex = e .DstTableColumnIndex
}
isHorizontal := math .Ceil (start .Y ) == math .Ceil (corner .Y )
dx , dy := endpoint .GetModifierElementAdjustments ()
switch {
case columnIndex != nil :
rowHeight := endpoint .Height / float64 (len (endpoint .SQLTable .Columns )+1 )
rowCenter := endpoint .TopLeft .Y + rowHeight *float64 (*columnIndex +1 ) + rowHeight /2
if math .Abs (end .Y -rowCenter ) > rowHeight /3 {
continue
}
case isHorizontal :
if end .Y <= endpoint .TopLeft .Y +10 -dy {
continue
}
if end .Y >= endpoint .TopLeft .Y +endpoint .Height -10 {
continue
}
default :
if end .X <= endpoint .TopLeft .X +10 {
continue
}
if end .X >= endpoint .TopLeft .X +endpoint .Width -10 +dx {
continue
}
}
var newStart *geo .Point
if isHorizontal {
newStart = geo .NewPoint (start .X , end .Y )
} else {
newStart = geo .NewPoint (end .X , start .Y )
}
endpointShape := shape .NewShape (d2target .DSL_SHAPE_TO_SHAPE_TYPE [strings .ToLower (endpoint .Shape .Value )], endpoint .Box )
newStart = shape .TraceToShapeBorder (endpointShape , newStart , end )
oldSegment := geo .NewSegment (start , corner )
newSegment := geo .NewSegment (newStart , end )
oldIntersects := countObjectIntersects (g , e .Src , e .Dst , *oldSegment )
newIntersects := countObjectIntersects (g , e .Src , e .Dst , *newSegment )
if newIntersects > oldIntersects {
continue
}
oldCrossingsCount , oldOverlapsCount , oldCloseOverlapsCount , oldTouchingCount := countEdgeIntersects (g , g .Edges [ei ], *oldSegment )
newCrossingsCount , newOverlapsCount , newCloseOverlapsCount , newTouchingCount := countEdgeIntersects (g , g .Edges [ei ], *newSegment )
if newCrossingsCount > oldCrossingsCount {
continue
}
if newOverlapsCount > oldOverlapsCount {
continue
}
if newCloseOverlapsCount > oldCloseOverlapsCount {
continue
}
if newTouchingCount > oldTouchingCount {
continue
}
if isSource {
g .Edges [ei ].Route = append (
[]*geo .Point {newStart },
e .Route [3 :]...,
)
} else {
g .Edges [ei ].Route = append (
e .Route [:len (e .Route )-3 ],
newStart ,
)
}
}
}
points := map [geo .Point ]int {}
for _ , e := range g .Edges {
for _ , p := range e .Route {
points [*p ]++
}
}
for ei , e := range g .Edges {
if len (e .Route ) < 6 {
continue
}
if e .Src == e .Dst {
continue
}
for i := 1 ; i < len (e .Route )-3 ; i ++ {
before := e .Route [i -1 ]
start := e .Route [i ]
corner := e .Route [i +1 ]
end := e .Route [i +2 ]
after := e .Route [i +3 ]
if c , _ := points [*corner ]; c > 1 {
continue
}
var newCorner *geo .Point
if math .Ceil (start .X ) == math .Ceil (corner .X ) {
newCorner = geo .NewPoint (end .X , start .Y )
if (end .X > start .X ) != (start .X > before .X ) {
continue
}
if (end .Y > start .Y ) != (after .Y > end .Y ) {
continue
}
} else {
newCorner = geo .NewPoint (start .X , end .Y )
if (end .Y > start .Y ) != (start .Y > before .Y ) {
continue
}
if (end .X > start .X ) != (after .X > end .X ) {
continue
}
}
oldS1 := geo .NewSegment (start , corner )
oldS2 := geo .NewSegment (corner , end )
newS1 := geo .NewSegment (start , newCorner )
newS2 := geo .NewSegment (newCorner , end )
oldIntersects := countObjectIntersects (g , e .Src , e .Dst , *oldS1 ) + countObjectIntersects (g , e .Src , e .Dst , *oldS2 )
newIntersects := countObjectIntersects (g , e .Src , e .Dst , *newS1 ) + countObjectIntersects (g , e .Src , e .Dst , *newS2 )
if newIntersects > oldIntersects {
continue
}
oldCrossingsCount1 , oldOverlapsCount1 , oldCloseOverlapsCount1 , oldTouchingCount1 := countEdgeIntersects (g , g .Edges [ei ], *oldS1 )
oldCrossingsCount2 , oldOverlapsCount2 , oldCloseOverlapsCount2 , oldTouchingCount2 := countEdgeIntersects (g , g .Edges [ei ], *oldS2 )
oldCrossingsCount := oldCrossingsCount1 + oldCrossingsCount2
oldOverlapsCount := oldOverlapsCount1 + oldOverlapsCount2
oldCloseOverlapsCount := oldCloseOverlapsCount1 + oldCloseOverlapsCount2
oldTouchingCount := oldTouchingCount1 + oldTouchingCount2
newCrossingsCount1 , newOverlapsCount1 , newCloseOverlapsCount1 , newTouchingCount1 := countEdgeIntersects (g , g .Edges [ei ], *newS1 )
newCrossingsCount2 , newOverlapsCount2 , newCloseOverlapsCount2 , newTouchingCount2 := countEdgeIntersects (g , g .Edges [ei ], *newS2 )
newCrossingsCount := newCrossingsCount1 + newCrossingsCount2
newOverlapsCount := newOverlapsCount1 + newOverlapsCount2
newCloseOverlapsCount := newCloseOverlapsCount1 + newCloseOverlapsCount2
newTouchingCount := newTouchingCount1 + newTouchingCount2
if newCrossingsCount > oldCrossingsCount {
continue
}
if newOverlapsCount > oldOverlapsCount {
continue
}
if newCloseOverlapsCount > oldCloseOverlapsCount {
continue
}
if newTouchingCount > oldTouchingCount {
continue
}
g .Edges [ei ].Route = append (append (
e .Route [:i ],
newCorner ,
),
e .Route [i +3 :]...,
)
break
}
}
}
func countObjectIntersects(g *d2graph .Graph , src , dst *d2graph .Object , s geo .Segment ) int {
count := 0
for i , o := range g .Objects {
if g .Objects [i ] == src || g .Objects [i ] == dst {
continue
}
if o .Intersects (s , float64 (edge_node_spacing )-1 ) {
count ++
}
}
return count
}
func countEdgeIntersects(g *d2graph .Graph , sEdge *d2graph .Edge , s geo .Segment ) (int , int , int , int ) {
isHorizontal := math .Ceil (s .Start .Y ) == math .Ceil (s .End .Y )
crossingsCount := 0
overlapsCount := 0
closeOverlapsCount := 0
touchingCount := 0
for i , e := range g .Edges {
if g .Edges [i ] == sEdge {
continue
}
for i := 0 ; i < len (e .Route )-1 ; i ++ {
otherS := geo .NewSegment (e .Route [i ], e .Route [i +1 ])
otherIsHorizontal := math .Ceil (otherS .Start .Y ) == math .Ceil (otherS .End .Y )
if isHorizontal == otherIsHorizontal {
if s .Overlaps (*otherS , !isHorizontal , 0. ) {
if isHorizontal {
if math .Abs (s .Start .Y -otherS .Start .Y ) < float64 (edge_node_spacing )/2. {
overlapsCount ++
if math .Abs (s .Start .Y -otherS .Start .Y ) < float64 (edge_node_spacing )/4. {
closeOverlapsCount ++
if math .Abs (s .Start .Y -otherS .Start .Y ) < 1. {
touchingCount ++
}
}
}
} else {
if math .Abs (s .Start .X -otherS .Start .X ) < float64 (edge_node_spacing )/2. {
overlapsCount ++
if math .Abs (s .Start .X -otherS .Start .X ) < float64 (edge_node_spacing )/4. {
closeOverlapsCount ++
if math .Abs (s .Start .Y -otherS .Start .Y ) < 1. {
touchingCount ++
}
}
}
}
}
} else {
if s .Intersects (*otherS ) {
crossingsCount ++
}
}
}
}
return crossingsCount , overlapsCount , closeOverlapsCount , touchingCount
}
func childrenMaxSelfLoop(parent *d2graph .Object , isWidth bool ) int {
max := 0
for _ , ch := range parent .Children {
for _ , e := range parent .Graph .Edges {
if e .Src == e .Dst && e .Src == ch && e .Label .Value != "" {
if isWidth {
max = go2 .Max (max , e .LabelDimensions .Width )
} else {
max = go2 .Max (max , e .LabelDimensions .Height )
}
}
}
}
return max
}
type shapePadding struct {
top, left, bottom, right int
}
func parsePadding(in string ) shapePadding {
reTop := regexp .MustCompile (`top=(\d+)` )
reLeft := regexp .MustCompile (`left=(\d+)` )
reBottom := regexp .MustCompile (`bottom=(\d+)` )
reRight := regexp .MustCompile (`right=(\d+)` )
padding := shapePadding {}
submatches := reTop .FindStringSubmatch (in )
if len (submatches ) == 2 {
i , err := strconv .ParseInt (submatches [1 ], 10 , 64 )
if err == nil {
padding .top = int (i )
}
}
submatches = reLeft .FindStringSubmatch (in )
if len (submatches ) == 2 {
i , err := strconv .ParseInt (submatches [1 ], 10 , 64 )
if err == nil {
padding .left = int (i )
}
}
submatches = reBottom .FindStringSubmatch (in )
if len (submatches ) == 2 {
i , err := strconv .ParseInt (submatches [1 ], 10 , 64 )
if err == nil {
padding .bottom = int (i )
}
}
submatches = reRight .FindStringSubmatch (in )
i , err := strconv .ParseInt (submatches [1 ], 10 , 64 )
if len (submatches ) == 2 {
if err == nil {
padding .right = int (i )
}
}
return padding
}
func (padding shapePadding ) String () string {
return fmt .Sprintf ("[top=%d,left=%d,bottom=%d,right=%d]" , padding .top , padding .left , padding .bottom , padding .right )
}
func adjustPadding(obj *d2graph .Object , width , height float64 , padding shapePadding ) shapePadding {
if !obj .IsContainer () {
return padding
}
var extraTop , extraBottom , extraLeft , extraRight int
if obj .HasLabel () && obj .LabelPosition != nil {
labelHeight := obj .LabelDimensions .Height + 2 *label .PADDING
labelWidth := obj .LabelDimensions .Width + 2 *label .PADDING
switch label .FromString (*obj .LabelPosition ) {
case label .InsideTopLeft , label .InsideTopCenter , label .InsideTopRight :
extraTop = labelHeight
case label .InsideBottomLeft , label .InsideBottomCenter , label .InsideBottomRight :
extraBottom = labelHeight
case label .InsideMiddleLeft :
extraLeft = labelWidth
case label .InsideMiddleRight :
extraRight = labelWidth
}
}
if obj .HasIcon () && obj .IconPosition != nil {
iconSize := d2target .MAX_ICON_SIZE + 2 *label .PADDING
switch label .FromString (*obj .IconPosition ) {
case label .InsideTopLeft , label .InsideTopCenter , label .InsideTopRight :
extraTop = go2 .Max (extraTop , iconSize )
case label .InsideBottomLeft , label .InsideBottomCenter , label .InsideBottomRight :
extraBottom = go2 .Max (extraBottom , iconSize )
case label .InsideMiddleLeft :
extraLeft = go2 .Max (extraLeft , iconSize )
case label .InsideMiddleRight :
extraRight = go2 .Max (extraRight , iconSize )
}
}
maxChildWidth , maxChildHeight := math .Inf (-1 ), math .Inf (-1 )
for _ , c := range obj .ChildrenArray {
if c .Width > maxChildWidth {
maxChildWidth = c .Width
}
if c .Height > maxChildHeight {
maxChildHeight = c .Height
}
}
width += maxChildWidth + float64 (extraLeft +extraRight )
height += maxChildHeight + float64 (extraTop +extraBottom )
contentBox := geo .NewBox (geo .NewPoint (0 , 0 ), width , height )
shapeType := d2target .DSL_SHAPE_TO_SHAPE_TYPE [obj .Shape .Value ]
s := shape .NewShape (shapeType , contentBox )
innerBox := s .GetInnerBox ()
innerTop := int (math .Ceil (innerBox .TopLeft .Y ))
innerBottom := int (math .Ceil (height - (innerBox .TopLeft .Y + innerBox .Height )))
innerLeft := int (math .Ceil (innerBox .TopLeft .X ))
innerRight := int (math .Ceil (width - (innerBox .TopLeft .X + innerBox .Width )))
padding .top = go2 .Max (padding .top , innerTop +extraTop )
padding .bottom = go2 .Max (padding .bottom , innerBottom +extraBottom )
padding .left = go2 .Max (padding .left , innerLeft +extraLeft )
padding .right = go2 .Max (padding .right , innerRight +extraRight )
return padding
}
func positionLabelsIcons(obj *d2graph .Object ) {
if obj .Icon != nil && obj .IconPosition == nil {
if len (obj .ChildrenArray ) > 0 {
obj .IconPosition = go2 .Pointer (label .InsideTopLeft .String ())
if obj .LabelPosition == nil {
obj .LabelPosition = go2 .Pointer (label .InsideTopRight .String ())
return
}
} else if obj .SQLTable != nil || obj .Class != nil || obj .Language != "" {
obj .IconPosition = go2 .Pointer (label .OutsideTopLeft .String ())
} else {
obj .IconPosition = go2 .Pointer (label .InsideMiddleCenter .String ())
}
}
if obj .HasLabel () && obj .LabelPosition == nil {
if len (obj .ChildrenArray ) > 0 {
obj .LabelPosition = go2 .Pointer (label .InsideTopCenter .String ())
} else if obj .HasOutsideBottomLabel () {
obj .LabelPosition = go2 .Pointer (label .OutsideBottomCenter .String ())
} else if obj .Icon != nil {
obj .LabelPosition = go2 .Pointer (label .InsideTopCenter .String ())
} else {
obj .LabelPosition = go2 .Pointer (label .InsideMiddleCenter .String ())
}
if float64 (obj .LabelDimensions .Width ) > obj .Width || float64 (obj .LabelDimensions .Height ) > obj .Height {
if len (obj .ChildrenArray ) > 0 {
obj .LabelPosition = go2 .Pointer (label .OutsideTopCenter .String ())
} else {
obj .LabelPosition = go2 .Pointer (label .OutsideBottomCenter .String ())
}
}
}
}
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 .