d2/d2layouts/d2sequence/layout.go

141 lines
5.2 KiB
Go
Raw Normal View History

package d2sequence
import (
"context"
2022-12-01 00:11:02 +00:00
"strings"
"oss.terrastruct.com/d2/d2graph"
2022-12-01 00:11:02 +00:00
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/lib/geo"
)
// Layout identifies and performs layout on sequence diagrams within a graph
2022-12-01 21:27:53 +00:00
// first, it traverses the graph from Root and once it finds an object of shape `sequence_diagram`
// it replaces the children with a rectangle with id `sequence_diagram`, collects all edges coming to this node and
// flag the edges to be removed. Then, using the children and the edges, it lays out the sequence diagram and
// sets the dimensions of the rectangle `sequence_diagram` rectangle.
// Once all nodes were processed, it continues to run the layout engine without the sequence diagram nodes and edges.
// Then it restores all objects with their proper layout engine and sequence diagram positions
2022-12-01 19:47:27 +00:00
func Layout(ctx context.Context, g *d2graph.Graph, layout func(ctx context.Context, g *d2graph.Graph) error) error {
2022-12-01 21:39:48 +00:00
// flag objects to keep to avoid having to flag all descendants of sequence diagram to be removed
objectsToKeep := make(map[*d2graph.Object]struct{})
2022-12-01 21:27:53 +00:00
// edges flagged to be removed (these are internal edges of the sequence diagrams)
2022-12-01 00:11:02 +00:00
edgesToRemove := make(map[*d2graph.Edge]struct{})
2022-12-01 21:27:53 +00:00
// store the sequence diagram related to a given node
2022-12-02 02:08:34 +00:00
sequenceDiagrams := make(map[string]*sequenceDiagram)
2022-12-01 21:27:53 +00:00
// keeps the reference of the children of a given node
2022-12-02 02:08:34 +00:00
childrenReplacement := make(map[string][]*d2graph.Object)
2022-12-01 00:11:02 +00:00
2022-12-01 21:27:53 +00:00
// goes from root and travers all descendants
2022-12-02 02:08:34 +00:00
queue := make([]*d2graph.Object, 1, len(g.Objects))
2022-12-01 00:11:02 +00:00
queue[0] = g.Root
for len(queue) > 0 {
obj := queue[0]
queue = queue[1:]
2022-12-01 21:39:48 +00:00
objectsToKeep[obj] = struct{}{}
2022-12-01 00:11:02 +00:00
if obj.Attributes.Shape.Value == d2target.ShapeSequenceDiagram {
// TODO: should update obj.References too?
// clean current children and keep a backup to restore them later
obj.Children = make(map[string]*d2graph.Object)
2022-12-02 02:08:34 +00:00
children := obj.ChildrenArray
2022-12-01 00:11:02 +00:00
obj.ChildrenArray = nil
// creates a mock rectangle so that layout considers this size
sdMock := obj.EnsureChild([]string{"sequence_diagram"})
sdMock.Attributes.Shape.Value = d2target.ShapeRectangle
sdMock.Attributes.Label.Value = ""
2022-12-01 21:39:48 +00:00
objectsToKeep[sdMock] = struct{}{}
2022-12-01 00:11:02 +00:00
2022-12-01 21:27:53 +00:00
// find the edges that belong to this sequence diagra
var edges []*d2graph.Edge
2022-12-01 00:11:02 +00:00
for _, edge := range g.Edges {
2022-12-01 21:27:53 +00:00
// both Src and Dst must be inside the sequence diagram
2022-12-01 00:11:02 +00:00
if strings.HasPrefix(edge.Src.AbsID(), obj.AbsID()) && strings.HasPrefix(edge.Dst.AbsID(), obj.AbsID()) {
edgesToRemove[edge] = struct{}{}
2022-12-01 21:27:53 +00:00
edges = append(edges, edge)
2022-12-01 00:11:02 +00:00
}
}
2022-12-02 02:08:34 +00:00
sd := newSequenceDiagram(children, edges)
2022-12-01 00:11:02 +00:00
sd.layout()
sdMock.Box = geo.NewBox(nil, sd.getWidth(), sd.getHeight())
2022-12-02 02:08:34 +00:00
sequenceDiagrams[sdMock.AbsID()] = sd
childrenReplacement[sdMock.AbsID()] = children
2022-12-01 00:11:02 +00:00
} else {
2022-12-01 21:27:53 +00:00
// only move to children if the parent is not a sequence diagram
2022-12-01 00:11:02 +00:00
queue = append(queue, obj.ChildrenArray...)
}
}
2022-12-02 02:08:34 +00:00
if len(sequenceDiagrams) == 0 {
return layout(ctx, g)
}
2022-12-01 21:27:53 +00:00
// removes the edges
2022-12-02 02:08:34 +00:00
layoutEdges := make([]*d2graph.Edge, 0, len(g.Edges)-len(edgesToRemove))
sequenceDiagramEdges := make([]*d2graph.Edge, 0, len(edgesToRemove))
2022-12-01 00:11:02 +00:00
for _, edge := range g.Edges {
if _, exists := edgesToRemove[edge]; !exists {
2022-12-02 02:08:34 +00:00
layoutEdges = append(layoutEdges, edge)
} else {
sequenceDiagramEdges = append(sequenceDiagramEdges, edge)
2022-12-01 00:11:02 +00:00
}
}
2022-12-02 02:08:34 +00:00
g.Edges = layoutEdges
2022-12-01 00:11:02 +00:00
2022-12-01 21:39:48 +00:00
// done this way (by flagging objects) instead of appending to `queue`
// because appending in that order would change the order of g.Objects which
// could lead to layout changes (as the order of the objects might be important for the underlying engine)
2022-12-02 02:08:34 +00:00
layoutObjects := make([]*d2graph.Object, 0, len(objectsToKeep))
sequenceDiagramObjects := make([]*d2graph.Object, 0, len(g.Objects)-len(objectsToKeep))
2022-12-01 21:39:48 +00:00
for _, obj := range g.Objects {
if _, exists := objectsToKeep[obj]; exists {
2022-12-02 02:08:34 +00:00
layoutObjects = append(layoutObjects, obj)
} else {
sequenceDiagramObjects = append(sequenceDiagramObjects, obj)
2022-12-01 21:39:48 +00:00
}
}
2022-12-02 02:08:34 +00:00
g.Objects = layoutObjects
2022-12-01 21:27:53 +00:00
if g.Root.Attributes.Shape.Value == d2target.ShapeSequenceDiagram {
// don't need to run the layout engine if the root is a sequence diagram
g.Root.ChildrenArray[0].TopLeft = geo.NewPoint(0, 0)
} else if err := layout(ctx, g); err != nil {
2022-12-01 00:11:02 +00:00
return err
}
2022-12-02 02:08:34 +00:00
g.Edges = append(g.Edges, sequenceDiagramEdges...)
// restores objects
// it must be this way because the objects pointer reference is lost when calling layout engines compiled in binaries
allObjects := sequenceDiagramObjects
for _, obj := range g.Objects {
if _, exists := sequenceDiagrams[obj.AbsID()]; !exists {
allObjects = append(allObjects, obj)
continue
}
// we don't want `sdMock` in `g.Objects` because they shouldn't be rendered
// just to make it easier to read
sdMock := obj
parent := obj.Parent
2022-12-01 21:27:53 +00:00
// shift the sequence diagrams as they are always placed at (0, 0)
2022-12-02 02:08:34 +00:00
sequenceDiagrams[sdMock.AbsID()].shift(sdMock.TopLeft)
2022-12-01 21:27:53 +00:00
// restore children
2022-12-02 02:08:34 +00:00
parent.Children = make(map[string]*d2graph.Object)
for _, child := range childrenReplacement[sdMock.AbsID()] {
parent.Children[child.ID] = child
2022-12-01 00:11:02 +00:00
}
2022-12-02 02:08:34 +00:00
parent.ChildrenArray = childrenReplacement[sdMock.AbsID()]
2022-12-01 21:27:53 +00:00
// add lifeline edges
2022-12-02 02:08:34 +00:00
g.Edges = append(g.Edges, sequenceDiagrams[sdMock.AbsID()].lifelines...)
2022-12-01 00:11:02 +00:00
}
2022-12-02 02:08:34 +00:00
g.Objects = allObjects
return nil
}