d2/d2layouts/d2dagrelayout/layout.go

1818 lines
50 KiB
Go

package d2dagrelayout
import (
"context"
_ "embed"
"encoding/json"
"fmt"
"math"
"sort"
"strings"
"cdr.dev/slog"
"github.com/dop251/goja"
"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/label"
"oss.terrastruct.com/d2/lib/log"
"oss.terrastruct.com/d2/lib/shape"
)
//go:embed setup.js
var setupJS string
//go:embed dagre.js
var dagreJS string
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 DagreNode struct {
ID string `json:"id"`
X float64 `json:"x"`
Y float64 `json:"y"`
Width float64 `json:"width"`
Height float64 `json:"height"`
}
type DagreEdge struct {
Points []*geo.Point `json:"points"`
}
type dagreOpts struct {
// for a top to bottom graph: ranksep is y spacing, nodesep is x spacing, edgesep is x spacing
ranksep int
// graph direction: tb (top to bottom)| bt | lr | rl
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 opts == nil {
opts = &DefaultOpts
}
defer xdefer.Errorf(&err, "failed to dagre layout")
debugJS := false
vm := goja.New()
if _, err := vm.RunString(dagreJS); err != nil {
return err
}
if _, err := vm.RunString(setupJS); 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"
}
// set label and icon positions for dagre
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)
// use existing config
// rootAttrs.NodeSep = rootAttrs.EdgeSep
// // configure vertical padding
// rootAttrs.EdgeSep = maxLabelHeight + 40
// Note: non-containers have both of these as padding (rootAttrs.NodeSep + rootAttrs.EdgeSep)
}
configJS := setGraphAttrs(rootAttrs)
if _, err := vm.RunString(configJS); err != nil {
return err
}
mapper := NewObjectMapper()
for _, obj := range g.Objects {
mapper.Register(obj)
}
loadScript := ""
for _, obj := range g.Objects {
loadScript += mapper.generateAddNodeLine(obj, int(obj.Width), int(obj.Height))
if obj.Parent != g.Root {
loadScript += mapper.generateAddParentLine(obj, obj.Parent)
}
}
for _, edge := range g.Edges {
src, dst := getEdgeEndpoints(g, edge)
width := edge.LabelDimensions.Width
height := edge.LabelDimensions.Height
numEdges := 0
for _, e := range g.Edges {
otherSrc, otherDst := getEdgeEndpoints(g, e)
if (otherSrc == src && otherDst == dst) || (otherSrc == dst && otherDst == src) {
numEdges++
}
}
// We want to leave some gap between multiple edges
if numEdges > 1 {
switch g.Root.Direction.Value {
case "down", "up", "":
width += EDGE_LABEL_GAP
case "left", "right":
height += EDGE_LABEL_GAP
}
}
loadScript += mapper.generateAddEdgeLine(src, dst, edge.AbsID(), width, height)
}
if debugJS {
log.Debug(ctx, "script", slog.F("all", setupJS+configJS+loadScript))
}
if _, err := vm.RunString(loadScript); err != nil {
return err
}
if _, err := vm.RunString(`dagre.layout(g)`); err != nil {
if debugJS {
log.Warn(ctx, "layout error", slog.F("err", err))
}
return err
}
for i := range g.Objects {
val, err := vm.RunString(fmt.Sprintf("JSON.stringify(g.node(g.nodes()[%d]))", i))
if err != nil {
return err
}
var dn DagreNode
if err := json.Unmarshal([]byte(val.String()), &dn); err != nil {
return err
}
if debugJS {
log.Debug(ctx, "graph", slog.F("json", dn))
}
obj := mapper.ToObj(dn.ID)
// dagre gives center of node
obj.TopLeft = geo.NewPoint(math.Round(dn.X-dn.Width/2), math.Round(dn.Y-dn.Height/2))
obj.Width = math.Ceil(dn.Width)
obj.Height = math.Ceil(dn.Height)
}
for i, edge := range g.Edges {
val, err := vm.RunString(fmt.Sprintf("JSON.stringify(g.edge(g.edges()[%d]))", i))
if err != nil {
return err
}
var de DagreEdge
if err := json.Unmarshal([]byte(val.String()), &de); err != nil {
return err
}
if debugJS {
log.Debug(ctx, "graph", slog.F("json", de))
}
points := make([]*geo.Point, len(de.Points))
for i := range de.Points {
if edge.SrcArrow && !edge.DstArrow {
points[len(de.Points)-i-1] = de.Points[i].Copy()
} else {
points[i] = de.Points[i].Copy()
}
}
startIndex, endIndex := 0, len(points)-1
start, end := points[startIndex], points[endIndex]
// chop where edge crosses the source/target boxes since container edges were routed to a descendant
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]
// arrowheads can appear broken if segments are very short from dagre routing a point just outside the shape
// to fix this, we try extending the previous segment into the shape instead of having a very short segment
if startIndex+2 < len(points) {
newStartingSegment := *geo.NewSegment(start, points[startIndex+1])
if newStartingSegment.Length() < d2graph.MIN_SEGMENT_LEN {
// we don't want a very short segment right next to the source because it will mess up the arrowhead
// instead we want to extend the next segment into the shape border if possible
nextStart := points[startIndex+1]
nextEnd := points[startIndex+2]
// Note: in other direction to extend towards source
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 {
// extend the prev segment into the shape border if possible
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 the edge passes through 3d/multiple, use the offset box for tracing to border
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]
// build a curved path from the dagre route
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
// compile needs to assign edge label positions
if edge.Label.Value != "" {
edge.LabelPosition = go2.Pointer(label.InsideMiddleCenter.String())
}
// undo 3d/multiple offset
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 getEdgeEndpoints(g *d2graph.Graph, edge *d2graph.Edge) (*d2graph.Object, *d2graph.Object) {
// dagre doesn't work with edges to containers so we connect container edges to their first child instead (going all the way down)
// we will chop the edge where it intersects the container border so it only shows the edge from the container
src := edge.Src
for len(src.Children) > 0 && src.Class == nil && src.SQLTable == nil {
// We want to get the bottom node of sources, setting its rank higher than all children
src = getLongestEdgeChainTail(g, src)
}
dst := edge.Dst
for len(dst.Children) > 0 && dst.Class == nil && dst.SQLTable == nil {
dst = getLongestEdgeChainHead(g, dst)
}
if edge.SrcArrow && !edge.DstArrow {
// for `b <- a`, edge.Edge is `a -> b` and we expect this routing result
src, dst = dst, src
}
return src, dst
}
func setGraphAttrs(attrs dagreOpts) string {
return fmt.Sprintf(`g.setGraph({
ranksep: %d,
edgesep: %d,
nodesep: %d,
rankdir: "%s",
});
`,
attrs.ranksep,
attrs.ConfigurableOpts.EdgeSep,
attrs.ConfigurableOpts.NodeSep,
attrs.rankdir,
)
}
// getLongestEdgeChainHead finds the longest chain in a container and gets its head
// If there are multiple chains of the same length, get the head closest to the center
func getLongestEdgeChainHead(g *d2graph.Graph, container *d2graph.Object) *d2graph.Object {
rank := make(map[*d2graph.Object]int)
chainLength := make(map[*d2graph.Object]int)
for _, obj := range container.ChildrenArray {
isHead := true
for _, e := range g.Edges {
if inContainer(e.Src, container) != nil && inContainer(e.Dst, obj) != nil {
isHead = false
break
}
}
if !isHead {
continue
}
rank[obj] = 1
chainLength[obj] = 1
// BFS
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 _, e := range g.Edges {
child := inContainer(e.Dst, container)
if child == curr {
continue
}
if child != nil && inContainer(e.Src, curr) != nil {
if rank[curr]+1 > rank[child] {
rank[child] = rank[curr] + 1
chainLength[obj] = go2.Max(chainLength[obj], rank[child])
}
queue = append(queue, child)
}
}
}
}
max := int(math.MinInt32)
for _, obj := range container.ChildrenArray {
if chainLength[obj] > max {
max = chainLength[obj]
}
}
var heads []*d2graph.Object
for i, obj := range container.ChildrenArray {
if rank[obj] == 1 && chainLength[obj] == max {
heads = append(heads, container.ChildrenArray[i])
}
}
if len(heads) > 0 {
return heads[int(math.Floor(float64(len(heads))/2.0))]
}
return container.ChildrenArray[0]
}
// getLongestEdgeChainTail gets the node at the end of the longest edge chain, because that will be the end of the container
// and is what external connections should connect with.
// If there are multiple of same length, get the one closest to the middle
func getLongestEdgeChainTail(g *d2graph.Graph, container *d2graph.Object) *d2graph.Object {
rank := make(map[*d2graph.Object]int)
for _, obj := range container.ChildrenArray {
isHead := true
for _, e := range g.Edges {
if inContainer(e.Src, container) != nil && inContainer(e.Dst, obj) != nil {
isHead = false
break
}
}
if !isHead {
continue
}
rank[obj] = 1
// BFS
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 _, e := range g.Edges {
child := inContainer(e.Dst, container)
if child == curr {
continue
}
if child != nil && inContainer(e.Src, curr) != nil {
rank[child] = go2.Max(rank[child], rank[curr]+1)
queue = append(queue, child)
}
}
}
}
max := int(math.MinInt32)
for _, obj := range container.ChildrenArray {
if rank[obj] > max {
max = rank[obj]
}
}
var tails []*d2graph.Object
for i, obj := range container.ChildrenArray {
if rank[obj] == max {
tails = append(tails, container.ChildrenArray[i])
}
}
return tails[int(math.Floor(float64(len(tails))/2.0))]
}
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]
// update all ancestor's min/max ranks
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
}
// shift everything down by distance if it is at or below start position
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 {
// src is not shifting and we are on src so don't shift
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 {
// src is not shifting and we are on src so don't shift
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 {
// src is not shifting and we are on src so don't shift
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 {
// don't shift first edge point if src is not shifting and we are on src
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]
// for _, p := range edge.Route {
if start < p.Y {
continue
}
p.Y -= distance
}
}
for _, obj := range g.Objects {
if start < obj.TopLeft.Y {
continue
}
obj.TopLeft.Y -= distance
}
}
}
// shift down everything that is below start
// shift all nodes that are reachable via an edge or being directly below a shifting node or expanding container
// expand containers to wrap shifted nodes
func shiftReachableDown(g *d2graph.Graph, 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)
}
// if object below is within this distance after shifting, also shift it
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
}
// skip other objects behind start
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 g.Edges {
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{}{}
}
}
// check for nodes below that need to move from the shift
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
// check if any other shifted is directly above
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 {
// doesn't line up vertically
continue
}
// above and within threshold
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 {
// doesn't line up horizontally
continue
}
// above and within threshold
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)
// shifting bottom rank down first, then moving up to next rank
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)
}
// adjust starting ancestors top-down
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)
}
// adjust ending ancestors bottom-up
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) {
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 := shiftReachableDown(g, 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 {
shiftReachableDown(g, 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 := shiftReachableDown(g, obj, obj.TopLeft.Y, margin.Top, isHorizontal, true)
for o := range increased {
prevMarginTop[o] = math.Max(prevMarginTop[o], margin.Top)
}
}
if padding.Top > 0 {
shiftReachableDown(g, 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 := shiftReachableDown(g, 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 {
shiftReachableDown(g, 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 := shiftReachableDown(g, obj, obj.TopLeft.X, margin.Left, isHorizontal, true)
for o := range increased {
prevMarginLeft[o] = math.Max(prevMarginLeft[o], margin.Left)
}
}
if padding.Left > 0 {
shiftReachableDown(g, 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]
// Note: there's no shape-specific padding/placement in dagre yet
if !obj.IsContainer() || shapeType != shape.SQUARE_TYPE {
return
}
for _, child := range obj.ChildrenArray {
fitPadding(child)
}
// we will compute a perfectly fit innerBox merging our padding with children's margin,
// but we need to add padding and margin together if an outside child label will overlap with our inside label
_, 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)
// where we are (current*) vs where we want to fit each side to (inner*)
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)
// we create boxes for our inside label and icon, and will check against overlaps with any internal boxes
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)
}
}
}
// update the inner positions for children's margin and collect the outside boxes that we cannot overlap with
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))
}
// collect edge label boxes and update inner box for internal edges
for _, edge := range obj.Graph.Edges {
if !edge.Src.IsDescendantOf(obj) || !edge.Dst.IsDescendantOf(obj) {
continue
}
// check internal edge + their labels
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)
}
}
// how much do we need to shrink each side
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
}
// move labelBox to its position with the merged delta and check for overlaps
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
}
// move iconBox to its position with the merged delta and check for overlaps
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
}
// check for edges on side corners
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 {
// only reduce to outermost + DEFAULT_PADDING
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 {
// check side corners
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])
}
}
}