d2/d2layouts/d2near/layout.go

215 lines
6.6 KiB
Go
Raw Normal View History

2022-12-26 00:33:32 +00:00
// d2near applies near keywords when they're constants
// Intended to be run as the last stage of layout after the diagram has already undergone layout
package d2near
import (
"context"
"math"
"strings"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/label"
)
const pad = 20
// Layout finds the shapes which are assigned constant near keywords and places them.
2023-03-24 18:38:05 +00:00
func Layout(ctx context.Context, g *d2graph.Graph, constantNearGraphs []*d2graph.Graph) error {
2023-03-24 18:09:28 +00:00
if len(constantNearGraphs) == 0 {
2022-12-26 00:33:32 +00:00
return nil
}
2023-04-05 21:00:12 +00:00
for _, tempGraph := range constantNearGraphs {
tempGraph.Root.ChildrenArray[0].Parent = g.Root
for _, obj := range tempGraph.Objects {
obj.Graph = g
}
}
2022-12-26 00:49:19 +00:00
// Imagine the graph has two long texts, one at top center and one at top left.
2022-12-26 00:33:32 +00:00
// Top left should go left enough to not collide with center.
// So place the center ones first, then the later ones will consider them for bounding box
2022-12-26 00:49:19 +00:00
for _, processCenters := range []bool{true, false} {
2023-03-24 18:38:05 +00:00
for _, tempGraph := range constantNearGraphs {
obj := tempGraph.Root.ChildrenArray[0]
2023-04-14 03:04:55 +00:00
if processCenters == strings.Contains(d2graph.Key(obj.NearKey)[0], "-center") {
2023-04-04 10:33:23 +00:00
prevX, prevY := obj.TopLeft.X, obj.TopLeft.Y
2022-12-26 00:49:19 +00:00
obj.TopLeft = geo.NewPoint(place(obj))
2023-04-04 10:33:23 +00:00
dx, dy := obj.TopLeft.X-prevX, obj.TopLeft.Y-prevY
2023-04-04 10:33:23 +00:00
for _, subObject := range tempGraph.Objects {
// `obj` already been replaced above by `place(obj)`
if subObject == obj {
continue
}
subObject.TopLeft.X += dx
subObject.TopLeft.Y += dy
}
2023-04-04 10:33:23 +00:00
for _, subEdge := range tempGraph.Edges {
for _, point := range subEdge.Route {
point.X += dx
point.Y += dy
}
}
2022-12-26 00:49:19 +00:00
}
2022-12-26 00:33:32 +00:00
}
2023-03-24 18:38:05 +00:00
for _, tempGraph := range constantNearGraphs {
obj := tempGraph.Root.ChildrenArray[0]
2023-04-14 03:04:55 +00:00
if processCenters == strings.Contains(d2graph.Key(obj.NearKey)[0], "-center") {
2022-12-26 00:49:19 +00:00
// The z-index for constant nears does not matter, as it will not collide
2023-04-07 02:33:19 +00:00
g.Objects = append(g.Objects, tempGraph.Objects...)
2023-04-08 17:07:56 +00:00
if obj.Parent.Children == nil {
obj.Parent.Children = make(map[string]*d2graph.Object)
}
2023-04-06 22:32:09 +00:00
obj.Parent.Children[strings.ToLower(obj.ID)] = obj
2022-12-26 00:49:19 +00:00
obj.Parent.ChildrenArray = append(obj.Parent.ChildrenArray, obj)
2023-04-07 02:33:19 +00:00
g.Edges = append(g.Edges, tempGraph.Edges...)
2022-12-26 00:49:19 +00:00
}
2022-12-26 00:33:32 +00:00
}
}
return nil
}
2022-12-26 01:43:43 +00:00
// place returns the position of obj, taking into consideration its near value and the diagram
2022-12-26 00:33:32 +00:00
func place(obj *d2graph.Object) (float64, float64) {
tl, br := boundingBox(obj.Graph)
w := br.X - tl.X
h := br.Y - tl.Y
2023-03-23 04:39:22 +00:00
2023-04-14 03:04:55 +00:00
nearKeyStr := d2graph.Key(obj.NearKey)[0]
2023-03-23 04:39:22 +00:00
var x, y float64
2023-03-27 12:58:01 +00:00
switch nearKeyStr {
2022-12-26 00:33:32 +00:00
case "top-left":
2023-03-23 04:39:22 +00:00
x, y = tl.X-obj.Width-pad, tl.Y-obj.Height-pad
break
2022-12-26 00:33:32 +00:00
case "top-center":
2023-03-23 04:39:22 +00:00
x, y = tl.X+w/2-obj.Width/2, tl.Y-obj.Height-pad
break
2022-12-26 00:33:32 +00:00
case "top-right":
2023-03-23 04:39:22 +00:00
x, y = br.X+pad, tl.Y-obj.Height-pad
break
2022-12-26 00:33:32 +00:00
case "center-left":
2023-03-23 04:39:22 +00:00
x, y = tl.X-obj.Width-pad, tl.Y+h/2-obj.Height/2
break
2022-12-26 00:33:32 +00:00
case "center-right":
2023-03-23 04:39:22 +00:00
x, y = br.X+pad, tl.Y+h/2-obj.Height/2
break
2022-12-26 00:33:32 +00:00
case "bottom-left":
2023-03-23 04:39:22 +00:00
x, y = tl.X-obj.Width-pad, br.Y+pad
break
2022-12-26 00:33:32 +00:00
case "bottom-center":
2023-03-23 04:39:22 +00:00
x, y = br.X-w/2-obj.Width/2, br.Y+pad
break
2022-12-26 00:33:32 +00:00
case "bottom-right":
2023-03-23 04:39:22 +00:00
x, y = br.X+pad, br.Y+pad
break
}
2023-03-27 13:40:36 +00:00
if obj.LabelPosition != nil && !strings.Contains(*obj.LabelPosition, "INSIDE") {
2023-03-23 04:39:22 +00:00
if strings.Contains(*obj.LabelPosition, "_TOP_") {
2023-03-27 12:58:01 +00:00
// label is on the top, and container is placed on the bottom
if strings.Contains(nearKeyStr, "bottom") {
y += float64(obj.LabelDimensions.Height)
2023-03-27 12:58:01 +00:00
}
2023-03-23 04:39:22 +00:00
} else if strings.Contains(*obj.LabelPosition, "_LEFT_") {
2023-03-27 12:58:01 +00:00
// label is on the left, and container is placed on the right
2023-03-27 14:48:02 +00:00
if strings.Contains(nearKeyStr, "right") {
x += float64(obj.LabelDimensions.Width)
2023-03-27 12:58:01 +00:00
}
2023-03-23 04:39:22 +00:00
} else if strings.Contains(*obj.LabelPosition, "_RIGHT_") {
2023-03-27 12:58:01 +00:00
// label is on the right, and container is placed on the left
2023-03-27 14:48:02 +00:00
if strings.Contains(nearKeyStr, "left") {
x -= float64(obj.LabelDimensions.Width)
2023-03-27 12:58:01 +00:00
}
2023-03-23 04:39:22 +00:00
} else if strings.Contains(*obj.LabelPosition, "_BOTTOM_") {
2023-03-27 12:58:01 +00:00
// label is on the bottom, and container is placed on the top
if strings.Contains(nearKeyStr, "top") {
y -= float64(obj.LabelDimensions.Height)
2023-03-27 12:58:01 +00:00
}
2023-03-23 04:39:22 +00:00
}
2022-12-26 00:33:32 +00:00
}
2023-03-23 04:39:22 +00:00
return x, y
2022-12-26 00:33:32 +00:00
}
2022-12-26 01:43:43 +00:00
// WithoutConstantNears plucks out the graph objects which have "near" set to a constant value
// This is to be called before layout engines so they don't take part in regular positioning
2023-03-24 18:38:05 +00:00
func WithoutConstantNears(ctx context.Context, g *d2graph.Graph) (constantNearGraphs []*d2graph.Graph) {
2022-12-26 00:33:32 +00:00
for i := 0; i < len(g.Objects); i++ {
obj := g.Objects[i]
2023-04-14 03:04:55 +00:00
if obj.NearKey == nil {
2022-12-26 00:33:32 +00:00
continue
}
2023-04-14 03:04:55 +00:00
_, isKey := g.Root.HasChild(d2graph.Key(obj.NearKey))
2022-12-26 00:33:32 +00:00
if isKey {
continue
}
2023-04-14 03:04:55 +00:00
_, isConst := d2graph.NearConstants[d2graph.Key(obj.NearKey)[0]]
2022-12-26 00:33:32 +00:00
if isConst {
2023-05-09 01:38:41 +00:00
tempGraph := g.ExtractAsNestedGraph(obj)
2023-03-24 18:38:05 +00:00
constantNearGraphs = append(constantNearGraphs, tempGraph)
2022-12-26 00:33:32 +00:00
i--
}
}
2023-03-24 18:09:28 +00:00
return constantNearGraphs
}
2022-12-26 00:33:32 +00:00
// boundingBox gets the center of the graph as defined by shapes
// The bounds taking into consideration only shapes gives more of a feeling of true center
// It differs from d2target.BoundingBox which needs to include every visible thing
func boundingBox(g *d2graph.Graph) (tl, br *geo.Point) {
if len(g.Objects) == 0 {
return geo.NewPoint(0, 0), geo.NewPoint(0, 0)
}
x1 := math.Inf(1)
y1 := math.Inf(1)
x2 := math.Inf(-1)
y2 := math.Inf(-1)
for _, obj := range g.Objects {
2023-04-14 03:04:55 +00:00
if obj.NearKey != nil {
2022-12-26 00:33:32 +00:00
// Top left should not be MORE top than top-center
// But it should go more left if top-center label extends beyond bounds of diagram
2023-04-14 03:04:55 +00:00
switch d2graph.Key(obj.NearKey)[0] {
2022-12-26 00:33:32 +00:00
case "top-center", "bottom-center":
x1 = math.Min(x1, obj.TopLeft.X)
x2 = math.Max(x2, obj.TopLeft.X+obj.Width)
case "center-left", "center-right":
y1 = math.Min(y1, obj.TopLeft.Y)
y2 = math.Max(y2, obj.TopLeft.Y+obj.Height)
}
} else {
2023-03-28 00:57:15 +00:00
if obj.OuterNearContainer() != nil {
continue
}
2022-12-26 00:33:32 +00:00
x1 = math.Min(x1, obj.TopLeft.X)
y1 = math.Min(y1, obj.TopLeft.Y)
x2 = math.Max(x2, obj.TopLeft.X+obj.Width)
y2 = math.Max(y2, obj.TopLeft.Y+obj.Height)
2023-04-14 03:04:55 +00:00
if obj.Label.Value != "" && obj.LabelPosition != nil {
2023-01-06 19:13:52 +00:00
labelPosition := label.Position(*obj.LabelPosition)
if labelPosition.IsOutside() {
labelTL := labelPosition.GetPointOnBox(obj.Box, label.PADDING, float64(obj.LabelDimensions.Width), float64(obj.LabelDimensions.Height))
2023-01-06 19:13:52 +00:00
x1 = math.Min(x1, labelTL.X)
y1 = math.Min(y1, labelTL.Y)
x2 = math.Max(x2, labelTL.X+float64(obj.LabelDimensions.Width))
y2 = math.Max(y2, labelTL.Y+float64(obj.LabelDimensions.Height))
2023-01-06 19:13:52 +00:00
}
}
2022-12-26 00:33:32 +00:00
}
}
2023-01-19 23:13:18 +00:00
if math.IsInf(x1, 1) && math.IsInf(x2, -1) {
x1 = 0
x2 = 0
}
if math.IsInf(y1, 1) && math.IsInf(y2, -1) {
y1 = 0
y2 = 0
}
2022-12-26 00:33:32 +00:00
return geo.NewPoint(x1, y1), geo.NewPoint(x2, y2)
}