2022-11-26 06:32:04 +00:00
|
|
|
package d2sequence
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
2022-12-01 00:11:02 +00:00
|
|
|
"strings"
|
2022-11-26 06:32:04 +00:00
|
|
|
|
|
|
|
|
"oss.terrastruct.com/d2/d2graph"
|
2022-12-01 00:11:02 +00:00
|
|
|
"oss.terrastruct.com/d2/d2target"
|
2022-11-26 06:32:04 +00:00
|
|
|
"oss.terrastruct.com/d2/lib/geo"
|
|
|
|
|
)
|
|
|
|
|
|
2022-12-02 01:11:41 +00:00
|
|
|
// 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:27:53 +00:00
|
|
|
// keeps the current graph state
|
2022-12-01 18:33:20 +00:00
|
|
|
oldObjects := g.Objects
|
|
|
|
|
oldEdges := g.Edges
|
|
|
|
|
|
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-01 00:11:02 +00:00
|
|
|
sequenceDiagrams := make(map[*d2graph.Object]*sequenceDiagram)
|
2022-12-01 21:27:53 +00:00
|
|
|
// keeps the reference of the children of a given node
|
2022-12-01 00:11:02 +00:00
|
|
|
objChildrenArray := make(map[*d2graph.Object][]*d2graph.Object)
|
|
|
|
|
|
2022-12-01 21:27:53 +00:00
|
|
|
// goes from root and travers all descendants
|
|
|
|
|
queue := make([]*d2graph.Object, 1, len(oldObjects))
|
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:27:53 +00:00
|
|
|
// root is not part of g.Objects, so we can't add it here
|
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)
|
|
|
|
|
objChildrenArray[obj] = obj.ChildrenArray
|
|
|
|
|
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-01 21:27:53 +00:00
|
|
|
sd := newSequenceDiagram(objChildrenArray[obj], edges)
|
2022-12-01 00:11:02 +00:00
|
|
|
sd.layout()
|
|
|
|
|
sdMock.Box = geo.NewBox(nil, sd.getWidth(), sd.getHeight())
|
|
|
|
|
sequenceDiagrams[obj] = sd
|
|
|
|
|
} 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-11-28 22:01:22 +00:00
|
|
|
}
|
2022-11-26 06:32:04 +00:00
|
|
|
|
2022-12-01 21:27:53 +00:00
|
|
|
// removes the edges
|
2022-12-01 00:11:02 +00:00
|
|
|
newEdges := make([]*d2graph.Edge, 0, len(g.Edges)-len(edgesToRemove))
|
|
|
|
|
for _, edge := range g.Edges {
|
|
|
|
|
if _, exists := edgesToRemove[edge]; !exists {
|
|
|
|
|
newEdges = append(newEdges, edge)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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)
|
|
|
|
|
newObjects := make([]*d2graph.Object, 0, len(objectsToKeep))
|
|
|
|
|
for _, obj := range g.Objects {
|
|
|
|
|
if _, exists := objectsToKeep[obj]; exists {
|
|
|
|
|
newObjects = append(newObjects, obj)
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-12-01 21:27:53 +00:00
|
|
|
|
2022-12-01 21:39:48 +00:00
|
|
|
g.Objects = newObjects
|
2022-12-01 21:27:53 +00:00
|
|
|
g.Edges = newEdges
|
2022-12-02 01:11:41 +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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// restores objects & edges
|
2022-12-01 18:33:20 +00:00
|
|
|
g.Edges = oldEdges
|
|
|
|
|
g.Objects = oldObjects
|
2022-12-01 21:27:53 +00:00
|
|
|
|
2022-12-01 00:11:02 +00:00
|
|
|
for obj, children := range objChildrenArray {
|
2022-12-01 21:27:53 +00:00
|
|
|
// shift the sequence diagrams as they are always placed at (0, 0)
|
2022-12-01 00:11:02 +00:00
|
|
|
sdMock := obj.ChildrenArray[0]
|
|
|
|
|
sequenceDiagrams[obj].shift(sdMock.TopLeft)
|
2022-12-01 21:27:53 +00:00
|
|
|
|
|
|
|
|
// restore children
|
2022-12-01 00:11:02 +00:00
|
|
|
obj.Children = make(map[string]*d2graph.Object)
|
|
|
|
|
for _, child := range children {
|
|
|
|
|
obj.Children[child.ID] = child
|
|
|
|
|
}
|
|
|
|
|
obj.ChildrenArray = children
|
2022-12-01 21:27:53 +00:00
|
|
|
|
|
|
|
|
// add lifeline edges
|
2022-12-01 18:33:20 +00:00
|
|
|
g.Edges = append(g.Edges, sequenceDiagrams[obj].lifelines...)
|
2022-12-01 00:11:02 +00:00
|
|
|
}
|
2022-11-26 06:32:04 +00:00
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|