2022-11-03 13:54:49 +00:00
|
|
|
package d2dagrelayout
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
_ "embed"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"math"
|
2022-12-06 21:44:59 +00:00
|
|
|
"regexp"
|
2022-11-03 13:54:49 +00:00
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"cdr.dev/slog"
|
2022-12-03 18:54:54 +00:00
|
|
|
"github.com/dop251/goja"
|
2022-11-03 13:54:49 +00:00
|
|
|
|
2022-12-01 19:32:57 +00:00
|
|
|
"oss.terrastruct.com/util-go/xdefer"
|
2022-11-03 13:54:49 +00:00
|
|
|
|
2022-12-01 18:48:01 +00:00
|
|
|
"oss.terrastruct.com/util-go/go2"
|
|
|
|
|
|
2022-11-03 13:54:49 +00:00
|
|
|
"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
|
|
|
|
|
|
2023-01-12 04:21:47 +00:00
|
|
|
const MIN_SEGMENT_LEN = 10
|
|
|
|
|
|
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{
|
2022-12-30 05:09:53 +00:00
|
|
|
NodeSep: 60,
|
2022-12-30 06:43:01 +00:00
|
|
|
EdgeSep: 40,
|
2022-12-30 05:09:53 +00:00
|
|
|
}
|
|
|
|
|
|
2022-11-03 13:54:49 +00:00
|
|
|
type DagreNode struct {
|
2022-12-02 23:51:57 +00:00
|
|
|
ID string `json:"id"`
|
2022-11-03 13:54:49 +00:00
|
|
|
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 {
|
2022-11-03 13:54:49 +00:00
|
|
|
// 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-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
|
}
|
2022-11-03 13:54:49 +00:00
|
|
|
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 {
|
2022-11-03 13:54:49 +00:00
|
|
|
return err
|
|
|
|
|
}
|
2022-12-03 18:54:54 +00:00
|
|
|
if _, err := vm.RunString(setupJS); err != nil {
|
2022-11-03 13:54:49 +00:00
|
|
|
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
|
|
|
}
|
2023-01-06 20:24:45 +00:00
|
|
|
isHorizontal := false
|
2022-12-02 23:51:57 +00:00
|
|
|
switch g.Root.Attributes.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"
|
2023-01-06 20:24:45 +00:00
|
|
|
isHorizontal = true
|
2022-11-30 00:02:37 +00:00
|
|
|
case "left":
|
|
|
|
|
rootAttrs.rankdir = "RL"
|
2023-01-06 20:24:45 +00:00
|
|
|
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
|
|
|
}
|
2022-12-21 00:05:35 +00:00
|
|
|
|
2023-02-10 20:18:50 +00:00
|
|
|
maxContainerLabelHeight := 0
|
|
|
|
|
for _, obj := range g.Objects {
|
|
|
|
|
if len(obj.ChildrenArray) == 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if obj.LabelHeight != nil {
|
2023-02-10 21:47:22 +00:00
|
|
|
maxContainerLabelHeight = go2.Max(maxContainerLabelHeight, *obj.LabelHeight+label.PADDING)
|
2023-02-10 20:18:50 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-06 20:24:45 +00:00
|
|
|
maxLabelSize := 0
|
|
|
|
|
for _, edge := range g.Edges {
|
|
|
|
|
size := edge.LabelDimensions.Width
|
|
|
|
|
if !isHorizontal {
|
|
|
|
|
size = edge.LabelDimensions.Height
|
|
|
|
|
}
|
|
|
|
|
maxLabelSize = go2.Max(maxLabelSize, size)
|
|
|
|
|
}
|
2023-02-10 20:18:50 +00:00
|
|
|
rootAttrs.ranksep = go2.Max(go2.Max(100, maxLabelSize+40), maxContainerLabelHeight)
|
2023-01-06 20:24:45 +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 {
|
2022-11-03 13:54:49 +00:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
loadScript := ""
|
2022-12-02 23:51:57 +00:00
|
|
|
idToObj := make(map[string]*d2graph.Object)
|
|
|
|
|
for _, obj := range g.Objects {
|
2022-11-03 13:54:49 +00:00
|
|
|
id := obj.AbsID()
|
2022-12-02 23:51:57 +00:00
|
|
|
idToObj[id] = obj
|
2022-12-20 03:47:31 +00:00
|
|
|
|
|
|
|
|
height := obj.Height
|
|
|
|
|
if obj.LabelWidth != nil && obj.LabelHeight != nil {
|
2022-12-20 04:48:01 +00:00
|
|
|
if obj.Attributes.Shape.Value == d2target.ShapeImage || obj.Attributes.Icon != nil {
|
2022-12-20 03:47:31 +00:00
|
|
|
height += float64(*obj.LabelHeight) + label.PADDING
|
|
|
|
|
}
|
2023-02-10 20:18:50 +00:00
|
|
|
if len(obj.ChildrenArray) > 0 {
|
2023-02-10 21:47:22 +00:00
|
|
|
height += float64(*obj.LabelHeight) + label.PADDING
|
2023-02-10 20:18:50 +00:00
|
|
|
}
|
2022-12-20 03:47:31 +00:00
|
|
|
}
|
|
|
|
|
loadScript += generateAddNodeLine(id, int(obj.Width), int(height))
|
2022-12-02 23:51:57 +00:00
|
|
|
if obj.Parent != g.Root {
|
2022-11-03 13:54:49 +00:00
|
|
|
loadScript += generateAddParentLine(id, obj.Parent.AbsID())
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-12-02 23:51:57 +00:00
|
|
|
for _, edge := range g.Edges {
|
2022-11-03 13:54:49 +00:00
|
|
|
// 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 {
|
|
|
|
|
src = src.ChildrenArray[0]
|
|
|
|
|
}
|
|
|
|
|
dst := edge.Dst
|
|
|
|
|
for len(dst.Children) > 0 && dst.Class == nil && dst.SQLTable == nil {
|
|
|
|
|
dst = dst.ChildrenArray[0]
|
|
|
|
|
}
|
|
|
|
|
if edge.SrcArrow && !edge.DstArrow {
|
|
|
|
|
// for `b <- a`, edge.Edge is `a -> b` and we expect this routing result
|
|
|
|
|
src, dst = dst, src
|
|
|
|
|
}
|
2023-01-06 20:18:26 +00:00
|
|
|
loadScript += generateAddEdgeLine(src.AbsID(), dst.AbsID(), edge.AbsID(), edge.LabelDimensions.Width, edge.LabelDimensions.Height)
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 {
|
2022-11-03 13:54:49 +00:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-03 18:54:54 +00:00
|
|
|
if _, err := vm.RunString(`dagre.layout(g)`); err != nil {
|
2022-11-03 13:54:49 +00:00
|
|
|
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))
|
2022-11-03 13:54:49 +00:00
|
|
|
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]
|
2022-11-03 13:54:49 +00:00
|
|
|
|
|
|
|
|
// 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 = dn.Width
|
|
|
|
|
obj.Height = dn.Height
|
|
|
|
|
|
|
|
|
|
if obj.LabelWidth != nil && obj.LabelHeight != nil {
|
|
|
|
|
if len(obj.ChildrenArray) > 0 {
|
2023-02-10 20:18:50 +00:00
|
|
|
obj.LabelPosition = go2.Pointer(string(label.OutsideTopCenter))
|
2022-12-20 03:47:31 +00:00
|
|
|
} else if obj.Attributes.Shape.Value == d2target.ShapeImage {
|
|
|
|
|
obj.LabelPosition = go2.Pointer(string(label.OutsideBottomCenter))
|
|
|
|
|
// remove the extra height we added to the node when passing to dagre
|
|
|
|
|
obj.Height -= float64(*obj.LabelHeight) + label.PADDING
|
|
|
|
|
} else if obj.Attributes.Icon != nil {
|
2022-12-20 04:48:01 +00:00
|
|
|
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
|
2022-11-03 13:54:49 +00:00
|
|
|
} else {
|
|
|
|
|
obj.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if obj.Attributes.Icon != nil {
|
|
|
|
|
obj.IconPosition = go2.Pointer(string(label.InsideMiddleCenter))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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))
|
2022-11-03 13:54:49 +00:00
|
|
|
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))
|
|
|
|
|
}
|
2022-11-03 13:54:49 +00:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-12 04:21:47 +00:00
|
|
|
// 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])
|
|
|
|
|
if newStartingSegment.Length() < 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(MIN_SEGMENT_LEN)).ToPoint()
|
|
|
|
|
extended := *geo.NewSegment(nextEnd, extendedStart)
|
|
|
|
|
|
|
|
|
|
if intersections := edge.Src.Box.Intersections(extended); len(intersections) > 0 {
|
|
|
|
|
start = intersections[0]
|
|
|
|
|
startIndex += 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !end.Equals(points[len(points)-1]) && endIndex-2 >= 0 {
|
|
|
|
|
newEndingSegment := *geo.NewSegment(end, points[endIndex-1])
|
|
|
|
|
if newEndingSegment.Length() < 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(MIN_SEGMENT_LEN)).ToPoint()
|
|
|
|
|
extended := *geo.NewSegment(prevStart, extendedEnd)
|
|
|
|
|
|
|
|
|
|
if intersections := edge.Dst.Box.Intersections(extended); len(intersections) > 0 {
|
|
|
|
|
end = intersections[0]
|
|
|
|
|
endIndex -= 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-03 13:54:49 +00:00
|
|
|
srcShape := shape.NewShape(d2target.DSL_SHAPE_TO_SHAPE_TYPE[strings.ToLower(edge.Src.Attributes.Shape.Value)], edge.Src.Box)
|
|
|
|
|
dstShape := shape.NewShape(d2target.DSL_SHAPE_TO_SHAPE_TYPE[strings.ToLower(edge.Dst.Attributes.Shape.Value)], edge.Dst.Box)
|
|
|
|
|
|
|
|
|
|
// trace the edge to the specific shape's border
|
|
|
|
|
points[startIndex] = shape.TraceToShapeBorder(srcShape, start, points[startIndex+1])
|
|
|
|
|
points[endIndex] = shape.TraceToShapeBorder(dstShape, end, points[endIndex-1])
|
|
|
|
|
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
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
path = append(path, points[len(points)-1])
|
|
|
|
|
|
|
|
|
|
edge.Route = path
|
|
|
|
|
// compile needs to assign edge label positions
|
|
|
|
|
if edge.Attributes.Label.Value != "" {
|
|
|
|
|
edge.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-10 20:20:00 +00:00
|
|
|
for _, obj := range g.Objects {
|
2023-02-10 20:18:50 +00:00
|
|
|
if obj.LabelHeight == nil || len(obj.ChildrenArray) <= 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-10 22:28:43 +00:00
|
|
|
// usually you don't want to take away here more than what was added, which is the label height
|
|
|
|
|
// however, if the label height is more than the ranksep/2, we'll have no padding around children anymore
|
|
|
|
|
// so cap the amount taken off at ranksep/2
|
|
|
|
|
subtract := float64(go2.Min(rootAttrs.ranksep/2, *obj.LabelHeight+label.PADDING))
|
2023-02-10 22:02:57 +00:00
|
|
|
|
2023-02-10 22:28:43 +00:00
|
|
|
obj.Height -= subtract
|
2023-02-10 20:18:50 +00:00
|
|
|
|
2023-02-10 22:28:43 +00:00
|
|
|
// If the edge is connected to two descendants that are about to be downshifted, their whole route gets downshifted
|
2023-02-10 20:18:50 +00:00
|
|
|
movedEdges := make(map[*d2graph.Edge]struct{})
|
|
|
|
|
for _, e := range g.Edges {
|
|
|
|
|
currSrc := e.Src
|
|
|
|
|
currDst := e.Dst
|
|
|
|
|
|
|
|
|
|
isSrcDesc := false
|
|
|
|
|
isDstDesc := false
|
|
|
|
|
|
|
|
|
|
for currSrc != nil {
|
|
|
|
|
if currSrc == obj {
|
|
|
|
|
isSrcDesc = true
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
currSrc = currSrc.Parent
|
|
|
|
|
}
|
|
|
|
|
for currDst != nil {
|
|
|
|
|
if currDst == obj {
|
|
|
|
|
isDstDesc = true
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
currDst = currDst.Parent
|
|
|
|
|
}
|
|
|
|
|
if isSrcDesc && isDstDesc {
|
2023-02-10 22:28:43 +00:00
|
|
|
stepSize := subtract
|
2023-02-10 21:38:31 +00:00
|
|
|
if e.Src != obj || e.Dst != obj {
|
|
|
|
|
stepSize /= 2.
|
|
|
|
|
}
|
2023-02-10 20:18:50 +00:00
|
|
|
movedEdges[e] = struct{}{}
|
|
|
|
|
for _, p := range e.Route {
|
|
|
|
|
p.Y += stepSize
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-10 22:28:43 +00:00
|
|
|
// Downshift descendents and edges that have one endpoint connected to a descendant
|
2023-02-10 20:18:50 +00:00
|
|
|
q := []*d2graph.Object{obj}
|
|
|
|
|
for len(q) > 0 {
|
|
|
|
|
curr := q[0]
|
|
|
|
|
q = q[1:]
|
|
|
|
|
|
2023-02-10 22:28:43 +00:00
|
|
|
stepSize := subtract
|
2023-02-10 20:18:50 +00:00
|
|
|
if curr != obj {
|
|
|
|
|
stepSize /= 2.
|
|
|
|
|
}
|
|
|
|
|
curr.TopLeft.Y += stepSize
|
|
|
|
|
if curr != obj {
|
|
|
|
|
for _, e := range g.Edges {
|
|
|
|
|
if _, ok := movedEdges[e]; ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if e.Src == curr {
|
|
|
|
|
e.Route[0].Y += stepSize
|
|
|
|
|
}
|
|
|
|
|
if e.Dst == curr {
|
|
|
|
|
e.Route[len(e.Route)-1].Y += stepSize
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for _, c := range curr.ChildrenArray {
|
|
|
|
|
q = append(q, c)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-03 13:54:49 +00:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-30 20:25:33 +00:00
|
|
|
func setGraphAttrs(attrs dagreOpts) string {
|
2022-11-03 13:54:49 +00:00
|
|
|
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,
|
2022-11-03 13:54:49 +00:00
|
|
|
attrs.rankdir,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-05 19:40:21 +00:00
|
|
|
func escapeID(id string) string {
|
2022-12-06 21:44:59 +00:00
|
|
|
// fixes \\
|
|
|
|
|
id = strings.ReplaceAll(id, "\\", `\\`)
|
|
|
|
|
// replaces \n with \\n whenever \n is not preceded by \ (does not replace \\n)
|
2022-12-08 03:21:56 +00:00
|
|
|
re := regexp.MustCompile(`[^\\]\n`)
|
2022-12-06 21:44:59 +00:00
|
|
|
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)
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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))
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
|
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)
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|