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"
|
2023-07-04 01:07:31 +00:00
|
|
|
"sort"
|
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"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
//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
|
|
|
)
|
2023-01-12 04:21:47 +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
|
|
|
}
|
|
|
|
|
|
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
|
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"
|
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-06-27 22:39:41 +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
|
2023-01-06 20:24:45 +00:00
|
|
|
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-01-06 20:24:45 +00:00
|
|
|
}
|
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
|
|
|
}
|
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)
|
2023-06-27 22:39:41 +00:00
|
|
|
idToWidth := make(map[string]float64)
|
|
|
|
|
idToHeight := make(map[string]float64)
|
2022-12-02 23:51:57 +00:00
|
|
|
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
|
|
|
|
2023-07-06 20:31:32 +00:00
|
|
|
// Note: we handle vertical spacing adjustments separately after layout,
|
|
|
|
|
// but for horizontal adjustments we change the width we pass to dagre.
|
|
|
|
|
// For containers, we use phantom nodes to adjust container widths
|
2023-07-04 01:07:31 +00:00
|
|
|
width, height := obj.Width, obj.Height
|
2023-07-06 20:31:32 +00:00
|
|
|
// if isHorizontal {
|
|
|
|
|
// height = adjustHeight(obj)
|
|
|
|
|
// idToHeight[id] = height
|
|
|
|
|
// } else {
|
|
|
|
|
// width = adjustWidth(obj)
|
|
|
|
|
// idToWidth[id] = width
|
|
|
|
|
// }
|
|
|
|
|
// TODO update to work with direction: right/left (in that case we need to adjust heights here,
|
|
|
|
|
// and horizontal spacing will be adjusted separately after layout
|
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 {
|
2022-11-03 13:54:49 +00:00
|
|
|
loadScript += generateAddParentLine(id, obj.Parent.AbsID())
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-07-06 20:31:32 +00:00
|
|
|
|
|
|
|
|
for _, obj := range g.Objects {
|
|
|
|
|
if !obj.IsContainer() || true {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
id := obj.AbsID()
|
|
|
|
|
phantomWidth, phantomHeight := 1, 1
|
|
|
|
|
// when a container has nodes with no connections, the layout will be in a row
|
|
|
|
|
// adding a node will add NodeSep width in addition to the node's width
|
|
|
|
|
// to add a specific amount of space we need to subtract this from the desired width
|
|
|
|
|
// if we add the phantom node at rank 0 it should be at the far right and top
|
|
|
|
|
if isHorizontal {
|
|
|
|
|
heightDelta := int(math.Ceil(idToHeight[id] - obj.Height))
|
|
|
|
|
phantomWidth = heightDelta - rootAttrs.NodeSep
|
|
|
|
|
} else {
|
|
|
|
|
widthDelta := int(math.Ceil(idToWidth[id] - obj.Width))
|
|
|
|
|
phantomWidth = widthDelta - rootAttrs.NodeSep
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// add phantom children to adjust container widths
|
|
|
|
|
phantomID := id + "___phantom"
|
|
|
|
|
loadScript += generateAddNodeLine(phantomID, phantomWidth, phantomHeight)
|
|
|
|
|
loadScript += generateAddParentLine(phantomID, id)
|
|
|
|
|
}
|
2023-06-27 22:39:41 +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-03-10 03:44:27 +00:00
|
|
|
}
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
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
|
|
|
|
|
}
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
2023-04-04 19:54:59 +00:00
|
|
|
|
|
|
|
|
loadScript += generateAddEdgeLine(src.AbsID(), dst.AbsID(), edge.AbsID(), width, 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))
|
2023-05-25 23:37:41 +00:00
|
|
|
obj.Width = math.Ceil(dn.Width)
|
|
|
|
|
obj.Height = math.Ceil(dn.Height)
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
|
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-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
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-06 20:31:32 +00:00
|
|
|
adjustSpacing(g, float64(rootAttrs.ranksep), isHorizontal)
|
2023-07-04 01:07:31 +00:00
|
|
|
// for _, obj := range g.Objects {
|
2023-07-06 20:31:32 +00:00
|
|
|
// cleanupAdjustment(obj, 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]
|
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])
|
2023-06-08 21:20:37 +00:00
|
|
|
if newStartingSegment.Length() < d2graph.MIN_SEGMENT_LEN {
|
2023-01-12 04:21:47 +00:00
|
|
|
// 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()
|
2023-01-12 04:21:47 +00:00
|
|
|
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]
|
2023-01-12 04:21:47 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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 {
|
2023-01-12 04:21:47 +00:00
|
|
|
// 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()
|
2023-01-12 04:21:47 +00:00
|
|
|
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-01-12 04:21:47 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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)
|
2022-11-03 13:54:49 +00:00
|
|
|
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
|
2023-04-14 03:04:55 +00:00
|
|
|
if edge.Label.Value != "" {
|
2022-11-03 13:54:49 +00:00
|
|
|
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
|
|
|
}
|
|
|
|
|
|
2022-11-03 13:54:49 +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 {
|
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
|
|
|
}
|
2023-03-10 03:44:27 +00:00
|
|
|
|
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]
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-10 03:44:27 +00:00
|
|
|
// 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
|
2023-03-10 03:44:27 +00:00
|
|
|
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 {
|
2023-03-10 03:44:27 +00:00
|
|
|
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))]
|
2023-03-10 03:44:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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-06-27 22:39:41 +00:00
|
|
|
|
2023-07-06 20:31:32 +00:00
|
|
|
type spacing struct {
|
|
|
|
|
top, bottom, left, right float64
|
|
|
|
|
}
|
2023-06-27 22:39:41 +00:00
|
|
|
|
2023-07-06 20:31:32 +00:00
|
|
|
func getSpacing(obj *d2graph.Object) (margin, padding spacing) {
|
2023-06-27 22:39:41 +00:00
|
|
|
// reserve spacing for labels
|
|
|
|
|
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
|
2023-06-27 22:39:41 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
2023-06-27 22:39:41 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// reserve extra space for 3d/multiple by providing dagre the larger dimensions
|
|
|
|
|
dx, dy := obj.GetModifierElementAdjustments()
|
2023-07-06 20:31:32 +00:00
|
|
|
margin.right += dx
|
|
|
|
|
margin.top += dy
|
2023-06-27 22:39:41 +00:00
|
|
|
|
|
|
|
|
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-04 01:07:31 +00:00
|
|
|
|
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) {
|
2023-07-04 01:07:31 +00:00
|
|
|
alignedObjects := make(map[float64][]*d2graph.Object)
|
|
|
|
|
for _, obj := range g.Objects {
|
|
|
|
|
if !obj.IsContainer() {
|
|
|
|
|
if !isHorizontal {
|
|
|
|
|
y := obj.TopLeft.Y + obj.Height/2
|
|
|
|
|
alignedObjects[y] = append(alignedObjects[y], obj)
|
|
|
|
|
} else {
|
|
|
|
|
x := 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)
|
2023-07-04 01:07:31 +00:00
|
|
|
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
|
|
|
// for _, obj := range g.Objects {
|
|
|
|
|
// if rank, has := objectRanks[obj]; has {
|
|
|
|
|
// fmt.Printf("%v rank: %d\n", obj.AbsID(), rank)
|
|
|
|
|
// } else {
|
|
|
|
|
// fmt.Printf("%v rank: none\n", obj.AbsID())
|
|
|
|
|
// }
|
|
|
|
|
// }
|
2023-07-04 01:07:31 +00:00
|
|
|
|
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
|
|
|
// for parent, start := range startingParentRanks {
|
|
|
|
|
// fmt.Printf("parent %v start %v end %v\n", parent.AbsID(), start, endingParentRanks[parent])
|
|
|
|
|
// }
|
2023-07-06 19:32:51 +00:00
|
|
|
|
2023-07-06 20:31:32 +00:00
|
|
|
return ranks, objectRanks, startingParentRanks, endingParentRanks
|
2023-07-04 01:07:31 +00:00
|
|
|
}
|
|
|
|
|
|
2023-07-06 20:31:32 +00:00
|
|
|
func getRankRange(rank []*d2graph.Object, isHorizontal bool) (min, max float64) {
|
|
|
|
|
min = math.Inf(1)
|
|
|
|
|
max = math.Inf(-1)
|
|
|
|
|
for _, obj := range rank {
|
|
|
|
|
if isHorizontal {
|
|
|
|
|
min = math.Min(min, obj.TopLeft.X)
|
|
|
|
|
max = math.Max(max, obj.TopLeft.X+obj.Width)
|
|
|
|
|
} else {
|
|
|
|
|
min = math.Min(min, obj.TopLeft.Y)
|
|
|
|
|
max = math.Max(max, obj.TopLeft.Y+obj.Height)
|
2023-07-04 01:07:31 +00:00
|
|
|
}
|
2023-07-06 20:31:32 +00:00
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func getPositions(ranks [][]*d2graph.Object, isHorizontal bool) (starts, centers, ends []float64) {
|
|
|
|
|
for _, objects := range ranks {
|
|
|
|
|
min, max := getRankRange(objects, isHorizontal)
|
|
|
|
|
starts = append(starts, min)
|
2023-07-04 01:07:31 +00:00
|
|
|
if isHorizontal {
|
|
|
|
|
centers = append(centers, objects[0].TopLeft.X+objects[0].Width/2.)
|
|
|
|
|
} else {
|
|
|
|
|
centers = append(centers, objects[0].TopLeft.Y+objects[0].Height/2.)
|
|
|
|
|
}
|
2023-07-06 20:31:32 +00:00
|
|
|
ends = append(ends, max)
|
2023-07-04 01:07:31 +00:00
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
|
// +1 in case it is off by 1
|
|
|
|
|
if p.X+1 <= start {
|
|
|
|
|
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
|
|
|
|
|
// +1 in case it is off by 1
|
|
|
|
|
if p.Y+1 <= start {
|
|
|
|
|
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) {
|
|
|
|
|
fmt.Printf("shifting %v at %v by %v\n", obj.AbsID(), start, distance)
|
|
|
|
|
// if obj.ID == "s" || obj.ID == "k" || true {
|
|
|
|
|
// return
|
|
|
|
|
// }
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
fmt.Printf("queue %v\n", o.AbsID())
|
|
|
|
|
q = append(q, o)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
checkBelow := func(curr *d2graph.Object) {
|
|
|
|
|
fmt.Printf("checking below %v\n", curr.AbsID())
|
|
|
|
|
currBottom := curr.TopLeft.Y + curr.Height
|
|
|
|
|
currRight := curr.TopLeft.X + curr.Width
|
|
|
|
|
// if object below is within this distance after shifting, also shift it
|
|
|
|
|
threshold := 100.
|
|
|
|
|
if isHorizontal {
|
|
|
|
|
originalRight := currRight
|
|
|
|
|
if _, in := shifted[curr]; in {
|
|
|
|
|
originalRight -= distance
|
|
|
|
|
}
|
|
|
|
|
for _, other := range g.Objects {
|
|
|
|
|
if other == curr || curr.IsDescendantOf(other) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
// fmt.Printf("%#v && %#v original right %v currRight %v other left %v\n\t%v\n", curr.AbsID(), other.AbsID(),
|
|
|
|
|
// originalRight, currRight, other.TopLeft.X,
|
|
|
|
|
// other.TopLeft.X-originalRight,
|
|
|
|
|
// )
|
|
|
|
|
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 {
|
|
|
|
|
fmt.Printf("\twas seen %v\n", curr.AbsID())
|
|
|
|
|
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
|
|
|
|
|
fmt.Printf("\tshifted %v\n", curr.AbsID())
|
|
|
|
|
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
|
|
|
|
|
fmt.Printf("\tshifted %v\n", curr.AbsID())
|
|
|
|
|
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 {
|
|
|
|
|
// shift the whole self-loop with object
|
|
|
|
|
if isHorizontal {
|
|
|
|
|
for _, p := range e.Route {
|
|
|
|
|
p.X += distance
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
for _, p := range e.Route {
|
|
|
|
|
p.Y += distance
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
shiftedEdges[e] = struct{}{}
|
|
|
|
|
fmt.Printf("\tshifted %v\n", e.AbsID())
|
|
|
|
|
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 {
|
2023-07-04 01:07:31 +00:00
|
|
|
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{}{}
|
|
|
|
|
fmt.Printf("grow %v\n", parent.AbsID())
|
|
|
|
|
|
|
|
|
|
checkBelow(parent)
|
|
|
|
|
processQueue()
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if parent.TopLeft.Y < start {
|
|
|
|
|
parent.Height += distance
|
|
|
|
|
grown[parent] = struct{}{}
|
|
|
|
|
fmt.Printf("grow %v\n", parent.AbsID())
|
|
|
|
|
|
|
|
|
|
checkBelow(parent)
|
|
|
|
|
processQueue()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-04 01:07:31 +00:00
|
|
|
}
|
2023-07-06 20:31:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func adjustRankSpacing(g *d2graph.Graph, rankSep float64, isHorizontal bool) {
|
|
|
|
|
ranks, _, startingParentRanks, endingParentRanks := getRanks(g, isHorizontal)
|
|
|
|
|
starts, _, ends := getPositions(ranks, isHorizontal)
|
|
|
|
|
|
|
|
|
|
// shifting bottom rank down first, then moving up to next rank
|
|
|
|
|
for rank := len(ranks) - 1; rank >= 0; rank-- {
|
|
|
|
|
objects := ranks[rank]
|
|
|
|
|
rankMin := starts[rank]
|
|
|
|
|
rankMax := ends[rank]
|
|
|
|
|
var topMargin, bottomMargin, leftMargin, rightMargin float64
|
|
|
|
|
var topPadding, bottomPadding, leftPadding, rightPadding float64
|
|
|
|
|
for _, obj := range objects {
|
|
|
|
|
margin, padding := getSpacing(obj)
|
|
|
|
|
|
|
|
|
|
if isHorizontal {
|
|
|
|
|
// if this object isn't the widest in the rank, the actual margin for the rank may be smaller
|
|
|
|
|
// so we compute how much margin goes past the rankMin
|
|
|
|
|
rankMarginLeft := obj.TopLeft.X - rankMin + margin.left
|
|
|
|
|
rankMarginRight := obj.TopLeft.X + obj.Width + margin.right - rankMax
|
|
|
|
|
leftMargin = math.Max(leftMargin, rankMarginLeft)
|
|
|
|
|
rightMargin = math.Max(rightMargin, rankMarginRight)
|
|
|
|
|
|
|
|
|
|
topMargin = math.Max(topMargin, margin.top)
|
|
|
|
|
bottomMargin = math.Max(bottomMargin, margin.bottom)
|
|
|
|
|
} else {
|
|
|
|
|
rankMarginTop := obj.TopLeft.Y - rankMin + margin.top
|
|
|
|
|
rankMarginBottom := obj.TopLeft.Y + obj.Height + margin.bottom - rankMax
|
|
|
|
|
topMargin = math.Max(topMargin, rankMarginTop)
|
|
|
|
|
bottomMargin = math.Max(bottomMargin, rankMarginBottom)
|
|
|
|
|
|
|
|
|
|
leftMargin = math.Max(leftMargin, margin.left)
|
|
|
|
|
rightMargin = math.Max(rightMargin, margin.right)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
padTopDelta := padding.top - obj.Height/2.
|
|
|
|
|
padBottomDelta := padding.bottom - obj.Height/2.
|
|
|
|
|
padLeftDelta := padding.left - obj.Width/2.
|
|
|
|
|
padRightDelta := padding.right - obj.Width/2.
|
|
|
|
|
// if padTopDelta > 0 {
|
|
|
|
|
// obj.Height += padTopDelta
|
|
|
|
|
// }
|
|
|
|
|
// if padBottomDelta > 0 {
|
|
|
|
|
// obj.Height += padBottomDelta
|
|
|
|
|
// }
|
|
|
|
|
// if padLeftDelta > 0 {
|
|
|
|
|
// obj.Width += padLeftDelta
|
|
|
|
|
// }
|
|
|
|
|
// if padRightDelta > 0 {
|
|
|
|
|
// obj.Width += padRightDelta
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
topPadding = math.Max(topPadding, padTopDelta)
|
|
|
|
|
bottomPadding = math.Max(bottomPadding, padBottomDelta)
|
|
|
|
|
leftPadding = math.Max(leftPadding, padLeftDelta)
|
|
|
|
|
rightPadding = math.Max(rightPadding, padRightDelta)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var startDelta, endDelta float64
|
|
|
|
|
var startPaddingDelta, endPaddingDelta float64
|
|
|
|
|
if isHorizontal {
|
|
|
|
|
startDelta = math.Max(0, MIN_MARGIN+leftMargin-rankSep/2.)
|
|
|
|
|
endDelta = math.Max(0, MIN_MARGIN+rightMargin-rankSep/2.)
|
|
|
|
|
|
|
|
|
|
// startPaddingDelta = leftPadding
|
|
|
|
|
// endPaddingDelta = rightPadding
|
|
|
|
|
} else {
|
|
|
|
|
startDelta = math.Max(0, MIN_MARGIN+topMargin-rankSep/2.)
|
|
|
|
|
endDelta = math.Max(0, MIN_MARGIN+bottomMargin-rankSep/2.)
|
|
|
|
|
|
|
|
|
|
// startPaddingDelta = topPadding
|
|
|
|
|
// endPaddingDelta = bottomPadding
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Printf("r%v start %v sp %v ep %v end %v\n", rank, startDelta, startPaddingDelta, endPaddingDelta, endDelta)
|
|
|
|
|
// +1 to not include edges at bottom
|
|
|
|
|
if endDelta > 0 {
|
|
|
|
|
shiftDown(g, ends[rank]+1, endDelta, isHorizontal)
|
|
|
|
|
}
|
|
|
|
|
// TODO each ancestor container of rank may need its own padding shift
|
|
|
|
|
if endPaddingDelta > 0 {
|
|
|
|
|
shiftDown(g, ends[rank]-endPaddingDelta, endPaddingDelta, isHorizontal)
|
|
|
|
|
}
|
|
|
|
|
if startPaddingDelta > 0 {
|
|
|
|
|
shiftDown(g, starts[rank]+startPaddingDelta, startPaddingDelta, isHorizontal)
|
|
|
|
|
}
|
|
|
|
|
if startDelta > 0 {
|
|
|
|
|
shiftDown(g, starts[rank], startDelta, isHorizontal)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
additionalStarts := make(map[float64]float64)
|
|
|
|
|
additionalEnds := make(map[float64]float64)
|
|
|
|
|
var startCoords, endCoords []float64
|
|
|
|
|
for _, obj := range g.Objects {
|
|
|
|
|
if !obj.IsContainer() {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
start := startingParentRanks[obj]
|
|
|
|
|
end := endingParentRanks[obj]
|
|
|
|
|
if start != rank && end != rank {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
// check to see if container needs additional margin to parent
|
|
|
|
|
margin, _ := getSpacing(obj)
|
|
|
|
|
|
|
|
|
|
addStart := func(k, v float64) {
|
|
|
|
|
if _, has := additionalStarts[k]; !has {
|
|
|
|
|
additionalStarts[k] = v
|
|
|
|
|
startCoords = append(startCoords, k)
|
|
|
|
|
} else {
|
|
|
|
|
additionalStarts[k] = math.Max(additionalStarts[k], v)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
addEnd := func(k, v float64) {
|
|
|
|
|
if _, has := additionalEnds[k]; !has {
|
|
|
|
|
additionalEnds[k] = v
|
|
|
|
|
endCoords = append(endCoords, k)
|
|
|
|
|
} else {
|
|
|
|
|
additionalEnds[k] = math.Max(additionalEnds[k], v)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if start == rank {
|
|
|
|
|
if isHorizontal && margin.left > 0 {
|
|
|
|
|
addStart(obj.TopLeft.X, margin.left)
|
|
|
|
|
} else if !isHorizontal && margin.top > 0 {
|
|
|
|
|
addStart(obj.TopLeft.Y, margin.top)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if end == rank {
|
|
|
|
|
if isHorizontal && margin.right > 0 {
|
|
|
|
|
addEnd(obj.TopLeft.X+obj.Width, margin.right)
|
|
|
|
|
} else if !isHorizontal && margin.bottom > 0 {
|
|
|
|
|
addEnd(obj.TopLeft.Y+obj.Height, margin.bottom)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// bottom up
|
|
|
|
|
sort.Slice(startCoords, func(i, j int) bool {
|
|
|
|
|
return startCoords[i] > startCoords[j]
|
|
|
|
|
})
|
|
|
|
|
sort.Slice(endCoords, func(i, j int) bool {
|
|
|
|
|
return endCoords[i] > endCoords[j]
|
|
|
|
|
})
|
|
|
|
|
for _, coord := range endCoords {
|
|
|
|
|
delta := MIN_MARGIN + additionalEnds[coord] - rankSep/2
|
|
|
|
|
if delta <= 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
for _, obj := range g.Objects {
|
|
|
|
|
if !obj.IsContainer() {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
start := startingParentRanks[obj]
|
|
|
|
|
end := endingParentRanks[obj]
|
|
|
|
|
if start <= rank && rank <= end {
|
|
|
|
|
// don't want to grow the container that is shifting
|
|
|
|
|
if isHorizontal && obj.TopLeft.X+obj.Width > coord {
|
|
|
|
|
obj.Width += delta
|
|
|
|
|
} else if !isHorizontal && obj.TopLeft.Y+obj.Height > coord {
|
|
|
|
|
obj.Height += delta
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
shiftDown(g, coord+delta, delta, isHorizontal)
|
|
|
|
|
}
|
|
|
|
|
for _, coord := range startCoords {
|
|
|
|
|
delta := MIN_MARGIN + additionalStarts[coord] - rankSep/2
|
|
|
|
|
if delta <= 0 {
|
2023-07-04 01:07:31 +00:00
|
|
|
continue
|
|
|
|
|
}
|
2023-07-06 20:31:32 +00:00
|
|
|
for _, obj := range g.Objects {
|
|
|
|
|
if !obj.IsContainer() {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
start := startingParentRanks[obj]
|
|
|
|
|
end := endingParentRanks[obj]
|
|
|
|
|
// expand all containers that pass this rank, except the ones that are moving down to fit the icon
|
|
|
|
|
if start <= rank && rank <= end {
|
|
|
|
|
// expand the containers that contain the ones moving down
|
|
|
|
|
if isHorizontal && obj.TopLeft.X < coord {
|
|
|
|
|
obj.Width += delta
|
|
|
|
|
} else if !isHorizontal && obj.TopLeft.Y < coord {
|
|
|
|
|
obj.Height += delta
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
shiftDown(g, coord, delta, isHorizontal)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We need to expand parents when shifting descendants downwards
|
|
|
|
|
for _, obj := range g.Objects {
|
|
|
|
|
if !obj.IsContainer() {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
start := startingParentRanks[obj]
|
|
|
|
|
end := endingParentRanks[obj]
|
|
|
|
|
if start <= rank && rank <= end {
|
|
|
|
|
if isHorizontal {
|
|
|
|
|
obj.Width += startDelta + endDelta + startPaddingDelta + endPaddingDelta
|
|
|
|
|
} else {
|
|
|
|
|
obj.Height += startDelta + endDelta + startPaddingDelta + endPaddingDelta
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func adjustSpacing(g *d2graph.Graph, rankSep float64, isHorizontal bool) {
|
|
|
|
|
adjustRankSpacing(g, rankSep, isHorizontal)
|
|
|
|
|
|
|
|
|
|
// adjust cross-rank spacing
|
|
|
|
|
crossRankIsHorizontal := !isHorizontal
|
|
|
|
|
for _, obj := range g.Objects {
|
|
|
|
|
margin, padding := getSpacing(obj)
|
|
|
|
|
if isHorizontal {
|
|
|
|
|
if margin.bottom > 0 {
|
|
|
|
|
shiftReachableDown(g, obj, obj.TopLeft.Y+obj.Height, margin.bottom, crossRankIsHorizontal, true)
|
|
|
|
|
}
|
|
|
|
|
if padding.bottom > 0 {
|
|
|
|
|
shiftReachableDown(g, obj, obj.TopLeft.Y+obj.Height, padding.bottom, crossRankIsHorizontal, false)
|
|
|
|
|
obj.Height += padding.bottom
|
|
|
|
|
}
|
|
|
|
|
if margin.top > 0 {
|
|
|
|
|
shiftReachableDown(g, obj, obj.TopLeft.Y, margin.top, crossRankIsHorizontal, true)
|
|
|
|
|
}
|
|
|
|
|
if padding.top > 0 {
|
|
|
|
|
shiftReachableDown(g, obj, obj.TopLeft.Y, padding.top, crossRankIsHorizontal, false)
|
|
|
|
|
obj.Height += padding.top
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if margin.right > 0 {
|
|
|
|
|
shiftReachableDown(g, obj, obj.TopLeft.X+obj.Width, margin.right, crossRankIsHorizontal, true)
|
|
|
|
|
}
|
|
|
|
|
if padding.right > 0 {
|
|
|
|
|
shiftReachableDown(g, obj, obj.TopLeft.X+obj.Width, padding.right, crossRankIsHorizontal, false)
|
|
|
|
|
obj.Width += padding.right
|
|
|
|
|
}
|
|
|
|
|
if margin.left > 0 {
|
|
|
|
|
shiftReachableDown(g, obj, obj.TopLeft.X, margin.left, crossRankIsHorizontal, true)
|
|
|
|
|
}
|
|
|
|
|
if padding.left > 0 {
|
|
|
|
|
shiftReachableDown(g, obj, obj.TopLeft.X, padding.left, crossRankIsHorizontal, false)
|
|
|
|
|
obj.Width += padding.left
|
|
|
|
|
}
|
2023-07-04 01:07:31 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|