d2/d2layouts/d2dagrelayout/layout.go

1270 lines
36 KiB
Go
Raw Normal View History

package d2dagrelayout
import (
"context"
_ "embed"
"encoding/json"
"fmt"
"math"
"regexp"
"sort"
"strings"
"cdr.dev/slog"
2022-12-03 18:54:54 +00:00
"github.com/dop251/goja"
2022-12-01 19:32:57 +00:00
"oss.terrastruct.com/util-go/xdefer"
2022-12-01 18:48:01 +00:00
"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"
)
//go:embed setup.js
var setupJS string
//go:embed dagre.js
var dagreJS string
2023-02-09 06:11:52 +00:00
const (
2023-06-08 21:20:37 +00:00
MIN_RANK_SEP = 60
EDGE_LABEL_GAP = 20
2023-07-06 20:31:32 +00:00
MIN_MARGIN = 10.
2023-02-09 06:11:52 +00:00
)
2022-12-30 20:25:33 +00:00
type ConfigurableOpts struct {
2022-12-30 21:26:01 +00:00
NodeSep int `json:"nodesep"`
EdgeSep int `json:"edgesep"`
2022-12-30 05:09:53 +00:00
}
2022-12-30 20:25:33 +00:00
var DefaultOpts = ConfigurableOpts{
2023-02-11 05:45:31 +00:00
NodeSep: 60,
2023-02-10 03:29:30 +00:00
EdgeSep: 20,
2022-12-30 05:09:53 +00:00
}
type DagreNode struct {
2022-12-02 23:51:57 +00:00
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"`
}
2022-12-30 20:25:33 +00:00
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
2022-12-30 20:25:33 +00:00
ConfigurableOpts
}
2022-12-30 21:36:49 +00:00
func DefaultLayout(ctx context.Context, g *d2graph.Graph) (err error) {
return Layout(ctx, g, nil)
}
2022-12-30 20:25:33 +00:00
func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err error) {
2022-12-30 05:09:53 +00:00
if opts == nil {
opts = &DefaultOpts
}
defer xdefer.Errorf(&err, "failed to dagre layout")
debugJS := false
2022-12-03 18:54:54 +00:00
vm := goja.New()
if _, err := vm.RunString(dagreJS); err != nil {
return err
}
2022-12-03 18:54:54 +00:00
if _, err := vm.RunString(setupJS); err != nil {
return err
}
2022-12-30 20:25:33 +00:00
rootAttrs := dagreOpts{
ConfigurableOpts: ConfigurableOpts{
EdgeSep: opts.EdgeSep,
NodeSep: opts.NodeSep,
},
2022-11-29 05:39:36 +00:00
}
isHorizontal := false
2023-04-14 03:04:55 +00:00
switch g.Root.Direction.Value {
2022-11-30 00:02:37 +00:00
case "down":
rootAttrs.rankdir = "TB"
case "right":
2022-11-29 05:39:36 +00:00
rootAttrs.rankdir = "LR"
isHorizontal = true
2022-11-30 00:02:37 +00:00
case "left":
rootAttrs.rankdir = "RL"
isHorizontal = true
2022-11-30 00:02:37 +00:00
case "up":
rootAttrs.rankdir = "BT"
2022-11-30 01:57:17 +00:00
default:
rootAttrs.rankdir = "TB"
2022-11-29 05:39:36 +00:00
}
// set label and icon positions for dagre
for _, obj := range g.Objects {
positionLabelsIcons(obj)
}
2023-03-04 06:31:09 +00:00
maxLabelWidth := 0
maxLabelHeight := 0
for _, edge := range g.Edges {
2023-03-04 06:31:09 +00:00
width := edge.LabelDimensions.Width
height := edge.LabelDimensions.Height
maxLabelWidth = go2.Max(maxLabelWidth, width)
maxLabelHeight = go2.Max(maxLabelHeight, height)
}
2023-03-04 06:08:33 +00:00
if !isHorizontal {
2023-07-06 20:31:32 +00:00
rootAttrs.ranksep = go2.Max(100, maxLabelHeight+40)
2023-03-04 06:08:33 +00:00
} else {
2023-03-04 06:31:09 +00:00
rootAttrs.ranksep = go2.Max(100, maxLabelWidth+40)
2023-03-04 06:08:33 +00:00
// use existing config
2023-07-06 20:31:32 +00:00
// rootAttrs.NodeSep = rootAttrs.EdgeSep
// // configure vertical padding
// rootAttrs.EdgeSep = maxLabelHeight + 40
2023-03-04 06:31:09 +00:00
// Note: non-containers have both of these as padding (rootAttrs.NodeSep + rootAttrs.EdgeSep)
2023-03-04 06:08:33 +00:00
}
2022-11-29 05:39:36 +00:00
configJS := setGraphAttrs(rootAttrs)
2022-12-03 18:54:54 +00:00
if _, err := vm.RunString(configJS); err != nil {
return err
}
loadScript := ""
2022-12-02 23:51:57 +00:00
idToObj := make(map[string]*d2graph.Object)
for _, obj := range g.Objects {
id := obj.AbsID()
2022-12-02 23:51:57 +00:00
idToObj[id] = obj
2022-12-20 03:47:31 +00:00
width, height := obj.Width, obj.Height
2023-05-26 02:11:37 +00:00
2023-05-24 02:25:27 +00:00
loadScript += generateAddNodeLine(id, int(width), int(height))
2022-12-02 23:51:57 +00:00
if obj.Parent != g.Root {
loadScript += generateAddParentLine(id, obj.Parent.AbsID())
}
}
2023-07-06 20:31:32 +00:00
2022-12-02 23:51:57 +00:00
for _, edge := range g.Edges {
2023-04-04 19:54:59 +00:00
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++
}
}
2023-04-04 19:54:59 +00:00
// We want to leave some gap between multiple edges
if numEdges > 1 {
2023-04-14 03:04:55 +00:00
switch g.Root.Direction.Value {
2023-04-04 19:54:59 +00:00
case "down", "up", "":
width += EDGE_LABEL_GAP
case "left", "right":
height += EDGE_LABEL_GAP
}
}
2023-04-04 19:54:59 +00:00
loadScript += generateAddEdgeLine(src.AbsID(), dst.AbsID(), edge.AbsID(), width, height)
}
if debugJS {
log.Debug(ctx, "script", slog.F("all", setupJS+configJS+loadScript))
}
2022-12-03 18:54:54 +00:00
if _, err := vm.RunString(loadScript); err != nil {
return err
}
2022-12-03 18:54:54 +00:00
if _, err := vm.RunString(`dagre.layout(g)`); err != nil {
if debugJS {
log.Warn(ctx, "layout error", slog.F("err", err))
}
return err
}
2022-12-02 23:51:57 +00:00
for i := range g.Objects {
2022-12-03 18:54:54 +00:00
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
}
2022-12-02 23:51:57 +00:00
if debugJS {
log.Debug(ctx, "graph", slog.F("json", dn))
}
obj := idToObj[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))
2023-05-25 23:37:41 +00:00
obj.Width = math.Ceil(dn.Width)
obj.Height = math.Ceil(dn.Height)
}
2022-12-02 23:51:57 +00:00
for i, edge := range g.Edges {
2022-12-03 18:54:54 +00:00
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
}
2022-12-02 23:51:57 +00:00
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
2022-11-30 01:54:25 +00:00
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
}
}
}
2023-02-11 04:47:58 +00:00
points = points[startIndex : endIndex+1]
2023-02-11 04:41:17 +00:00
points[0] = start
points[len(points)-1] = end
edge.Route = points
}
adjustRankSpacing(g, float64(rootAttrs.ranksep), isHorizontal)
adjustCrossRankSpacing(g, float64(rootAttrs.ranksep), !isHorizontal)
2023-05-25 02:31:01 +00:00
2023-02-11 04:41:17 +00:00
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 !start.Equals(points[0]) && startIndex+2 < len(points) {
newStartingSegment := *geo.NewSegment(start, points[startIndex+1])
2023-06-08 21:20:37 +00:00
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()
2023-06-08 21:20:37 +00:00
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 {
2023-06-08 21:20:37 +00:00
startIndex++
points[startIndex] = intersections[0]
start = points[startIndex]
}
}
}
if !end.Equals(points[len(points)-1]) && endIndex-2 >= 0 {
newEndingSegment := *geo.NewSegment(end, points[endIndex-1])
2023-06-08 21:20:37 +00:00
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()
2023-06-08 21:20:37 +00:00
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 {
2023-06-08 21:20:37 +00:00
endIndex--
points[endIndex] = intersections[0]
end = points[endIndex]
}
}
}
2023-05-26 02:11:37 +00:00
var originalSrcTL, originalDstTL *geo.Point
2023-05-25 02:31:01 +00:00
// if the edge passes through 3d/multiple, use the offset box for tracing to border
2023-05-26 19:37:51 +00:00
if srcDx, srcDy := edge.Src.GetModifierElementAdjustments(); srcDx != 0 || srcDy != 0 {
2023-05-26 02:11:37 +00:00
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
2023-05-25 02:31:01 +00:00
}
}
2023-05-26 19:37:51 +00:00
if dstDx, dstDy := edge.Dst.GetModifierElementAdjustments(); dstDx != 0 || dstDy != 0 {
2023-05-26 02:11:37 +00:00
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
2023-05-25 02:31:01 +00:00
}
}
2023-06-08 21:20:37 +00:00
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])
2023-01-11 23:31:27 +00:00
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
2023-04-14 03:04:55 +00:00
if edge.Label.Value != "" {
edge.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
}
2023-05-25 02:31:01 +00:00
// undo 3d/multiple offset
2023-05-26 02:11:37 +00:00
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
}
2023-05-24 22:39:38 +00:00
}
return nil
}
2023-04-04 19:54:59 +00:00
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 {
2023-06-08 21:25:13 +00:00
dst = getLongestEdgeChainHead(g, dst)
2023-04-04 19:54:59 +00:00
}
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
}
2022-12-30 20:25:33 +00:00
func setGraphAttrs(attrs dagreOpts) string {
return fmt.Sprintf(`g.setGraph({
ranksep: %d,
edgesep: %d,
nodesep: %d,
rankdir: "%s",
});
`,
attrs.ranksep,
2022-12-30 20:25:33 +00:00
attrs.ConfigurableOpts.EdgeSep,
attrs.ConfigurableOpts.NodeSep,
attrs.rankdir,
)
}
2022-12-05 19:40:21 +00:00
func escapeID(id string) string {
// fixes \\
id = strings.ReplaceAll(id, "\\", `\\`)
// replaces \n with \\n whenever \n is not preceded by \ (does not replace \\n)
re := regexp.MustCompile(`[^\\]\n`)
id = re.ReplaceAllString(id, `\\n`)
2022-12-05 21:15:43 +00:00
// avoid an unescaped \r becoming a \n in the layout result
2022-12-05 21:24:40 +00:00
id = strings.ReplaceAll(id, "\r", `\r`)
2022-12-05 21:15:43 +00:00
return id
2022-12-05 19:40:21 +00:00
}
2022-11-07 19:14:16 +00:00
func generateAddNodeLine(id string, width, height int) string {
2022-12-05 19:40:21 +00:00
id = escapeID(id)
2022-12-02 23:51:57 +00:00
return fmt.Sprintf("g.setNode(`%s`, { id: `%s`, width: %d, height: %d });\n", id, id, width, height)
}
func generateAddParentLine(childID, parentID string) string {
2022-12-05 19:40:21 +00:00
return fmt.Sprintf("g.setParent(`%s`, `%s`);\n", escapeID(childID), escapeID(parentID))
}
2023-01-06 20:18:26 +00:00
func generateAddEdgeLine(fromID, toID, edgeID string, width, height int) string {
2023-01-06 20:21:47 +00:00
return fmt.Sprintf("g.setEdge({v:`%s`, w:`%s`, name:`%s`}, { width:%d, height:%d, labelpos: `c` });\n", escapeID(fromID), escapeID(toID), escapeID(edgeID), width, height)
}
2023-06-08 21:25:13 +00:00
// 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
2023-06-08 21:25:13 +00:00
// 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 {
2023-06-08 21:25:13 +00:00
if rank[obj] > max {
max = rank[obj]
}
}
2023-06-08 21:25:13 +00:00
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)
}
2023-07-06 20:31:32 +00:00
type spacing struct {
top, bottom, left, right float64
}
2023-07-06 20:31:32 +00:00
func getSpacing(obj *d2graph.Object) (margin, padding spacing) {
if obj.HasLabel() {
var position label.Position
if obj.LabelPosition != nil {
position = label.Position(*obj.LabelPosition)
} else if len(obj.ChildrenArray) == 0 && obj.HasOutsideBottomLabel() {
position = label.OutsideBottomCenter
}
2023-07-06 20:31:32 +00:00
labelWidth := float64(obj.LabelDimensions.Width) + 2*label.PADDING
labelHeight := float64(obj.LabelDimensions.Height) + 2*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
case label.InsideTopLeft, label.InsideTopCenter, label.InsideTopRight:
padding.top = labelHeight
case label.InsideBottomLeft, label.InsideBottomCenter, label.InsideBottomRight:
padding.bottom = labelHeight
case label.InsideMiddleLeft:
padding.left = labelWidth
case label.InsideMiddleRight:
padding.right = labelWidth
}
}
if obj.Icon != nil && obj.Shape.Value != d2target.ShapeImage {
var position label.Position
if obj.IconPosition != nil {
position = label.Position(*obj.IconPosition)
}
2023-07-06 20:31:32 +00:00
iconSize := float64(d2target.MAX_ICON_SIZE + 2*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)
case label.InsideTopLeft, label.InsideTopCenter, label.InsideTopRight:
padding.top = math.Max(padding.top, iconSize)
case label.InsideBottomLeft, label.InsideBottomCenter, label.InsideBottomRight:
padding.bottom = math.Max(padding.bottom, iconSize)
case label.InsideMiddleLeft:
padding.left = math.Max(padding.left, iconSize)
case label.InsideMiddleRight:
padding.right = math.Max(padding.right, iconSize)
}
}
dx, dy := obj.GetModifierElementAdjustments()
2023-07-06 20:31:32 +00:00
margin.right += dx
margin.top += dy
return
}
func positionLabelsIcons(obj *d2graph.Object) {
if obj.Icon != nil && obj.IconPosition == nil {
if len(obj.ChildrenArray) > 0 {
obj.IconPosition = go2.Pointer(string(label.OutsideTopLeft))
if obj.LabelPosition == nil {
obj.LabelPosition = go2.Pointer(string(label.OutsideTopRight))
return
}
} else {
obj.IconPosition = go2.Pointer(string(label.InsideMiddleCenter))
}
}
if obj.HasLabel() && obj.LabelPosition == nil {
if len(obj.ChildrenArray) > 0 {
obj.LabelPosition = go2.Pointer(string(label.OutsideTopCenter))
} else if obj.HasOutsideBottomLabel() {
obj.LabelPosition = go2.Pointer(string(label.OutsideBottomCenter))
} else if obj.Icon != nil {
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
} else {
obj.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
}
}
}
2023-07-06 20:31:32 +00:00
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 {
2023-07-11 23:30:16 +00:00
y := math.Ceil(obj.TopLeft.Y + obj.Height/2)
alignedObjects[y] = append(alignedObjects[y], obj)
} else {
2023-07-11 23:30:16 +00:00
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]
})
2023-07-06 19:32:51 +00:00
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])
}
2023-07-06 20:31:32 +00:00
startingParentRanks = make(map[*d2graph.Object]int)
endingParentRanks = make(map[*d2graph.Object]int)
2023-07-06 19:32:51 +00:00
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
}
}
}
2023-07-06 20:31:32 +00:00
return ranks, objectRanks, startingParentRanks, endingParentRanks
}
2023-07-06 20:31:32 +00:00
// 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 _, obj := range g.Objects {
if obj.TopLeft.X < start {
continue
}
obj.TopLeft.X += distance
}
for _, edge := range g.Edges {
for _, p := range edge.Route {
// Note: == so incoming edge shifts down with object at startY
if p.X <= start {
2023-07-06 20:31:32 +00:00
continue
}
p.X += distance
}
}
} else {
for _, obj := range g.Objects {
if obj.TopLeft.Y < start {
continue
}
obj.TopLeft.Y += distance
}
for _, edge := range g.Edges {
for _, p := range edge.Route {
// Note: == so incoming edge shifts down with object at startY
if p.Y <= start {
2023-07-06 20:31:32 +00:00
continue
}
p.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{} {
2023-07-06 20:31:32 +00:00
q := []*d2graph.Object{obj}
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.
2023-07-06 20:31:32 +00:00
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 isHorizontal {
if curr.TopLeft.X < start {
continue
}
} else {
if curr.TopLeft.Y < start {
continue
}
}
}
if isHorizontal {
shift := false
if !isMargin {
shift = start < curr.TopLeft.X
} else {
shift = start <= curr.TopLeft.X
}
if shift {
curr.TopLeft.X += distance
shifted[curr] = struct{}{}
}
} else {
shift := false
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 {
queue(e.Dst)
if isHorizontal {
for _, p := range e.Route {
if start <= p.X {
p.X += distance
}
}
} else {
for _, p := range e.Route {
if start <= p.Y {
p.Y += distance
}
}
}
shiftedEdges[e] = struct{}{}
} else if e.Dst == curr {
queue(e.Src)
if isHorizontal {
for _, p := range e.Route {
if start <= p.X {
p.X += distance
}
}
} else {
for _, p := range e.Route {
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
}
2023-07-06 20:31:32 +00:00
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{})
2023-07-11 23:30:16 +00:00
movedObjects := make([]*d2graph.Object, 0, len(shifted))
for obj := range shifted {
2023-07-11 23:30:16 +00:00
movedObjects = append(movedObjects, obj)
}
2023-07-11 23:30:16 +00:00
for obj := range grown {
movedObjects = append(movedObjects, obj)
}
for _, moved := range movedObjects {
counts := true
// check if any other shifted is directly above
2023-07-11 23:30:16 +00:00
for _, other := range movedObjects {
if other == moved {
continue
}
if isHorizontal {
2023-07-11 23:30:16 +00:00
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
2023-07-11 23:30:16 +00:00
if other.TopLeft.X < moved.TopLeft.X &&
moved.TopLeft.X < other.TopLeft.X+other.Width+threshold {
counts = false
break
}
} else {
2023-07-11 23:30:16 +00:00
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
2023-07-11 23:30:16 +00:00
if other.TopLeft.Y < moved.TopLeft.Y &&
moved.TopLeft.Y < other.TopLeft.Y+other.Height+threshold {
counts = false
break
}
}
}
if counts {
2023-07-11 23:30:16 +00:00
increasedMargins[moved] = struct{}{}
}
}
return increasedMargins
2023-07-06 20:31:32 +00:00
}
func adjustRankSpacing(g *d2graph.Graph, rankSep float64, isHorizontal bool) {
ranks, objectRanks, startingParentRanks, endingParentRanks := getRanks(g, isHorizontal)
2023-07-06 20:31:32 +00:00
// 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 {
2023-07-06 20:31:32 +00:00
continue
}
if r, has := endingParentRanks[obj.Parent]; has && r == rank {
endingParents = append(endingParents, obj.Parent)
2023-07-06 20:31:32 +00:00
}
if r, has := startingParentRanks[obj.Parent]; has && r == rank {
startingParents = append(startingParents, obj.Parent)
2023-07-06 20:31:32 +00:00
}
}
2023-07-11 23:30:16 +00:00
startingAncestorPositions := make(map[*d2graph.Object]float64)
for len(startingParents) > 0 {
var ancestors []*d2graph.Object
for _, parent := range startingParents {
_, padding := getSpacing(parent)
2023-07-11 23:30:16 +00:00
if _, has := startingAncestorPositions[parent]; !has {
startingAncestorPositions[parent] = math.Inf(1)
}
if isHorizontal {
2023-07-11 23:30:16 +00:00
startingAncestorPositions[parent] = math.Min(startingAncestorPositions[parent], parent.TopLeft.X+rankSep/2.-MIN_MARGIN-padding.left)
2023-07-06 20:31:32 +00:00
} else {
2023-07-11 23:30:16 +00:00
startingAncestorPositions[parent] = math.Min(startingAncestorPositions[parent], parent.TopLeft.Y+rankSep/2.-MIN_MARGIN-padding.top)
2023-07-06 20:31:32 +00:00
}
for _, child := range parent.ChildrenArray {
if r, has := objectRanks[child]; has {
if r != rank {
continue
}
} else {
startingRank := startingParentRanks[child]
endingRank := endingParentRanks[child]
if startingRank != rank && endingRank != rank {
continue
}
}
margin, _ := getSpacing(child)
if isHorizontal {
2023-07-11 23:30:16 +00:00
startingAncestorPositions[parent] = math.Min(startingAncestorPositions[parent], child.TopLeft.X-margin.left-MIN_MARGIN)
} else {
2023-07-11 23:30:16 +00:00
startingAncestorPositions[parent] = math.Min(startingAncestorPositions[parent], child.TopLeft.Y-margin.top-MIN_MARGIN)
}
2023-07-06 20:31:32 +00:00
}
if parent.Parent != g.Root {
ancestors = append(ancestors, parent.Parent)
2023-07-06 20:31:32 +00:00
}
}
startingParents = ancestors
2023-07-06 20:31:32 +00:00
}
2023-07-11 23:30:16 +00:00
endingAncestorPositions := make(map[*d2graph.Object]float64)
for len(endingParents) > 0 {
2023-07-11 23:30:16 +00:00
delta := 0.
var ancestors []*d2graph.Object
for _, parent := range endingParents {
_, padding := getSpacing(parent)
2023-07-11 23:30:16 +00:00
if _, has := endingAncestorPositions[parent]; !has {
endingAncestorPositions[parent] = math.Inf(-1)
}
if isHorizontal {
delta = math.Max(delta, padding.right)
2023-07-11 23:30:16 +00:00
endingAncestorPositions[parent] = math.Max(endingAncestorPositions[parent], parent.TopLeft.X+parent.Width-rankSep/2+MIN_MARGIN+padding.right)
} else {
delta = math.Max(delta, padding.bottom)
2023-07-11 23:30:16 +00:00
endingAncestorPositions[parent] = math.Max(endingAncestorPositions[parent], parent.TopLeft.Y+parent.Height-rankSep/2+MIN_MARGIN+padding.bottom)
2023-07-06 20:31:32 +00:00
}
for _, child := range parent.ChildrenArray {
if r, has := objectRanks[child]; has {
if r != rank {
continue
}
} else {
startingRank := startingParentRanks[child]
endingRank := endingParentRanks[child]
if startingRank != rank && endingRank != rank {
continue
}
}
margin, _ := getSpacing(child)
if isHorizontal {
delta = math.Max(delta, margin.right)
2023-07-11 23:30:16 +00:00
endingAncestorPositions[parent] = math.Max(endingAncestorPositions[parent], child.TopLeft.X+child.Width+margin.right+MIN_MARGIN)
} else {
delta = math.Max(delta, margin.bottom)
2023-07-11 23:30:16 +00:00
endingAncestorPositions[parent] = math.Max(endingAncestorPositions[parent], child.TopLeft.Y+child.Height+margin.bottom+MIN_MARGIN)
2023-07-06 20:31:32 +00:00
}
}
if parent.Parent != g.Root {
ancestors = append(ancestors, parent.Parent)
2023-07-06 20:31:32 +00:00
}
}
endingParents = ancestors
}
2023-07-11 23:30:16 +00:00
endingAncestorPositionOrder := make([]*d2graph.Object, 0, len(endingAncestorPositions))
for ancestor := range endingAncestorPositions {
endingAncestorPositionOrder = append(endingAncestorPositionOrder, ancestor)
}
// adjust rank ancestors bottom-up
sort.Slice(endingAncestorPositionOrder, func(i, j int) bool {
return endingAncestorPositions[endingAncestorPositionOrder[i]] > endingAncestorPositions[endingAncestorPositionOrder[j]]
})
for _, ancestor := range endingAncestorPositionOrder {
var endDelta float64
if isHorizontal {
endDelta = endingAncestorPositions[ancestor] - (ancestor.TopLeft.X + ancestor.Width)
} else {
endDelta = endingAncestorPositions[ancestor] - (ancestor.TopLeft.Y + ancestor.Height)
}
if endDelta > 0 {
2023-07-11 23:30:16 +00:00
var position float64
if isHorizontal {
position = ancestor.TopLeft.X + ancestor.Width - 1
} else {
position = ancestor.TopLeft.Y + ancestor.Height - 1
}
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+obj.Width > position {
obj.Width += endDelta
} else if !isHorizontal && obj.TopLeft.Y+obj.Height > position {
obj.Height += endDelta
}
2023-07-06 20:31:32 +00:00
}
}
shiftDown(g, position, endDelta, isHorizontal)
2023-07-06 20:31:32 +00:00
}
}
2023-07-11 23:30:16 +00:00
startingAncestorPositionOrder := make([]*d2graph.Object, 0, len(startingAncestorPositions))
for ancestor := range startingAncestorPositions {
startingAncestorPositionOrder = append(startingAncestorPositionOrder, ancestor)
}
// adjust rank ancestors bottom-up
sort.Slice(startingAncestorPositionOrder, func(i, j int) bool {
return startingAncestorPositions[startingAncestorPositionOrder[i]] > startingAncestorPositions[startingAncestorPositionOrder[j]]
})
for _, ancestor := range startingAncestorPositionOrder {
var endDelta float64
if isHorizontal {
endDelta = ancestor.TopLeft.X - startingAncestorPositions[ancestor]
} else {
endDelta = ancestor.TopLeft.Y - startingAncestorPositions[ancestor]
}
if endDelta > 0 {
var position float64
if isHorizontal {
position = ancestor.TopLeft.X + 1
} else {
position = ancestor.TopLeft.Y + 1
}
for _, obj := range g.Objects {
if !obj.IsContainer() {
continue
}
start := startingParentRanks[obj]
end := endingParentRanks[obj]
if start <= rank && rank <= end {
2023-07-11 23:30:16 +00:00
if isHorizontal && obj.TopLeft.X+obj.Width > position {
obj.Width += endDelta
} else if !isHorizontal && obj.TopLeft.Y+obj.Height > position {
obj.Height += endDelta
}
}
2023-07-06 20:31:32 +00:00
}
2023-07-11 23:30:16 +00:00
shiftDown(g, position, endDelta, isHorizontal)
2023-07-06 20:31:32 +00:00
}
}
}
}
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)
}
2023-07-06 20:31:32 +00:00
for _, obj := range g.Objects {
margin, padding := getSpacing(obj)
if !isHorizontal {
if prevShift, has := prevMarginBottom[obj]; has {
margin.bottom -= prevShift
}
2023-07-06 20:31:32 +00:00
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)
}
2023-07-06 20:31:32 +00:00
}
if padding.bottom > 0 {
shiftReachableDown(g, obj, obj.TopLeft.Y+obj.Height, padding.bottom, isHorizontal, false)
2023-07-06 20:31:32 +00:00
obj.Height += padding.bottom
}
if prevShift, has := prevMarginTop[obj]; has {
margin.top -= prevShift
}
2023-07-06 20:31:32 +00:00
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)
}
2023-07-06 20:31:32 +00:00
}
if padding.top > 0 {
shiftReachableDown(g, obj, obj.TopLeft.Y, padding.top, isHorizontal, false)
2023-07-06 20:31:32 +00:00
obj.Height += padding.top
}
} else {
if prevShift, has := prevMarginRight[obj]; has {
margin.right -= prevShift
}
2023-07-06 20:31:32 +00:00
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)
}
2023-07-06 20:31:32 +00:00
}
if padding.right > 0 {
shiftReachableDown(g, obj, obj.TopLeft.X+obj.Width, padding.right, isHorizontal, false)
2023-07-06 20:31:32 +00:00
obj.Width += padding.right
}
if prevShift, has := prevMarginLeft[obj]; has {
margin.left -= prevShift
}
2023-07-06 20:31:32 +00:00
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)
}
2023-07-06 20:31:32 +00:00
}
if padding.left > 0 {
shiftReachableDown(g, obj, obj.TopLeft.X, padding.left, isHorizontal, false)
2023-07-06 20:31:32 +00:00
obj.Width += padding.left
}
}
}
}