calculating object ranks
adusting spacing for each rank
This commit is contained in:
parent
de8406d124
commit
8e18103ffd
1 changed files with 217 additions and 57 deletions
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"cdr.dev/slog"
|
"cdr.dev/slog"
|
||||||
|
|
@ -162,7 +163,8 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
||||||
id := obj.AbsID()
|
id := obj.AbsID()
|
||||||
idToObj[id] = obj
|
idToObj[id] = obj
|
||||||
|
|
||||||
width, height := adjustDimensions(obj)
|
// width, height := adjustDimensions(obj)
|
||||||
|
width, height := obj.Width, obj.Height
|
||||||
|
|
||||||
idToWidth[id] = width
|
idToWidth[id] = width
|
||||||
idToHeight[id] = height
|
idToHeight[id] = height
|
||||||
|
|
@ -172,58 +174,68 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
||||||
loadScript += generateAddParentLine(id, obj.Parent.AbsID())
|
loadScript += generateAddParentLine(id, obj.Parent.AbsID())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, obj := range g.Objects {
|
// for _, obj := range g.Objects {
|
||||||
if !obj.IsContainer() {
|
// if !obj.IsContainer() {
|
||||||
continue
|
// continue
|
||||||
}
|
// }
|
||||||
id := obj.AbsID()
|
// id := obj.AbsID()
|
||||||
// add phantom children to adjust container dimensions
|
// // add phantom children to adjust container dimensions
|
||||||
phantomID := id + "___phantom"
|
// // phantomID := id + "___phantom"
|
||||||
widthDelta := int(math.Ceil(idToWidth[id] - obj.Width))
|
// // widthDelta := int(math.Ceil(idToWidth[id] - obj.Width))
|
||||||
height := int(math.Ceil(idToHeight[id]))
|
// height := int(math.Ceil(idToHeight[id]))
|
||||||
// when a container has nodes with no connections, the layout will be in a row
|
// // 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
|
// // 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
|
// // 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 we add the phantom node at rank 0 it should be at the far right and top
|
||||||
xSpace := rootAttrs.NodeSep
|
// // xSpace := rootAttrs.NodeSep
|
||||||
ySpace := rootAttrs.ranksep
|
// ySpace := rootAttrs.ranksep
|
||||||
|
|
||||||
maxChildHeight := math.Inf(-1)
|
// if false {
|
||||||
for _, c := range obj.ChildrenArray {
|
|
||||||
if c.Height > maxChildHeight {
|
|
||||||
maxChildHeight = c.Height
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// adjust for children with outside positioned icons
|
// maxChildHeight := math.Inf(-1)
|
||||||
var hasTop, hasBottom bool
|
// for _, c := range obj.ChildrenArray {
|
||||||
for _, child := range obj.ChildrenArray {
|
// if c.Height > maxChildHeight {
|
||||||
if child.Shape.Value == d2target.ShapeImage || child.IconPosition == nil {
|
// maxChildHeight = c.Height
|
||||||
continue
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
switch label.Position(*child.IconPosition) {
|
// if c.Shape.Value == d2target.ShapeImage || c.IconPosition == nil {
|
||||||
case label.OutsideTopLeft, label.OutsideTopCenter, label.OutsideTopRight:
|
// continue
|
||||||
hasTop = true
|
// }
|
||||||
case label.OutsideBottomLeft, label.OutsideBottomCenter, label.OutsideBottomRight:
|
// h := c.Height
|
||||||
hasBottom = true
|
// switch label.Position(*c.IconPosition) {
|
||||||
}
|
// case label.OutsideTopLeft, label.OutsideTopCenter, label.OutsideTopRight,
|
||||||
if hasTop && hasBottom {
|
// label.OutsideBottomLeft, label.OutsideBottomCenter, label.OutsideBottomRight:
|
||||||
break
|
// h += d2target.MAX_ICON_SIZE + 2*label.PADDING
|
||||||
}
|
// }
|
||||||
}
|
// if h > maxChildHeight {
|
||||||
if hasTop || hasBottom {
|
// maxChildHeight = h
|
||||||
// TODO ranksep is already accounting for maxLabelHeight
|
// }
|
||||||
maxChildHeight += d2target.MAX_ICON_SIZE + 2*label.PADDING
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
height = go2.Max(height, ySpace+int(maxChildHeight))
|
// // adjust for children with outside positioned icons
|
||||||
|
// var hasTop, hasBottom bool
|
||||||
|
// for _, child := range obj.ChildrenArray {
|
||||||
|
// if child.Shape.Value == d2target.ShapeImage || child.IconPosition == nil {
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
|
||||||
// TODO after layout remove extra height and shift downwards
|
// switch label.Position(*child.IconPosition) {
|
||||||
|
// case label.OutsideTopLeft, label.OutsideTopCenter, label.OutsideTopRight:
|
||||||
loadScript += generateAddNodeLine(phantomID, widthDelta-xSpace, height-ySpace)
|
// hasTop = true
|
||||||
loadScript += generateAddParentLine(phantomID, id)
|
// case label.OutsideBottomLeft, label.OutsideBottomCenter, label.OutsideBottomRight:
|
||||||
}
|
// hasBottom = true
|
||||||
|
// }
|
||||||
|
// if hasTop && hasBottom {
|
||||||
|
// break
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// if hasTop || hasBottom {
|
||||||
|
// // TODO ranksep is already accounting for maxLabelHeight
|
||||||
|
// // maxChildHeight += d2target.MAX_ICON_SIZE + 2*label.PADDING
|
||||||
|
// }
|
||||||
|
// height = go2.Max(height, ySpace+int(maxChildHeight))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
for _, edge := range g.Edges {
|
for _, edge := range g.Edges {
|
||||||
src, dst := getEdgeEndpoints(g, edge)
|
src, dst := getEdgeEndpoints(g, edge)
|
||||||
|
|
@ -288,6 +300,33 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
||||||
obj.Height = math.Ceil(dn.Height)
|
obj.Height = math.Ceil(dn.Height)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ranks, objectRanks := getRanks(g, isHorizontal)
|
||||||
|
if ranks != nil && objectRanks != nil {
|
||||||
|
fmt.Printf("got ranks: %v\n", ranks)
|
||||||
|
}
|
||||||
|
|
||||||
|
tops, centers, bottoms := getPositions(ranks, isHorizontal)
|
||||||
|
|
||||||
|
if tops != nil {
|
||||||
|
fmt.Printf("got tops: %v\ncenters: %v\nbottoms: %v\n", tops, centers, bottoms)
|
||||||
|
fmt.Printf("spacing: ")
|
||||||
|
for i := 1; i < len(tops); i++ {
|
||||||
|
fmt.Printf("%v, ", tops[i]-bottoms[i-1])
|
||||||
|
}
|
||||||
|
fmt.Printf("\n")
|
||||||
|
}
|
||||||
|
fmt.Printf("ranksep %v, nodesep %v\n", rootAttrs.ranksep, rootAttrs.NodeSep)
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// 1. Compute all current spacings
|
||||||
|
// 2. Compute desired spacings
|
||||||
|
// 3. Apply changes (shifting anything below)
|
||||||
|
//
|
||||||
|
// Two kinds of spacing, 1. rank spacing, 2. rank alignment spacing
|
||||||
|
// all objects at a rank are center aligned, if one is much taller, then the rest will have more spacing to align with the taller node
|
||||||
|
// if there is extra spacing due to rank alignment, we may not need to increase rank spacing
|
||||||
|
// for now, just applying spacing increase for whole rank
|
||||||
|
|
||||||
for i, edge := range g.Edges {
|
for i, edge := range g.Edges {
|
||||||
val, err := vm.RunString(fmt.Sprintf("JSON.stringify(g.edge(g.edges()[%d]))", i))
|
val, err := vm.RunString(fmt.Sprintf("JSON.stringify(g.edge(g.edges()[%d]))", i))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -336,7 +375,22 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
||||||
edge.Route = points
|
edge.Route = points
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, obj := range g.Objects {
|
// shifting bottom rank down first, then moving up to next rank
|
||||||
|
for i := len(ranks) - 1; i >= 0; i-- {
|
||||||
|
objects := ranks[i]
|
||||||
|
topSpacing := 0.
|
||||||
|
for _, obj := range objects {
|
||||||
|
_, adjustedHeight := adjustDimensions(obj)
|
||||||
|
// TODO width
|
||||||
|
topSpacing = math.Max(topSpacing, adjustedHeight-obj.Height)
|
||||||
|
}
|
||||||
|
fmt.Printf("rank %d topSpacing %v\n", i, topSpacing)
|
||||||
|
// shiftDown(g, tops[i], topSpacing)
|
||||||
|
// TODO: Testing
|
||||||
|
shiftDown(g, tops[i], float64(100))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, obj := range []*d2graph.Object{} {
|
||||||
if !obj.HasLabel() || len(obj.ChildrenArray) == 0 {
|
if !obj.HasLabel() || len(obj.ChildrenArray) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -437,7 +491,8 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, obj := range g.Objects {
|
// for _, obj := range g.Objects {
|
||||||
|
for _, obj := range []*d2graph.Object{} {
|
||||||
cleanupAdjustment(obj)
|
cleanupAdjustment(obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -778,13 +833,9 @@ func adjustDimensions(obj *d2graph.Object) (width, height float64) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// special handling
|
|
||||||
if obj.HasOutsideBottomLabel() || obj.Icon != nil {
|
|
||||||
height += float64(obj.LabelDimensions.Height) + label.PADDING
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasIconAboveBelow := false
|
||||||
if obj.Icon != nil && obj.Shape.Value != d2target.ShapeImage {
|
if obj.Icon != nil && obj.Shape.Value != d2target.ShapeImage {
|
||||||
var position label.Position
|
var position label.Position
|
||||||
if obj.IconPosition != nil {
|
if obj.IconPosition != nil {
|
||||||
|
|
@ -810,6 +861,19 @@ func adjustDimensions(obj *d2graph.Object) (width, height float64) {
|
||||||
width = go2.Max(width, d2target.MAX_ICON_SIZE+2*label.PADDING)
|
width = go2.Max(width, d2target.MAX_ICON_SIZE+2*label.PADDING)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch position {
|
||||||
|
case label.OutsideTopLeft, label.OutsideTopCenter, label.OutsideTopRight,
|
||||||
|
label.OutsideBottomLeft, label.OutsideBottomCenter, label.OutsideBottomRight:
|
||||||
|
hasIconAboveBelow = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if true {
|
||||||
|
// special handling
|
||||||
|
if obj.HasOutsideBottomLabel() || hasIconAboveBelow {
|
||||||
|
height += float64(obj.LabelDimensions.Height) + label.PADDING
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -843,6 +907,7 @@ func cleanupAdjustment(obj *d2graph.Object) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
hasIconAboveBelow := false
|
||||||
if obj.Icon != nil && obj.Shape.Value != d2target.ShapeImage {
|
if obj.Icon != nil && obj.Shape.Value != d2target.ShapeImage {
|
||||||
position := label.Position(*obj.IconPosition)
|
position := label.Position(*obj.IconPosition)
|
||||||
if position.IsShapePosition() {
|
if position.IsShapePosition() {
|
||||||
|
|
@ -861,12 +926,23 @@ func cleanupAdjustment(obj *d2graph.Object) {
|
||||||
obj.ShiftDescendants(iconWidth, 0)
|
obj.ShiftDescendants(iconWidth, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
switch position {
|
||||||
|
case label.OutsideTopLeft, label.OutsideTopCenter, label.OutsideTopRight,
|
||||||
|
label.OutsideBottomLeft, label.OutsideBottomCenter, label.OutsideBottomRight:
|
||||||
|
hasIconAboveBelow = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// special handling to start/end connections below label
|
// special handling to start/end connections below label
|
||||||
if obj.HasOutsideBottomLabel() {
|
if true {
|
||||||
obj.Height -= float64(obj.LabelDimensions.Height) + label.PADDING
|
if obj.HasOutsideBottomLabel() || hasIconAboveBelow {
|
||||||
|
dy := float64(obj.LabelDimensions.Height) + label.PADDING
|
||||||
|
obj.Height -= dy
|
||||||
|
if obj.IsContainer() {
|
||||||
|
obj.ShiftDescendants(0, -dy/2)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove the extra width/height we added for 3d/multiple after all objects/connections are placed
|
// remove the extra width/height we added for 3d/multiple after all objects/connections are placed
|
||||||
|
|
@ -906,3 +982,87 @@ func positionLabelsIcons(obj *d2graph.Object) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getRanks(g *d2graph.Graph, isHorizontal bool) ([][]*d2graph.Object, map[*d2graph.Object]int) {
|
||||||
|
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]
|
||||||
|
})
|
||||||
|
|
||||||
|
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])
|
||||||
|
}
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ranks, objectRanks
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPositions(ranks [][]*d2graph.Object, isHorizontal bool) (tops, centers, bottoms []float64) {
|
||||||
|
for _, objects := range ranks {
|
||||||
|
min := math.Inf(1)
|
||||||
|
max := math.Inf(-1)
|
||||||
|
for _, obj := range objects {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tops = append(tops, min)
|
||||||
|
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.)
|
||||||
|
}
|
||||||
|
bottoms = append(bottoms, max)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// shift everything down by distance if it is at or below startY
|
||||||
|
func shiftDown(g *d2graph.Graph, startY, distance float64) {
|
||||||
|
for _, obj := range g.Objects {
|
||||||
|
if obj.TopLeft.Y < startY {
|
||||||
|
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 <= startY {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p.Y += distance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue