package d2graph
import (
"math"
"sort"
"strings"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/label"
"oss.terrastruct.com/d2/lib/shape"
)
const MIN_SEGMENT_LEN = 10
func (obj *Object ) MoveWithDescendants (dx , dy float64 ) {
obj .TopLeft .X += dx
obj .TopLeft .Y += dy
for _ , child := range obj .ChildrenArray {
child .MoveWithDescendants (dx , dy )
}
}
func (obj *Object ) MoveWithDescendantsTo (x , y float64 ) {
dx := x - obj .TopLeft .X
dy := y - obj .TopLeft .Y
obj .MoveWithDescendants (dx , dy )
}
func (parent *Object ) RemoveChild (child *Object ) {
delete (parent .Children , strings .ToLower (child .ID ))
for i := 0 ; i < len (parent .ChildrenArray ); i ++ {
if parent .ChildrenArray [i ] == child {
parent .ChildrenArray = append (parent .ChildrenArray [:i ], parent .ChildrenArray [i +1 :]...)
break
}
}
}
func (g *Graph ) ExtractAsNestedGraph (obj *Object ) *Graph {
descendantObjects , edges := pluckObjAndEdges (g , obj )
tempGraph := NewGraph ()
tempGraph .RootLevel = int (obj .Level ()) - 1
tempGraph .Root .ChildrenArray = []*Object {obj }
tempGraph .Root .Children [strings .ToLower (obj .ID )] = obj
for _ , descendantObj := range descendantObjects {
descendantObj .Graph = tempGraph
}
tempGraph .Objects = descendantObjects
tempGraph .Edges = edges
obj .Parent .RemoveChild (obj )
obj .Parent = tempGraph .Root
return tempGraph
}
func pluckObjAndEdges(g *Graph , obj *Object ) (descendantsObjects []*Object , edges []*Edge ) {
for i := 0 ; i < len (g .Edges ); i ++ {
edge := g .Edges [i ]
if edge .Src .IsDescendantOf (obj ) && edge .Dst .IsDescendantOf (obj ) {
edges = append (edges , edge )
g .Edges = append (g .Edges [:i ], g .Edges [i +1 :]...)
i --
}
}
for i := 0 ; i < len (g .Objects ); i ++ {
temp := g .Objects [i ]
if temp .IsDescendantOf (obj ) {
descendantsObjects = append (descendantsObjects , temp )
g .Objects = append (g .Objects [:i ], g .Objects [i +1 :]...)
i --
}
}
return descendantsObjects , edges
}
func (g *Graph ) InjectNestedGraph (tempGraph *Graph , parent *Object ) {
obj := tempGraph .Root .ChildrenArray [0 ]
dx := 0 - obj .TopLeft .X
dy := 0 - obj .TopLeft .Y
obj .MoveWithDescendants (dx , dy )
for _ , e := range tempGraph .Edges {
e .Move (dx , dy )
}
obj .Parent = parent
for _ , obj := range tempGraph .Objects {
obj .Graph = g
}
g .Objects = append (g .Objects , tempGraph .Objects ...)
parent .Children [strings .ToLower (obj .ID )] = obj
parent .ChildrenArray = append (parent .ChildrenArray , obj )
g .Edges = append (g .Edges , tempGraph .Edges ...)
}
func (obj *Object ) ShiftDescendants (dx , dy float64 ) {
movedEdges := make (map [*Edge ]struct {})
for _ , e := range obj .Graph .Edges {
isSrcDesc := e .Src .IsDescendantOf (obj )
isDstDesc := e .Dst .IsDescendantOf (obj )
if isSrcDesc && isDstDesc {
movedEdges [e ] = struct {}{}
for _ , p := range e .Route {
p .X += dx
p .Y += dy
}
}
}
obj .IterDescendants (func (_ , curr *Object ) {
curr .TopLeft .X += dx
curr .TopLeft .Y += dy
for _ , e := range obj .Graph .Edges {
if _ , ok := movedEdges [e ]; ok {
continue
}
isSrc := e .Src == curr
isDst := e .Dst == curr
if isSrc && isDst {
for _ , p := range e .Route {
p .X += dx
p .Y += dy
}
} else if isSrc {
if dx == 0 {
e .ShiftStart (dy , false )
} else if dy == 0 {
e .ShiftStart (dx , true )
} else {
e .Route [0 ].X += dx
e .Route [0 ].Y += dy
}
} else if isDst {
if dx == 0 {
e .ShiftEnd (dy , false )
} else if dy == 0 {
e .ShiftEnd (dx , true )
} else {
e .Route [len (e .Route )-1 ].X += dx
e .Route [len (e .Route )-1 ].Y += dy
}
}
if isSrc || isDst {
movedEdges [e ] = struct {}{}
}
}
})
}
func (edge *Edge ) ShiftStart (delta float64 , isHorizontal bool ) {
position := func (p *geo .Point ) float64 {
if isHorizontal {
return p .X
}
return p .Y
}
start := edge .Route [0 ]
next := edge .Route [1 ]
isIncreasing := position (start ) < position (next )
if isHorizontal {
start .X += delta
} else {
start .Y += delta
}
if isIncreasing == (delta < 0 ) {
return
}
isAligned := func (p *geo .Point ) bool {
if isHorizontal {
return p .Y == start .Y
}
return p .X == start .X
}
isPastStart := func (p *geo .Point ) bool {
if delta > 0 {
return position (p ) < position (start )
} else {
return position (p ) > position (start )
}
}
needsRemoval := false
toRemove := make ([]bool , len (edge .Route ))
for i := 1 ; i < len (edge .Route )-1 ; i ++ {
if !isAligned (edge .Route [i ]) {
break
}
if isPastStart (edge .Route [i ]) {
toRemove [i ] = true
needsRemoval = true
}
}
if needsRemoval {
edge .Route = geo .RemovePoints (edge .Route , toRemove )
}
}
func (edge *Edge ) ShiftEnd (delta float64 , isHorizontal bool ) {
position := func (p *geo .Point ) float64 {
if isHorizontal {
return p .X
}
return p .Y
}
end := edge .Route [len (edge .Route )-1 ]
prev := edge .Route [len (edge .Route )-2 ]
isIncreasing := position (prev ) < position (end )
if isHorizontal {
end .X += delta
} else {
end .Y += delta
}
if isIncreasing == (delta > 0 ) {
return
}
isAligned := func (p *geo .Point ) bool {
if isHorizontal {
return p .Y == end .Y
}
return p .X == end .X
}
isPastEnd := func (p *geo .Point ) bool {
if delta > 0 {
return position (p ) < position (end )
} else {
return position (p ) > position (end )
}
}
needsRemoval := false
toRemove := make ([]bool , len (edge .Route ))
for i := len (edge .Route ) - 2 ; i > 0 ; i -- {
if !isAligned (edge .Route [i ]) {
break
}
if isPastEnd (edge .Route [i ]) {
toRemove [i ] = true
needsRemoval = true
}
}
if needsRemoval {
edge .Route = geo .RemovePoints (edge .Route , toRemove )
}
}
func (obj *Object ) GetModifierElementAdjustments () (dx , dy float64 ) {
if obj .Is3D () {
if obj .Shape .Value == d2target .ShapeHexagon {
dy = d2target .THREE_DEE_OFFSET / 2
} else {
dy = d2target .THREE_DEE_OFFSET
}
dx = d2target .THREE_DEE_OFFSET
} else if obj .IsMultiple () {
dy = d2target .MULTIPLE_OFFSET
dx = d2target .MULTIPLE_OFFSET
}
return dx , dy
}
func (obj *Object ) GetMargin () geo .Spacing {
margin := geo .Spacing {}
if obj .HasLabel () && obj .LabelPosition != nil {
position := label .FromString (*obj .LabelPosition )
labelWidth := float64 (obj .LabelDimensions .Width + label .PADDING )
labelHeight := float64 (obj .LabelDimensions .Height + label .PADDING )
switch position {
case label .OutsideTopLeft , label .OutsideTopCenter , label .OutsideTopRight :
margin .Top = labelHeight
case label .OutsideBottomLeft , label .OutsideBottomCenter , label .OutsideBottomRight :
margin .Bottom = labelHeight
case label .OutsideLeftTop , label .OutsideLeftMiddle , label .OutsideLeftBottom :
margin .Left = labelWidth
case label .OutsideRightTop , label .OutsideRightMiddle , label .OutsideRightBottom :
margin .Right = labelWidth
}
if labelWidth > obj .Width {
dx := labelWidth - obj .Width
switch position {
case label .OutsideTopLeft , label .OutsideBottomLeft :
margin .Right = dx
case label .OutsideTopCenter , label .OutsideBottomCenter :
margin .Left = math .Ceil (dx / 2 )
margin .Right = math .Ceil (dx / 2 )
case label .OutsideTopRight , label .OutsideBottomRight :
margin .Left = dx
}
}
if labelHeight > obj .Height {
dy := labelHeight - obj .Height
switch position {
case label .OutsideLeftTop , label .OutsideRightTop :
margin .Bottom = dy
case label .OutsideLeftMiddle , label .OutsideRightMiddle :
margin .Top = math .Ceil (dy / 2 )
margin .Bottom = math .Ceil (dy / 2 )
case label .OutsideLeftBottom , label .OutsideRightBottom :
margin .Top = dy
}
}
}
if obj .HasIcon () && obj .IconPosition != nil {
position := label .FromString (*obj .IconPosition )
iconSize := float64 (d2target .MAX_ICON_SIZE + label .PADDING )
switch position {
case label .OutsideTopLeft , label .OutsideTopCenter , label .OutsideTopRight :
margin .Top = math .Max (margin .Top , iconSize )
case label .OutsideBottomLeft , label .OutsideBottomCenter , label .OutsideBottomRight :
margin .Bottom = math .Max (margin .Bottom , iconSize )
case label .OutsideLeftTop , label .OutsideLeftMiddle , label .OutsideLeftBottom :
margin .Left = math .Max (margin .Left , iconSize )
case label .OutsideRightTop , label .OutsideRightMiddle , label .OutsideRightBottom :
margin .Right = math .Max (margin .Right , iconSize )
}
}
dx , dy := obj .GetModifierElementAdjustments ()
margin .Right += dx
margin .Top += dy
return margin
}
func (obj *Object ) ToShape () shape .Shape {
tl := obj .TopLeft
if tl == nil {
tl = geo .NewPoint (0 , 0 )
}
dslShape := strings .ToLower (obj .Shape .Value )
shapeType := d2target .DSL_SHAPE_TO_SHAPE_TYPE [dslShape ]
contentBox := geo .NewBox (tl , obj .Width , obj .Height )
s := shape .NewShape (shapeType , contentBox )
if shapeType == shape .CLOUD_TYPE && obj .ContentAspectRatio != nil {
s .SetInnerBoxAspectRatio (*obj .ContentAspectRatio )
}
return s
}
func (obj *Object ) GetLabelTopLeft () *geo .Point {
if obj .LabelPosition == nil {
return nil
}
s := obj .ToShape ()
labelPosition := label .FromString (*obj .LabelPosition )
var box *geo .Box
if labelPosition .IsOutside () {
box = s .GetBox ()
} else {
box = s .GetInnerBox ()
}
labelTL := labelPosition .GetPointOnBox (box , label .PADDING ,
float64 (obj .LabelDimensions .Width ),
float64 (obj .LabelDimensions .Height ),
)
return labelTL
}
func (obj *Object ) GetIconTopLeft () *geo .Point {
if obj .IconPosition == nil {
return nil
}
s := obj .ToShape ()
iconPosition := label .FromString (*obj .IconPosition )
var box *geo .Box
if iconPosition .IsOutside () {
box = s .GetBox ()
} else {
box = s .GetInnerBox ()
}
return iconPosition .GetPointOnBox (box , label .PADDING , d2target .MAX_ICON_SIZE , d2target .MAX_ICON_SIZE )
}
func (edge *Edge ) TraceToShape (points []*geo .Point , startIndex , endIndex int ) (newStart , newEnd int ) {
srcShape := edge .Src .ToShape ()
dstShape := edge .Dst .ToShape ()
startingSegment := geo .Segment {Start : points [startIndex +1 ], End : points [startIndex ]}
var overlapsOutsideLabel , overlapsOutsideIcon bool
if edge .Src .HasLabel () {
labelPosition := label .FromString (*edge .Src .LabelPosition )
if labelPosition .IsOutside () {
labelWidth := float64 (edge .Src .LabelDimensions .Width )
labelHeight := float64 (edge .Src .LabelDimensions .Height )
labelTL := labelPosition .GetPointOnBox (edge .Src .Box , label .PADDING , labelWidth , labelHeight )
labelBox := geo .NewBox (labelTL , labelWidth , labelHeight )
labelBox .TopLeft .X -= label .PADDING
labelBox .Width += 2 * label .PADDING
for labelBox .Contains (startingSegment .End ) && startIndex +1 > endIndex {
startingSegment .Start = startingSegment .End
startingSegment .End = points [startIndex +2 ]
startIndex ++
}
if intersections := labelBox .Intersections (startingSegment ); len (intersections ) > 0 {
overlapsOutsideLabel = true
p := intersections [0 ]
if len (intersections ) > 1 {
p = findOuterIntersection (labelPosition , intersections )
}
points [startIndex ] = p
startingSegment .End = p
if startIndex +1 < endIndex && startingSegment .Length () < MIN_SEGMENT_LEN {
points [startIndex +1 ] = points [startIndex ]
startIndex ++
}
}
}
}
if !overlapsOutsideLabel && edge .Src .HasIcon () {
iconPosition := label .FromString (*edge .Src .IconPosition )
if iconPosition .IsOutside () {
iconWidth := float64 (d2target .MAX_ICON_SIZE )
iconHeight := float64 (d2target .MAX_ICON_SIZE )
iconTL := iconPosition .GetPointOnBox (edge .Src .Box , label .PADDING , iconWidth , iconHeight )
iconBox := geo .NewBox (iconTL , iconWidth , iconHeight )
for iconBox .Contains (startingSegment .End ) && startIndex +1 > endIndex {
startingSegment .Start = startingSegment .End
startingSegment .End = points [startIndex +2 ]
startIndex ++
}
if intersections := iconBox .Intersections (startingSegment ); len (intersections ) > 0 {
overlapsOutsideIcon = true
p := intersections [0 ]
if len (intersections ) > 1 {
p = findOuterIntersection (iconPosition , intersections )
}
points [startIndex ] = p
startingSegment .End = p
if startIndex +1 < endIndex && startingSegment .Length () < MIN_SEGMENT_LEN {
points [startIndex +1 ] = points [startIndex ]
startIndex ++
}
}
}
}
if !overlapsOutsideLabel && !overlapsOutsideIcon {
if intersections := edge .Src .Intersections (startingSegment ); len (intersections ) > 0 {
points [startIndex ] = intersections [0 ]
startingSegment .End = intersections [0 ]
if startIndex +1 < endIndex && startingSegment .Length () < MIN_SEGMENT_LEN {
points [startIndex +1 ] = points [startIndex ]
startIndex ++
}
}
points [startIndex ] = shape .TraceToShapeBorder (srcShape , points [startIndex ], points [startIndex +1 ])
}
endingSegment := geo .Segment {Start : points [endIndex -1 ], End : points [endIndex ]}
overlapsOutsideLabel = false
overlapsOutsideIcon = false
if edge .Dst .HasLabel () {
labelPosition := label .FromString (*edge .Dst .LabelPosition )
if labelPosition .IsOutside () {
labelWidth := float64 (edge .Dst .LabelDimensions .Width )
labelHeight := float64 (edge .Dst .LabelDimensions .Height )
labelTL := labelPosition .GetPointOnBox (edge .Dst .Box , label .PADDING , labelWidth , labelHeight )
labelBox := geo .NewBox (labelTL , labelWidth , labelHeight )
labelBox .TopLeft .X -= label .PADDING
labelBox .Width += 2 * label .PADDING
for labelBox .Contains (endingSegment .Start ) && endIndex -1 > startIndex {
endingSegment .End = endingSegment .Start
endingSegment .Start = points [endIndex -2 ]
endIndex --
}
if intersections := labelBox .Intersections (endingSegment ); len (intersections ) > 0 {
overlapsOutsideLabel = true
p := intersections [0 ]
if len (intersections ) > 1 {
p = findOuterIntersection (labelPosition , intersections )
}
points [endIndex ] = p
endingSegment .End = p
if endIndex -1 > startIndex && endingSegment .Length () < MIN_SEGMENT_LEN {
points [endIndex -1 ] = points [endIndex ]
endIndex --
}
}
}
}
if !overlapsOutsideLabel && edge .Dst .HasIcon () {
iconPosition := label .FromString (*edge .Dst .IconPosition )
if iconPosition .IsOutside () {
iconSize := d2target .GetIconSize (edge .Dst .Box , iconPosition .String ())
iconWidth := float64 (iconSize )
iconHeight := float64 (iconSize )
labelTL := iconPosition .GetPointOnBox (edge .Dst .Box , label .PADDING , iconWidth , iconHeight )
iconBox := geo .NewBox (labelTL , iconWidth , iconHeight )
for iconBox .Contains (endingSegment .Start ) && endIndex -1 > startIndex {
endingSegment .End = endingSegment .Start
endingSegment .Start = points [endIndex -2 ]
endIndex --
}
if intersections := iconBox .Intersections (endingSegment ); len (intersections ) > 0 {
overlapsOutsideIcon = true
p := intersections [0 ]
if len (intersections ) > 1 {
p = findOuterIntersection (iconPosition , intersections )
}
points [endIndex ] = p
endingSegment .End = p
if endIndex -1 > startIndex && endingSegment .Length () < MIN_SEGMENT_LEN {
points [endIndex -1 ] = points [endIndex ]
endIndex --
}
}
}
}
if !overlapsOutsideLabel && !overlapsOutsideIcon {
if intersections := edge .Dst .Intersections (endingSegment ); len (intersections ) > 0 {
points [endIndex ] = intersections [0 ]
endingSegment .End = intersections [0 ]
if endIndex -1 > startIndex && endingSegment .Length () < MIN_SEGMENT_LEN {
points [endIndex -1 ] = points [endIndex ]
endIndex --
}
}
points [endIndex ] = shape .TraceToShapeBorder (dstShape , points [endIndex ], points [endIndex -1 ])
}
return startIndex , endIndex
}
func findOuterIntersection(labelPosition label .Position , intersections []*geo .Point ) *geo .Point {
switch labelPosition {
case label .OutsideTopLeft , label .OutsideTopRight , label .OutsideTopCenter :
sort .Slice (intersections , func (i , j int ) bool {
return intersections [i ].Y < intersections [j ].Y
})
case label .OutsideBottomLeft , label .OutsideBottomRight , label .OutsideBottomCenter :
sort .Slice (intersections , func (i , j int ) bool {
return intersections [i ].Y > intersections [j ].Y
})
case label .OutsideLeftTop , label .OutsideLeftMiddle , label .OutsideLeftBottom :
sort .Slice (intersections , func (i , j int ) bool {
return intersections [i ].X < intersections [j ].X
})
case label .OutsideRightTop , label .OutsideRightMiddle , label .OutsideRightBottom :
sort .Slice (intersections , func (i , j int ) bool {
return intersections [i ].X > intersections [j ].X
})
}
return intersections [0 ]
}
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 .