2023-04-01 00:18:17 +00:00
|
|
|
package d2grid
|
|
|
|
|
|
|
|
|
|
import (
|
2023-05-10 00:19:33 +00:00
|
|
|
"bytes"
|
2023-04-01 00:18:17 +00:00
|
|
|
"context"
|
2023-04-29 01:23:00 +00:00
|
|
|
"fmt"
|
2023-04-04 04:38:08 +00:00
|
|
|
"math"
|
2023-04-01 00:18:17 +00:00
|
|
|
|
2023-11-16 02:25:18 +00:00
|
|
|
"cdr.dev/slog"
|
2023-04-01 00:18:17 +00:00
|
|
|
"oss.terrastruct.com/d2/d2graph"
|
2023-05-30 18:59:39 +00:00
|
|
|
"oss.terrastruct.com/d2/d2target"
|
2023-04-01 00:18:17 +00:00
|
|
|
"oss.terrastruct.com/d2/lib/geo"
|
|
|
|
|
"oss.terrastruct.com/d2/lib/label"
|
2023-11-16 02:25:18 +00:00
|
|
|
"oss.terrastruct.com/d2/lib/log"
|
2023-04-01 00:18:17 +00:00
|
|
|
"oss.terrastruct.com/util-go/go2"
|
|
|
|
|
)
|
|
|
|
|
|
2023-04-03 18:36:01 +00:00
|
|
|
const (
|
|
|
|
|
CONTAINER_PADDING = 60
|
2023-04-11 18:20:21 +00:00
|
|
|
DEFAULT_GAP = 40
|
2023-04-03 18:36:01 +00:00
|
|
|
)
|
2023-04-01 00:18:17 +00:00
|
|
|
|
|
|
|
|
// Layout runs the grid layout on containers with rows/columns
|
|
|
|
|
// Note: children are not allowed edges or descendants
|
2023-09-22 04:00:02 +00:00
|
|
|
// 1. Run grid layout on the graph root
|
|
|
|
|
// 2. Set the resulting dimensions to the graph root
|
|
|
|
|
func Layout(ctx context.Context, g *d2graph.Graph) error {
|
2023-09-16 05:07:06 +00:00
|
|
|
obj := g.Root
|
|
|
|
|
|
|
|
|
|
gd, err := layoutGrid(g, obj)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-09 00:17:58 +00:00
|
|
|
if obj.HasLabel() && obj.LabelPosition == nil {
|
|
|
|
|
obj.LabelPosition = go2.Pointer(label.InsideTopCenter.String())
|
|
|
|
|
}
|
|
|
|
|
if obj.Icon != nil && obj.IconPosition == nil {
|
|
|
|
|
obj.IconPosition = go2.Pointer(label.InsideTopLeft.String())
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-16 05:07:06 +00:00
|
|
|
if obj.Box != nil {
|
|
|
|
|
// CONTAINER_PADDING is default, but use gap value if set
|
|
|
|
|
horizontalPadding, verticalPadding := CONTAINER_PADDING, CONTAINER_PADDING
|
|
|
|
|
if obj.GridGap != nil || obj.HorizontalGap != nil {
|
|
|
|
|
horizontalPadding = gd.horizontalGap
|
|
|
|
|
}
|
|
|
|
|
if obj.GridGap != nil || obj.VerticalGap != nil {
|
|
|
|
|
verticalPadding = gd.verticalGap
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-14 18:49:08 +00:00
|
|
|
contentWidth, contentHeight := gd.width, gd.height
|
2023-09-16 05:07:06 +00:00
|
|
|
|
2023-11-09 00:17:58 +00:00
|
|
|
var labelPosition, iconPosition label.Position
|
|
|
|
|
if obj.LabelPosition != nil {
|
|
|
|
|
labelPosition = label.FromString(*obj.LabelPosition)
|
|
|
|
|
}
|
|
|
|
|
if obj.IconPosition != nil {
|
|
|
|
|
iconPosition = label.FromString(*obj.IconPosition)
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-16 05:07:06 +00:00
|
|
|
// compute how much space the label and icon occupy
|
2023-11-09 01:12:12 +00:00
|
|
|
_, padding := obj.Spacing()
|
2023-09-16 05:07:06 +00:00
|
|
|
|
2023-11-09 04:29:07 +00:00
|
|
|
var labelWidth, labelHeight float64
|
|
|
|
|
if obj.LabelDimensions.Width > 0 {
|
|
|
|
|
labelWidth = float64(obj.LabelDimensions.Width) + 2*label.PADDING
|
|
|
|
|
}
|
|
|
|
|
if obj.LabelDimensions.Height > 0 {
|
|
|
|
|
labelHeight = float64(obj.LabelDimensions.Height) + 2*label.PADDING
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if labelWidth > 0 {
|
|
|
|
|
switch labelPosition {
|
|
|
|
|
case label.OutsideTopLeft, label.OutsideTopCenter, label.OutsideTopRight,
|
|
|
|
|
label.InsideTopLeft, label.InsideTopCenter, label.InsideTopRight,
|
|
|
|
|
label.InsideBottomLeft, label.InsideBottomCenter, label.InsideBottomRight,
|
|
|
|
|
label.OutsideBottomLeft, label.OutsideBottomCenter, label.OutsideBottomRight:
|
2023-11-14 18:49:08 +00:00
|
|
|
overflow := labelWidth - contentWidth
|
2023-11-09 04:29:07 +00:00
|
|
|
if overflow > 0 {
|
|
|
|
|
padding.Left += overflow / 2
|
|
|
|
|
padding.Right += overflow / 2
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if labelHeight > 0 {
|
|
|
|
|
switch labelPosition {
|
|
|
|
|
case label.OutsideLeftTop, label.OutsideLeftMiddle, label.OutsideLeftBottom,
|
|
|
|
|
label.InsideMiddleLeft, label.InsideMiddleCenter, label.InsideMiddleRight,
|
|
|
|
|
label.OutsideRightTop, label.OutsideRightMiddle, label.OutsideRightBottom:
|
2023-11-14 18:49:08 +00:00
|
|
|
overflow := labelHeight - contentHeight
|
2023-11-09 04:29:07 +00:00
|
|
|
if overflow > 0 {
|
|
|
|
|
padding.Top += overflow / 2
|
|
|
|
|
padding.Bottom += overflow / 2
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// configure spacing for default label+icon
|
2023-11-09 01:12:12 +00:00
|
|
|
if iconPosition == label.InsideTopLeft && labelPosition == label.InsideTopCenter {
|
2023-09-16 05:07:06 +00:00
|
|
|
// . ├────┤───────├────┤
|
|
|
|
|
// . icon label icon
|
|
|
|
|
// with an icon in top left we need 2x the space to fit the label in the center
|
2023-11-09 01:12:12 +00:00
|
|
|
iconSize := float64(d2target.MAX_ICON_SIZE) + 2*label.PADDING
|
|
|
|
|
padding.Left = math.Max(padding.Left, iconSize)
|
|
|
|
|
padding.Right = math.Max(padding.Right, iconSize)
|
|
|
|
|
minWidth := 2*iconSize + float64(obj.LabelDimensions.Width) + 2*label.PADDING
|
2023-11-14 18:49:08 +00:00
|
|
|
overflow := minWidth - contentWidth
|
2023-11-09 04:29:07 +00:00
|
|
|
if overflow > 0 {
|
2023-11-09 01:12:12 +00:00
|
|
|
padding.Left = math.Max(padding.Left, overflow/2)
|
|
|
|
|
padding.Right = math.Max(padding.Right, overflow/2)
|
2023-09-16 05:07:06 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-16 02:25:18 +00:00
|
|
|
padding.Top = math.Max(padding.Top, float64(verticalPadding))
|
|
|
|
|
padding.Bottom = math.Max(padding.Bottom, float64(verticalPadding))
|
|
|
|
|
padding.Left = math.Max(padding.Left, float64(horizontalPadding))
|
|
|
|
|
padding.Right = math.Max(padding.Right, float64(horizontalPadding))
|
|
|
|
|
|
|
|
|
|
// TODO: rethink how this works with shapes and padding
|
|
|
|
|
// // manually handle desiredWidth/Height so we can center the grid
|
|
|
|
|
// var desiredWidth, desiredHeight int
|
|
|
|
|
// var originalWidthAttr, originalHeightAttr *d2graph.Scalar
|
|
|
|
|
// if obj.WidthAttr != nil {
|
|
|
|
|
// desiredWidth, _ = strconv.Atoi(obj.WidthAttr.Value)
|
|
|
|
|
// // SizeToContent without desired width
|
|
|
|
|
// originalWidthAttr = obj.WidthAttr
|
|
|
|
|
// obj.WidthAttr = nil
|
|
|
|
|
// }
|
|
|
|
|
// if obj.HeightAttr != nil {
|
|
|
|
|
// desiredHeight, _ = strconv.Atoi(obj.HeightAttr.Value)
|
|
|
|
|
// originalHeightAttr = obj.HeightAttr
|
|
|
|
|
// obj.HeightAttr = nil
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
totalWidth := padding.Left + contentWidth + padding.Right
|
|
|
|
|
totalHeight := padding.Top + contentHeight + padding.Bottom
|
|
|
|
|
obj.SizeToContent(totalWidth, totalHeight, 0, 0)
|
|
|
|
|
|
|
|
|
|
// if originalWidthAttr != nil {
|
|
|
|
|
// obj.WidthAttr = originalWidthAttr
|
|
|
|
|
// }
|
|
|
|
|
// if originalHeightAttr != nil {
|
|
|
|
|
// obj.HeightAttr = originalHeightAttr
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// var offsetX, offsetY float64
|
|
|
|
|
// if desiredWidth > 0 {
|
|
|
|
|
// ddx := float64(desiredWidth) - obj.Width
|
|
|
|
|
// if ddx > 0 {
|
|
|
|
|
// offsetX = ddx / 2
|
|
|
|
|
// obj.Width = float64(desiredWidth)
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
// if desiredHeight > 0 {
|
|
|
|
|
// ddy := float64(desiredHeight) - obj.Height
|
|
|
|
|
// if ddy > 0 {
|
|
|
|
|
// offsetY = ddy / 2
|
|
|
|
|
// obj.Height = float64(desiredHeight)
|
|
|
|
|
// }
|
|
|
|
|
// }
|
2023-11-14 18:49:08 +00:00
|
|
|
|
|
|
|
|
// compute where the grid should be placed inside shape
|
2023-11-16 02:25:18 +00:00
|
|
|
s := obj.ToShape()
|
|
|
|
|
innerTL := s.GetInsidePlacement(totalWidth, totalHeight, 0, 0)
|
|
|
|
|
|
|
|
|
|
log.Warn(ctx, obj.Shape.Value,
|
|
|
|
|
slog.F("box", obj.Box.ToString()),
|
|
|
|
|
slog.F("innerTL", innerTL.ToString()),
|
|
|
|
|
slog.F("contentWidth", contentWidth),
|
|
|
|
|
slog.F("contentHeight", contentHeight),
|
|
|
|
|
slog.F("labelWidth", labelWidth),
|
|
|
|
|
slog.F("labelHeight", labelHeight),
|
|
|
|
|
slog.F("padding", padding),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// move from horizontalPadding,verticalPadding to innerTL.X+padding.Left, innerTL.Y+padding.Top
|
|
|
|
|
dx := -float64(horizontalPadding) + innerTL.X + padding.Left
|
|
|
|
|
dy := -float64(verticalPadding) + innerTL.Y + padding.Top
|
2023-09-16 05:07:06 +00:00
|
|
|
if dx != 0 || dy != 0 {
|
|
|
|
|
gd.shift(dx, dy)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// simple straight line edge routing between grid objects
|
|
|
|
|
for _, e := range g.Edges {
|
|
|
|
|
if !e.Src.Parent.IsDescendantOf(obj) && !e.Dst.Parent.IsDescendantOf(obj) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
// if edge is within grid, remove it from outer layout
|
|
|
|
|
gd.edges = append(gd.edges, e)
|
|
|
|
|
|
|
|
|
|
if e.Src.Parent != obj || e.Dst.Parent != obj {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
// if edge is grid child, use simple routing
|
|
|
|
|
e.Route = []*geo.Point{e.Src.Center(), e.Dst.Center()}
|
|
|
|
|
e.TraceToShape(e.Route, 0, 1)
|
|
|
|
|
if e.Label.Value != "" {
|
2023-07-17 21:21:36 +00:00
|
|
|
e.LabelPosition = go2.Pointer(label.InsideMiddleCenter.String())
|
2023-09-16 05:07:06 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if g.Root.IsGridDiagram() && len(g.Root.ChildrenArray) != 0 {
|
|
|
|
|
g.Root.TopLeft = geo.NewPoint(0, 0)
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-22 00:45:52 +00:00
|
|
|
if g.RootLevel > 0 {
|
|
|
|
|
horizontalPadding, verticalPadding := CONTAINER_PADDING, CONTAINER_PADDING
|
|
|
|
|
if obj.GridGap != nil || obj.HorizontalGap != nil {
|
|
|
|
|
horizontalPadding = gd.horizontalGap
|
|
|
|
|
}
|
|
|
|
|
if obj.GridGap != nil || obj.VerticalGap != nil {
|
|
|
|
|
verticalPadding = gd.verticalGap
|
|
|
|
|
}
|
2023-09-16 05:07:06 +00:00
|
|
|
|
2023-09-22 00:45:52 +00:00
|
|
|
// shift the grid from (0, 0)
|
|
|
|
|
gd.shift(
|
|
|
|
|
obj.TopLeft.X+float64(horizontalPadding),
|
|
|
|
|
obj.TopLeft.Y+float64(verticalPadding),
|
|
|
|
|
)
|
|
|
|
|
}
|
2023-09-16 05:07:06 +00:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-05 18:11:31 +00:00
|
|
|
func layoutGrid(g *d2graph.Graph, obj *d2graph.Object) (*gridDiagram, error) {
|
|
|
|
|
gd := newGridDiagram(obj)
|
2023-04-05 03:02:22 +00:00
|
|
|
|
|
|
|
|
// position labels and icons
|
2023-04-06 21:30:45 +00:00
|
|
|
for _, o := range gd.objects {
|
2023-10-16 23:04:12 +00:00
|
|
|
positionedLabel := false
|
|
|
|
|
if o.Icon != nil && o.IconPosition == nil {
|
|
|
|
|
if len(o.ChildrenArray) > 0 {
|
2023-07-17 21:21:36 +00:00
|
|
|
o.IconPosition = go2.Pointer(label.OutsideTopLeft.String())
|
2023-10-16 23:04:12 +00:00
|
|
|
// don't overwrite position if nested graph layout positioned label/icon
|
|
|
|
|
if o.LabelPosition == nil {
|
2023-07-17 21:21:36 +00:00
|
|
|
o.LabelPosition = go2.Pointer(label.OutsideTopRight.String())
|
2023-10-16 23:04:12 +00:00
|
|
|
positionedLabel = true
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2023-07-17 21:21:36 +00:00
|
|
|
o.IconPosition = go2.Pointer(label.InsideMiddleCenter.String())
|
2023-05-26 23:38:20 +00:00
|
|
|
}
|
2023-10-16 23:04:12 +00:00
|
|
|
}
|
|
|
|
|
if !positionedLabel && o.HasLabel() && o.LabelPosition == nil {
|
|
|
|
|
if len(o.ChildrenArray) > 0 {
|
2023-07-17 21:21:36 +00:00
|
|
|
o.LabelPosition = go2.Pointer(label.OutsideTopCenter.String())
|
2023-10-16 23:04:12 +00:00
|
|
|
} else if o.HasOutsideBottomLabel() {
|
2023-07-17 21:21:36 +00:00
|
|
|
o.LabelPosition = go2.Pointer(label.OutsideBottomCenter.String())
|
2023-10-16 23:04:12 +00:00
|
|
|
} else if o.Icon != nil {
|
2023-07-17 21:21:36 +00:00
|
|
|
o.LabelPosition = go2.Pointer(label.InsideTopCenter.String())
|
2023-10-16 23:04:12 +00:00
|
|
|
} else {
|
2023-07-17 21:21:36 +00:00
|
|
|
o.LabelPosition = go2.Pointer(label.InsideMiddleCenter.String())
|
2023-05-26 23:38:20 +00:00
|
|
|
}
|
2023-04-05 03:02:22 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-16 23:36:40 +00:00
|
|
|
// to handle objects with outside labels, we adjust their dimensions before layout and
|
|
|
|
|
// after layout, we remove the label adjustment and reposition TopLeft if needed
|
|
|
|
|
revertAdjustments := gd.sizeForOutsideLabels()
|
|
|
|
|
|
|
|
|
|
if gd.rows != 0 && gd.columns != 0 {
|
|
|
|
|
gd.layoutEvenly(g, obj)
|
|
|
|
|
} else {
|
|
|
|
|
gd.layoutDynamic(g, obj)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
revertAdjustments()
|
|
|
|
|
|
2023-04-05 18:11:31 +00:00
|
|
|
return gd, nil
|
2023-04-05 03:02:22 +00:00
|
|
|
}
|
|
|
|
|
|
2023-04-05 18:11:31 +00:00
|
|
|
func (gd *gridDiagram) layoutEvenly(g *d2graph.Graph, obj *d2graph.Object) {
|
2023-04-06 21:30:45 +00:00
|
|
|
// layout objects in a grid with these 2 properties:
|
|
|
|
|
// all objects in the same row should have the same height
|
|
|
|
|
// all objects in the same column should have the same width
|
2023-04-05 03:02:22 +00:00
|
|
|
|
2023-04-06 21:30:45 +00:00
|
|
|
getObject := func(rowIndex, columnIndex int) *d2graph.Object {
|
2023-04-05 03:02:22 +00:00
|
|
|
var index int
|
2023-04-06 21:30:45 +00:00
|
|
|
if gd.rowDirected {
|
2023-04-05 18:11:31 +00:00
|
|
|
index = rowIndex*gd.columns + columnIndex
|
2023-04-05 03:02:22 +00:00
|
|
|
} else {
|
2023-04-05 18:11:31 +00:00
|
|
|
index = columnIndex*gd.rows + rowIndex
|
2023-04-05 03:02:22 +00:00
|
|
|
}
|
2023-04-06 21:30:45 +00:00
|
|
|
if index < len(gd.objects) {
|
|
|
|
|
return gd.objects[index]
|
2023-04-05 03:02:22 +00:00
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-05 18:11:31 +00:00
|
|
|
rowHeights := make([]float64, 0, gd.rows)
|
|
|
|
|
colWidths := make([]float64, 0, gd.columns)
|
|
|
|
|
for i := 0; i < gd.rows; i++ {
|
2023-04-05 03:02:22 +00:00
|
|
|
rowHeight := 0.
|
2023-04-05 18:11:31 +00:00
|
|
|
for j := 0; j < gd.columns; j++ {
|
2023-04-06 21:30:45 +00:00
|
|
|
o := getObject(i, j)
|
|
|
|
|
if o == nil {
|
2023-04-05 03:02:22 +00:00
|
|
|
break
|
|
|
|
|
}
|
2023-04-06 21:30:45 +00:00
|
|
|
rowHeight = math.Max(rowHeight, o.Height)
|
2023-04-05 03:02:22 +00:00
|
|
|
}
|
|
|
|
|
rowHeights = append(rowHeights, rowHeight)
|
|
|
|
|
}
|
2023-04-05 18:11:31 +00:00
|
|
|
for j := 0; j < gd.columns; j++ {
|
2023-04-05 03:02:22 +00:00
|
|
|
columnWidth := 0.
|
2023-04-05 18:11:31 +00:00
|
|
|
for i := 0; i < gd.rows; i++ {
|
2023-04-06 21:30:45 +00:00
|
|
|
o := getObject(i, j)
|
|
|
|
|
if o == nil {
|
2023-04-05 03:02:22 +00:00
|
|
|
break
|
|
|
|
|
}
|
2023-04-06 21:30:45 +00:00
|
|
|
columnWidth = math.Max(columnWidth, o.Width)
|
2023-04-05 03:02:22 +00:00
|
|
|
}
|
|
|
|
|
colWidths = append(colWidths, columnWidth)
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-12 20:48:53 +00:00
|
|
|
horizontalGap := float64(gd.horizontalGap)
|
|
|
|
|
verticalGap := float64(gd.verticalGap)
|
2023-04-11 18:32:35 +00:00
|
|
|
|
2023-04-05 03:02:22 +00:00
|
|
|
cursor := geo.NewPoint(0, 0)
|
2023-04-06 21:30:45 +00:00
|
|
|
if gd.rowDirected {
|
2023-04-05 18:11:31 +00:00
|
|
|
for i := 0; i < gd.rows; i++ {
|
|
|
|
|
for j := 0; j < gd.columns; j++ {
|
2023-04-06 21:30:45 +00:00
|
|
|
o := getObject(i, j)
|
|
|
|
|
if o == nil {
|
2023-04-05 03:02:22 +00:00
|
|
|
break
|
|
|
|
|
}
|
2023-04-06 21:30:45 +00:00
|
|
|
o.Width = colWidths[j]
|
|
|
|
|
o.Height = rowHeights[i]
|
2023-05-09 01:38:41 +00:00
|
|
|
o.MoveWithDescendantsTo(cursor.X, cursor.Y)
|
2023-04-11 18:32:35 +00:00
|
|
|
cursor.X += o.Width + horizontalGap
|
2023-04-05 03:02:22 +00:00
|
|
|
}
|
|
|
|
|
cursor.X = 0
|
2023-04-11 18:32:35 +00:00
|
|
|
cursor.Y += rowHeights[i] + verticalGap
|
2023-04-05 03:02:22 +00:00
|
|
|
}
|
|
|
|
|
} else {
|
2023-04-05 18:11:31 +00:00
|
|
|
for j := 0; j < gd.columns; j++ {
|
|
|
|
|
for i := 0; i < gd.rows; i++ {
|
2023-04-06 21:30:45 +00:00
|
|
|
o := getObject(i, j)
|
|
|
|
|
if o == nil {
|
2023-04-05 03:02:22 +00:00
|
|
|
break
|
|
|
|
|
}
|
2023-04-06 21:30:45 +00:00
|
|
|
o.Width = colWidths[j]
|
|
|
|
|
o.Height = rowHeights[i]
|
2023-05-09 01:38:41 +00:00
|
|
|
o.MoveWithDescendantsTo(cursor.X, cursor.Y)
|
2023-04-11 18:32:35 +00:00
|
|
|
cursor.Y += o.Height + verticalGap
|
2023-04-05 03:02:22 +00:00
|
|
|
}
|
2023-04-11 18:32:35 +00:00
|
|
|
cursor.X += colWidths[j] + horizontalGap
|
2023-04-05 03:02:22 +00:00
|
|
|
cursor.Y = 0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var totalWidth, totalHeight float64
|
|
|
|
|
for _, w := range colWidths {
|
2023-04-11 18:32:35 +00:00
|
|
|
totalWidth += w + horizontalGap
|
2023-04-05 03:02:22 +00:00
|
|
|
}
|
|
|
|
|
for _, h := range rowHeights {
|
2023-04-11 18:32:35 +00:00
|
|
|
totalHeight += h + verticalGap
|
2023-04-05 03:02:22 +00:00
|
|
|
}
|
2023-04-11 18:32:35 +00:00
|
|
|
totalWidth -= horizontalGap
|
|
|
|
|
totalHeight -= verticalGap
|
2023-04-05 18:11:31 +00:00
|
|
|
gd.width = totalWidth
|
|
|
|
|
gd.height = totalHeight
|
2023-04-05 03:02:22 +00:00
|
|
|
}
|
|
|
|
|
|
2023-04-05 18:11:31 +00:00
|
|
|
func (gd *gridDiagram) layoutDynamic(g *d2graph.Graph, obj *d2graph.Object) {
|
2023-04-06 21:30:45 +00:00
|
|
|
// assume we have the following objects to layout:
|
2023-04-04 04:38:08 +00:00
|
|
|
// . ┌A──────────────┐ ┌B──┐ ┌C─────────┐ ┌D────────┐ ┌E────────────────┐
|
|
|
|
|
// . └───────────────┘ │ │ │ │ │ │ │ │
|
|
|
|
|
// . │ │ └──────────┘ │ │ │ │
|
|
|
|
|
// . │ │ │ │ └─────────────────┘
|
|
|
|
|
// . └───┘ │ │
|
|
|
|
|
// . └─────────┘
|
2023-04-06 21:30:45 +00:00
|
|
|
// Note: if the grid is row dominant, all objects should be the same height (same width if column dominant)
|
2023-04-04 04:38:08 +00:00
|
|
|
// . ┌A─────────────┐ ┌B──┐ ┌C─────────┐ ┌D────────┐ ┌E────────────────┐
|
|
|
|
|
// . ├ ─ ─ ─ ─ ─ ─ ─┤ │ │ │ │ │ │ │ │
|
|
|
|
|
// . │ │ │ │ ├ ─ ─ ─ ─ ─┤ │ │ │ │
|
|
|
|
|
// . │ │ │ │ │ │ │ │ ├ ─ ─ ─ ─ ─ ─ ─ ─ ┤
|
|
|
|
|
// . │ │ ├ ─ ┤ │ │ │ │ │ │
|
|
|
|
|
// . └──────────────┘ └───┘ └──────────┘ └─────────┘ └─────────────────┘
|
|
|
|
|
|
2023-04-12 20:48:53 +00:00
|
|
|
horizontalGap := float64(gd.horizontalGap)
|
|
|
|
|
verticalGap := float64(gd.verticalGap)
|
2023-04-11 18:32:35 +00:00
|
|
|
|
2023-04-04 04:38:08 +00:00
|
|
|
// we want to split up the total width across the N rows or columns as evenly as possible
|
|
|
|
|
var totalWidth, totalHeight float64
|
2023-04-06 21:30:45 +00:00
|
|
|
for _, o := range gd.objects {
|
|
|
|
|
totalWidth += o.Width
|
|
|
|
|
totalHeight += o.Height
|
2023-04-04 04:38:08 +00:00
|
|
|
}
|
2023-04-11 18:32:35 +00:00
|
|
|
totalWidth += horizontalGap * float64(len(gd.objects)-gd.rows)
|
|
|
|
|
totalHeight += verticalGap * float64(len(gd.objects)-gd.columns)
|
2023-04-04 04:38:08 +00:00
|
|
|
|
2023-04-05 18:49:04 +00:00
|
|
|
var layout [][]*d2graph.Object
|
2023-04-06 21:30:45 +00:00
|
|
|
if gd.rowDirected {
|
2023-04-05 18:11:31 +00:00
|
|
|
targetWidth := totalWidth / float64(gd.rows)
|
2023-04-05 18:49:04 +00:00
|
|
|
layout = gd.getBestLayout(targetWidth, false)
|
2023-04-04 04:38:08 +00:00
|
|
|
} else {
|
2023-04-05 18:11:31 +00:00
|
|
|
targetHeight := totalHeight / float64(gd.columns)
|
2023-04-05 18:49:04 +00:00
|
|
|
layout = gd.getBestLayout(targetHeight, true)
|
2023-04-04 04:38:08 +00:00
|
|
|
}
|
2023-04-03 18:36:01 +00:00
|
|
|
|
|
|
|
|
cursor := geo.NewPoint(0, 0)
|
2023-04-04 04:38:08 +00:00
|
|
|
var maxY, maxX float64
|
2023-04-06 21:30:45 +00:00
|
|
|
if gd.rowDirected {
|
2023-04-29 00:05:20 +00:00
|
|
|
// measure row widths
|
2023-04-04 04:38:08 +00:00
|
|
|
rowWidths := []float64{}
|
|
|
|
|
for _, row := range layout {
|
2023-04-29 00:05:20 +00:00
|
|
|
x := 0.
|
2023-04-06 21:30:45 +00:00
|
|
|
for _, o := range row {
|
2023-04-29 00:05:20 +00:00
|
|
|
x += o.Width + horizontalGap
|
2023-04-04 04:38:08 +00:00
|
|
|
}
|
2023-04-29 00:05:20 +00:00
|
|
|
rowWidth := x - horizontalGap
|
2023-04-04 04:38:08 +00:00
|
|
|
rowWidths = append(rowWidths, rowWidth)
|
|
|
|
|
maxX = math.Max(maxX, rowWidth)
|
2023-04-03 18:36:01 +00:00
|
|
|
}
|
2023-04-04 04:38:08 +00:00
|
|
|
|
2023-05-10 22:23:11 +00:00
|
|
|
// TODO if object is a nested grid, consider growing descendants according to the inner grid layout
|
|
|
|
|
|
2023-04-06 21:30:45 +00:00
|
|
|
// then expand thinnest objects to make each row the same width
|
2023-04-04 04:38:08 +00:00
|
|
|
// . ┌A─────────────┐ ┌B──┐ ┌C─────────┐ ┬ maxHeight(A,B,C)
|
|
|
|
|
// . │ │ │ │ │ │ │
|
|
|
|
|
// . │ │ │ │ │ │ │
|
|
|
|
|
// . │ │ │ │ │ │ │
|
|
|
|
|
// . └──────────────┘ └───┘ └──────────┘ ┴
|
|
|
|
|
// . ┌D────────┬────┐ ┌E────────────────┐ ┬ maxHeight(D,E)
|
|
|
|
|
// . │ │ │ │ │
|
|
|
|
|
// . │ │ │ │ │ │
|
|
|
|
|
// . │ │ │ │ │
|
|
|
|
|
// . │ │ │ │ │ │
|
|
|
|
|
// . └─────────┴────┘ └─────────────────┘ ┴
|
|
|
|
|
for i, row := range layout {
|
|
|
|
|
rowWidth := rowWidths[i]
|
|
|
|
|
if rowWidth == maxX {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
delta := maxX - rowWidth
|
|
|
|
|
var widest float64
|
2023-04-06 21:30:45 +00:00
|
|
|
for _, o := range row {
|
|
|
|
|
widest = math.Max(widest, o.Width)
|
2023-04-04 04:38:08 +00:00
|
|
|
}
|
2023-04-29 00:05:20 +00:00
|
|
|
diffs := make([]float64, len(row))
|
|
|
|
|
totalDiff := 0.
|
|
|
|
|
for i, o := range row {
|
|
|
|
|
diffs[i] = widest - o.Width
|
|
|
|
|
totalDiff += diffs[i]
|
|
|
|
|
}
|
|
|
|
|
if totalDiff > 0 {
|
|
|
|
|
// expand smaller nodes up to the size of the larger ones with delta
|
|
|
|
|
// percentage diff
|
|
|
|
|
for i := range diffs {
|
|
|
|
|
diffs[i] /= totalDiff
|
|
|
|
|
}
|
|
|
|
|
growth := math.Min(delta, totalDiff)
|
|
|
|
|
// expand smaller objects to fill remaining space
|
|
|
|
|
for i, o := range row {
|
|
|
|
|
o.Width += diffs[i] * growth
|
2023-04-04 04:38:08 +00:00
|
|
|
}
|
|
|
|
|
}
|
2023-04-29 00:05:20 +00:00
|
|
|
if delta > totalDiff {
|
|
|
|
|
growth := (delta - totalDiff) / float64(len(row))
|
|
|
|
|
for _, o := range row {
|
|
|
|
|
o.Width += growth
|
2023-04-04 04:38:08 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-04-29 00:05:20 +00:00
|
|
|
|
|
|
|
|
// if we have 2 rows, then each row's objects should have the same height
|
|
|
|
|
// . ┌A─────────────┐ ┌B──┐ ┌C─────────┐ ┬ maxHeight(A,B,C)
|
|
|
|
|
// . ├ ─ ─ ─ ─ ─ ─ ─┤ │ │ │ │ │
|
|
|
|
|
// . │ │ │ │ ├ ─ ─ ─ ─ ─┤ │
|
|
|
|
|
// . │ │ │ │ │ │ │
|
|
|
|
|
// . └──────────────┘ └───┘ └──────────┘ ┴
|
|
|
|
|
// . ┌D────────┐ ┌E────────────────┐ ┬ maxHeight(D,E)
|
|
|
|
|
// . │ │ │ │ │
|
|
|
|
|
// . │ │ │ │ │
|
|
|
|
|
// . │ │ ├ ─ ─ ─ ─ ─ ─ ─ ─ ┤ │
|
|
|
|
|
// . │ │ │ │ │
|
|
|
|
|
// . └─────────┘ └─────────────────┘ ┴
|
|
|
|
|
for _, row := range layout {
|
|
|
|
|
rowHeight := 0.
|
|
|
|
|
for _, o := range row {
|
2023-05-09 01:38:41 +00:00
|
|
|
o.MoveWithDescendantsTo(cursor.X, cursor.Y)
|
2023-04-29 00:05:20 +00:00
|
|
|
cursor.X += o.Width + horizontalGap
|
|
|
|
|
rowHeight = math.Max(rowHeight, o.Height)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// set all objects in row to the same height
|
|
|
|
|
for _, o := range row {
|
|
|
|
|
o.Height = rowHeight
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// new row
|
|
|
|
|
cursor.X = 0
|
|
|
|
|
cursor.Y += rowHeight + verticalGap
|
|
|
|
|
}
|
2023-10-06 01:48:51 +00:00
|
|
|
maxY = cursor.Y - verticalGap
|
2023-04-04 04:38:08 +00:00
|
|
|
} else {
|
2023-04-29 00:05:20 +00:00
|
|
|
// measure column heights
|
2023-04-04 18:38:33 +00:00
|
|
|
colHeights := []float64{}
|
2023-04-04 04:38:08 +00:00
|
|
|
for _, column := range layout {
|
2023-04-29 00:05:20 +00:00
|
|
|
y := 0.
|
2023-04-06 21:30:45 +00:00
|
|
|
for _, o := range column {
|
2023-04-29 00:05:20 +00:00
|
|
|
y += o.Height + verticalGap
|
2023-04-04 04:38:08 +00:00
|
|
|
}
|
2023-04-29 00:05:20 +00:00
|
|
|
colHeight := y - verticalGap
|
2023-04-04 18:38:33 +00:00
|
|
|
colHeights = append(colHeights, colHeight)
|
|
|
|
|
maxY = math.Max(maxY, colHeight)
|
2023-04-04 04:38:08 +00:00
|
|
|
}
|
2023-04-29 00:05:20 +00:00
|
|
|
|
2023-04-06 21:30:45 +00:00
|
|
|
// then expand shortest objects to make each column the same height
|
2023-04-04 04:38:08 +00:00
|
|
|
// . ├maxWidth(A,B)─┤ ├maxW(C,D)─┤ ├maxWidth(E)──────┤
|
|
|
|
|
// . ┌A─────────────┐ ┌C─────────┐ ┌E────────────────┐
|
|
|
|
|
// . ├ ─ ─ ─ ─ ─ ─ ┤ │ │ │ │
|
|
|
|
|
// . │ │ └──────────┘ │ │
|
|
|
|
|
// . └──────────────┘ ┌D─────────┐ ├ ─ ─ ─ ─ ─ ─ ─ ─ ┤
|
|
|
|
|
// . ┌B─────────────┐ │ │ │ │
|
|
|
|
|
// . │ │ │ │ │ │
|
|
|
|
|
// . │ │ │ │ │ │
|
|
|
|
|
// . │ │ │ │ │ │
|
|
|
|
|
// . └──────────────┘ └──────────┘ └─────────────────┘
|
2023-04-04 18:38:33 +00:00
|
|
|
for i, column := range layout {
|
|
|
|
|
colHeight := colHeights[i]
|
|
|
|
|
if colHeight == maxY {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
delta := maxY - colHeight
|
|
|
|
|
var tallest float64
|
2023-04-06 21:30:45 +00:00
|
|
|
for _, o := range column {
|
|
|
|
|
tallest = math.Max(tallest, o.Height)
|
2023-04-04 18:38:33 +00:00
|
|
|
}
|
2023-04-29 00:05:20 +00:00
|
|
|
diffs := make([]float64, len(column))
|
|
|
|
|
totalDiff := 0.
|
|
|
|
|
for i, o := range column {
|
|
|
|
|
diffs[i] = tallest - o.Height
|
|
|
|
|
totalDiff += diffs[i]
|
|
|
|
|
}
|
|
|
|
|
if totalDiff > 0 {
|
|
|
|
|
// expand smaller nodes up to the size of the larger ones with delta
|
|
|
|
|
// percentage diff
|
|
|
|
|
for i := range diffs {
|
|
|
|
|
diffs[i] /= totalDiff
|
|
|
|
|
}
|
|
|
|
|
growth := math.Min(delta, totalDiff)
|
|
|
|
|
// expand smaller objects to fill remaining space
|
|
|
|
|
for i, o := range column {
|
|
|
|
|
o.Height += diffs[i] * growth
|
2023-04-04 18:38:33 +00:00
|
|
|
}
|
|
|
|
|
}
|
2023-04-29 00:05:20 +00:00
|
|
|
if delta > totalDiff {
|
|
|
|
|
growth := (delta - totalDiff) / float64(len(column))
|
|
|
|
|
for _, o := range column {
|
|
|
|
|
o.Height += growth
|
2023-04-04 18:38:33 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-04-29 00:05:20 +00:00
|
|
|
// if we have 3 columns, then each column's objects should have the same width
|
|
|
|
|
// . ├maxWidth(A,B)─┤ ├maxW(C,D)─┤ ├maxWidth(E)──────┤
|
|
|
|
|
// . ┌A─────────────┐ ┌C─────────┐ ┌E────────────────┐
|
|
|
|
|
// . └──────────────┘ │ │ │ │
|
|
|
|
|
// . ┌B──┬──────────┐ └──────────┘ │ │
|
|
|
|
|
// . │ │ ┌D────────┬┐ └─────────────────┘
|
|
|
|
|
// . │ │ │ │ │
|
|
|
|
|
// . │ │ │ ││
|
|
|
|
|
// . └───┴──────────┘ │ │
|
|
|
|
|
// . │ ││
|
|
|
|
|
// . └─────────┴┘
|
|
|
|
|
for _, column := range layout {
|
|
|
|
|
colWidth := 0.
|
|
|
|
|
for _, o := range column {
|
2023-05-09 01:38:41 +00:00
|
|
|
o.MoveWithDescendantsTo(cursor.X, cursor.Y)
|
2023-04-29 00:05:20 +00:00
|
|
|
cursor.Y += o.Height + verticalGap
|
|
|
|
|
colWidth = math.Max(colWidth, o.Width)
|
|
|
|
|
}
|
|
|
|
|
// set all objects in column to the same width
|
|
|
|
|
for _, o := range column {
|
|
|
|
|
o.Width = colWidth
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// new column
|
|
|
|
|
cursor.Y = 0
|
|
|
|
|
cursor.X += colWidth + horizontalGap
|
|
|
|
|
}
|
|
|
|
|
maxX = cursor.X - horizontalGap
|
2023-04-03 18:36:01 +00:00
|
|
|
}
|
2023-04-05 18:11:31 +00:00
|
|
|
gd.width = maxX
|
|
|
|
|
gd.height = maxY
|
2023-04-03 18:36:01 +00:00
|
|
|
}
|
|
|
|
|
|
2023-04-06 21:30:45 +00:00
|
|
|
// generate the best layout of objects aiming for each row to be the targetSize width
|
2023-04-05 18:49:04 +00:00
|
|
|
// if columns is true, each column aims to have the targetSize height
|
|
|
|
|
func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2graph.Object {
|
2023-05-09 22:05:56 +00:00
|
|
|
debug := false
|
2023-04-05 18:49:04 +00:00
|
|
|
var nCuts int
|
|
|
|
|
if columns {
|
|
|
|
|
nCuts = gd.columns - 1
|
|
|
|
|
} else {
|
|
|
|
|
nCuts = gd.rows - 1
|
|
|
|
|
}
|
|
|
|
|
if nCuts == 0 {
|
2023-06-08 19:24:11 +00:00
|
|
|
return GenLayout(gd.objects, nil)
|
2023-04-05 18:49:04 +00:00
|
|
|
}
|
|
|
|
|
|
2023-05-09 22:05:56 +00:00
|
|
|
var bestLayout [][]*d2graph.Object
|
|
|
|
|
bestDist := math.MaxFloat64
|
2023-05-10 00:19:33 +00:00
|
|
|
fastIsBest := false
|
2023-05-09 22:05:56 +00:00
|
|
|
// try fast layout algorithm as a baseline
|
2023-05-10 00:19:33 +00:00
|
|
|
if fastLayout := gd.fastLayout(targetSize, nCuts, columns); fastLayout != nil {
|
|
|
|
|
dist := getDistToTarget(fastLayout, targetSize, float64(gd.horizontalGap), float64(gd.verticalGap), columns)
|
2023-05-09 22:05:56 +00:00
|
|
|
if debug {
|
|
|
|
|
fmt.Printf("fast dist %v dist per row %v\n", dist, dist/(float64(nCuts)+1))
|
|
|
|
|
}
|
|
|
|
|
if dist == 0 {
|
2023-05-10 00:19:33 +00:00
|
|
|
return fastLayout
|
2023-05-09 22:05:56 +00:00
|
|
|
}
|
|
|
|
|
bestDist = dist
|
2023-05-10 00:19:33 +00:00
|
|
|
bestLayout = fastLayout
|
|
|
|
|
fastIsBest = true
|
2023-05-09 22:05:56 +00:00
|
|
|
}
|
|
|
|
|
|
2023-04-26 18:49:41 +00:00
|
|
|
var gap float64
|
|
|
|
|
if columns {
|
|
|
|
|
gap = float64(gd.verticalGap)
|
|
|
|
|
} else {
|
|
|
|
|
gap = float64(gd.horizontalGap)
|
|
|
|
|
}
|
|
|
|
|
getSize := func(o *d2graph.Object) float64 {
|
|
|
|
|
if columns {
|
|
|
|
|
return o.Height
|
|
|
|
|
} else {
|
|
|
|
|
return o.Width
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-09 22:05:56 +00:00
|
|
|
sizes := []float64{}
|
|
|
|
|
for _, obj := range gd.objects {
|
|
|
|
|
size := getSize(obj)
|
|
|
|
|
sizes = append(sizes, size)
|
|
|
|
|
}
|
|
|
|
|
sd := stddev(sizes)
|
|
|
|
|
if debug {
|
|
|
|
|
fmt.Printf("sizes (%d): %v\n", len(sizes), sizes)
|
2023-05-10 00:19:33 +00:00
|
|
|
fmt.Printf("std dev: %v; targetSize %v\n", sd, targetSize)
|
2023-05-09 22:05:56 +00:00
|
|
|
}
|
|
|
|
|
|
2023-04-26 18:49:41 +00:00
|
|
|
skipCount := 0
|
2023-05-09 22:05:56 +00:00
|
|
|
count := 0
|
2023-04-26 18:49:41 +00:00
|
|
|
// quickly eliminate bad row groupings
|
|
|
|
|
startingCache := make(map[int]bool)
|
2023-04-29 01:23:00 +00:00
|
|
|
// Note: we want a low threshold to explore good options within attemptLimit,
|
|
|
|
|
// but the best option may require a few rows that are far from the target size.
|
2023-05-10 00:19:33 +00:00
|
|
|
okThreshold := STARTING_THRESHOLD
|
2023-04-26 18:49:41 +00:00
|
|
|
rowOk := func(row []*d2graph.Object, starting bool) (ok bool) {
|
|
|
|
|
if starting {
|
2023-04-29 01:23:00 +00:00
|
|
|
// we can cache results from starting positions since they repeat and don't change
|
|
|
|
|
// with starting=true it will always be the 1st N objects based on len(row)
|
2023-04-26 18:49:41 +00:00
|
|
|
if ok, has := startingCache[len(row)]; has {
|
|
|
|
|
return ok
|
|
|
|
|
}
|
|
|
|
|
defer func() {
|
2023-04-29 01:23:00 +00:00
|
|
|
// cache result before returning
|
2023-04-26 18:49:41 +00:00
|
|
|
startingCache[len(row)] = ok
|
|
|
|
|
}()
|
|
|
|
|
}
|
2023-04-29 01:23:00 +00:00
|
|
|
|
2023-04-26 18:49:41 +00:00
|
|
|
rowSize := 0.
|
|
|
|
|
for _, obj := range row {
|
|
|
|
|
rowSize += getSize(obj)
|
|
|
|
|
}
|
|
|
|
|
if len(row) > 1 {
|
|
|
|
|
rowSize += gap * float64(len(row)-1)
|
2023-04-29 01:23:00 +00:00
|
|
|
// if multiple nodes are too big, it isn't ok. but a single node can't shrink so only check here
|
2023-04-26 18:49:41 +00:00
|
|
|
if rowSize > okThreshold*targetSize {
|
|
|
|
|
skipCount++
|
2023-09-20 18:51:43 +00:00
|
|
|
// there may even be too many to skip
|
|
|
|
|
return skipCount >= SKIP_LIMIT
|
2023-04-26 18:49:41 +00:00
|
|
|
}
|
|
|
|
|
}
|
2023-04-29 01:23:00 +00:00
|
|
|
// row is too small to be good overall
|
2023-04-26 18:49:41 +00:00
|
|
|
if rowSize < targetSize/okThreshold {
|
|
|
|
|
skipCount++
|
2023-09-20 18:51:43 +00:00
|
|
|
return skipCount >= SKIP_LIMIT
|
2023-04-26 18:49:41 +00:00
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-05 18:49:04 +00:00
|
|
|
// get all options for where to place these cuts, preferring later cuts over earlier cuts
|
2023-04-06 21:30:45 +00:00
|
|
|
// with 5 objects and 2 cuts we have these options:
|
2023-04-05 18:49:04 +00:00
|
|
|
// . A B C │ D │ E <- these cuts would produce: ┌A─┐ ┌B─┐ ┌C─┐
|
|
|
|
|
// . A B │ C D │ E └──┘ └──┘ └──┘
|
|
|
|
|
// . A │ B C D │ E ┌D───────────┐
|
|
|
|
|
// . A B │ C │ D E └────────────┘
|
|
|
|
|
// . A │ B C │ D E ┌E───────────┐
|
|
|
|
|
// . A │ B │ C D E └────────────┘
|
|
|
|
|
// of these divisions, find the layout with rows closest to the targetSize
|
2023-04-26 18:49:41 +00:00
|
|
|
tryDivision := func(division []int) bool {
|
2023-06-08 19:24:11 +00:00
|
|
|
layout := GenLayout(gd.objects, division)
|
2023-04-12 20:48:53 +00:00
|
|
|
dist := getDistToTarget(layout, targetSize, float64(gd.horizontalGap), float64(gd.verticalGap), columns)
|
2023-04-05 18:49:04 +00:00
|
|
|
if dist < bestDist {
|
|
|
|
|
bestLayout = layout
|
|
|
|
|
bestDist = dist
|
2023-05-10 00:19:33 +00:00
|
|
|
fastIsBest = false
|
|
|
|
|
} else if fastIsBest && dist == bestDist {
|
|
|
|
|
// prefer ordered search solution to fast layout solution
|
|
|
|
|
bestLayout = layout
|
|
|
|
|
fastIsBest = false
|
2023-04-05 18:49:04 +00:00
|
|
|
}
|
2023-04-26 18:31:27 +00:00
|
|
|
count++
|
2023-04-29 01:23:00 +00:00
|
|
|
// with few objects we can try all options to get best result but this won't scale, so only try up to 100k options
|
2023-05-10 00:19:33 +00:00
|
|
|
return count >= ATTEMPT_LIMIT || skipCount >= SKIP_LIMIT
|
2023-04-26 18:49:41 +00:00
|
|
|
}
|
2023-04-05 18:49:04 +00:00
|
|
|
|
2023-05-09 22:05:56 +00:00
|
|
|
// try number of different okThresholds depending on std deviation of sizes
|
2023-05-10 00:19:33 +00:00
|
|
|
thresholdAttempts := int(math.Ceil(sd))
|
|
|
|
|
if thresholdAttempts < MIN_THRESHOLD_ATTEMPTS {
|
|
|
|
|
thresholdAttempts = MIN_THRESHOLD_ATTEMPTS
|
|
|
|
|
} else if thresholdAttempts > MAX_THRESHOLD_ATTEMPTS {
|
|
|
|
|
thresholdAttempts = MAX_THRESHOLD_ATTEMPTS
|
|
|
|
|
}
|
|
|
|
|
for i := 0; i < thresholdAttempts || bestLayout == nil; i++ {
|
2023-05-09 22:05:56 +00:00
|
|
|
count = 0.
|
|
|
|
|
skipCount = 0.
|
2023-04-26 18:49:41 +00:00
|
|
|
iterDivisions(gd.objects, nCuts, tryDivision, rowOk)
|
2023-05-10 00:19:33 +00:00
|
|
|
okThreshold += THRESHOLD_STEP_SIZE
|
2023-04-29 01:23:00 +00:00
|
|
|
if debug {
|
2023-05-09 22:05:56 +00:00
|
|
|
fmt.Printf("count %d, skip count %d, bestDist %v increasing ok threshold to %v\n", count, skipCount, bestDist, okThreshold)
|
2023-04-29 01:23:00 +00:00
|
|
|
}
|
2023-04-26 18:49:41 +00:00
|
|
|
startingCache = make(map[int]bool)
|
2023-05-09 22:05:56 +00:00
|
|
|
if skipCount == 0 {
|
|
|
|
|
// threshold isn't skipping anything so increasing it won't help
|
|
|
|
|
break
|
|
|
|
|
}
|
2023-05-10 00:19:33 +00:00
|
|
|
// okThreshold isn't high enough yet, we skipped every option so don't count it
|
|
|
|
|
if count == 0 && thresholdAttempts < MAX_THRESHOLD_ATTEMPTS {
|
|
|
|
|
thresholdAttempts++
|
|
|
|
|
}
|
2023-04-29 01:23:00 +00:00
|
|
|
}
|
2023-05-09 22:05:56 +00:00
|
|
|
|
2023-04-29 01:23:00 +00:00
|
|
|
if debug {
|
2023-05-10 00:19:33 +00:00
|
|
|
fmt.Printf("best layout: %v\n", layoutString(bestLayout, sizes))
|
2023-05-09 22:05:56 +00:00
|
|
|
}
|
|
|
|
|
return bestLayout
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func sum(values []float64) float64 {
|
|
|
|
|
s := 0.
|
|
|
|
|
for _, v := range values {
|
|
|
|
|
s += v
|
|
|
|
|
}
|
|
|
|
|
return s
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func avg(values []float64) float64 {
|
|
|
|
|
return sum(values) / float64(len(values))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func variance(values []float64) float64 {
|
|
|
|
|
mean := avg(values)
|
|
|
|
|
total := 0.
|
|
|
|
|
for _, value := range values {
|
|
|
|
|
dev := mean - value
|
|
|
|
|
total += dev * dev
|
|
|
|
|
}
|
|
|
|
|
return total / float64(len(values))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func stddev(values []float64) float64 {
|
|
|
|
|
return math.Sqrt(variance(values))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (gd *gridDiagram) fastLayout(targetSize float64, nCuts int, columns bool) (layout [][]*d2graph.Object) {
|
|
|
|
|
var gap float64
|
|
|
|
|
if columns {
|
|
|
|
|
gap = float64(gd.verticalGap)
|
|
|
|
|
} else {
|
|
|
|
|
gap = float64(gd.horizontalGap)
|
2023-04-26 18:49:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
debt := 0.
|
|
|
|
|
fastDivision := make([]int, 0, nCuts)
|
|
|
|
|
rowSize := 0.
|
|
|
|
|
for i := 0; i < len(gd.objects); i++ {
|
|
|
|
|
o := gd.objects[i]
|
2023-05-09 22:05:56 +00:00
|
|
|
var size float64
|
|
|
|
|
if columns {
|
|
|
|
|
size = o.Height
|
|
|
|
|
} else {
|
|
|
|
|
size = o.Width
|
|
|
|
|
}
|
2023-04-26 18:49:41 +00:00
|
|
|
if rowSize == 0 {
|
2023-06-08 19:24:11 +00:00
|
|
|
// if a single object meets the target size, end the row here
|
2023-04-26 18:49:41 +00:00
|
|
|
if size > targetSize-debt {
|
2023-06-08 19:24:11 +00:00
|
|
|
// cut row with just this object
|
2023-06-08 20:02:34 +00:00
|
|
|
fastDivision = append(fastDivision, i)
|
2023-04-29 01:23:00 +00:00
|
|
|
// we build up a debt of distance past the target size across rows
|
2023-04-26 18:49:41 +00:00
|
|
|
newDebt := size - targetSize
|
|
|
|
|
debt += newDebt
|
|
|
|
|
} else {
|
|
|
|
|
rowSize += size
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
2023-04-29 01:23:00 +00:00
|
|
|
// debt is paid by decreasing threshold to start new row and ending below targetSize
|
2023-06-08 19:24:11 +00:00
|
|
|
if rowSize+gap+(size)/2. > targetSize-debt {
|
|
|
|
|
// start a new row before this object since it is mostly past the target size
|
|
|
|
|
// . size
|
|
|
|
|
// ├...row─┼gap┼───┼───┤
|
|
|
|
|
// ├──targetSize──┤ (debt=0)
|
2023-04-26 18:49:41 +00:00
|
|
|
fastDivision = append(fastDivision, i-1)
|
|
|
|
|
newDebt := rowSize - targetSize
|
|
|
|
|
debt += newDebt
|
|
|
|
|
rowSize = size
|
|
|
|
|
} else {
|
|
|
|
|
rowSize += gap + size
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if len(fastDivision) == nCuts {
|
2023-06-08 19:24:11 +00:00
|
|
|
layout = GenLayout(gd.objects, fastDivision)
|
2023-04-26 18:49:41 +00:00
|
|
|
}
|
2023-05-09 22:05:56 +00:00
|
|
|
|
|
|
|
|
return layout
|
2023-04-05 18:49:04 +00:00
|
|
|
}
|
|
|
|
|
|
2023-05-10 00:19:33 +00:00
|
|
|
func layoutString(layout [][]*d2graph.Object, sizes []float64) string {
|
|
|
|
|
buf := &bytes.Buffer{}
|
|
|
|
|
i := 0
|
|
|
|
|
fmt.Fprintf(buf, "[\n")
|
|
|
|
|
for _, r := range layout {
|
|
|
|
|
vals := sizes[i : i+len(r)]
|
|
|
|
|
fmt.Fprintf(buf, "%v:\t%v\n", sum(vals), vals)
|
|
|
|
|
i += len(r)
|
|
|
|
|
}
|
|
|
|
|
fmt.Fprintf(buf, "]\n")
|
|
|
|
|
return buf.String()
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-26 18:31:27 +00:00
|
|
|
// process current division, return true to stop iterating
|
|
|
|
|
type iterDivision func(division []int) (done bool)
|
2023-04-26 18:49:41 +00:00
|
|
|
type checkCut func(objects []*d2graph.Object, starting bool) (ok bool)
|
2023-04-26 18:31:27 +00:00
|
|
|
|
2023-04-06 21:30:45 +00:00
|
|
|
// get all possible divisions of objects by the number of cuts
|
2023-04-26 18:49:41 +00:00
|
|
|
func iterDivisions(objects []*d2graph.Object, nCuts int, f iterDivision, check checkCut) {
|
2023-04-06 21:30:45 +00:00
|
|
|
if len(objects) < 2 || nCuts == 0 {
|
2023-04-26 00:19:34 +00:00
|
|
|
return
|
2023-04-05 18:49:04 +00:00
|
|
|
}
|
2023-04-26 18:31:27 +00:00
|
|
|
done := false
|
2023-04-06 21:30:45 +00:00
|
|
|
// we go in this order to prefer extra objects in starting rows rather than later ones
|
|
|
|
|
lastObj := len(objects) - 1
|
2023-04-26 18:49:41 +00:00
|
|
|
// with objects=[A, B, C, D, E]; nCuts=2
|
|
|
|
|
// d:depth; i:index; n:nCuts;
|
|
|
|
|
// ┌────┬───┬───┬─────────────────────┬────────────┐
|
|
|
|
|
// │ d │ i │ n │ objects │ cuts │
|
|
|
|
|
// ├────┼───┼───┼─────────────────────┼────────────┤
|
|
|
|
|
// │ 0 │ 4 │ 2 │ [A B C D | E] │ │
|
|
|
|
|
// ├────┼───┼───┼─────────────────────┼────────────┤
|
|
|
|
|
// │ └1 │ 3 │ 1 │ [A B C | D] │ + | E] │
|
|
|
|
|
// ├────┼───┼───┼─────────────────────┼────────────┤
|
|
|
|
|
// │ └1 │ 2 │ 1 │ [A B | C D] │ + | E] │
|
|
|
|
|
// ├────┼───┼───┼─────────────────────┼────────────┤
|
|
|
|
|
// │ └1 │ 1 │ 1 │ [A | B C D] │ + | E] │
|
|
|
|
|
// ├────┼───┼───┼─────────────────────┼────────────┤
|
|
|
|
|
// │ 0 │ 3 │ 2 │ [A B C | D E] │ │
|
|
|
|
|
// ├────┼───┼───┼─────────────────────┼────────────┤
|
|
|
|
|
// │ └1 │ 2 │ 1 │ [A B | C] │ + | D E] │
|
|
|
|
|
// ├────┼───┼───┼─────────────────────┼────────────┤
|
|
|
|
|
// │ └1 │ 1 │ 1 │ [A | B C] │ + | D E] │
|
|
|
|
|
// ├────┼───┼───┼─────────────────────┼────────────┤
|
|
|
|
|
// │ 0 │ 2 │ 2 │ [A B | C D E] │ │
|
|
|
|
|
// ├────┼───┼───┼─────────────────────┼────────────┤
|
|
|
|
|
// │ └1 │ 1 │ 1 │ [A | B] │ + | C D E] │
|
|
|
|
|
// └────┴───┴───┴─────────────────────┴────────────┘
|
2023-04-06 21:30:45 +00:00
|
|
|
for index := lastObj; index >= nCuts; index-- {
|
2023-04-26 18:49:41 +00:00
|
|
|
if !check(objects[index:], false) {
|
|
|
|
|
// optimization: if current cut gives a bad grouping, don't recurse
|
|
|
|
|
continue
|
|
|
|
|
}
|
2023-04-05 18:49:04 +00:00
|
|
|
if nCuts > 1 {
|
2023-04-26 18:31:27 +00:00
|
|
|
iterDivisions(objects[:index], nCuts-1, func(inner []int) bool {
|
|
|
|
|
done = f(append(inner, index-1))
|
|
|
|
|
return done
|
2023-04-26 18:49:41 +00:00
|
|
|
}, check)
|
2023-04-05 18:49:04 +00:00
|
|
|
} else {
|
2023-04-26 18:49:41 +00:00
|
|
|
if !check(objects[:index], true) {
|
|
|
|
|
// e.g. [A B C | D] if [A,B,C] is bad, skip it
|
|
|
|
|
continue
|
|
|
|
|
}
|
2023-04-26 18:31:27 +00:00
|
|
|
done = f([]int{index - 1})
|
|
|
|
|
}
|
|
|
|
|
if done {
|
|
|
|
|
return
|
2023-04-05 18:49:04 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-06 21:30:45 +00:00
|
|
|
// generate a grid of objects from the given cut indices
|
2023-06-08 19:24:11 +00:00
|
|
|
// each cut index applies after the object at that index
|
|
|
|
|
// e.g. [0 1 2 3 4 5 6 7] with cutIndices [0, 2, 6] => [[0], [1, 2], [3,4,5,6], [7]]
|
|
|
|
|
func GenLayout(objects []*d2graph.Object, cutIndices []int) [][]*d2graph.Object {
|
2023-04-05 18:49:04 +00:00
|
|
|
layout := make([][]*d2graph.Object, len(cutIndices)+1)
|
2023-04-06 21:30:45 +00:00
|
|
|
objIndex := 0
|
2023-04-05 18:49:04 +00:00
|
|
|
for i := 0; i <= len(cutIndices); i++ {
|
|
|
|
|
var stop int
|
|
|
|
|
if i < len(cutIndices) {
|
|
|
|
|
stop = cutIndices[i]
|
|
|
|
|
} else {
|
2023-04-06 21:30:45 +00:00
|
|
|
stop = len(objects) - 1
|
2023-04-05 18:49:04 +00:00
|
|
|
}
|
2023-04-26 00:19:34 +00:00
|
|
|
if stop >= objIndex {
|
|
|
|
|
layout[i] = make([]*d2graph.Object, 0, stop-objIndex+1)
|
|
|
|
|
}
|
2023-04-06 21:30:45 +00:00
|
|
|
for ; objIndex <= stop; objIndex++ {
|
|
|
|
|
layout[i] = append(layout[i], objects[objIndex])
|
2023-04-05 18:49:04 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return layout
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-11 18:32:35 +00:00
|
|
|
func getDistToTarget(layout [][]*d2graph.Object, targetSize float64, horizontalGap, verticalGap float64, columns bool) float64 {
|
2023-04-05 18:49:04 +00:00
|
|
|
totalDelta := 0.
|
|
|
|
|
for _, row := range layout {
|
|
|
|
|
rowSize := 0.
|
2023-04-06 21:30:45 +00:00
|
|
|
for _, o := range row {
|
2023-04-05 18:49:04 +00:00
|
|
|
if columns {
|
2023-04-11 18:32:35 +00:00
|
|
|
rowSize += o.Height + verticalGap
|
2023-04-05 18:49:04 +00:00
|
|
|
} else {
|
2023-04-11 18:32:35 +00:00
|
|
|
rowSize += o.Width + horizontalGap
|
2023-04-05 18:49:04 +00:00
|
|
|
}
|
|
|
|
|
}
|
2023-06-08 20:02:34 +00:00
|
|
|
if len(row) > 0 {
|
|
|
|
|
if columns {
|
|
|
|
|
rowSize -= verticalGap
|
|
|
|
|
} else {
|
|
|
|
|
rowSize -= horizontalGap
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-04-05 18:49:04 +00:00
|
|
|
totalDelta += math.Abs(rowSize - targetSize)
|
|
|
|
|
}
|
|
|
|
|
return totalDelta
|
|
|
|
|
}
|
2023-09-26 23:14:19 +00:00
|
|
|
|
2023-09-27 00:42:42 +00:00
|
|
|
func (gd *gridDiagram) sizeForOutsideLabels() (revert func()) {
|
2023-10-17 03:37:56 +00:00
|
|
|
margins := make(map[*d2graph.Object]geo.Spacing)
|
2023-09-26 23:14:19 +00:00
|
|
|
|
|
|
|
|
for _, o := range gd.objects {
|
2023-09-27 00:38:06 +00:00
|
|
|
margin := o.GetMargin()
|
2023-10-17 03:37:56 +00:00
|
|
|
margins[o] = margin
|
2023-09-27 00:42:42 +00:00
|
|
|
|
|
|
|
|
o.Height += margin.Top + margin.Bottom
|
|
|
|
|
o.Width += margin.Left + margin.Right
|
2023-09-26 23:14:19 +00:00
|
|
|
}
|
|
|
|
|
|
2023-10-17 03:37:56 +00:00
|
|
|
// Example: a single column with 3 shapes and
|
|
|
|
|
// `x.label: long label {near: outside-bottom-left}`
|
|
|
|
|
// `y.label: outsider {near: outside-right-center}`
|
|
|
|
|
// . ┌───────────────────┐
|
|
|
|
|
// . │ widest shape here │
|
|
|
|
|
// . └───────────────────┘
|
|
|
|
|
// . ┌───┐
|
|
|
|
|
// . │ x │
|
|
|
|
|
// . └───┘
|
|
|
|
|
// . long label
|
|
|
|
|
// . ├─────────┤ x's new width
|
|
|
|
|
// . ├─mr──┤ margin.right added to width during layout
|
|
|
|
|
// . ┌───┐
|
|
|
|
|
// . │ y │ outsider
|
|
|
|
|
// . └───┘
|
|
|
|
|
// . ├─────────────┤ y's new width
|
|
|
|
|
// . ├───mr────┤ margin.right added to width during layout
|
|
|
|
|
|
|
|
|
|
// BEFORE LAYOUT
|
|
|
|
|
// . ┌───────────────────┐
|
|
|
|
|
// . │ widest shape here │
|
|
|
|
|
// . └───────────────────┘
|
|
|
|
|
// . ┌─────────┐
|
|
|
|
|
// . │ x │
|
|
|
|
|
// . └─────────┘
|
|
|
|
|
// . ┌─────────────┐
|
|
|
|
|
// . │ y │
|
|
|
|
|
// . └─────────────┘
|
|
|
|
|
|
|
|
|
|
// AFTER LAYOUT
|
|
|
|
|
// . ┌───────────────────┐
|
|
|
|
|
// . │ widest shape here │
|
|
|
|
|
// . └───────────────────┘
|
|
|
|
|
// . ┌───────────────────┐
|
|
|
|
|
// . │ x │
|
|
|
|
|
// . └───────────────────┘
|
|
|
|
|
// . ┌───────────────────┐
|
|
|
|
|
// . │ y │
|
|
|
|
|
// . └───────────────────┘
|
|
|
|
|
|
|
|
|
|
// CLEANUP 1/2
|
|
|
|
|
// . ┌───────────────────┐
|
|
|
|
|
// . │ widest shape here │
|
|
|
|
|
// . └───────────────────┘
|
|
|
|
|
// . ┌─────────────┐
|
|
|
|
|
// . │ x │
|
|
|
|
|
// . └─────────────┘
|
|
|
|
|
// . long label ├─mr──┤ remove margin we added
|
|
|
|
|
// . ┌─────────┐
|
|
|
|
|
// . │ y │ outsider
|
|
|
|
|
// . └─────────┘
|
|
|
|
|
// . ├───mr────┤ remove margin we added
|
|
|
|
|
// CLEANUP 2/2
|
|
|
|
|
// . ┌───────────────────┐
|
|
|
|
|
// . │ widest shape here │
|
|
|
|
|
// . └───────────────────┘
|
|
|
|
|
// . ┌───────────────────┐
|
|
|
|
|
// . │ x │
|
|
|
|
|
// . └───────────────────┘
|
|
|
|
|
// . long label ├─mr──┤ we removed too much so add back margin we subtracted, then subtract new margin
|
|
|
|
|
// . ┌─────────┐
|
|
|
|
|
// . │ y │ outsider
|
|
|
|
|
// . └─────────┘
|
|
|
|
|
// . ├───mr────┤ margin.right is still needed
|
|
|
|
|
|
2023-09-26 23:14:19 +00:00
|
|
|
return func() {
|
|
|
|
|
for _, o := range gd.objects {
|
2023-10-17 03:37:56 +00:00
|
|
|
m, has := margins[o]
|
2023-09-27 00:05:32 +00:00
|
|
|
if !has {
|
2023-09-26 23:14:19 +00:00
|
|
|
continue
|
|
|
|
|
}
|
2023-10-17 03:37:56 +00:00
|
|
|
dy := m.Top + m.Bottom
|
|
|
|
|
dx := m.Left + m.Right
|
2023-10-17 00:33:44 +00:00
|
|
|
o.Height -= dy
|
|
|
|
|
o.Width -= dx
|
|
|
|
|
|
2023-10-17 03:37:56 +00:00
|
|
|
// less margin may be needed if layout grew the object
|
|
|
|
|
// compute the new margin after removing the old margin we added
|
2023-10-17 00:33:44 +00:00
|
|
|
margin := o.GetMargin()
|
2023-10-17 03:37:56 +00:00
|
|
|
marginX := margin.Left + margin.Right
|
|
|
|
|
marginY := margin.Top + margin.Bottom
|
|
|
|
|
if marginX < dx {
|
|
|
|
|
// layout grew width and now we need less of a margin (but we subtracted too much)
|
|
|
|
|
// add back dx and subtract the new amount
|
|
|
|
|
o.Width += dx - marginX
|
|
|
|
|
}
|
|
|
|
|
if marginY < dy {
|
|
|
|
|
o.Height += dy - marginY
|
2023-10-17 00:33:44 +00:00
|
|
|
}
|
2023-09-26 23:14:19 +00:00
|
|
|
|
2023-09-27 00:38:06 +00:00
|
|
|
if margin.Left > 0 || margin.Top > 0 {
|
|
|
|
|
o.MoveWithDescendants(margin.Left, margin.Top)
|
2023-09-26 23:14:19 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|