d2/d2layouts/d2layouts.go

461 lines
13 KiB
Go
Raw Normal View History

2023-09-14 20:38:59 +00:00
package d2layouts
2023-09-14 21:17:18 +00:00
import (
2023-09-15 21:44:20 +00:00
"context"
"fmt"
2023-09-15 00:00:44 +00:00
"math"
2023-09-22 00:19:43 +00:00
"sort"
2023-09-14 22:38:52 +00:00
"strings"
2023-09-16 05:07:06 +00:00
"cdr.dev/slog"
2023-09-22 00:27:22 +00:00
2023-09-14 21:17:18 +00:00
"oss.terrastruct.com/d2/d2graph"
2023-09-15 21:44:20 +00:00
"oss.terrastruct.com/d2/d2layouts/d2grid"
"oss.terrastruct.com/d2/d2layouts/d2near"
"oss.terrastruct.com/d2/d2layouts/d2sequence"
2023-09-15 00:00:44 +00:00
"oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/label"
2023-09-16 05:07:06 +00:00
"oss.terrastruct.com/d2/lib/log"
"oss.terrastruct.com/util-go/go2"
2023-09-14 21:17:18 +00:00
)
2023-09-14 20:38:59 +00:00
2023-09-15 21:44:20 +00:00
type DiagramType string
2023-09-14 21:17:18 +00:00
2023-09-15 21:44:20 +00:00
// a grid diagram at a constant near is
2023-09-14 21:17:18 +00:00
const (
2023-09-15 21:44:20 +00:00
DefaultGraphType DiagramType = ""
ConstantNearGraph DiagramType = "constant-near"
GridDiagram DiagramType = "grid-diagram"
SequenceDiagram DiagramType = "sequence-diagram"
2023-09-14 21:17:18 +00:00
)
2023-09-15 21:44:20 +00:00
type GraphInfo struct {
IsConstantNear bool
DiagramType DiagramType
}
func (gi GraphInfo) isDefault() bool {
return !gi.IsConstantNear && gi.DiagramType == DefaultGraphType
}
2023-09-22 00:19:43 +00:00
func SaveChildrenOrder(container *d2graph.Object) (restoreOrder func()) {
objectOrder := make(map[string]int, len(container.ChildrenArray))
for i, obj := range container.ChildrenArray {
objectOrder[obj.AbsID()] = i
}
return func() {
sort.SliceStable(container.ChildrenArray, func(i, j int) bool {
return objectOrder[container.ChildrenArray[i].AbsID()] < objectOrder[container.ChildrenArray[j].AbsID()]
})
}
}
func SaveOrder(g *d2graph.Graph) (restoreOrder func()) {
objectOrder := make(map[string]int, len(g.Objects))
for i, obj := range g.Objects {
objectOrder[obj.AbsID()] = i
}
edgeOrder := make(map[string]int, len(g.Edges))
for i, edge := range g.Edges {
edgeOrder[edge.AbsID()] = i
}
restoreRootOrder := SaveChildrenOrder(g.Root)
return func() {
sort.SliceStable(g.Objects, func(i, j int) bool {
return objectOrder[g.Objects[i].AbsID()] < objectOrder[g.Objects[j].AbsID()]
})
sort.SliceStable(g.Edges, func(i, j int) bool {
2023-09-22 01:10:51 +00:00
iIndex, iHas := edgeOrder[g.Edges[i].AbsID()]
jIndex, jHas := edgeOrder[g.Edges[j].AbsID()]
if iHas && jHas {
return iIndex < jIndex
}
return iHas
2023-09-22 00:19:43 +00:00
})
restoreRootOrder()
}
}
2023-09-22 00:58:55 +00:00
func LayoutNested(ctx context.Context, g *d2graph.Graph, graphInfo GraphInfo, coreLayout d2graph.LayoutGraph) error {
2023-09-21 20:54:41 +00:00
g.Root.Box = &geo.Box{}
2023-09-15 21:44:20 +00:00
2023-09-14 20:38:59 +00:00
// Before we can layout these nodes, we need to handle all nested diagrams first.
extracted := make(map[string]*d2graph.Graph)
var extractedOrder []string
var extractedEdges []*d2graph.Edge
2023-09-15 21:44:20 +00:00
var constantNears []*d2graph.Graph
2023-09-22 00:19:43 +00:00
restoreOrder := SaveOrder(g)
defer restoreOrder()
2023-09-14 20:38:59 +00:00
// Iterate top-down from Root so all nested diagrams can process their own contents
queue := make([]*d2graph.Object, 0, len(g.Root.ChildrenArray))
2023-09-19 03:12:50 +00:00
queue = append(queue, g.Root.ChildrenArray...)
2023-09-14 20:38:59 +00:00
2023-09-22 00:27:22 +00:00
for len(queue) > 0 {
curr := queue[0]
queue = queue[1:]
2023-09-21 22:22:47 +00:00
isGridCellContainer := graphInfo.DiagramType == GridDiagram &&
curr.IsContainer() && curr.Parent == g.Root
gi := NestedGraphInfo(curr)
2023-09-22 03:34:47 +00:00
if isGridCellContainer && gi.isDefault() {
// if we are in a grid diagram, and our children have descendants
// we need to run layout on them first, even if they are not special diagram types
nestedGraph, externalEdges := ExtractSubgraph(curr, true)
2023-09-25 22:38:22 +00:00
id := curr.AbsID()
2023-09-22 00:58:55 +00:00
err := LayoutNested(ctx, nestedGraph, GraphInfo{}, coreLayout)
if err != nil {
return err
}
2023-09-21 22:22:47 +00:00
InjectNested(g.Root, nestedGraph, false)
g.Edges = append(g.Edges, externalEdges...)
2023-09-22 00:19:43 +00:00
restoreOrder()
2023-09-25 22:38:22 +00:00
// need to update curr *Object incase layout changed it
var obj *d2graph.Object
for _, o := range g.Objects {
if o.AbsID() == id {
obj = o
break
}
}
if obj == nil {
return fmt.Errorf("could not find object %#v after layout", id)
}
curr = obj
2023-09-27 22:31:45 +00:00
// position nested graph (excluding curr) relative to curr
2023-09-27 22:21:30 +00:00
dx := 0 - curr.TopLeft.X
dy := 0 - curr.TopLeft.Y
2023-09-21 22:22:47 +00:00
for _, o := range nestedGraph.Objects {
2023-09-27 22:31:45 +00:00
if o.AbsID() == curr.AbsID() {
continue
}
2023-09-21 22:22:47 +00:00
o.TopLeft.X += dx
o.TopLeft.Y += dy
}
for _, e := range nestedGraph.Edges {
e.Move(dx, dy)
}
// now we keep the descendants out until after grid layout
nestedGraph, externalEdges = ExtractSubgraph(curr, false)
extractedEdges = append(extractedEdges, externalEdges...)
extracted[id] = nestedGraph
extractedOrder = append(extractedOrder, id)
2023-09-22 00:45:52 +00:00
continue
}
if !gi.isDefault() {
2023-09-22 01:22:19 +00:00
// empty grid or sequence can have 0 objects..
if !gi.IsConstantNear && len(curr.Children) == 0 {
2023-09-21 23:33:32 +00:00
continue
}
2023-09-16 05:07:06 +00:00
// There is a nested diagram here, so extract its contents and process in the same way
nestedGraph, externalEdges := ExtractSubgraph(curr, gi.IsConstantNear)
extractedEdges = append(extractedEdges, externalEdges...)
2023-09-14 20:38:59 +00:00
2023-09-22 03:41:06 +00:00
log.Info(ctx, "layout nested", slog.F("level", curr.Level()), slog.F("child", curr.AbsID()), slog.F("gi", gi))
2023-09-21 23:18:39 +00:00
nestedInfo := gi
nearKey := curr.NearKey
if gi.IsConstantNear {
// layout nested as a non-near
nestedInfo = GraphInfo{}
curr.NearKey = nil
}
2023-09-22 00:58:55 +00:00
err := LayoutNested(ctx, nestedGraph, nestedInfo, coreLayout)
if err != nil {
return err
}
// coreLayout can overwrite graph contents with newly created *Object pointers
// so we need to update `curr` with nestedGraph's value
if gi.IsConstantNear {
curr = nestedGraph.Root.ChildrenArray[0]
}
2023-09-16 05:07:06 +00:00
if gi.IsConstantNear {
2023-09-21 23:18:39 +00:00
curr.NearKey = nearKey
} else {
2023-09-22 00:58:55 +00:00
FitToGraph(curr, nestedGraph, geo.Spacing{})
2023-09-21 23:18:39 +00:00
curr.TopLeft = geo.NewPoint(0, 0)
2023-09-15 21:44:20 +00:00
}
2023-09-14 20:38:59 +00:00
2023-09-21 23:18:39 +00:00
if gi.IsConstantNear {
// near layout will inject these nestedGraphs
constantNears = append(constantNears, nestedGraph)
} else {
// We will restore the contents after running layout with child as the placeholder
// We need to reference using ID because there may be a new object to use after coreLayout
id := curr.AbsID()
extracted[id] = nestedGraph
extractedOrder = append(extractedOrder, id)
2023-09-21 23:18:39 +00:00
}
2023-09-22 00:27:22 +00:00
} else if len(curr.ChildrenArray) > 0 {
2023-09-21 22:22:47 +00:00
queue = append(queue, curr.ChildrenArray...)
2023-09-14 20:38:59 +00:00
}
}
// We can now run layout with accurate sizes of nested layout containers
// Layout according to the type of diagram
2023-09-23 03:12:20 +00:00
var err error
if len(g.Objects) > 0 {
switch graphInfo.DiagramType {
case GridDiagram:
log.Debug(ctx, "layout grid", slog.F("rootlevel", g.RootLevel), slog.F("shapes", g.PrintString()))
if err = d2grid.Layout(ctx, g); err != nil {
return err
}
2023-09-15 21:44:20 +00:00
case SequenceDiagram:
log.Debug(ctx, "layout sequence", slog.F("rootlevel", g.RootLevel), slog.F("shapes", g.PrintString()))
err = d2sequence.Layout(ctx, g, coreLayout)
if err != nil {
return err
}
default:
log.Debug(ctx, "default layout", slog.F("rootlevel", g.RootLevel), slog.F("shapes", g.PrintString()))
err := coreLayout(ctx, g)
if err != nil {
return err
}
2023-09-15 21:44:20 +00:00
}
}
2023-09-14 20:38:59 +00:00
2023-09-15 21:44:20 +00:00
if len(constantNears) > 0 {
err = d2near.Layout(ctx, g, constantNears)
2023-09-15 21:44:20 +00:00
if err != nil {
return err
2023-09-15 21:44:20 +00:00
}
2023-09-14 20:38:59 +00:00
}
2023-09-15 00:00:44 +00:00
idToObj := make(map[string]*d2graph.Object)
for _, o := range g.Objects {
idToObj[o.AbsID()] = o
}
2023-09-16 05:07:06 +00:00
// With the layout set, inject all the extracted graphs
for _, id := range extractedOrder {
nestedGraph := extracted[id]
// we have to find the object by ID because coreLayout can replace the Objects in graph
obj, exists := idToObj[id]
if !exists {
return fmt.Errorf("could not find object %#v after layout", id)
}
InjectNested(obj, nestedGraph, true)
PositionNested(obj, nestedGraph)
2023-09-16 05:07:06 +00:00
}
// update map with injected objects
for _, o := range g.Objects {
idToObj[o.AbsID()] = o
}
// Restore cross-graph edges and route them
g.Edges = append(g.Edges, extractedEdges...)
for _, e := range extractedEdges {
// update object references
src, exists := idToObj[e.Src.AbsID()]
if !exists {
return fmt.Errorf("could not find object %#v after layout", e.Src.AbsID())
}
e.Src = src
dst, exists := idToObj[e.Dst.AbsID()]
if !exists {
return fmt.Errorf("could not find object %#v after layout", e.Dst.AbsID())
}
e.Dst = dst
// simple straight line edge routing when going across graphs
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-22 03:41:06 +00:00
log.Debug(ctx, "done", slog.F("rootlevel", g.RootLevel), slog.F("shapes", g.PrintString()))
2023-09-22 00:58:55 +00:00
return err
2023-09-14 20:38:59 +00:00
}
2023-09-15 21:44:20 +00:00
func NestedGraphInfo(obj *d2graph.Object) (gi GraphInfo) {
2023-09-14 21:55:52 +00:00
if obj.Graph.RootLevel == 0 && obj.IsConstantNear() {
2023-09-15 21:44:20 +00:00
gi.IsConstantNear = true
2023-09-14 21:17:18 +00:00
}
2023-09-14 21:55:52 +00:00
if obj.IsSequenceDiagram() {
2023-09-15 21:44:20 +00:00
gi.DiagramType = SequenceDiagram
} else if obj.IsGridDiagram() {
gi.DiagramType = GridDiagram
}
return gi
}
func ExtractSubgraph(container *d2graph.Object, includeSelf bool) (nestedGraph *d2graph.Graph, externalEdges []*d2graph.Edge) {
2023-09-21 23:18:39 +00:00
// includeSelf: when we have a constant near or a grid cell that is a container,
// we want to include itself in the nested graph, not just its descendants,
nestedGraph = d2graph.NewGraph()
2023-09-21 22:22:47 +00:00
nestedGraph.RootLevel = int(container.Level())
if includeSelf {
nestedGraph.RootLevel--
}
nestedGraph.Root.Attributes = container.Attributes
2023-09-18 23:41:46 +00:00
nestedGraph.Root.Box = &geo.Box{}
2023-09-15 21:44:20 +00:00
2023-09-21 22:22:47 +00:00
isNestedObject := func(obj *d2graph.Object) bool {
if includeSelf {
return obj.IsDescendantOf(container)
}
return obj.Parent.IsDescendantOf(container)
}
2023-09-15 21:44:20 +00:00
// separate out nested edges
g := container.Graph
remainingEdges := make([]*d2graph.Edge, 0, len(g.Edges))
for _, edge := range g.Edges {
srcIsNested := isNestedObject(edge.Src)
2023-09-29 00:54:01 +00:00
if d2sequence.IsLifelineEnd(edge.Dst) {
// special handling for lifelines since their edge.Dst is a special Object
if srcIsNested {
nestedGraph.Edges = append(nestedGraph.Edges, edge)
} else {
remainingEdges = append(remainingEdges, edge)
}
continue
}
dstIsNested := isNestedObject(edge.Dst)
if srcIsNested && dstIsNested {
2023-09-15 21:44:20 +00:00
nestedGraph.Edges = append(nestedGraph.Edges, edge)
} else if srcIsNested || dstIsNested {
externalEdges = append(externalEdges, edge)
2023-09-15 21:44:20 +00:00
} else {
remainingEdges = append(remainingEdges, edge)
}
}
g.Edges = remainingEdges
// separate out nested objects
remainingObjects := make([]*d2graph.Object, 0, len(g.Objects))
for _, obj := range g.Objects {
2023-09-21 22:22:47 +00:00
if isNestedObject(obj) {
2023-09-15 21:44:20 +00:00
nestedGraph.Objects = append(nestedGraph.Objects, obj)
} else {
remainingObjects = append(remainingObjects, obj)
}
}
g.Objects = remainingObjects
// update object and new root references
for _, o := range nestedGraph.Objects {
o.Graph = nestedGraph
2023-09-14 21:17:18 +00:00
}
2023-09-15 21:44:20 +00:00
2023-09-21 22:22:47 +00:00
if includeSelf {
// remove container parent's references
if container.Parent != nil {
container.Parent.RemoveChild(container)
2023-09-14 22:38:52 +00:00
}
2023-09-21 22:22:47 +00:00
// set root references
nestedGraph.Root.ChildrenArray = []*d2graph.Object{container}
container.Parent = nestedGraph.Root
nestedGraph.Root.Children[strings.ToLower(container.ID)] = container
} else {
// set root references
nestedGraph.Root.ChildrenArray = append(nestedGraph.Root.ChildrenArray, container.ChildrenArray...)
for _, child := range container.ChildrenArray {
child.Parent = nestedGraph.Root
nestedGraph.Root.Children[strings.ToLower(child.ID)] = child
2023-09-14 22:38:52 +00:00
}
2023-09-21 22:22:47 +00:00
// remove container's references
for k := range container.Children {
delete(container.Children, k)
}
container.ChildrenArray = nil
2023-09-14 22:38:52 +00:00
}
return nestedGraph, externalEdges
2023-09-14 20:38:59 +00:00
}
2023-09-21 22:22:47 +00:00
func InjectNested(container *d2graph.Object, nestedGraph *d2graph.Graph, isRoot bool) {
2023-09-15 00:00:44 +00:00
g := container.Graph
for _, obj := range nestedGraph.Root.ChildrenArray {
obj.Parent = container
if container.Children == nil {
container.Children = make(map[string]*d2graph.Object)
}
2023-09-15 00:00:44 +00:00
container.Children[strings.ToLower(obj.ID)] = obj
container.ChildrenArray = append(container.ChildrenArray, obj)
}
for _, obj := range nestedGraph.Objects {
obj.Graph = g
}
g.Objects = append(g.Objects, nestedGraph.Objects...)
g.Edges = append(g.Edges, nestedGraph.Edges...)
2023-09-21 22:22:47 +00:00
if isRoot {
if nestedGraph.Root.LabelPosition != nil {
container.LabelPosition = nestedGraph.Root.LabelPosition
}
2023-09-21 23:20:25 +00:00
if nestedGraph.Root.IconPosition != nil {
container.IconPosition = nestedGraph.Root.IconPosition
}
2023-09-21 22:22:47 +00:00
container.Attributes = nestedGraph.Root.Attributes
2023-09-16 05:07:06 +00:00
}
2023-09-15 21:44:20 +00:00
}
func PositionNested(container *d2graph.Object, nestedGraph *d2graph.Graph) {
2023-09-19 00:20:59 +00:00
// tl, _ := boundingBox(nestedGraph)
2023-09-15 00:00:44 +00:00
// Note: assumes nestedGraph's layout has contents positioned relative to 0,0
2023-09-16 05:07:06 +00:00
dx := container.TopLeft.X //- tl.X
dy := container.TopLeft.Y //- tl.Y
2023-09-27 22:21:30 +00:00
if dx == 0 && dy == 0 {
return
}
2023-09-15 00:00:44 +00:00
for _, o := range nestedGraph.Objects {
o.TopLeft.X += dx
o.TopLeft.Y += dy
}
for _, e := range nestedGraph.Edges {
e.Move(dx, dy)
}
2023-09-14 20:38:59 +00:00
}
2023-09-15 00:00:44 +00:00
func boundingBox(g *d2graph.Graph) (tl, br *geo.Point) {
if len(g.Objects) == 0 {
return geo.NewPoint(0, 0), geo.NewPoint(0, 0)
}
tl = geo.NewPoint(math.Inf(1), math.Inf(1))
br = geo.NewPoint(math.Inf(-1), math.Inf(-1))
for _, obj := range g.Objects {
2023-09-16 05:07:06 +00:00
if obj.TopLeft == nil {
panic(obj.AbsID())
}
2023-09-15 00:00:44 +00:00
tl.X = math.Min(tl.X, obj.TopLeft.X)
tl.Y = math.Min(tl.Y, obj.TopLeft.Y)
br.X = math.Max(br.X, obj.TopLeft.X+obj.Width)
br.Y = math.Max(br.Y, obj.TopLeft.Y+obj.Height)
}
return tl, br
}
func FitToGraph(container *d2graph.Object, nestedGraph *d2graph.Graph, padding geo.Spacing) {
2023-09-16 05:07:06 +00:00
var width, height float64
width = nestedGraph.Root.Width
height = nestedGraph.Root.Height
if width == 0 || height == 0 {
tl, br := boundingBox(nestedGraph)
width = br.X - tl.X
height = br.Y - tl.Y
}
container.Width = padding.Left + width + padding.Right
container.Height = padding.Top + height + padding.Bottom
2023-09-14 20:38:59 +00:00
}