package d2dagrelayout
import (
"context"
"fmt"
"math"
"sort"
"strings"
"github.com/d2lang/util-go/xdefer"
"github.com/d2lang/util-go/go2"
"github.com/pancsta/d2/d2graph"
"github.com/pancsta/d2/d2layouts/internal/layoutguard"
"github.com/pancsta/d2/d2target"
"github.com/pancsta/d2/lib/geo"
"github.com/pancsta/d2/lib/label"
"github.com/pancsta/d2/lib/shape"
"github.com/d2lang/dagro"
)
const (
MIN_RANK_SEP = 60
EDGE_LABEL_GAP = 20
DEFAULT_PADDING = 30.
MIN_SPACING = 10.
)
type ConfigurableOpts struct {
NodeSep int `json:"nodesep"`
EdgeSep int `json:"edgesep"`
}
var DefaultOpts = ConfigurableOpts {
NodeSep : 60 ,
EdgeSep : 20 ,
}
type dagreOpts struct {
ranksep int
rankdir string
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 ctx == nil {
ctx = context .Background ()
}
if opts == nil {
opts = &DefaultOpts
}
defer xdefer .Errorf (&err , "failed to dagre layout" )
if err := layoutguard .CheckGraph (ctx , "dagre" , g ); err != nil {
return err
}
rootAttrs := dagreOpts {
ConfigurableOpts : ConfigurableOpts {
EdgeSep : opts .EdgeSep ,
NodeSep : opts .NodeSep ,
},
}
isHorizontal := false
switch g .Root .Direction .Value {
case "down" :
rootAttrs .rankdir = "TB"
case "right" :
rootAttrs .rankdir = "LR"
isHorizontal = true
case "left" :
rootAttrs .rankdir = "RL"
isHorizontal = true
case "up" :
rootAttrs .rankdir = "BT"
default :
rootAttrs .rankdir = "TB"
}
for _ , obj := range g .Objects {
positionLabelsIcons (obj )
}
maxLabelWidth := 0
maxLabelHeight := 0
for _ , edge := range g .Edges {
width := edge .LabelDimensions .Width
height := edge .LabelDimensions .Height
maxLabelWidth = go2 .Max (maxLabelWidth , width )
maxLabelHeight = go2 .Max (maxLabelHeight , height )
}
if !isHorizontal {
rootAttrs .ranksep = go2 .Max (100 , maxLabelHeight +40 )
} else {
rootAttrs .ranksep = go2 .Max (100 , maxLabelWidth +40 )
}
mapper := NewObjectMapper ()
for _ , obj := range g .Objects {
mapper .Register (obj )
}
resolver := newEndpointResolver (g )
type endpointPair struct {
src , dst *d2graph .Object
}
resolvedEndpoints := make (map [*d2graph .Edge ]endpointPair , len (g .Edges ))
directedPairCounts := make (map [endpointPair ]int , len (g .Edges ))
for _ , edge := range g .Edges {
src , dst := resolver .resolve (edge )
pair := endpointPair {src : src , dst : dst }
resolvedEndpoints [edge ] = pair
directedPairCounts [pair ]++
}
dagreGraph := dagro .NewGraph (dagro .GraphOptions {Compound : true , Multigraph : true }).SetGraph (dagro .Attrs {
"ranksep" : float64 (rootAttrs .ranksep ),
"edgesep" : float64 (rootAttrs .EdgeSep ),
"nodesep" : float64 (rootAttrs .NodeSep ),
"rankdir" : rootAttrs .rankdir ,
})
for _ , obj := range g .Objects {
id := mapper .ToID (obj )
dagreGraph .SetNode (id , dagro .Attrs {
"id" : id ,
"width" : float64 (int (obj .Width )),
"height" : float64 (int (obj .Height )),
})
if obj .Parent != g .Root {
if err := dagreGraph .SetParent (id , mapper .ToID (obj .Parent )); err != nil {
return err
}
}
}
for _ , edge := range g .Edges {
pair := resolvedEndpoints [edge ]
src , dst := pair .src , pair .dst
width := edge .LabelDimensions .Width
height := edge .LabelDimensions .Height
numEdges := directedPairCounts [pair ]
if src != dst {
numEdges += directedPairCounts [endpointPair {src : dst , dst : src }]
}
if numEdges > 1 {
switch g .Root .Direction .Value {
case "down" , "up" , "" :
width += EDGE_LABEL_GAP
case "left" , "right" :
height += EDGE_LABEL_GAP
}
}
dagreGraph .SetEdge (
mapper .ToID (src ),
mapper .ToID (dst ),
dagro .Attrs {
"width" : float64 (width ),
"height" : float64 (height ),
"labelpos" : "c" ,
},
edge .AbsID (),
)
}
if err := ctx .Err (); err != nil {
return err
}
if err := dagro .Layout (dagreGraph ); err != nil {
return err
}
if err := ctx .Err (); err != nil {
return err
}
for _ , id := range dagreGraph .Nodes () {
dn , ok := dagreGraph .Node (id ).(dagro .Attrs )
if !ok {
return fmt .Errorf ("dagro returned invalid node label for %q" , id )
}
dnID , ok := dn ["id" ].(string )
if !ok {
return fmt .Errorf ("dagro returned node %q without an id" , id )
}
obj := mapper .ToObj (dnID )
if obj == nil {
return fmt .Errorf ("dagro returned unknown node id %q" , dnID )
}
x , xOK := dn ["x" ].(float64 )
y , yOK := dn ["y" ].(float64 )
width , widthOK := dn ["width" ].(float64 )
height , heightOK := dn ["height" ].(float64 )
if !xOK || !yOK || !widthOK || !heightOK {
return fmt .Errorf ("dagro returned invalid geometry for node %q" , id )
}
obj .TopLeft = geo .NewPoint (math .Round (x -width /2 ), math .Round (y -height /2 ))
obj .Width = math .Ceil (width )
obj .Height = math .Ceil (height )
}
dagreEdges := dagreGraph .Edges ()
if len (dagreEdges ) != len (g .Edges ) {
return fmt .Errorf ("dagro returned %d edges, expected %d" , len (dagreEdges ), len (g .Edges ))
}
for i , edge := range g .Edges {
de , ok := dagreGraph .Edge (dagreEdges [i ]).(dagro .Attrs )
if !ok {
return fmt .Errorf ("dagro returned invalid edge label for %q" , dagreEdges [i ].Name )
}
dagrePoints , ok := de ["points" ].([]dagro .Point )
if !ok {
return fmt .Errorf ("dagro returned edge %q without points" , dagreEdges [i ].Name )
}
points := make ([]*geo .Point , len (dagrePoints ))
for i , point := range dagrePoints {
p := geo .NewPoint (point .X , point .Y )
if edge .SrcArrow && !edge .DstArrow {
points [len (dagrePoints )-i -1 ] = p
} else {
points [i ] = p
}
}
points = deduplicateRoutePoints (points )
if edge .Src == edge .Dst {
center := edge .Src .Center ()
points = append ([]*geo .Point {center .Copy ()}, points ...)
points = append (points , center .Copy ())
}
if len (points ) < 2 {
return fmt .Errorf ("dagro returned edge %q without a usable route" , dagreEdges [i ].Name )
}
startIndex , endIndex := 0 , len (points )-1
start , end := points [startIndex ], points [endIndex ]
if edge .Src != edge .Dst {
for i := 1 ; i < len (points ); i ++ {
segment := *geo .NewSegment (points [i -1 ], points [i ])
if intersections := edge .Src .Box .Intersections (segment ); len (intersections ) > 0 {
start = intersections [0 ]
startIndex = i - 1
}
if intersections := edge .Dst .Box .Intersections (segment ); len (intersections ) > 0 {
end = intersections [0 ]
endIndex = i
break
}
}
}
points = points [startIndex : endIndex +1 ]
points [0 ] = start
points [len (points )-1 ] = end
edge .Route = points
}
adjustRankSpacing (g , float64 (rootAttrs .ranksep ), isHorizontal )
adjustCrossRankSpacing (g , float64 (rootAttrs .ranksep ), !isHorizontal )
fitContainerPadding (g , float64 (rootAttrs .ranksep ), isHorizontal )
for _ , edge := range g .Edges {
points := edge .Route
startIndex , endIndex := 0 , len (points )-1
start , end := points [startIndex ], points [endIndex ]
if startIndex +2 < len (points ) {
newStartingSegment := *geo .NewSegment (start , points [startIndex +1 ])
if newStartingSegment .Length () < d2graph .MIN_SEGMENT_LEN {
nextStart := points [startIndex +1 ]
nextEnd := points [startIndex +2 ]
nextSegment := *geo .NewSegment (nextStart , nextEnd )
v := nextSegment .ToVector ()
extendedStart := nextEnd .ToVector ().Add (v .AddLength (d2graph .MIN_SEGMENT_LEN )).ToPoint ()
extended := *geo .NewSegment (nextEnd , extendedStart )
if intersections := edge .Src .Box .Intersections (extended ); len (intersections ) > 0 {
startIndex ++
points [startIndex ] = intersections [0 ]
start = points [startIndex ]
}
}
}
if endIndex -2 >= 0 {
newEndingSegment := *geo .NewSegment (end , points [endIndex -1 ])
if newEndingSegment .Length () < d2graph .MIN_SEGMENT_LEN {
prevStart := points [endIndex -2 ]
prevEnd := points [endIndex -1 ]
prevSegment := *geo .NewSegment (prevStart , prevEnd )
v := prevSegment .ToVector ()
extendedEnd := prevStart .ToVector ().Add (v .AddLength (d2graph .MIN_SEGMENT_LEN )).ToPoint ()
extended := *geo .NewSegment (prevStart , extendedEnd )
if intersections := edge .Dst .Box .Intersections (extended ); len (intersections ) > 0 {
endIndex --
points [endIndex ] = intersections [0 ]
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 ]
vectors := make ([]geo .Vector , 0 , len (points )-1 )
for i := 1 ; i < len (points ); i ++ {
vectors = append (vectors , points [i -1 ].VectorTo (points [i ]))
}
path := make ([]*geo .Point , 0 )
path = append (path , points [0 ])
if len (vectors ) > 1 {
path = append (path , points [0 ].AddVector (vectors [0 ].Multiply (.8 )))
for i := 1 ; i < len (vectors )-2 ; i ++ {
p := points [i ]
v := vectors [i ]
path = append (path , p .AddVector (v .Multiply (.2 )))
path = append (path , p .AddVector (v .Multiply (.5 )))
path = append (path , p .AddVector (v .Multiply (.8 )))
}
path = append (path , points [len (points )-2 ].AddVector (vectors [len (vectors )-1 ].Multiply (.2 )))
edge .IsCurve = true
}
path = append (path , points [len (points )-1 ])
edge .Route = path
if edge .Label .Value != "" {
edge .LabelPosition = go2 .Pointer (label .InsideMiddleCenter .String ())
}
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
}
}
return nil
}
func deduplicateRoutePoints(points []*geo .Point ) []*geo .Point {
if len (points ) < 2 {
return points
}
deduplicated := points [:1 ]
for _ , point := range points [1 :] {
if !point .Equals (deduplicated [len (deduplicated )-1 ]) {
deduplicated = append (deduplicated , point )
}
}
return deduplicated
}
type containerEndpoints struct {
head, tail *d2graph .Object
}
type endpointResolver struct {
g *d2graph .Graph
containers map [*d2graph .Object ]containerEndpoints
}
func newEndpointResolver(g *d2graph .Graph ) *endpointResolver {
return &endpointResolver {
g : g ,
containers : make (map [*d2graph .Object ]containerEndpoints ),
}
}
func (r *endpointResolver ) head (container *d2graph .Object ) *d2graph .Object {
return r .endpoints (container ).head
}
func (r *endpointResolver ) tail (container *d2graph .Object ) *d2graph .Object {
return r .endpoints (container ).tail
}
func (r *endpointResolver ) endpoints (container *d2graph .Object ) containerEndpoints {
endpoints , ok := r .containers [container ]
if !ok {
endpoints .head , endpoints .tail = getLongestEdgeChainEndpoints (r .g , container )
r .containers [container ] = endpoints
}
return endpoints
}
func (r *endpointResolver ) resolve (edge *d2graph .Edge ) (*d2graph .Object , *d2graph .Object ) {
src := edge .Src
for len (src .Children ) > 0 && src .Class == nil && src .SQLTable == nil {
src = r .tail (src )
}
dst := edge .Dst
for len (dst .Children ) > 0 && dst .Class == nil && dst .SQLTable == nil {
dst = r .head (dst )
}
if edge .SrcArrow && !edge .DstArrow {
src , dst = dst , src
}
return src , dst
}
type containerTopology struct {
container *d2graph .Object
hasIncoming map [*d2graph .Object ]bool
outgoing map [*d2graph .Object ][]*d2graph .Object
}
func newContainerTopology(g *d2graph .Graph , container *d2graph .Object ) *containerTopology {
topology := &containerTopology {
container : container ,
hasIncoming : make (map [*d2graph .Object ]bool , len (container .ChildrenArray )),
outgoing : make (map [*d2graph .Object ][]*d2graph .Object , len (container .ChildrenArray )+1 ),
}
for _ , edge := range g .Edges {
src := inContainer (edge .Src , container )
dst := inContainer (edge .Dst , container )
if src == nil || dst == nil {
continue
}
if dst != container {
topology .hasIncoming [dst ] = true
}
if src != dst {
topology .outgoing [src ] = append (topology .outgoing [src ], dst )
}
if src != container && dst != container {
topology .outgoing [container ] = append (topology .outgoing [container ], dst )
}
}
return topology
}
func (t *containerTopology ) longestChainEndpoints () (*d2graph .Object , *d2graph .Object ) {
rank := make (map [*d2graph .Object ]int )
chainLength := make (map [*d2graph .Object ]int )
for _ , obj := range t .container .ChildrenArray {
if t .hasIncoming [obj ] {
continue
}
rank [obj ] = 1
chainLength [obj ] = 1
queue := []*d2graph .Object {obj }
visited := make (map [*d2graph .Object ]struct {})
for len (queue ) > 0 {
curr := queue [0 ]
queue = queue [1 :]
if _ , ok := visited [curr ]; ok {
continue
}
visited [curr ] = struct {}{}
for _ , child := range t .outgoing [curr ] {
if rank [curr ]+1 > rank [child ] {
rank [child ] = rank [curr ] + 1
chainLength [obj ] = go2 .Max (chainLength [obj ], rank [child ])
}
queue = append (queue , child )
}
}
}
maxChainLength := int (math .MinInt32 )
maxRank := int (math .MinInt32 )
for _ , obj := range t .container .ChildrenArray {
maxChainLength = go2 .Max (maxChainLength , chainLength [obj ])
maxRank = go2 .Max (maxRank , rank [obj ])
}
var heads , tails []*d2graph .Object
for _ , obj := range t .container .ChildrenArray {
if rank [obj ] == 1 && chainLength [obj ] == maxChainLength {
heads = append (heads , obj )
}
if rank [obj ] == maxRank {
tails = append (tails , obj )
}
}
head := t .container .ChildrenArray [0 ]
if len (heads ) > 0 {
head = heads [len (heads )/2 ]
}
return head , tails [len (tails )/2 ]
}
func getLongestEdgeChainEndpoints(g *d2graph .Graph , container *d2graph .Object ) (*d2graph .Object , *d2graph .Object ) {
return newContainerTopology (g , container ).longestChainEndpoints ()
}
func inContainer(obj , container *d2graph .Object ) *d2graph .Object {
if obj == nil {
return nil
}
if obj == container {
return obj
}
if obj .Parent == container {
return obj
}
return inContainer (obj .Parent , container )
}
func positionLabelsIcons(obj *d2graph .Object ) {
if obj .Icon != nil && obj .IconPosition == nil {
if len (obj .ChildrenArray ) > 0 {
obj .IconPosition = go2 .Pointer (label .OutsideTopLeft .String ())
if obj .LabelPosition == nil {
obj .LabelPosition = go2 .Pointer (label .OutsideTopRight .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 .OutsideTopCenter .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 ())
}
}
}
}
func getRanks(g *d2graph .Graph , isHorizontal bool ) (ranks [][]*d2graph .Object , objectRanks , startingParentRanks , endingParentRanks map [*d2graph .Object ]int ) {
alignedObjects := make (map [float64 ][]*d2graph .Object )
for _ , obj := range g .Objects {
if !obj .IsContainer () {
if !isHorizontal {
y := math .Ceil (obj .TopLeft .Y + obj .Height /2 )
alignedObjects [y ] = append (alignedObjects [y ], obj )
} else {
x := math .Ceil (obj .TopLeft .X + obj .Width /2 )
alignedObjects [x ] = append (alignedObjects [x ], obj )
}
}
}
levels := make ([]float64 , 0 , len (alignedObjects ))
for l := range alignedObjects {
levels = append (levels , l )
}
sort .Slice (levels , func (i , j int ) bool {
return levels [i ] < levels [j ]
})
ranks = make ([][]*d2graph .Object , 0 , len (levels ))
objectRanks = make (map [*d2graph .Object ]int )
for i , l := range levels {
for _ , obj := range alignedObjects [l ] {
objectRanks [obj ] = i
}
ranks = append (ranks , alignedObjects [l ])
}
startingParentRanks = make (map [*d2graph .Object ]int )
endingParentRanks = make (map [*d2graph .Object ]int )
for _ , obj := range g .Objects {
if obj .IsContainer () {
continue
}
r := objectRanks [obj ]
for parent := obj .Parent ; parent != nil && parent != g .Root ; parent = parent .Parent {
if start , has := startingParentRanks [parent ]; !has || r < start {
startingParentRanks [parent ] = r
}
if end , has := endingParentRanks [parent ]; !has || r > end {
endingParentRanks [parent ] = r
}
}
}
return ranks , objectRanks , startingParentRanks , endingParentRanks
}
func shiftDown(g *d2graph .Graph , start , distance float64 , isHorizontal bool ) {
if isHorizontal {
for _ , edge := range g .Edges {
first , last := edge .Route [0 ], edge .Route [len (edge .Route )-1 ]
if start <= first .X {
onStaticSrc := first .X == edge .Src .TopLeft .X +edge .Src .Width && edge .Src .TopLeft .X < start
if !onStaticSrc {
first .X += distance
}
}
if start <= last .X {
onStaticDst := last .X == edge .Dst .TopLeft .X +edge .Dst .Width && edge .Dst .TopLeft .X < start
if !onStaticDst {
last .X += distance
}
}
for i := 1 ; i < len (edge .Route )-1 ; i ++ {
p := edge .Route [i ]
if p .X < start {
continue
}
p .X += distance
}
}
for _ , obj := range g .Objects {
if obj .TopLeft .X < start {
continue
}
obj .TopLeft .X += distance
}
} else {
for _ , edge := range g .Edges {
first , last := edge .Route [0 ], edge .Route [len (edge .Route )-1 ]
if start <= first .Y {
onStaticSrc := first .Y == edge .Src .TopLeft .Y +edge .Src .Height && edge .Src .TopLeft .Y < start
if !onStaticSrc {
first .Y += distance
}
}
if start <= last .Y {
onStaticDst := last .Y == edge .Dst .TopLeft .Y +edge .Dst .Height && edge .Dst .TopLeft .Y < start
if !onStaticDst {
last .Y += distance
}
}
for i := 1 ; i < len (edge .Route )-1 ; i ++ {
p := edge .Route [i ]
if p .Y < start {
continue
}
p .Y += distance
}
}
for _ , obj := range g .Objects {
if obj .TopLeft .Y < start {
continue
}
obj .TopLeft .Y += distance
}
}
}
func shiftUp(g *d2graph .Graph , start , distance float64 , isHorizontal bool ) {
if isHorizontal {
for _ , edge := range g .Edges {
first , last := edge .Route [0 ], edge .Route [len (edge .Route )-1 ]
if first .X <= start {
onStaticSrc := first .X == edge .Src .TopLeft .X && start < edge .Src .TopLeft .X +edge .Src .Width
if !onStaticSrc {
first .X -= distance
}
}
if last .X <= start {
onStaticDst := last .X == edge .Dst .TopLeft .X && start < edge .Dst .TopLeft .X +edge .Dst .Width
if !onStaticDst {
last .X -= distance
}
}
for i := 1 ; i < len (edge .Route )-1 ; i ++ {
p := edge .Route [i ]
if start < p .X {
continue
}
p .X -= distance
}
}
for _ , obj := range g .Objects {
if start < obj .TopLeft .X {
continue
}
obj .TopLeft .X -= distance
}
} else {
for _ , edge := range g .Edges {
first , last := edge .Route [0 ], edge .Route [len (edge .Route )-1 ]
if first .Y <= start {
onStaticSrc := first .Y == edge .Src .TopLeft .Y && start < edge .Src .TopLeft .Y +edge .Src .Height
if !onStaticSrc {
first .Y -= distance
}
}
if last .Y <= start {
onStaticDst := last .Y == edge .Dst .TopLeft .Y && start < edge .Dst .TopLeft .Y
if !onStaticDst {
last .Y -= distance
}
}
for i := 1 ; i < len (edge .Route )-1 ; i ++ {
p := edge .Route [i ]
if start < p .Y {
continue
}
p .Y -= distance
}
}
for _ , obj := range g .Objects {
if start < obj .TopLeft .Y {
continue
}
obj .TopLeft .Y -= distance
}
}
}
type crossRankIndex struct {
incident map [*d2graph .Object ][]*d2graph .Edge
}
func newCrossRankIndex(g *d2graph .Graph ) *crossRankIndex {
index := &crossRankIndex {
incident : make (map [*d2graph .Object ][]*d2graph .Edge , len (g .Objects )),
}
for _ , edge := range g .Edges {
index .incident [edge .Src ] = append (index .incident [edge .Src ], edge )
if edge .Dst != edge .Src {
index .incident [edge .Dst ] = append (index .incident [edge .Dst ], edge )
}
}
return index
}
func shiftReachableDownIndexed(g *d2graph .Graph , index *crossRankIndex , obj *d2graph .Object , start , distance float64 , isHorizontal , isMargin bool ) map [*d2graph .Object ]struct {} {
q := []*d2graph .Object {obj }
needsMove := make (map [*d2graph .Object ]struct {})
seen := make (map [*d2graph .Object ]struct {})
shifted := make (map [*d2graph .Object ]struct {})
shiftedEdges := make (map [*d2graph .Edge ]struct {})
queue := func (o *d2graph .Object ) {
if _ , in := seen [o ]; in {
return
}
q = append (q , o )
}
threshold := 100.
checkBelow := func (curr *d2graph .Object ) {
currBottom := curr .TopLeft .Y + curr .Height
currRight := curr .TopLeft .X + curr .Width
if isHorizontal {
originalRight := currRight
if _ , in := shifted [curr ]; in {
originalRight -= distance
}
for _ , other := range g .Objects {
if other == curr || curr .IsDescendantOf (other ) {
continue
}
if originalRight < other .TopLeft .X &&
other .TopLeft .X < originalRight +distance +threshold &&
curr .TopLeft .Y < other .TopLeft .Y +other .Height &&
other .TopLeft .Y < currBottom {
queue (other )
}
}
} else {
originalBottom := currBottom
if _ , in := shifted [curr ]; in {
originalBottom -= distance
}
for _ , other := range g .Objects {
if other == curr || curr .IsDescendantOf (other ) {
continue
}
if originalBottom < other .TopLeft .Y &&
other .TopLeft .Y < originalBottom +distance +threshold &&
curr .TopLeft .X < other .TopLeft .X +other .Width &&
other .TopLeft .X < currRight {
queue (other )
}
}
}
}
processQueue := func () {
for len (q ) > 0 {
curr := q [0 ]
q = q [1 :]
if _ , was := seen [curr ]; was {
continue
}
if curr != obj {
if _ , in := needsMove [curr ]; !in {
if isHorizontal {
if curr .TopLeft .X < start {
continue
}
} else {
if curr .TopLeft .Y < start {
continue
}
}
}
}
if isHorizontal {
_ , shift := needsMove [curr ]
if !shift {
if !isMargin {
shift = start < curr .TopLeft .X
} else {
shift = start <= curr .TopLeft .X
}
}
if shift {
curr .TopLeft .X += distance
shifted [curr ] = struct {}{}
}
} else {
_ , shift := needsMove [curr ]
if !shift {
if !isMargin {
shift = start < curr .TopLeft .Y
} else {
shift = start <= curr .TopLeft .Y
}
}
if shift {
curr .TopLeft .Y += distance
shifted [curr ] = struct {}{}
}
}
seen [curr ] = struct {}{}
if curr .Parent != g .Root && !curr .IsDescendantOf (obj ) {
queue (curr .Parent )
}
for _ , child := range curr .ChildrenArray {
queue (child )
}
for _ , e := range index .incident [curr ] {
if _ , in := shiftedEdges [e ]; in {
continue
}
if e .Src == curr && e .Dst == curr {
if isHorizontal {
for _ , p := range e .Route {
p .X += distance
}
} else {
for _ , p := range e .Route {
p .Y += distance
}
}
shiftedEdges [e ] = struct {}{}
continue
} else if e .Src == curr {
last := e .Route [len (e .Route )-1 ]
if isHorizontal {
if start <= last .X &&
e .Dst .TopLeft .X +e .Dst .Width < last .X +distance {
needsMove [e .Dst ] = struct {}{}
}
} else {
if start <= last .Y &&
e .Dst .TopLeft .Y +e .Dst .Height < last .Y +distance {
needsMove [e .Dst ] = struct {}{}
}
}
queue (e .Dst )
first := e .Route [0 ]
startIndex := 0
_ , wasShifted := shifted [curr ]
if isHorizontal {
if wasShifted && first .X < curr .TopLeft .X && first .X < start {
first .X += distance
startIndex ++
}
for i := startIndex ; i < len (e .Route ); i ++ {
p := e .Route [i ]
if start <= p .X {
p .X += distance
}
}
} else {
if wasShifted && first .Y < curr .TopLeft .Y && first .Y < start {
first .Y += distance
startIndex ++
}
for i := startIndex ; i < len (e .Route ); i ++ {
p := e .Route [i ]
if start <= p .Y {
p .Y += distance
}
}
}
shiftedEdges [e ] = struct {}{}
} else if e .Dst == curr {
first := e .Route [0 ]
if isHorizontal {
if start <= first .X &&
e .Src .TopLeft .X +e .Src .Width < first .X +distance {
needsMove [e .Src ] = struct {}{}
}
} else {
if start <= first .Y &&
e .Src .TopLeft .Y +e .Src .Height < first .Y +distance {
needsMove [e .Src ] = struct {}{}
}
}
queue (e .Src )
last := e .Route [len (e .Route )-1 ]
endIndex := len (e .Route )
_ , wasShifted := shifted [curr ]
if isHorizontal {
if wasShifted && last .X < curr .TopLeft .X && last .X < start {
last .X += distance
endIndex --
}
for i := 0 ; i < endIndex ; i ++ {
p := e .Route [i ]
if start <= p .X {
p .X += distance
}
}
} else {
if wasShifted && last .Y < curr .TopLeft .Y && last .Y < start {
last .Y += distance
endIndex --
}
for i := 0 ; i < endIndex ; i ++ {
p := e .Route [i ]
if start <= p .Y {
p .Y += distance
}
}
}
shiftedEdges [e ] = struct {}{}
}
}
checkBelow (curr )
}
}
processQueue ()
grown := make (map [*d2graph .Object ]struct {})
for o := range seen {
if o .Parent == g .Root {
continue
}
if _ , in := shifted [o .Parent ]; in {
continue
}
if _ , in := grown [o .Parent ]; in {
continue
}
for parent := o .Parent ; parent != g .Root ; parent = parent .Parent {
if _ , in := shifted [parent ]; in {
break
}
if _ , in := grown [parent ]; in {
break
}
if isHorizontal {
if parent .TopLeft .X < start {
parent .Width += distance
grown [parent ] = struct {}{}
checkBelow (parent )
processQueue ()
}
} else {
if parent .TopLeft .Y < start {
parent .Height += distance
grown [parent ] = struct {}{}
checkBelow (parent )
processQueue ()
}
}
}
}
increasedMargins := make (map [*d2graph .Object ]struct {})
movedObjects := make ([]*d2graph .Object , 0 , len (shifted ))
for obj := range shifted {
movedObjects = append (movedObjects , obj )
}
for obj := range grown {
movedObjects = append (movedObjects , obj )
}
for _ , moved := range movedObjects {
counts := true
for _ , other := range movedObjects {
if other == moved {
continue
}
if isHorizontal {
if other .TopLeft .Y +other .Height < moved .TopLeft .Y ||
moved .TopLeft .Y +moved .Height < other .TopLeft .Y {
continue
}
if other .TopLeft .X < moved .TopLeft .X &&
moved .TopLeft .X < other .TopLeft .X +other .Width +threshold {
counts = false
break
}
} else {
if other .TopLeft .X +other .Width < moved .TopLeft .X ||
moved .TopLeft .X +moved .Width < other .TopLeft .X {
continue
}
if other .TopLeft .Y < moved .TopLeft .Y &&
moved .TopLeft .Y < other .TopLeft .Y +other .Height +threshold {
counts = false
break
}
}
}
if counts {
increasedMargins [moved ] = struct {}{}
}
}
return increasedMargins
}
func adjustRankSpacing(g *d2graph .Graph , rankSep float64 , isHorizontal bool ) {
ranks , objectRanks , startingParentRanks , endingParentRanks := getRanks (g , isHorizontal )
for rank := len (ranks ) - 1 ; rank >= 0 ; rank -- {
var startingParents []*d2graph .Object
var endingParents []*d2graph .Object
for _ , obj := range ranks [rank ] {
if obj .Parent == g .Root {
continue
}
if r , has := endingParentRanks [obj .Parent ]; has && r == rank {
endingParents = append (endingParents , obj .Parent )
}
if r , has := startingParentRanks [obj .Parent ]; has && r == rank {
startingParents = append (startingParents , obj .Parent )
}
}
startingAncestorPositions := make (map [*d2graph .Object ]float64 )
for len (startingParents ) > 0 {
var ancestors []*d2graph .Object
for _ , parent := range startingParents {
_ , padding := parent .Spacing ()
if _ , has := startingAncestorPositions [parent ]; !has {
startingAncestorPositions [parent ] = math .Inf (1 )
}
var startPosition float64
if isHorizontal {
paddingIncrease := math .Max (0 , padding .Left -rankSep /2 )
startPosition = parent .TopLeft .X - paddingIncrease
} else {
paddingIncrease := math .Max (0 , padding .Top -rankSep /2 )
startPosition = parent .TopLeft .Y - paddingIncrease
}
startingAncestorPositions [parent ] = math .Min (startingAncestorPositions [parent ], startPosition )
for _ , child := range parent .ChildrenArray {
if r , has := objectRanks [child ]; has {
if r != rank {
continue
}
} else {
if startingParentRanks [child ] != rank {
continue
}
}
margin , _ := child .Spacing ()
if isHorizontal {
startPosition = child .TopLeft .X - margin .Left - padding .Left
} else {
startPosition = child .TopLeft .Y - margin .Top - padding .Top
}
startingAncestorPositions [parent ] = math .Min (startingAncestorPositions [parent ], startPosition )
}
if parent .Parent != g .Root {
ancestors = append (ancestors , parent .Parent )
}
}
startingParents = ancestors
}
endingAncestorPositions := make (map [*d2graph .Object ]float64 )
for len (endingParents ) > 0 {
var ancestors []*d2graph .Object
for _ , parent := range endingParents {
_ , padding := parent .Spacing ()
if _ , has := endingAncestorPositions [parent ]; !has {
endingAncestorPositions [parent ] = math .Inf (-1 )
}
var endPosition float64
if isHorizontal {
endPosition = parent .TopLeft .X + parent .Width + padding .Right - rankSep /2.
} else {
endPosition = parent .TopLeft .Y + parent .Height + padding .Bottom - rankSep /2.
}
endingAncestorPositions [parent ] = math .Max (endingAncestorPositions [parent ], endPosition )
for _ , child := range parent .ChildrenArray {
if r , has := objectRanks [child ]; has {
if r != rank {
continue
}
} else {
if endingParentRanks [child ] != rank {
continue
}
}
margin , _ := child .Spacing ()
if isHorizontal {
endPosition = child .TopLeft .X + child .Width + margin .Right + padding .Right
} else {
endPosition = child .TopLeft .Y + child .Height + margin .Bottom + padding .Bottom
}
endingAncestorPositions [parent ] = math .Max (endingAncestorPositions [parent ], endPosition )
}
if parent .Parent != g .Root {
ancestors = append (ancestors , parent .Parent )
}
}
endingParents = ancestors
}
startingAdjustmentOrder := make ([]*d2graph .Object , 0 , len (startingAncestorPositions ))
for ancestor := range startingAncestorPositions {
startingAdjustmentOrder = append (startingAdjustmentOrder , ancestor )
}
sort .Slice (startingAdjustmentOrder , func (i , j int ) bool {
iPos := startingAncestorPositions [startingAdjustmentOrder [i ]]
jPos := startingAncestorPositions [startingAdjustmentOrder [j ]]
return iPos < jPos
})
endingAdjustmentOrder := make ([]*d2graph .Object , 0 , len (endingAncestorPositions ))
for ancestor := range endingAncestorPositions {
endingAdjustmentOrder = append (endingAdjustmentOrder , ancestor )
}
sort .Slice (endingAdjustmentOrder , func (i , j int ) bool {
iPos := endingAncestorPositions [endingAdjustmentOrder [i ]]
jPos := endingAncestorPositions [endingAdjustmentOrder [j ]]
return jPos < iPos
})
for _ , ancestor := range endingAdjustmentOrder {
var position float64
if isHorizontal {
position = ancestor .TopLeft .X + ancestor .Width
} else {
position = ancestor .TopLeft .Y + ancestor .Height
}
endDelta := endingAncestorPositions [ancestor ] - position
if endDelta > 0 {
for _ , obj := range g .Objects {
if !obj .IsContainer () {
continue
}
start := startingParentRanks [obj ]
end := endingParentRanks [obj ]
if start <= rank && rank <= end {
if isHorizontal && position <= obj .TopLeft .X +obj .Width {
obj .Width += endDelta
} else if !isHorizontal &&
position <= obj .TopLeft .Y +obj .Height {
obj .Height += endDelta
}
}
}
shiftDown (g , position , endDelta , isHorizontal )
}
}
for _ , ancestor := range startingAdjustmentOrder {
var position float64
if isHorizontal {
position = ancestor .TopLeft .X
} else {
position = ancestor .TopLeft .Y
}
startDelta := position - startingAncestorPositions [ancestor ]
if startDelta > 0 {
for _ , obj := range g .Objects {
if !obj .IsContainer () {
continue
}
start := startingParentRanks [obj ]
end := endingParentRanks [obj ]
if start <= rank && rank <= end {
if isHorizontal && obj .TopLeft .X <= position {
obj .Width += startDelta
} else if !isHorizontal && obj .TopLeft .Y <= position {
obj .Height += startDelta
}
}
}
shiftUp (g , position , startDelta , isHorizontal )
}
}
}
}
func adjustCrossRankSpacing(g *d2graph .Graph , rankSep float64 , isHorizontal bool ) {
index := newCrossRankIndex (g )
var prevMarginTop , prevMarginBottom , prevMarginLeft , prevMarginRight map [*d2graph .Object ]float64
if isHorizontal {
prevMarginLeft = make (map [*d2graph .Object ]float64 )
prevMarginRight = make (map [*d2graph .Object ]float64 )
} else {
prevMarginTop = make (map [*d2graph .Object ]float64 )
prevMarginBottom = make (map [*d2graph .Object ]float64 )
}
for _ , obj := range g .Objects {
if obj .IsGridDiagram () {
continue
}
margin , padding := obj .Spacing ()
if !isHorizontal {
if prevShift , has := prevMarginBottom [obj ]; has {
margin .Bottom -= prevShift
}
if margin .Bottom > 0 {
increased := shiftReachableDownIndexed (g , index , obj , obj .TopLeft .Y +obj .Height , margin .Bottom , isHorizontal , true )
for o := range increased {
prevMarginBottom [o ] = math .Max (prevMarginBottom [o ], margin .Bottom )
}
}
if padding .Bottom > 0 {
shiftReachableDownIndexed (g , index , obj , obj .TopLeft .Y +obj .Height , padding .Bottom , isHorizontal , false )
obj .Height += padding .Bottom
}
if prevShift , has := prevMarginTop [obj ]; has {
margin .Top -= prevShift
}
if margin .Top > 0 {
increased := shiftReachableDownIndexed (g , index , obj , obj .TopLeft .Y , margin .Top , isHorizontal , true )
for o := range increased {
prevMarginTop [o ] = math .Max (prevMarginTop [o ], margin .Top )
}
}
if padding .Top > 0 {
shiftReachableDownIndexed (g , index , obj , obj .TopLeft .Y , padding .Top , isHorizontal , false )
obj .Height += padding .Top
}
} else {
if prevShift , has := prevMarginRight [obj ]; has {
margin .Right -= prevShift
}
if margin .Right > 0 {
increased := shiftReachableDownIndexed (g , index , obj , obj .TopLeft .X +obj .Width , margin .Right , isHorizontal , true )
for o := range increased {
prevMarginRight [o ] = math .Max (prevMarginRight [o ], margin .Right )
}
}
if padding .Right > 0 {
shiftReachableDownIndexed (g , index , obj , obj .TopLeft .X +obj .Width , padding .Right , isHorizontal , false )
obj .Width += padding .Right
}
if prevShift , has := prevMarginLeft [obj ]; has {
margin .Left -= prevShift
}
if margin .Left > 0 {
increased := shiftReachableDownIndexed (g , index , obj , obj .TopLeft .X , margin .Left , isHorizontal , true )
for o := range increased {
prevMarginLeft [o ] = math .Max (prevMarginLeft [o ], margin .Left )
}
}
if padding .Left > 0 {
shiftReachableDownIndexed (g , index , obj , obj .TopLeft .X , padding .Left , isHorizontal , false )
obj .Width += padding .Left
}
}
}
}
func fitContainerPadding(g *d2graph .Graph , rankSep float64 , isHorizontal bool ) {
for _ , obj := range g .Root .ChildrenArray {
fitPadding (obj )
}
}
func fitPadding(obj *d2graph .Object ) {
dslShape := strings .ToLower (obj .Shape .Value )
shapeType := d2target .DSL_SHAPE_TO_SHAPE_TYPE [dslShape ]
if !obj .IsContainer () || shapeType != shape .SQUARE_TYPE {
return
}
for _ , child := range obj .ChildrenArray {
fitPadding (child )
}
_ , padding := obj .Spacing ()
padding .Top = math .Max (padding .Top , DEFAULT_PADDING )
padding .Bottom = math .Max (padding .Bottom , DEFAULT_PADDING )
padding .Left = math .Max (padding .Left , DEFAULT_PADDING )
padding .Right = math .Max (padding .Right , DEFAULT_PADDING )
currentTop := obj .TopLeft .Y
currentBottom := obj .TopLeft .Y + obj .Height
currentLeft := obj .TopLeft .X
currentRight := obj .TopLeft .X + obj .Width
innerTop := math .Inf (1 )
innerBottom := math .Inf (-1 )
innerLeft := math .Inf (1 )
innerRight := math .Inf (-1 )
var labelPosition , iconPosition label .Position
var labelBox , iconBox *geo .Box
if obj .HasLabel () && obj .LabelPosition != nil {
labelPosition = label .FromString (*obj .LabelPosition )
switch labelPosition {
case label .InsideTopLeft , label .InsideTopCenter , label .InsideTopRight ,
label .InsideBottomLeft , label .InsideBottomCenter , label .InsideBottomRight ,
label .InsideMiddleLeft , label .InsideMiddleRight :
labelTL := obj .GetLabelTopLeft ()
if labelTL != nil {
labelBox = geo .NewBox (labelTL , float64 (obj .LabelDimensions .Width )+2 *label .PADDING , float64 (obj .LabelDimensions .Height ))
}
}
}
if obj .HasIcon () && obj .IconPosition != nil {
iconPosition = label .FromString (*obj .IconPosition )
switch iconPosition {
case label .InsideTopLeft , label .InsideTopCenter , label .InsideTopRight ,
label .InsideBottomLeft , label .InsideBottomCenter , label .InsideBottomRight ,
label .InsideMiddleLeft , label .InsideMiddleRight :
iconTL := obj .GetIconTopLeft ()
if iconTL != nil {
iconBox = geo .NewBox (iconTL , d2target .MAX_ICON_SIZE , d2target .MAX_ICON_SIZE )
}
}
}
var innerBoxes []geo .Box
for _ , child := range obj .ChildrenArray {
margin , _ := child .Spacing ()
dx , dy := child .GetModifierElementAdjustments ()
if labelBox != nil || iconBox != nil {
var childLabelBox *geo .Box
var childLabelPosition , childIconPosition label .Position
if child .HasLabel () && child .LabelPosition != nil {
childLabelPosition = label .FromString (*child .LabelPosition )
if childLabelPosition .IsOutside () {
childLabelTL := child .GetLabelTopLeft ()
childLabelBox = geo .NewBox (
childLabelTL ,
float64 (child .LabelDimensions .Width ),
float64 (child .LabelDimensions .Height ),
)
innerBoxes = append (innerBoxes , *childLabelBox )
}
}
if child .HasIcon () && child .IconPosition != nil {
childIconPosition = label .FromString (*child .IconPosition )
if childIconPosition .IsOutside () {
childIconTL := child .GetIconTopLeft ()
childIconBox := geo .NewBox (childIconTL , d2target .MAX_ICON_SIZE , d2target .MAX_ICON_SIZE )
innerBoxes = append (innerBoxes , *childIconBox )
}
}
}
innerTop = math .Min (innerTop , child .TopLeft .Y -dy -math .Max (margin .Top , padding .Top ))
innerBottom = math .Max (innerBottom , child .TopLeft .Y +child .Height +math .Max (margin .Bottom , padding .Bottom ))
innerLeft = math .Min (innerLeft , child .TopLeft .X -math .Max (margin .Left , padding .Left ))
innerRight = math .Max (innerRight , child .TopLeft .X +child .Width +dx +math .Max (margin .Right , padding .Right ))
}
for _ , edge := range obj .Graph .Edges {
if !edge .Src .IsDescendantOf (obj ) || !edge .Dst .IsDescendantOf (obj ) {
continue
}
if edge .Label .Value != "" {
labelPosition := label .InsideMiddleCenter
if edge .LabelPosition != nil {
labelPosition = label .FromString (*edge .LabelPosition )
}
labelWidth := float64 (edge .LabelDimensions .Width )
labelHeight := float64 (edge .LabelDimensions .Height )
point , _ := labelPosition .GetPointOnRoute (edge .Route , 2 , 0 , labelWidth , labelHeight )
if labelBox != nil || iconBox != nil {
innerBoxes = append (innerBoxes , geo .Box {TopLeft : point , Width : labelWidth , Height : labelHeight })
}
innerTop = math .Min (innerTop , point .Y -padding .Top )
innerBottom = math .Max (innerBottom , point .Y +labelHeight +padding .Bottom )
innerLeft = math .Min (innerLeft , point .X -padding .Left )
innerRight = math .Max (innerRight , point .X +labelWidth +padding .Right )
}
for _ , point := range edge .Route {
innerTop = math .Min (innerTop , point .Y -padding .Top )
innerBottom = math .Max (innerBottom , point .Y +padding .Bottom )
innerLeft = math .Min (innerLeft , point .X -padding .Left )
innerRight = math .Max (innerRight , point .X +padding .Right )
}
}
topDelta := innerTop - currentTop
bottomDelta := currentBottom - innerBottom
leftDelta := innerLeft - currentLeft
rightDelta := currentRight - innerRight
if topDelta > 0 || bottomDelta > 0 || leftDelta > 0 || rightDelta > 0 {
var leftOverlap , rightOverlap , topOverlap , bottomOverlap float64
var labelSide , iconSide geo .Orientation
if labelBox != nil {
switch labelPosition {
case label .InsideTopLeft , label .InsideTopCenter , label .InsideTopRight :
labelSide = geo .Top
case label .InsideBottomLeft , label .InsideBottomCenter , label .InsideBottomRight :
labelSide = geo .Bottom
case label .InsideMiddleLeft :
labelSide = geo .Left
case label .InsideMiddleRight :
labelSide = geo .Right
default :
labelSide = geo .NONE
}
switch labelSide {
case geo .Top :
if topDelta > 0 {
labelBox .TopLeft .Y += topDelta
}
case geo .Bottom :
if bottomDelta > 0 {
labelBox .TopLeft .Y -= bottomDelta
}
case geo .Left :
if leftDelta > 0 {
labelBox .TopLeft .X += leftDelta
}
case geo .Right :
if rightDelta > 0 {
labelBox .TopLeft .X -= rightDelta
}
}
switch labelSide {
case geo .Top :
if topDelta > 0 {
for _ , box := range innerBoxes {
if labelBox .Overlaps (box ) {
dy := labelBox .TopLeft .Y + labelBox .Height - box .TopLeft .Y
topOverlap = go2 .Max (topOverlap , dy )
}
}
}
case geo .Bottom :
if bottomDelta > 0 {
for _ , box := range innerBoxes {
if labelBox .Overlaps (box ) {
dy := box .TopLeft .Y + box .Height - labelBox .TopLeft .Y
bottomOverlap = go2 .Max (bottomOverlap , dy )
}
}
}
case geo .Left :
if leftDelta > 0 {
for _ , box := range innerBoxes {
if labelBox .Overlaps (box ) {
dx := labelBox .TopLeft .X + labelBox .Width - box .TopLeft .X
leftOverlap = go2 .Max (leftOverlap , dx )
}
}
}
case geo .Right :
if rightDelta > 0 {
for _ , box := range innerBoxes {
if labelBox .Overlaps (box ) {
dx := box .TopLeft .X + box .Width - labelBox .TopLeft .X
rightOverlap = go2 .Max (rightOverlap , dx )
}
}
}
}
}
if iconBox != nil {
switch iconPosition {
case label .InsideTopLeft , label .InsideTopCenter , label .InsideTopRight :
iconSide = geo .Top
case label .InsideBottomLeft , label .InsideBottomCenter , label .InsideBottomRight :
iconSide = geo .Bottom
case label .InsideMiddleLeft :
iconSide = geo .Left
case label .InsideMiddleRight :
iconSide = geo .Right
default :
iconSide = geo .NONE
}
switch iconSide {
case geo .Top :
if topDelta > 0 {
iconBox .TopLeft .Y += topDelta
}
case geo .Bottom :
if bottomDelta > 0 {
iconBox .TopLeft .Y -= bottomDelta
}
case geo .Left :
if leftDelta > 0 {
iconBox .TopLeft .X += leftDelta
}
case geo .Right :
if rightDelta > 0 {
iconBox .TopLeft .X -= rightDelta
}
}
switch iconSide {
case geo .Top :
if topDelta > 0 {
for _ , box := range innerBoxes {
if iconBox .Overlaps (box ) {
dy := iconBox .TopLeft .Y + iconBox .Height - box .TopLeft .Y
topOverlap = go2 .Max (topOverlap , dy )
}
}
}
case geo .Bottom :
if bottomDelta > 0 {
for _ , box := range innerBoxes {
if iconBox .Overlaps (box ) {
dy := box .TopLeft .Y + box .Height - iconBox .TopLeft .Y
bottomOverlap = go2 .Max (bottomOverlap , dy )
}
}
}
case geo .Left :
if leftDelta > 0 {
for _ , box := range innerBoxes {
if iconBox .Overlaps (box ) {
dx := iconBox .TopLeft .X + iconBox .Width - box .TopLeft .X
leftOverlap = go2 .Max (leftOverlap , dx )
}
}
}
case geo .Right :
if rightDelta > 0 {
for _ , box := range innerBoxes {
if iconBox .Overlaps (box ) {
dx := box .TopLeft .X + box .Width - iconBox .TopLeft .X
rightOverlap = go2 .Max (rightOverlap , dx )
}
}
}
}
}
if leftOverlap > 0 {
leftDelta -= leftOverlap + MIN_SPACING
}
if rightOverlap > 0 {
rightDelta -= rightOverlap + MIN_SPACING
}
if topOverlap > 0 {
topDelta -= topOverlap + MIN_SPACING
}
if bottomOverlap > 0 {
bottomDelta -= bottomOverlap + MIN_SPACING
}
}
if 0 < topDelta {
topDelta = adjustDeltaForEdges (obj , currentTop , topDelta , false )
if 0 < topDelta {
adjustEdges (obj , currentTop , topDelta , false )
obj .TopLeft .Y += topDelta
obj .Height -= topDelta
}
}
if 0 < bottomDelta {
bottomDelta = adjustDeltaForEdges (obj , currentBottom , -bottomDelta , false )
if 0 < bottomDelta {
adjustEdges (obj , currentBottom , -bottomDelta , false )
obj .Height -= bottomDelta
}
}
if 0 < leftDelta {
leftDelta = adjustDeltaForEdges (obj , currentLeft , leftDelta , true )
if 0 < leftDelta {
adjustEdges (obj , currentLeft , leftDelta , true )
obj .TopLeft .X += leftDelta
obj .Width -= leftDelta
}
}
if 0 < rightDelta {
rightDelta = adjustDeltaForEdges (obj , currentRight , -rightDelta , true )
if 0 < rightDelta {
adjustEdges (obj , currentRight , -rightDelta , true )
obj .Width -= rightDelta
}
}
}
func adjustDeltaForEdges(obj *d2graph .Object , objPosition , delta float64 , isHorizontal bool ) (newMagnitude float64 ) {
isOnCollapsingSide := func (p *geo .Point ) bool {
var position float64
if isHorizontal {
position = p .X
} else {
position = p .Y
}
if geo .PrecisionCompare (position , objPosition , 1 ) == 0 {
return false
}
var isOnSide bool
if isHorizontal {
if geo .PrecisionCompare (p .Y , obj .TopLeft .Y , 1 ) == 0 ||
geo .PrecisionCompare (p .Y , obj .TopLeft .Y +obj .Height , 1 ) == 0 {
isOnSide = true
}
} else {
if geo .PrecisionCompare (p .X , obj .TopLeft .X , 1 ) == 0 ||
geo .PrecisionCompare (p .X , obj .TopLeft .X +obj .Width , 1 ) == 0 {
isOnSide = true
}
}
if !isOnSide {
return false
}
buffer := MIN_SPACING
var isInRange bool
if delta > 0 {
if objPosition <= position && position <= objPosition +delta +buffer {
isInRange = true
}
} else {
if objPosition +delta -buffer <= position && position <= objPosition {
isInRange = true
}
}
return isInRange
}
hasEdgeOnCollapsingSide := false
outermost := objPosition + delta
for _ , edge := range obj .Graph .Edges {
if edge .Src == obj {
p := edge .Route [0 ]
if isOnCollapsingSide (p ) {
hasEdgeOnCollapsingSide = true
var position float64
if isHorizontal {
position = p .X
} else {
position = p .Y
}
if delta < 0 {
outermost = math .Max (outermost , position )
} else {
outermost = math .Min (outermost , position )
}
}
}
if edge .Dst == obj {
p := edge .Route [len (edge .Route )-1 ]
if isOnCollapsingSide (p ) {
hasEdgeOnCollapsingSide = true
var position float64
if isHorizontal {
position = p .X
} else {
position = p .Y
}
if delta < 0 {
outermost = math .Max (outermost , position )
} else {
outermost = math .Min (outermost , position )
}
}
}
}
newMagnitude = math .Abs (delta )
if hasEdgeOnCollapsingSide {
if delta < 0 {
newMagnitude = math .Max (0 , objPosition -(outermost +DEFAULT_PADDING ))
} else {
newMagnitude = math .Max (0 , (outermost -DEFAULT_PADDING )-objPosition )
}
}
return newMagnitude
}
func adjustEdges(obj *d2graph .Object , objPosition , delta float64 , isHorizontal bool ) {
adjust := func (p *geo .Point ) {
var position float64
if isHorizontal {
position = p .X
} else {
position = p .Y
}
if geo .PrecisionCompare (position , objPosition , 1 ) == 0 {
if isHorizontal {
p .X += delta
} else {
p .Y += delta
}
} else {
var isOnSide bool
if isHorizontal {
if geo .PrecisionCompare (p .Y , obj .TopLeft .Y , 1 ) == 0 ||
geo .PrecisionCompare (p .Y , obj .TopLeft .Y +obj .Height , 1 ) == 0 {
isOnSide = true
}
} else {
if geo .PrecisionCompare (p .X , obj .TopLeft .X , 1 ) == 0 ||
geo .PrecisionCompare (p .X , obj .TopLeft .X +obj .Width , 1 ) == 0 {
isOnSide = true
}
}
if isOnSide {
var isInRange bool
if delta > 0 {
if objPosition < position && position < objPosition +delta {
isInRange = true
}
} else {
if objPosition +delta < position && position < objPosition {
isInRange = true
}
}
if isInRange {
if isHorizontal {
p .X = objPosition + delta
} else {
p .Y = objPosition + delta
}
}
}
}
}
for _ , edge := range obj .Graph .Edges {
if edge .Src == obj {
adjust (edge .Route [0 ])
}
if edge .Dst == obj {
adjust (edge .Route [len (edge .Route )-1 ])
}
}
}
The pages are generated with Golds v0.8.7 . (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 .