d2/d2layouts/d2sequence/layout.go

210 lines
6.8 KiB
Go
Raw Normal View History

package d2sequence
import (
"context"
"sort"
2022-12-01 00:11:02 +00:00
"strings"
2023-01-28 07:05:42 +00:00
"oss.terrastruct.com/util-go/go2"
"oss.terrastruct.com/d2/d2graph"
2022-12-01 00:11:02 +00:00
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/lib/geo"
2022-12-02 17:11:02 +00:00
"oss.terrastruct.com/d2/lib/label"
)
2023-04-03 18:45:45 +00:00
// Layout runs the sequence diagram layout engine on objects of shape sequence_diagram
//
// 1. Traverse graph from root, skip objects with shape not `sequence_diagram`
// 2. Construct a sequence diagram from all descendant objects and edges
// 3. Remove those objects and edges from the main graph
// 4. Run layout on sequence diagrams
// 5. Set the resulting dimensions to the main graph shape
// 6. Run core layouts (still without sequence diagram innards)
// 7. Put back sequence diagram innards in correct location
func Layout(ctx context.Context, g *d2graph.Graph, layout d2graph.LayoutGraph) error {
sequenceDiagrams, objectOrder, edgeOrder, err := WithoutSequenceDiagrams(ctx, g)
if err != nil {
return err
}
if g.Root.IsSequenceDiagram() {
// the sequence diagram is the only layout engine if the whole diagram is
// shape: sequence_diagram
g.Root.TopLeft = geo.NewPoint(0, 0)
} else if err := layout(ctx, g); err != nil {
return err
}
cleanup(g, sequenceDiagrams, objectOrder, edgeOrder)
return nil
}
2022-12-27 04:51:37 +00:00
func WithoutSequenceDiagrams(ctx context.Context, g *d2graph.Graph) (map[string]*sequenceDiagram, map[string]int, map[string]int, error) {
objectsToRemove := make(map[*d2graph.Object]struct{})
2022-12-01 00:11:02 +00:00
edgesToRemove := make(map[*d2graph.Edge]struct{})
2022-12-02 02:08:34 +00:00
sequenceDiagrams := make(map[string]*sequenceDiagram)
2022-12-01 00:11:02 +00:00
2023-01-19 23:03:52 +00:00
if len(g.Objects) > 0 {
queue := make([]*d2graph.Object, 1, len(g.Objects))
queue[0] = g.Root
for len(queue) > 0 {
obj := queue[0]
queue = queue[1:]
if len(obj.ChildrenArray) == 0 {
continue
}
2023-04-14 03:04:55 +00:00
if obj.Shape.Value != d2target.ShapeSequenceDiagram {
2023-01-19 23:03:52 +00:00
queue = append(queue, obj.ChildrenArray...)
continue
}
2023-01-19 23:03:52 +00:00
sd, err := layoutSequenceDiagram(g, obj)
if err != nil {
return nil, nil, nil, err
}
obj.Children = make(map[string]*d2graph.Object)
obj.ChildrenArray = nil
obj.Box = geo.NewBox(nil, sd.getWidth()+GROUP_CONTAINER_PADDING*2, sd.getHeight()+GROUP_CONTAINER_PADDING*2)
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
sequenceDiagrams[obj.AbsID()] = sd
2023-01-19 23:03:52 +00:00
for _, edge := range sd.messages {
edgesToRemove[edge] = struct{}{}
}
for _, obj := range sd.actors {
objectsToRemove[obj] = struct{}{}
}
for _, obj := range sd.notes {
objectsToRemove[obj] = struct{}{}
}
for _, obj := range sd.groups {
objectsToRemove[obj] = struct{}{}
}
for _, obj := range sd.spans {
objectsToRemove[obj] = struct{}{}
}
2022-12-01 00:11:02 +00:00
}
}
layoutEdges, edgeOrder := getLayoutEdges(g, edgesToRemove)
2022-12-02 02:08:34 +00:00
g.Edges = layoutEdges
layoutObjects, objectOrder := getLayoutObjects(g, objectsToRemove)
2022-12-26 00:33:32 +00:00
// TODO this isn't a proper deletion because the objects still appear as children of the object
2022-12-02 02:08:34 +00:00
g.Objects = layoutObjects
2022-12-01 21:27:53 +00:00
2022-12-27 04:51:37 +00:00
return sequenceDiagrams, objectOrder, edgeOrder, nil
}
// layoutSequenceDiagram finds the edges inside the sequence diagram and performs the layout on the object descendants
2022-12-03 17:38:08 +00:00
func layoutSequenceDiagram(g *d2graph.Graph, obj *d2graph.Object) (*sequenceDiagram, error) {
var edges []*d2graph.Edge
for _, edge := range g.Edges {
// both Src and Dst must be inside the sequence diagram
2022-12-19 04:03:07 +00:00
if obj == g.Root || (strings.HasPrefix(edge.Src.AbsID(), obj.AbsID()+".") && strings.HasPrefix(edge.Dst.AbsID(), obj.AbsID()+".")) {
edges = append(edges, edge)
}
}
sd, err := newSequenceDiagram(obj.ChildrenArray, edges)
if err != nil {
return nil, err
}
err = sd.layout()
2022-12-03 17:38:08 +00:00
return sd, err
}
func getLayoutEdges(g *d2graph.Graph, toRemove map[*d2graph.Edge]struct{}) ([]*d2graph.Edge, map[string]int) {
edgeOrder := make(map[string]int)
layoutEdges := make([]*d2graph.Edge, 0, len(g.Edges)-len(toRemove))
2023-01-28 07:05:42 +00:00
for i, edge := range g.Edges {
edgeOrder[edge.AbsID()] = i
if _, exists := toRemove[edge]; !exists {
layoutEdges = append(layoutEdges, edge)
}
}
return layoutEdges, edgeOrder
}
func getLayoutObjects(g *d2graph.Graph, toRemove map[*d2graph.Object]struct{}) ([]*d2graph.Object, map[string]int) {
objectOrder := make(map[string]int)
layoutObjects := make([]*d2graph.Object, 0, len(toRemove))
for i, obj := range g.Objects {
objectOrder[obj.AbsID()] = i
if _, exists := toRemove[obj]; !exists {
layoutObjects = append(layoutObjects, obj)
}
}
return layoutObjects, objectOrder
}
2022-12-02 19:53:40 +00:00
// cleanup restores the graph after the core layout engine finishes
// - translating the sequence diagram to its position placed by the core layout engine
// - restore the children of the sequence diagram graph object
// - adds the sequence diagram edges (messages) back to the graph
// - adds the sequence diagram lifelines to the graph edges
// - adds the sequence diagram descendants back to the graph objects
// - sorts edges and objects to their original graph order
func cleanup(g *d2graph.Graph, sequenceDiagrams map[string]*sequenceDiagram, objectsOrder, edgesOrder map[string]int) {
2022-12-02 18:23:11 +00:00
var objects []*d2graph.Object
2022-12-03 03:15:04 +00:00
if g.Root.IsSequenceDiagram() {
2022-12-02 18:23:11 +00:00
objects = []*d2graph.Object{g.Root}
} else {
objects = g.Objects
}
for _, obj := range objects {
2023-04-03 18:45:45 +00:00
sd, exists := sequenceDiagrams[obj.AbsID()]
if !exists {
2022-12-02 02:08:34 +00:00
continue
}
2022-12-02 17:11:02 +00:00
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
2022-12-05 21:53:09 +00:00
// shift the sequence diagrams as they are always placed at (0, 0) with some padding
sd.shift(
geo.NewPoint(
obj.TopLeft.X+GROUP_CONTAINER_PADDING,
obj.TopLeft.Y+GROUP_CONTAINER_PADDING,
),
)
2022-12-01 21:27:53 +00:00
2022-12-02 17:11:02 +00:00
obj.Children = make(map[string]*d2graph.Object)
2022-12-05 21:53:09 +00:00
obj.ChildrenArray = make([]*d2graph.Object, 0)
for _, child := range sd.actors {
2023-04-06 22:32:09 +00:00
obj.Children[strings.ToLower(child.ID)] = child
2022-12-05 21:53:09 +00:00
obj.ChildrenArray = append(obj.ChildrenArray, child)
}
for _, child := range sd.groups {
2022-12-05 22:30:40 +00:00
if child.Parent.AbsID() == obj.AbsID() {
2023-04-06 22:32:09 +00:00
obj.Children[strings.ToLower(child.ID)] = child
2022-12-05 21:53:09 +00:00
obj.ChildrenArray = append(obj.ChildrenArray, child)
}
2022-12-01 00:11:02 +00:00
}
2022-12-01 21:27:53 +00:00
2023-04-03 18:45:45 +00:00
g.Edges = append(g.Edges, sd.messages...)
g.Edges = append(g.Edges, sd.lifelines...)
g.Objects = append(g.Objects, sd.actors...)
g.Objects = append(g.Objects, sd.notes...)
g.Objects = append(g.Objects, sd.groups...)
g.Objects = append(g.Objects, sd.spans...)
2022-12-01 00:11:02 +00:00
}
// no new objects, so just keep the same position
sort.SliceStable(g.Objects, func(i, j int) bool {
return objectsOrder[g.Objects[i].AbsID()] < objectsOrder[g.Objects[j].AbsID()]
})
// sequence diagrams add lifelines, and they must be the last ones in this slice
sort.SliceStable(g.Edges, func(i, j int) bool {
iOrder, iExists := edgesOrder[g.Edges[i].AbsID()]
jOrder, jExists := edgesOrder[g.Edges[j].AbsID()]
if iExists && jExists {
return iOrder < jOrder
} else if iExists && !jExists {
return true
}
// either both don't exist or i doesn't exist and j exists
return false
})
}