Merge pull request #317 from ejulio-ts/nested-sequence-diagrams
layout: sequence diagrams
|
|
@ -1,5 +1,7 @@
|
||||||
#### Features 🚀
|
#### Features 🚀
|
||||||
|
|
||||||
|
- Sequence diagrams are now supported. See [docs](https://d2lang.com/tour/sequence-diagrams) for more.
|
||||||
|
[#99](https://github.com/terrastruct/d2/issues/99)
|
||||||
- Formatting of d2 scripts is supported on the CLI with the `fmt` subcommand.
|
- Formatting of d2 scripts is supported on the CLI with the `fmt` subcommand.
|
||||||
[#292](https://github.com/terrastruct/d2/pull/292)
|
[#292](https://github.com/terrastruct/d2/pull/292)
|
||||||
- Latex is now supported. See [docs](https://d2lang.com/tour/text) for more.
|
- Latex is now supported. See [docs](https://d2lang.com/tour/text) for more.
|
||||||
|
|
|
||||||
|
|
@ -3,22 +3,22 @@ package d2sequence
|
||||||
// leaves at least 25 units of space on the left/right when computing the space required between actors
|
// leaves at least 25 units of space on the left/right when computing the space required between actors
|
||||||
const HORIZONTAL_PAD = 50.
|
const HORIZONTAL_PAD = 50.
|
||||||
|
|
||||||
// leaves at least 25 units of space on the top/bottom when computing the space required between edges
|
// leaves at least 25 units of space on the top/bottom when computing the space required between messages
|
||||||
const VERTICAL_PAD = 50.
|
const VERTICAL_PAD = 50.
|
||||||
|
|
||||||
const MIN_ACTOR_DISTANCE = 200.
|
const MIN_ACTOR_DISTANCE = 200.
|
||||||
|
|
||||||
// min vertical distance between edges
|
// min vertical distance between messages
|
||||||
const MIN_EDGE_DISTANCE = 100.
|
const MIN_MESSAGE_DISTANCE = 100.
|
||||||
|
|
||||||
// default size
|
// default size
|
||||||
const SPAN_WIDTH = 20.
|
const SPAN_WIDTH = 20.
|
||||||
|
|
||||||
// small pad so that edges don't touch lifelines and spans
|
// small pad so that messages don't touch lifelines and spans
|
||||||
const SPAN_EDGE_PAD = 5.
|
const SPAN_MESSAGE_PAD = 5.
|
||||||
|
|
||||||
// as the spans start getting nested, their size grows
|
// as the spans start getting nested, their size grows
|
||||||
const SPAN_DEPTH_GROW_FACTOR = 10.
|
const SPAN_DEPTH_GROW_FACTOR = 10.
|
||||||
|
|
||||||
// when a span has a single edge
|
// when a span has a single messages
|
||||||
const MIN_SPAN_HEIGHT = MIN_EDGE_DISTANCE / 2.
|
const MIN_SPAN_HEIGHT = MIN_MESSAGE_DISTANCE / 2.
|
||||||
|
|
|
||||||
|
|
@ -2,280 +2,169 @@ package d2sequence
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"oss.terrastruct.com/util-go/go2"
|
"oss.terrastruct.com/util-go/go2"
|
||||||
|
|
||||||
"oss.terrastruct.com/d2/d2graph"
|
"oss.terrastruct.com/d2/d2graph"
|
||||||
|
"oss.terrastruct.com/d2/d2target"
|
||||||
"oss.terrastruct.com/d2/lib/geo"
|
"oss.terrastruct.com/d2/lib/geo"
|
||||||
"oss.terrastruct.com/d2/lib/label"
|
"oss.terrastruct.com/d2/lib/label"
|
||||||
"oss.terrastruct.com/d2/lib/shape"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Layout(ctx context.Context, g *d2graph.Graph) (err error) {
|
// Layout runs the sequence diagram layout engine on objects of shape sequence_diagram
|
||||||
sd := &sequenceDiagram{
|
//
|
||||||
graph: g,
|
// 1. Traverse graph from root, skip objects with shape not `sequence_diagram`
|
||||||
objectRank: make(map[*d2graph.Object]int),
|
// 2. Construct a sequence diagram from all descendant objects and edges
|
||||||
minEdgeRank: make(map[*d2graph.Object]int),
|
// 3. Remove those objects and edges from the main graph
|
||||||
maxEdgeRank: make(map[*d2graph.Object]int),
|
// 4. Run layout on sequence diagrams
|
||||||
edgeYStep: MIN_EDGE_DISTANCE,
|
// 5. Set the resulting dimensions to the main graph shape
|
||||||
actorXStep: MIN_ACTOR_DISTANCE,
|
// 6. Run core layouts (still without sequence diagram innards)
|
||||||
maxActorHeight: 0.,
|
// 7. Put back sequence diagram innards in correct location
|
||||||
}
|
func Layout(ctx context.Context, g *d2graph.Graph, layout func(ctx context.Context, g *d2graph.Graph) error) error {
|
||||||
|
objectsToRemove := make(map[*d2graph.Object]struct{})
|
||||||
|
edgesToRemove := make(map[*d2graph.Edge]struct{})
|
||||||
|
sequenceDiagrams := make(map[string]*sequenceDiagram)
|
||||||
|
|
||||||
sd.init()
|
queue := make([]*d2graph.Object, 1, len(g.Objects))
|
||||||
sd.placeActors()
|
queue[0] = g.Root
|
||||||
sd.placeSpans()
|
|
||||||
sd.routeEdges()
|
|
||||||
sd.addLifelineEdges()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type sequenceDiagram struct {
|
|
||||||
graph *d2graph.Graph
|
|
||||||
|
|
||||||
edges []*d2graph.Edge
|
|
||||||
actors []*d2graph.Object
|
|
||||||
spans []*d2graph.Object
|
|
||||||
|
|
||||||
// can be either actors or spans
|
|
||||||
// rank: left to right position of actors/spans (spans have the same rank as their parents)
|
|
||||||
objectRank map[*d2graph.Object]int
|
|
||||||
|
|
||||||
// keep track of the first and last edge of a given actor
|
|
||||||
// the edge rank is the order in which it appears from top to bottom
|
|
||||||
minEdgeRank map[*d2graph.Object]int
|
|
||||||
maxEdgeRank map[*d2graph.Object]int
|
|
||||||
|
|
||||||
edgeYStep float64
|
|
||||||
actorXStep float64
|
|
||||||
maxActorHeight float64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sd *sequenceDiagram) init() {
|
|
||||||
sd.edges = make([]*d2graph.Edge, len(sd.graph.Edges))
|
|
||||||
copy(sd.edges, sd.graph.Edges)
|
|
||||||
|
|
||||||
queue := make([]*d2graph.Object, len(sd.graph.Root.ChildrenArray))
|
|
||||||
copy(queue, sd.graph.Root.ChildrenArray)
|
|
||||||
for len(queue) > 0 {
|
for len(queue) > 0 {
|
||||||
obj := queue[0]
|
obj := queue[0]
|
||||||
queue = queue[1:]
|
queue = queue[1:]
|
||||||
|
if obj.Attributes.Shape.Value != d2target.ShapeSequenceDiagram {
|
||||||
if sd.isActor(obj) {
|
queue = append(queue, obj.ChildrenArray...)
|
||||||
sd.actors = append(sd.actors, obj)
|
continue
|
||||||
sd.objectRank[obj] = len(sd.actors)
|
|
||||||
sd.maxActorHeight = math.Max(sd.maxActorHeight, obj.Height)
|
|
||||||
} else {
|
|
||||||
// spans are always rectangles and have no labels
|
|
||||||
obj.Attributes.Label = d2graph.Scalar{Value: ""}
|
|
||||||
obj.Attributes.Shape = d2graph.Scalar{Value: shape.SQUARE_TYPE}
|
|
||||||
sd.spans = append(sd.spans, obj)
|
|
||||||
sd.objectRank[obj] = sd.objectRank[obj.Parent]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
queue = append(queue, obj.ChildrenArray...)
|
sd := layoutSequenceDiagram(g, obj)
|
||||||
|
obj.Children = make(map[string]*d2graph.Object)
|
||||||
|
obj.ChildrenArray = nil
|
||||||
|
obj.Box = geo.NewBox(nil, sd.getWidth(), sd.getHeight())
|
||||||
|
sequenceDiagrams[obj.AbsID()] = sd
|
||||||
|
|
||||||
|
for _, edge := range sd.messages {
|
||||||
|
edgesToRemove[edge] = struct{}{}
|
||||||
|
}
|
||||||
|
for _, obj := range sd.actors {
|
||||||
|
objectsToRemove[obj] = struct{}{}
|
||||||
|
}
|
||||||
|
for _, obj := range sd.spans {
|
||||||
|
objectsToRemove[obj] = struct{}{}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for rank, edge := range sd.edges {
|
layoutEdges, edgeOrder := getLayoutEdges(g, edgesToRemove)
|
||||||
sd.edgeYStep = math.Max(sd.edgeYStep, float64(edge.LabelDimensions.Height))
|
g.Edges = layoutEdges
|
||||||
|
layoutObjects, objectOrder := getLayoutObjects(g, objectsToRemove)
|
||||||
|
g.Objects = layoutObjects
|
||||||
|
|
||||||
sd.setMinMaxEdgeRank(edge.Src, rank)
|
if isRootSequenceDiagram(g) {
|
||||||
sd.setMinMaxEdgeRank(edge.Dst, rank)
|
// the sequence diagram is the only layout engine if the whole diagram is
|
||||||
|
// shape: sequence_diagram
|
||||||
// ensures that long labels, spanning over multiple actors, don't make for large gaps between actors
|
g.Root.TopLeft = geo.NewPoint(0, 0)
|
||||||
// by distributing the label length across the actors rank difference
|
} else if err := layout(ctx, g); err != nil {
|
||||||
rankDiff := math.Abs(float64(sd.objectRank[edge.Src]) - float64(sd.objectRank[edge.Dst]))
|
return err
|
||||||
distributedLabelWidth := float64(edge.LabelDimensions.Width) / rankDiff
|
|
||||||
sd.actorXStep = math.Max(sd.actorXStep, distributedLabelWidth+HORIZONTAL_PAD)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sd.maxActorHeight += VERTICAL_PAD
|
cleanup(g, sequenceDiagrams, objectOrder, edgeOrder)
|
||||||
sd.edgeYStep += VERTICAL_PAD
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sd *sequenceDiagram) setMinMaxEdgeRank(actor *d2graph.Object, rank int) {
|
func isRootSequenceDiagram(g *d2graph.Graph) bool {
|
||||||
if minRank, exists := sd.minEdgeRank[actor]; exists {
|
return g.Root.Attributes.Shape.Value == d2target.ShapeSequenceDiagram
|
||||||
sd.minEdgeRank[actor] = go2.IntMin(minRank, rank)
|
}
|
||||||
|
|
||||||
|
// layoutSequenceDiagram finds the edges inside the sequence diagram and performs the layout on the object descendants
|
||||||
|
func layoutSequenceDiagram(g *d2graph.Graph, obj *d2graph.Object) *sequenceDiagram {
|
||||||
|
var edges []*d2graph.Edge
|
||||||
|
for _, edge := range g.Edges {
|
||||||
|
// both Src and Dst must be inside the sequence diagram
|
||||||
|
if strings.HasPrefix(edge.Src.AbsID(), obj.AbsID()) && strings.HasPrefix(edge.Dst.AbsID(), obj.AbsID()) {
|
||||||
|
edges = append(edges, edge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sd := newSequenceDiagram(obj.ChildrenArray, edges)
|
||||||
|
sd.layout()
|
||||||
|
return sd
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
var objects []*d2graph.Object
|
||||||
|
if isRootSequenceDiagram(g) {
|
||||||
|
objects = []*d2graph.Object{g.Root}
|
||||||
} else {
|
} else {
|
||||||
sd.minEdgeRank[actor] = rank
|
objects = g.Objects
|
||||||
|
}
|
||||||
|
for _, obj := range objects {
|
||||||
|
if _, exists := sequenceDiagrams[obj.AbsID()]; !exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
|
||||||
|
sd := sequenceDiagrams[obj.AbsID()]
|
||||||
|
|
||||||
|
// shift the sequence diagrams as they are always placed at (0, 0)
|
||||||
|
sd.shift(obj.TopLeft)
|
||||||
|
|
||||||
|
obj.Children = make(map[string]*d2graph.Object)
|
||||||
|
for _, child := range sd.actors {
|
||||||
|
obj.Children[child.ID] = child
|
||||||
|
}
|
||||||
|
obj.ChildrenArray = sd.actors
|
||||||
|
|
||||||
|
g.Edges = append(g.Edges, sequenceDiagrams[obj.AbsID()].messages...)
|
||||||
|
g.Edges = append(g.Edges, sequenceDiagrams[obj.AbsID()].lifelines...)
|
||||||
|
g.Objects = append(g.Objects, sequenceDiagrams[obj.AbsID()].actors...)
|
||||||
|
g.Objects = append(g.Objects, sequenceDiagrams[obj.AbsID()].spans...)
|
||||||
}
|
}
|
||||||
|
|
||||||
sd.maxEdgeRank[actor] = go2.IntMax(sd.maxEdgeRank[actor], rank)
|
// 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()]
|
||||||
// placeActors places actors bottom aligned, side by side
|
|
||||||
func (sd *sequenceDiagram) placeActors() {
|
|
||||||
x := 0.
|
|
||||||
for _, actors := range sd.actors {
|
|
||||||
yOffset := sd.maxActorHeight - actors.Height
|
|
||||||
actors.TopLeft = geo.NewPoint(x, yOffset)
|
|
||||||
x += actors.Width + sd.actorXStep
|
|
||||||
actors.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// addLifelineEdges adds a new edge for each actor in the graph that represents the its lifeline
|
|
||||||
// ┌──────────────┐
|
|
||||||
// │ actor │
|
|
||||||
// └──────┬───────┘
|
|
||||||
// │
|
|
||||||
// │ lifeline
|
|
||||||
// │
|
|
||||||
// │
|
|
||||||
func (sd *sequenceDiagram) addLifelineEdges() {
|
|
||||||
endY := sd.getEdgeY(len(sd.edges))
|
|
||||||
for _, actor := range sd.actors {
|
|
||||||
actorBottom := actor.Center()
|
|
||||||
actorBottom.Y = actor.TopLeft.Y + actor.Height
|
|
||||||
actorLifelineEnd := actor.Center()
|
|
||||||
actorLifelineEnd.Y = endY
|
|
||||||
sd.graph.Edges = append(sd.graph.Edges, &d2graph.Edge{
|
|
||||||
Attributes: d2graph.Attributes{
|
|
||||||
Style: d2graph.Style{
|
|
||||||
StrokeDash: &d2graph.Scalar{Value: "10"},
|
|
||||||
Stroke: actor.Attributes.Style.Stroke,
|
|
||||||
StrokeWidth: actor.Attributes.Style.StrokeWidth,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Src: actor,
|
|
||||||
SrcArrow: false,
|
|
||||||
Dst: &d2graph.Object{
|
|
||||||
ID: actor.ID + fmt.Sprintf("-lifeline-end-%d", go2.StringToIntHash(actor.ID+"-lifeline-end")),
|
|
||||||
},
|
|
||||||
DstArrow: false,
|
|
||||||
Route: []*geo.Point{actorBottom, actorLifelineEnd},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// placeSpans places spans over the object lifeline
|
|
||||||
// ┌──────────┐
|
|
||||||
// │ actor │
|
|
||||||
// └────┬─────┘
|
|
||||||
// ┌─┴──┐
|
|
||||||
// │ │
|
|
||||||
// |span|
|
|
||||||
// │ │
|
|
||||||
// └─┬──┘
|
|
||||||
// │
|
|
||||||
// lifeline
|
|
||||||
// │
|
|
||||||
func (sd *sequenceDiagram) placeSpans() {
|
|
||||||
// quickly find the span center X
|
|
||||||
rankToX := make(map[int]float64)
|
|
||||||
for _, actor := range sd.actors {
|
|
||||||
rankToX[sd.objectRank[actor]] = actor.Center().X
|
|
||||||
}
|
|
||||||
|
|
||||||
// places spans from most to least nested
|
|
||||||
// the order is important because the only way a child span exists is if there'e an edge to it
|
|
||||||
// however, the parent span might not have an edge to it and then its position is based on the child position
|
|
||||||
// or, there can be edge to it, but it comes after the child one meaning the top left position is still based on the child
|
|
||||||
// and not on its own edge
|
|
||||||
spanFromMostNested := make([]*d2graph.Object, len(sd.spans))
|
|
||||||
copy(spanFromMostNested, sd.spans)
|
|
||||||
sort.SliceStable(spanFromMostNested, func(i, j int) bool {
|
|
||||||
return spanFromMostNested[i].Level() > spanFromMostNested[j].Level()
|
|
||||||
})
|
})
|
||||||
for _, span := range spanFromMostNested {
|
|
||||||
// finds the position based on children
|
|
||||||
minChildY := math.Inf(1)
|
|
||||||
maxChildY := math.Inf(-1)
|
|
||||||
for _, child := range span.ChildrenArray {
|
|
||||||
minChildY = math.Min(minChildY, child.TopLeft.Y)
|
|
||||||
maxChildY = math.Max(maxChildY, child.TopLeft.Y+child.Height)
|
|
||||||
}
|
|
||||||
|
|
||||||
// finds the position if there are edges to this span
|
// sequence diagrams add lifelines, and they must be the last ones in this slice
|
||||||
minEdgeY := math.Inf(1)
|
sort.SliceStable(g.Edges, func(i, j int) bool {
|
||||||
if minRank, exists := sd.minEdgeRank[span]; exists {
|
iOrder, iExists := edgesOrder[g.Edges[i].AbsID()]
|
||||||
minEdgeY = sd.getEdgeY(minRank)
|
jOrder, jExists := edgesOrder[g.Edges[j].AbsID()]
|
||||||
|
if iExists && jExists {
|
||||||
|
return iOrder < jOrder
|
||||||
|
} else if iExists && !jExists {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
maxEdgeY := math.Inf(-1)
|
// either both don't exist or i doesn't exist and j exists
|
||||||
if maxRank, exists := sd.maxEdgeRank[span]; exists {
|
return false
|
||||||
maxEdgeY = sd.getEdgeY(maxRank)
|
})
|
||||||
}
|
|
||||||
|
|
||||||
// if it is the same as the child top left, add some padding
|
|
||||||
minY := math.Min(minEdgeY, minChildY)
|
|
||||||
if minY == minChildY {
|
|
||||||
minY -= SPAN_DEPTH_GROW_FACTOR
|
|
||||||
} else {
|
|
||||||
minY -= SPAN_EDGE_PAD
|
|
||||||
}
|
|
||||||
maxY := math.Max(maxEdgeY, maxChildY)
|
|
||||||
if maxY == maxChildY {
|
|
||||||
maxY += SPAN_DEPTH_GROW_FACTOR
|
|
||||||
} else {
|
|
||||||
maxY += SPAN_EDGE_PAD
|
|
||||||
}
|
|
||||||
|
|
||||||
height := math.Max(maxY-minY, MIN_SPAN_HEIGHT)
|
|
||||||
// -2 because the actors count as level 1 making the first level span getting 2*SPAN_DEPTH_GROW_FACTOR
|
|
||||||
width := SPAN_WIDTH + (float64(span.Level()-2) * SPAN_DEPTH_GROW_FACTOR)
|
|
||||||
x := rankToX[sd.objectRank[span]] - (width / 2.)
|
|
||||||
span.Box = geo.NewBox(geo.NewPoint(x, minY), width, height)
|
|
||||||
span.ZIndex = 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// routeEdges routes horizontal edges from Src to Dst
|
|
||||||
func (sd *sequenceDiagram) routeEdges() {
|
|
||||||
for rank, edge := range sd.edges {
|
|
||||||
isLeftToRight := edge.Src.TopLeft.X < edge.Dst.TopLeft.X
|
|
||||||
|
|
||||||
// finds the proper anchor point based on the edge direction
|
|
||||||
var startX, endX float64
|
|
||||||
if sd.isActor(edge.Src) {
|
|
||||||
startX = edge.Src.Center().X
|
|
||||||
} else if isLeftToRight {
|
|
||||||
startX = edge.Src.TopLeft.X + edge.Src.Width
|
|
||||||
} else {
|
|
||||||
startX = edge.Src.TopLeft.X
|
|
||||||
}
|
|
||||||
|
|
||||||
if sd.isActor(edge.Dst) {
|
|
||||||
endX = edge.Dst.Center().X
|
|
||||||
} else if isLeftToRight {
|
|
||||||
endX = edge.Dst.TopLeft.X
|
|
||||||
} else {
|
|
||||||
endX = edge.Dst.TopLeft.X + edge.Dst.Width
|
|
||||||
}
|
|
||||||
|
|
||||||
if isLeftToRight {
|
|
||||||
startX += SPAN_EDGE_PAD
|
|
||||||
endX -= SPAN_EDGE_PAD
|
|
||||||
} else {
|
|
||||||
startX -= SPAN_EDGE_PAD
|
|
||||||
endX += SPAN_EDGE_PAD
|
|
||||||
}
|
|
||||||
|
|
||||||
edgeY := sd.getEdgeY(rank)
|
|
||||||
edge.Route = []*geo.Point{
|
|
||||||
geo.NewPoint(startX, edgeY),
|
|
||||||
geo.NewPoint(endX, edgeY),
|
|
||||||
}
|
|
||||||
|
|
||||||
if edge.Attributes.Label.Value != "" {
|
|
||||||
if isLeftToRight {
|
|
||||||
edge.LabelPosition = go2.Pointer(string(label.OutsideTopCenter))
|
|
||||||
} else {
|
|
||||||
// the label will be placed above the edge because the orientation is based on the edge normal vector
|
|
||||||
edge.LabelPosition = go2.Pointer(string(label.OutsideBottomCenter))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sd *sequenceDiagram) getEdgeY(rank int) float64 {
|
|
||||||
// +1 so that the first edge has the top padding for its label
|
|
||||||
return ((float64(rank) + 1.) * sd.edgeYStep) + sd.maxActorHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sd *sequenceDiagram) isActor(obj *d2graph.Object) bool {
|
|
||||||
return obj.Parent == sd.graph.Root
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"oss.terrastruct.com/d2/d2graph"
|
"oss.terrastruct.com/d2/d2graph"
|
||||||
|
"oss.terrastruct.com/d2/d2target"
|
||||||
"oss.terrastruct.com/d2/lib/geo"
|
"oss.terrastruct.com/d2/lib/geo"
|
||||||
"oss.terrastruct.com/d2/lib/label"
|
"oss.terrastruct.com/d2/lib/label"
|
||||||
"oss.terrastruct.com/d2/lib/log"
|
"oss.terrastruct.com/d2/lib/log"
|
||||||
|
|
@ -25,6 +26,7 @@ func TestBasicSequenceDiagram(t *testing.T) {
|
||||||
// ◄───────────────────────┤
|
// ◄───────────────────────┤
|
||||||
// │ │
|
// │ │
|
||||||
g := d2graph.NewGraph(nil)
|
g := d2graph.NewGraph(nil)
|
||||||
|
g.Root.Attributes.Shape = d2graph.Scalar{Value: d2target.ShapeSequenceDiagram}
|
||||||
n1 := g.Root.EnsureChild([]string{"n1"})
|
n1 := g.Root.EnsureChild([]string{"n1"})
|
||||||
n1.Box = geo.NewBox(nil, 100, 100)
|
n1.Box = geo.NewBox(nil, 100, 100)
|
||||||
n2 := g.Root.EnsureChild([]string{"n2"})
|
n2 := g.Root.EnsureChild([]string{"n2"})
|
||||||
|
|
@ -32,32 +34,46 @@ func TestBasicSequenceDiagram(t *testing.T) {
|
||||||
|
|
||||||
g.Edges = []*d2graph.Edge{
|
g.Edges = []*d2graph.Edge{
|
||||||
{
|
{
|
||||||
Src: n1,
|
Src: n1,
|
||||||
Dst: n2,
|
Dst: n2,
|
||||||
|
Index: 0,
|
||||||
Attributes: d2graph.Attributes{
|
Attributes: d2graph.Attributes{
|
||||||
Label: d2graph.Scalar{Value: "left to right"},
|
Label: d2graph.Scalar{Value: "left to right"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Src: n2,
|
Src: n2,
|
||||||
Dst: n1,
|
Dst: n1,
|
||||||
|
Index: 0,
|
||||||
Attributes: d2graph.Attributes{
|
Attributes: d2graph.Attributes{
|
||||||
Label: d2graph.Scalar{Value: "right to left"},
|
Label: d2graph.Scalar{Value: "right to left"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Src: n1,
|
Src: n1,
|
||||||
Dst: n2,
|
Dst: n2,
|
||||||
|
Index: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Src: n2,
|
Src: n2,
|
||||||
Dst: n1,
|
Dst: n1,
|
||||||
|
Index: 1,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
nEdges := len(g.Edges)
|
nEdges := len(g.Edges)
|
||||||
|
|
||||||
ctx := log.WithTB(context.Background(), t, nil)
|
ctx := log.WithTB(context.Background(), t, nil)
|
||||||
Layout(ctx, g)
|
Layout(ctx, g, func(ctx context.Context, g *d2graph.Graph) error {
|
||||||
|
// just set some position as if it had been properly placed
|
||||||
|
for _, obj := range g.Objects {
|
||||||
|
obj.TopLeft = geo.NewPoint(0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, edge := range g.Edges {
|
||||||
|
edge.Route = []*geo.Point{geo.NewPoint(1, 1)}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
// asserts that actors were placed in the expected x order and at y=0
|
// asserts that actors were placed in the expected x order and at y=0
|
||||||
actors := []*d2graph.Object{
|
actors := []*d2graph.Object{
|
||||||
|
|
@ -92,19 +108,19 @@ func TestBasicSequenceDiagram(t *testing.T) {
|
||||||
}
|
}
|
||||||
if edge.Src.TopLeft.X < edge.Dst.TopLeft.X {
|
if edge.Src.TopLeft.X < edge.Dst.TopLeft.X {
|
||||||
// left to right
|
// left to right
|
||||||
if edge.Route[0].X != edge.Src.Center().X+SPAN_EDGE_PAD {
|
if edge.Route[0].X != edge.Src.Center().X+SPAN_MESSAGE_PAD {
|
||||||
t.Fatalf("expected edge[%d] x to be at the actor center", i)
|
t.Fatalf("expected edge[%d] x to be at the actor center", i)
|
||||||
}
|
}
|
||||||
|
|
||||||
if edge.Route[1].X != edge.Dst.Center().X-SPAN_EDGE_PAD {
|
if edge.Route[1].X != edge.Dst.Center().X-SPAN_MESSAGE_PAD {
|
||||||
t.Fatalf("expected edge[%d] x to be at the actor center", i)
|
t.Fatalf("expected edge[%d] x to be at the actor center", i)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if edge.Route[0].X != edge.Src.Center().X-SPAN_EDGE_PAD {
|
if edge.Route[0].X != edge.Src.Center().X-SPAN_MESSAGE_PAD {
|
||||||
t.Fatalf("expected edge[%d] x to be at the actor center", i)
|
t.Fatalf("expected edge[%d] x to be at the actor center", i)
|
||||||
}
|
}
|
||||||
|
|
||||||
if edge.Route[1].X != edge.Dst.Center().X+SPAN_EDGE_PAD {
|
if edge.Route[1].X != edge.Dst.Center().X+SPAN_MESSAGE_PAD {
|
||||||
t.Fatalf("expected edge[%d] x to be at the actor center", i)
|
t.Fatalf("expected edge[%d] x to be at the actor center", i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -157,6 +173,7 @@ func TestSpansSequenceDiagram(t *testing.T) {
|
||||||
// t2 ││ │
|
// t2 ││ │
|
||||||
// ├┘◄─────────────────────┤
|
// ├┘◄─────────────────────┤
|
||||||
g := d2graph.NewGraph(nil)
|
g := d2graph.NewGraph(nil)
|
||||||
|
g.Root.Attributes.Shape = d2graph.Scalar{Value: d2target.ShapeSequenceDiagram}
|
||||||
a := g.Root.EnsureChild([]string{"a"})
|
a := g.Root.EnsureChild([]string{"a"})
|
||||||
a.Box = geo.NewBox(nil, 100, 100)
|
a.Box = geo.NewBox(nil, 100, 100)
|
||||||
a.Attributes = d2graph.Attributes{
|
a.Attributes = d2graph.Attributes{
|
||||||
|
|
@ -174,22 +191,36 @@ func TestSpansSequenceDiagram(t *testing.T) {
|
||||||
|
|
||||||
g.Edges = []*d2graph.Edge{
|
g.Edges = []*d2graph.Edge{
|
||||||
{
|
{
|
||||||
Src: a_t1,
|
Src: a_t1,
|
||||||
Dst: b_t1,
|
Dst: b_t1,
|
||||||
|
Index: 0,
|
||||||
}, {
|
}, {
|
||||||
Src: b_t1,
|
Src: b_t1,
|
||||||
Dst: a_t1,
|
Dst: a_t1,
|
||||||
|
Index: 0,
|
||||||
}, {
|
}, {
|
||||||
Src: a_t2,
|
Src: a_t2,
|
||||||
Dst: b,
|
Dst: b,
|
||||||
|
Index: 0,
|
||||||
}, {
|
}, {
|
||||||
Src: b,
|
Src: b,
|
||||||
Dst: a_t2,
|
Dst: a_t2,
|
||||||
|
Index: 0,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := log.WithTB(context.Background(), t, nil)
|
ctx := log.WithTB(context.Background(), t, nil)
|
||||||
Layout(ctx, g)
|
Layout(ctx, g, func(ctx context.Context, g *d2graph.Graph) error {
|
||||||
|
// just set some position as if it had been properly placed
|
||||||
|
for _, obj := range g.Objects {
|
||||||
|
obj.TopLeft = geo.NewPoint(0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, edge := range g.Edges {
|
||||||
|
edge.Route = []*geo.Point{geo.NewPoint(1, 1)}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
// check properties
|
// check properties
|
||||||
if a.Attributes.Shape.Value != shape.PERSON_TYPE {
|
if a.Attributes.Shape.Value != shape.PERSON_TYPE {
|
||||||
|
|
@ -215,7 +246,7 @@ func TestSpansSequenceDiagram(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Y diff of the 2 first edges
|
// Y diff of the 2 first edges
|
||||||
expectedHeight := g.Edges[1].Route[0].Y - g.Edges[0].Route[0].Y + (2 * SPAN_EDGE_PAD)
|
expectedHeight := g.Edges[1].Route[0].Y - g.Edges[0].Route[0].Y + (2 * SPAN_MESSAGE_PAD)
|
||||||
if a_t1.Height != expectedHeight {
|
if a_t1.Height != expectedHeight {
|
||||||
t.Fatalf("expected a.t1 height to be %.5f, got %.5f", expectedHeight, a_t1.Height)
|
t.Fatalf("expected a.t1 height to be %.5f, got %.5f", expectedHeight, a_t1.Height)
|
||||||
}
|
}
|
||||||
|
|
@ -237,20 +268,118 @@ func TestSpansSequenceDiagram(t *testing.T) {
|
||||||
if a_t1.TopLeft.Y != b_t1.TopLeft.Y {
|
if a_t1.TopLeft.Y != b_t1.TopLeft.Y {
|
||||||
t.Fatal("expected a.t1 and b.t1 to be placed at the same Y")
|
t.Fatal("expected a.t1 and b.t1 to be placed at the same Y")
|
||||||
}
|
}
|
||||||
if a_t1.TopLeft.Y != g.Edges[0].Route[0].Y-SPAN_EDGE_PAD {
|
if a_t1.TopLeft.Y != g.Edges[0].Route[0].Y-SPAN_MESSAGE_PAD {
|
||||||
t.Fatal("expected a.t1 to be placed at the same Y of the first edge")
|
t.Fatal("expected a.t1 to be placed at the same Y of the first message")
|
||||||
}
|
}
|
||||||
|
|
||||||
// check routes
|
// check routes
|
||||||
if g.Edges[0].Route[0].X != a_t1.TopLeft.X+a_t1.Width+SPAN_EDGE_PAD {
|
if g.Edges[0].Route[0].X != a_t1.TopLeft.X+a_t1.Width+SPAN_MESSAGE_PAD {
|
||||||
t.Fatal("expected the first edge to start on a.t1 top right X")
|
t.Fatal("expected the first message to start on a.t1 top right X")
|
||||||
}
|
}
|
||||||
|
|
||||||
if g.Edges[0].Route[1].X != b_t1.TopLeft.X-SPAN_EDGE_PAD {
|
if g.Edges[0].Route[1].X != b_t1.TopLeft.X-SPAN_MESSAGE_PAD {
|
||||||
t.Fatal("expected the first edge to end on b.t1 top left X")
|
t.Fatal("expected the first message to end on b.t1 top left X")
|
||||||
}
|
}
|
||||||
|
|
||||||
if g.Edges[2].Route[1].X != b.Center().X-SPAN_EDGE_PAD {
|
if g.Edges[2].Route[1].X != b.Center().X-SPAN_MESSAGE_PAD {
|
||||||
t.Fatal("expected the third edge to end on b.t1 center X")
|
t.Fatal("expected the third message to end on b.t1 center X")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNestedSequenceDiagrams(t *testing.T) {
|
||||||
|
// ┌────────────────────────────────────────┐
|
||||||
|
// | ┌─────┐ container ┌─────┐ |
|
||||||
|
// | │ a │ │ b │ | ┌─────┐
|
||||||
|
// | └──┬──┘ └──┬──┘ ├────edge1───┤ c │
|
||||||
|
// | ├┐───────sdEdge1──────►┌┤ | └─────┘
|
||||||
|
// | t1 ││ ││ t1 |
|
||||||
|
// | ├┘◄──────sdEdge2───────└┤ |
|
||||||
|
// └────────────────────────────────────────┘
|
||||||
|
g := d2graph.NewGraph(nil)
|
||||||
|
container := g.Root.EnsureChild([]string{"container"})
|
||||||
|
container.Attributes.Shape = d2graph.Scalar{Value: d2target.ShapeSequenceDiagram}
|
||||||
|
container.Box = geo.NewBox(nil, 500, 500)
|
||||||
|
a := container.EnsureChild([]string{"a"})
|
||||||
|
a.Box = geo.NewBox(nil, 100, 100)
|
||||||
|
a.Attributes.Shape = d2graph.Scalar{Value: shape.PERSON_TYPE}
|
||||||
|
a_t1 := a.EnsureChild([]string{"t1"})
|
||||||
|
b := container.EnsureChild([]string{"b"})
|
||||||
|
b.Box = geo.NewBox(nil, 30, 30)
|
||||||
|
b_t1 := b.EnsureChild([]string{"t1"})
|
||||||
|
|
||||||
|
c := g.Root.EnsureChild([]string{"c"})
|
||||||
|
c.Box = geo.NewBox(nil, 100, 100)
|
||||||
|
c.Attributes.Shape = d2graph.Scalar{Value: d2target.ShapeSquare}
|
||||||
|
|
||||||
|
sdEdge1, err := g.Root.Connect(a_t1.AbsIDArray(), b_t1.AbsIDArray(), false, true, "sequence diagram edge 1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
sdEdge2, err := g.Root.Connect(b_t1.AbsIDArray(), a_t1.AbsIDArray(), false, true, "sequence diagram edge 2")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
edge1, err := g.Root.Connect(container.AbsIDArray(), c.AbsIDArray(), false, false, "edge 1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
layoutFn := func(ctx context.Context, g *d2graph.Graph) error {
|
||||||
|
if len(g.Objects) != 2 {
|
||||||
|
t.Fatal("expected only diagram objects for layout")
|
||||||
|
}
|
||||||
|
for _, obj := range g.Objects {
|
||||||
|
if obj == a || obj == a_t1 || obj == b || obj == b_t1 {
|
||||||
|
t.Fatal("expected to have removed all sequence diagram objects")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(container.ChildrenArray) != 0 {
|
||||||
|
t.Fatalf("expected no `container` children, got %d", len(container.ChildrenArray))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(container.Children) != len(container.ChildrenArray) {
|
||||||
|
t.Fatal("container children mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, edge := range g.Edges {
|
||||||
|
if edge == sdEdge1 || edge == sdEdge2 {
|
||||||
|
t.Fatal("expected to have removed all sequence diagram edges from graph")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if g.Edges[0] != edge1 {
|
||||||
|
t.Fatal("expected graph edge to be in the graph")
|
||||||
|
}
|
||||||
|
|
||||||
|
// just set some position as if it had been properly placed
|
||||||
|
for _, obj := range g.Objects {
|
||||||
|
obj.TopLeft = geo.NewPoint(0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, edge := range g.Edges {
|
||||||
|
edge.Route = []*geo.Point{geo.NewPoint(1, 1)}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := log.WithTB(context.Background(), t, nil)
|
||||||
|
if err = Layout(ctx, g, layoutFn); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(g.Edges) != 5 {
|
||||||
|
t.Fatal("expected graph to have all edges and lifelines after layout")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, obj := range g.Objects {
|
||||||
|
if obj.TopLeft == nil {
|
||||||
|
t.Fatal("expected to have placed all objects")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, edge := range g.Edges {
|
||||||
|
if len(edge.Route) == 0 {
|
||||||
|
t.Fatal("expected to have routed all edges")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
309
d2layouts/d2sequence/sequence_diagram.go
Normal file
|
|
@ -0,0 +1,309 @@
|
||||||
|
package d2sequence
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"oss.terrastruct.com/util-go/go2"
|
||||||
|
|
||||||
|
"oss.terrastruct.com/d2/d2graph"
|
||||||
|
"oss.terrastruct.com/d2/lib/geo"
|
||||||
|
"oss.terrastruct.com/d2/lib/label"
|
||||||
|
"oss.terrastruct.com/d2/lib/shape"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sequenceDiagram struct {
|
||||||
|
root *d2graph.Object
|
||||||
|
messages []*d2graph.Edge
|
||||||
|
lifelines []*d2graph.Edge
|
||||||
|
actors []*d2graph.Object
|
||||||
|
spans []*d2graph.Object
|
||||||
|
|
||||||
|
// can be either actors or spans
|
||||||
|
// rank: left to right position of actors/spans (spans have the same rank as their parents)
|
||||||
|
objectRank map[*d2graph.Object]int
|
||||||
|
|
||||||
|
// keep track of the first and last message of a given actor/span
|
||||||
|
// the message rank is the order in which it appears from top to bottom
|
||||||
|
minMessageRank map[*d2graph.Object]int
|
||||||
|
maxMessageRank map[*d2graph.Object]int
|
||||||
|
|
||||||
|
messageYStep float64
|
||||||
|
actorXStep float64
|
||||||
|
maxActorHeight float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSequenceDiagram(actors []*d2graph.Object, messages []*d2graph.Edge) *sequenceDiagram {
|
||||||
|
sd := &sequenceDiagram{
|
||||||
|
messages: messages,
|
||||||
|
actors: actors,
|
||||||
|
spans: nil,
|
||||||
|
lifelines: nil,
|
||||||
|
objectRank: make(map[*d2graph.Object]int),
|
||||||
|
minMessageRank: make(map[*d2graph.Object]int),
|
||||||
|
maxMessageRank: make(map[*d2graph.Object]int),
|
||||||
|
messageYStep: MIN_MESSAGE_DISTANCE,
|
||||||
|
actorXStep: MIN_ACTOR_DISTANCE,
|
||||||
|
maxActorHeight: 0.,
|
||||||
|
}
|
||||||
|
|
||||||
|
for rank, actor := range actors {
|
||||||
|
sd.root = actor.Parent
|
||||||
|
sd.objectRank[actor] = rank
|
||||||
|
sd.maxActorHeight = math.Max(sd.maxActorHeight, actor.Height)
|
||||||
|
|
||||||
|
queue := make([]*d2graph.Object, len(actor.ChildrenArray))
|
||||||
|
copy(queue, actor.ChildrenArray)
|
||||||
|
for len(queue) > 0 {
|
||||||
|
span := queue[0]
|
||||||
|
queue = queue[1:]
|
||||||
|
|
||||||
|
// spans are always rectangles and have no labels
|
||||||
|
span.Attributes.Label = d2graph.Scalar{Value: ""}
|
||||||
|
span.Attributes.Shape = d2graph.Scalar{Value: shape.SQUARE_TYPE}
|
||||||
|
sd.spans = append(sd.spans, span)
|
||||||
|
sd.objectRank[span] = rank
|
||||||
|
|
||||||
|
queue = append(queue, span.ChildrenArray...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for rank, message := range sd.messages {
|
||||||
|
sd.messageYStep = math.Max(sd.messageYStep, float64(message.LabelDimensions.Height))
|
||||||
|
|
||||||
|
sd.setMinMaxMessageRank(message.Src, rank)
|
||||||
|
sd.setMinMaxMessageRank(message.Dst, rank)
|
||||||
|
|
||||||
|
// ensures that long labels, spanning over multiple actors, don't make for large gaps between actors
|
||||||
|
// by distributing the label length across the actors rank difference
|
||||||
|
rankDiff := math.Abs(float64(sd.objectRank[message.Src]) - float64(sd.objectRank[message.Dst]))
|
||||||
|
distributedLabelWidth := float64(message.LabelDimensions.Width) / rankDiff
|
||||||
|
sd.actorXStep = math.Max(sd.actorXStep, distributedLabelWidth+HORIZONTAL_PAD)
|
||||||
|
}
|
||||||
|
|
||||||
|
sd.maxActorHeight += VERTICAL_PAD
|
||||||
|
sd.messageYStep += VERTICAL_PAD
|
||||||
|
|
||||||
|
return sd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sd *sequenceDiagram) setMinMaxMessageRank(actor *d2graph.Object, rank int) {
|
||||||
|
if minRank, exists := sd.minMessageRank[actor]; exists {
|
||||||
|
sd.minMessageRank[actor] = go2.IntMin(minRank, rank)
|
||||||
|
} else {
|
||||||
|
sd.minMessageRank[actor] = rank
|
||||||
|
}
|
||||||
|
|
||||||
|
sd.maxMessageRank[actor] = go2.IntMax(sd.maxMessageRank[actor], rank)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sd *sequenceDiagram) layout() {
|
||||||
|
sd.placeActors()
|
||||||
|
sd.placeSpans()
|
||||||
|
sd.routeMessages()
|
||||||
|
sd.addLifelineEdges()
|
||||||
|
}
|
||||||
|
|
||||||
|
// placeActors places actors bottom aligned, side by side
|
||||||
|
func (sd *sequenceDiagram) placeActors() {
|
||||||
|
x := 0.
|
||||||
|
for _, actors := range sd.actors {
|
||||||
|
yOffset := sd.maxActorHeight - actors.Height
|
||||||
|
actors.TopLeft = geo.NewPoint(x, yOffset)
|
||||||
|
x += actors.Width + sd.actorXStep
|
||||||
|
actors.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// addLifelineEdges adds a new edge for each actor in the graph that represents the its lifeline
|
||||||
|
// ┌──────────────┐
|
||||||
|
// │ actor │
|
||||||
|
// └──────┬───────┘
|
||||||
|
// │
|
||||||
|
// │ lifeline
|
||||||
|
// │
|
||||||
|
// │
|
||||||
|
func (sd *sequenceDiagram) addLifelineEdges() {
|
||||||
|
endY := sd.getMessageY(len(sd.messages))
|
||||||
|
for _, actor := range sd.actors {
|
||||||
|
actorBottom := actor.Center()
|
||||||
|
actorBottom.Y = actor.TopLeft.Y + actor.Height
|
||||||
|
actorLifelineEnd := actor.Center()
|
||||||
|
actorLifelineEnd.Y = endY
|
||||||
|
sd.lifelines = append(sd.lifelines, &d2graph.Edge{
|
||||||
|
Attributes: d2graph.Attributes{
|
||||||
|
Style: d2graph.Style{
|
||||||
|
StrokeDash: &d2graph.Scalar{Value: "10"},
|
||||||
|
Stroke: actor.Attributes.Style.Stroke,
|
||||||
|
StrokeWidth: actor.Attributes.Style.StrokeWidth,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Src: actor,
|
||||||
|
SrcArrow: false,
|
||||||
|
Dst: &d2graph.Object{
|
||||||
|
ID: actor.ID + fmt.Sprintf("-lifeline-end-%d", go2.StringToIntHash(actor.ID+"-lifeline-end")),
|
||||||
|
},
|
||||||
|
DstArrow: false,
|
||||||
|
Route: []*geo.Point{actorBottom, actorLifelineEnd},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// placeSpans places spans over the object lifeline
|
||||||
|
// ┌──────────┐
|
||||||
|
// │ actor │
|
||||||
|
// └────┬─────┘
|
||||||
|
// ┌─┴──┐
|
||||||
|
// │ │
|
||||||
|
// |span|
|
||||||
|
// │ │
|
||||||
|
// └─┬──┘
|
||||||
|
// │
|
||||||
|
// lifeline
|
||||||
|
// │
|
||||||
|
func (sd *sequenceDiagram) placeSpans() {
|
||||||
|
// quickly find the span center X
|
||||||
|
rankToX := make(map[int]float64)
|
||||||
|
for _, actor := range sd.actors {
|
||||||
|
rankToX[sd.objectRank[actor]] = actor.Center().X
|
||||||
|
}
|
||||||
|
|
||||||
|
// places spans from most to least nested
|
||||||
|
// the order is important because the only way a child span exists is if there'e an message to it
|
||||||
|
// however, the parent span might not have a message to it and then its position is based on the child position
|
||||||
|
// or, there can be a message to it, but it comes after the child one meaning the top left position is still based on the child
|
||||||
|
// and not on its own message
|
||||||
|
spanFromMostNested := make([]*d2graph.Object, len(sd.spans))
|
||||||
|
copy(spanFromMostNested, sd.spans)
|
||||||
|
sort.SliceStable(spanFromMostNested, func(i, j int) bool {
|
||||||
|
return spanFromMostNested[i].Level() > spanFromMostNested[j].Level()
|
||||||
|
})
|
||||||
|
for _, span := range spanFromMostNested {
|
||||||
|
// finds the position based on children
|
||||||
|
minChildY := math.Inf(1)
|
||||||
|
maxChildY := math.Inf(-1)
|
||||||
|
for _, child := range span.ChildrenArray {
|
||||||
|
minChildY = math.Min(minChildY, child.TopLeft.Y)
|
||||||
|
maxChildY = math.Max(maxChildY, child.TopLeft.Y+child.Height)
|
||||||
|
}
|
||||||
|
|
||||||
|
// finds the position if there are messages to this span
|
||||||
|
minMessageY := math.Inf(1)
|
||||||
|
if minRank, exists := sd.minMessageRank[span]; exists {
|
||||||
|
minMessageY = sd.getMessageY(minRank)
|
||||||
|
}
|
||||||
|
maxMessageY := math.Inf(-1)
|
||||||
|
if maxRank, exists := sd.maxMessageRank[span]; exists {
|
||||||
|
maxMessageY = sd.getMessageY(maxRank)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if it is the same as the child top left, add some padding
|
||||||
|
minY := math.Min(minMessageY, minChildY)
|
||||||
|
if minY == minChildY {
|
||||||
|
minY -= SPAN_DEPTH_GROW_FACTOR
|
||||||
|
} else {
|
||||||
|
minY -= SPAN_MESSAGE_PAD
|
||||||
|
}
|
||||||
|
maxY := math.Max(maxMessageY, maxChildY)
|
||||||
|
if maxY == maxChildY {
|
||||||
|
maxY += SPAN_DEPTH_GROW_FACTOR
|
||||||
|
} else {
|
||||||
|
maxY += SPAN_MESSAGE_PAD
|
||||||
|
}
|
||||||
|
|
||||||
|
height := math.Max(maxY-minY, MIN_SPAN_HEIGHT)
|
||||||
|
// -2 because the actors count as level 1 making the first level span getting 2*SPAN_DEPTH_GROW_FACTOR
|
||||||
|
width := SPAN_WIDTH + (float64(span.Level()-2) * SPAN_DEPTH_GROW_FACTOR)
|
||||||
|
x := rankToX[sd.objectRank[span]] - (width / 2.)
|
||||||
|
span.Box = geo.NewBox(geo.NewPoint(x, minY), width, height)
|
||||||
|
span.ZIndex = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// routeMessages routes horizontal edges (messages) from Src to Dst
|
||||||
|
func (sd *sequenceDiagram) routeMessages() {
|
||||||
|
for rank, message := range sd.messages {
|
||||||
|
message.ZIndex = 2
|
||||||
|
isLeftToRight := message.Src.TopLeft.X < message.Dst.TopLeft.X
|
||||||
|
|
||||||
|
// finds the proper anchor point based on the message direction
|
||||||
|
var startX, endX float64
|
||||||
|
if sd.isActor(message.Src) {
|
||||||
|
startX = message.Src.Center().X
|
||||||
|
} else if isLeftToRight {
|
||||||
|
startX = message.Src.TopLeft.X + message.Src.Width
|
||||||
|
} else {
|
||||||
|
startX = message.Src.TopLeft.X
|
||||||
|
}
|
||||||
|
|
||||||
|
if sd.isActor(message.Dst) {
|
||||||
|
endX = message.Dst.Center().X
|
||||||
|
} else if isLeftToRight {
|
||||||
|
endX = message.Dst.TopLeft.X
|
||||||
|
} else {
|
||||||
|
endX = message.Dst.TopLeft.X + message.Dst.Width
|
||||||
|
}
|
||||||
|
|
||||||
|
if isLeftToRight {
|
||||||
|
startX += SPAN_MESSAGE_PAD
|
||||||
|
endX -= SPAN_MESSAGE_PAD
|
||||||
|
} else {
|
||||||
|
startX -= SPAN_MESSAGE_PAD
|
||||||
|
endX += SPAN_MESSAGE_PAD
|
||||||
|
}
|
||||||
|
|
||||||
|
messageY := sd.getMessageY(rank)
|
||||||
|
message.Route = []*geo.Point{
|
||||||
|
geo.NewPoint(startX, messageY),
|
||||||
|
geo.NewPoint(endX, messageY),
|
||||||
|
}
|
||||||
|
|
||||||
|
if message.Attributes.Label.Value != "" {
|
||||||
|
if isLeftToRight {
|
||||||
|
message.LabelPosition = go2.Pointer(string(label.OutsideTopCenter))
|
||||||
|
} else {
|
||||||
|
// the label will be placed above the message because the orientation is based on the edge normal vector
|
||||||
|
message.LabelPosition = go2.Pointer(string(label.OutsideBottomCenter))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sd *sequenceDiagram) getMessageY(rank int) float64 {
|
||||||
|
// +1 so that the first message has the top padding for its label
|
||||||
|
return ((float64(rank) + 1.) * sd.messageYStep) + sd.maxActorHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sd *sequenceDiagram) isActor(obj *d2graph.Object) bool {
|
||||||
|
return obj.Parent == sd.root
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sd *sequenceDiagram) getWidth() float64 {
|
||||||
|
// the layout is always placed starting at 0, so the width is just the last actor
|
||||||
|
lastActor := sd.actors[len(sd.actors)-1]
|
||||||
|
return lastActor.TopLeft.X + lastActor.Width
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sd *sequenceDiagram) getHeight() float64 {
|
||||||
|
// the layout is always placed starting at 0, so the height is just the last message
|
||||||
|
return sd.getMessageY(len(sd.messages))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sd *sequenceDiagram) shift(tl *geo.Point) {
|
||||||
|
allObjects := append([]*d2graph.Object{}, sd.actors...)
|
||||||
|
allObjects = append(allObjects, sd.spans...)
|
||||||
|
for _, obj := range allObjects {
|
||||||
|
obj.TopLeft.X += tl.X
|
||||||
|
obj.TopLeft.Y += tl.Y
|
||||||
|
}
|
||||||
|
|
||||||
|
allEdges := append([]*d2graph.Edge{}, sd.messages...)
|
||||||
|
allEdges = append(allEdges, sd.lifelines...)
|
||||||
|
for _, edge := range allEdges {
|
||||||
|
for _, p := range edge.Route {
|
||||||
|
p.X += tl.X
|
||||||
|
p.Y += tl.Y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
d2lib/d2.go
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"oss.terrastruct.com/d2/d2compiler"
|
"oss.terrastruct.com/d2/d2compiler"
|
||||||
"oss.terrastruct.com/d2/d2exporter"
|
"oss.terrastruct.com/d2/d2exporter"
|
||||||
"oss.terrastruct.com/d2/d2graph"
|
"oss.terrastruct.com/d2/d2graph"
|
||||||
|
"oss.terrastruct.com/d2/d2layouts/d2sequence"
|
||||||
"oss.terrastruct.com/d2/d2target"
|
"oss.terrastruct.com/d2/d2target"
|
||||||
"oss.terrastruct.com/d2/lib/textmeasure"
|
"oss.terrastruct.com/d2/lib/textmeasure"
|
||||||
)
|
)
|
||||||
|
|
@ -39,14 +40,9 @@ func Compile(ctx context.Context, input string, opts *CompileOptions) (*d2target
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.Layout != nil {
|
if layout, err := getLayout(opts); err != nil {
|
||||||
err = opts.Layout(ctx, g)
|
return nil, nil, err
|
||||||
} else if os.Getenv("D2_LAYOUT") == "dagre" && dagreLayout != nil {
|
} else if err := d2sequence.Layout(ctx, g, layout); err != nil {
|
||||||
err = dagreLayout(ctx, g)
|
|
||||||
} else {
|
|
||||||
err = errors.New("no available layout")
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,5 +50,15 @@ func Compile(ctx context.Context, input string, opts *CompileOptions) (*d2target
|
||||||
return diagram, g, err
|
return diagram, g, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getLayout(opts *CompileOptions) (func(context.Context, *d2graph.Graph) error, error) {
|
||||||
|
if opts.Layout != nil {
|
||||||
|
return opts.Layout, nil
|
||||||
|
} else if os.Getenv("D2_LAYOUT") == "dagre" && dagreLayout != nil {
|
||||||
|
return dagreLayout, nil
|
||||||
|
} else {
|
||||||
|
return nil, errors.New("no available layout")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// See c.go
|
// See c.go
|
||||||
var dagreLayout func(context.Context, *d2graph.Graph) error
|
var dagreLayout func(context.Context, *d2graph.Graph) error
|
||||||
|
|
|
||||||
|
|
@ -1043,6 +1043,172 @@ size XXXL -> custom 64: custom 48 {
|
||||||
style.font-size: 48
|
style.font-size: 48
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
}, {
|
||||||
|
name: "sequence_diagram_simple",
|
||||||
|
script: `shape: sequence_diagram
|
||||||
|
alice: "Alice\nline\nbreaker" {
|
||||||
|
shape: person
|
||||||
|
style.stroke: red
|
||||||
|
}
|
||||||
|
bob: "Bob" {
|
||||||
|
shape: person
|
||||||
|
style.stroke-width: 5
|
||||||
|
}
|
||||||
|
db: {
|
||||||
|
shape: cylinder
|
||||||
|
}
|
||||||
|
queue: {
|
||||||
|
shape: queue
|
||||||
|
}
|
||||||
|
service: "an\nodd\nservice\nwith\na\nname\nin\nmultiple lines"
|
||||||
|
|
||||||
|
alice -> bob: "Authentication Request"
|
||||||
|
bob -> service: "make request for something that is quite far away and requires a really long label to take all the space between the objects"
|
||||||
|
service -> db: "validate credentials"
|
||||||
|
db -> service: {
|
||||||
|
style.stroke-dash: 4
|
||||||
|
}
|
||||||
|
service -> bob: {
|
||||||
|
style.stroke-dash: 4
|
||||||
|
}
|
||||||
|
bob -> alice: "Authentication Response"
|
||||||
|
alice -> bob: "Another authentication Request"
|
||||||
|
bob -> queue: "do it later"
|
||||||
|
queue -> bob: "stored" {
|
||||||
|
style.stroke-dash: 3
|
||||||
|
style.stroke-width: 5
|
||||||
|
style.stroke: green
|
||||||
|
}
|
||||||
|
|
||||||
|
bob -> alice: "Another authentication Response"`,
|
||||||
|
}, {
|
||||||
|
name: "sequence_diagram_span",
|
||||||
|
script: `shape: sequence_diagram
|
||||||
|
|
||||||
|
scorer.t -> itemResponse.t: getItem()
|
||||||
|
scorer.t <- itemResponse.t: item
|
||||||
|
|
||||||
|
scorer.t -> item.t1: getRubric()
|
||||||
|
scorer.t <- item.t1: rubric
|
||||||
|
|
||||||
|
scorer.t -> essayRubric.t: applyTo(essayResp)
|
||||||
|
itemResponse -> essayRubric.t.c
|
||||||
|
essayRubric.t.c -> concept.t: match(essayResponse)
|
||||||
|
scorer <- essayRubric.t: score
|
||||||
|
|
||||||
|
scorer.t -> itemOutcome.t1: new
|
||||||
|
scorer.t -> item.t2: getNormalMinimum()
|
||||||
|
scorer.t -> item.t3: getNormalMaximum()
|
||||||
|
|
||||||
|
scorer.t -> itemOutcome.t2: setScore(score)
|
||||||
|
scorer.t -> itemOutcome.t3: setFeedback(missingConcepts)`,
|
||||||
|
}, {
|
||||||
|
name: "sequence_diagram_nested_span",
|
||||||
|
script: `shape: sequence_diagram
|
||||||
|
|
||||||
|
scorer: {
|
||||||
|
stroke: red
|
||||||
|
stroke-width: 5
|
||||||
|
}
|
||||||
|
|
||||||
|
scorer.abc: {
|
||||||
|
fill: yellow
|
||||||
|
stroke-width: 7
|
||||||
|
}
|
||||||
|
|
||||||
|
scorer -> itemResponse.a: {
|
||||||
|
stroke-width: 10
|
||||||
|
}
|
||||||
|
itemResponse.a -> item.a.b
|
||||||
|
item.a.b -> essayRubric.a.b.c
|
||||||
|
essayRubric.a.b.c -> concept.a.b.c.d
|
||||||
|
item.a -> essayRubric.a.b
|
||||||
|
concept.a.b.c.d -> itemOutcome.a.b.c.d.e
|
||||||
|
|
||||||
|
scorer.abc -> item.a
|
||||||
|
|
||||||
|
itemOutcome.a.b.c.d.e -> scorer
|
||||||
|
scorer -> itemResponse.c`,
|
||||||
|
}, {
|
||||||
|
name: "sequence_diagrams",
|
||||||
|
script: `a_shape.shape: circle
|
||||||
|
a_sequence: {
|
||||||
|
shape: sequence_diagram
|
||||||
|
|
||||||
|
scorer.t -> itemResponse.t: getItem()
|
||||||
|
scorer.t <- itemResponse.t: item
|
||||||
|
|
||||||
|
scorer.t -> item.t1: getRubric()
|
||||||
|
scorer.t <- item.t1: rubric
|
||||||
|
|
||||||
|
scorer.t -> essayRubric.t: applyTo(essayResp)
|
||||||
|
itemResponse -> essayRubric.t.c
|
||||||
|
essayRubric.t.c -> concept.t: match(essayResponse)
|
||||||
|
scorer <- essayRubric.t: score
|
||||||
|
|
||||||
|
scorer.t <-> itemOutcome.t1: new
|
||||||
|
scorer.t <-> item.t2: getNormalMinimum()
|
||||||
|
scorer.t -> item.t3: getNormalMaximum()
|
||||||
|
|
||||||
|
scorer.t -- itemOutcome.t2: setScore(score)
|
||||||
|
scorer.t -- itemOutcome.t3: setFeedback(missingConcepts)
|
||||||
|
}
|
||||||
|
|
||||||
|
another: {
|
||||||
|
sequence: {
|
||||||
|
shape: sequence_diagram
|
||||||
|
|
||||||
|
# scoped edges
|
||||||
|
scorer.t -> itemResponse.t: getItem()
|
||||||
|
scorer.t <- itemResponse.t: item
|
||||||
|
|
||||||
|
scorer.t -> item.t1: getRubric()
|
||||||
|
scorer.t <- item.t1: rubric
|
||||||
|
|
||||||
|
scorer.t -> essayRubric.t: applyTo(essayResp)
|
||||||
|
itemResponse -> essayRubric.t.c
|
||||||
|
essayRubric.t.c -> concept.t: match(essayResponse)
|
||||||
|
scorer <- essayRubric.t: score
|
||||||
|
|
||||||
|
scorer.t -> itemOutcome.t1: new
|
||||||
|
scorer.t <-> item.t2: getNormalMinimum()
|
||||||
|
scorer.t -> item.t3: getNormalMaximum()
|
||||||
|
|
||||||
|
scorer.t -> itemOutcome.t2: setScore(score)
|
||||||
|
scorer.t -> itemOutcome.t3: setFeedback(missingConcepts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a_shape -> a_sequence
|
||||||
|
a_shape -> another.sequence
|
||||||
|
a_sequence -> sequence
|
||||||
|
another.sequence <-> finally.sequence
|
||||||
|
a_shape -- finally
|
||||||
|
|
||||||
|
|
||||||
|
finally: {
|
||||||
|
shape: queue
|
||||||
|
sequence: {
|
||||||
|
shape: sequence_diagram
|
||||||
|
# items appear in this order
|
||||||
|
scorer
|
||||||
|
concept
|
||||||
|
essayRubric
|
||||||
|
item
|
||||||
|
itemOutcome
|
||||||
|
itemResponse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# full path edges
|
||||||
|
finally.sequence.itemResponse.a -> finally.sequence.item.a.b
|
||||||
|
finally.sequence.item.a.b -> finally.sequence.essayRubric.a.b.c
|
||||||
|
finally.sequence.essayRubric.a.b.c -> finally.sequence.concept.a.b.c.d
|
||||||
|
finally.sequence.item.a -> finally.sequence.essayRubric.a.b
|
||||||
|
finally.sequence.concept.a.b.c.d -> finally.sequence.itemOutcome.a.b.c.d.e
|
||||||
|
finally.sequence.scorer.abc -> finally.sequence.item.a
|
||||||
|
finally.sequence.itemOutcome.a.b.c.d.e -> finally.sequence.scorer
|
||||||
|
finally.sequence.scorer -> finally.sequence.itemResponse.c`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
1472
e2etests/testdata/stable/sequence_diagram_nested_span/dagre/board.exp.json
generated
vendored
Normal file
24
e2etests/testdata/stable/sequence_diagram_nested_span/dagre/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 332 KiB |
1472
e2etests/testdata/stable/sequence_diagram_nested_span/elk/board.exp.json
generated
vendored
Normal file
24
e2etests/testdata/stable/sequence_diagram_nested_span/elk/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 332 KiB |
787
e2etests/testdata/stable/sequence_diagram_simple/dagre/board.exp.json
generated
vendored
Normal file
|
|
@ -0,0 +1,787 @@
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"shapes": [
|
||||||
|
{
|
||||||
|
"id": "alice",
|
||||||
|
"type": "person",
|
||||||
|
"pos": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 130
|
||||||
|
},
|
||||||
|
"width": 163,
|
||||||
|
"height": 158,
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 0,
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"borderRadius": 0,
|
||||||
|
"fill": "#E3E9FD",
|
||||||
|
"stroke": "red",
|
||||||
|
"shadow": false,
|
||||||
|
"3d": false,
|
||||||
|
"multiple": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"link": "",
|
||||||
|
"icon": null,
|
||||||
|
"iconPosition": "",
|
||||||
|
"fields": null,
|
||||||
|
"methods": null,
|
||||||
|
"columns": null,
|
||||||
|
"label": "Alice\nline\nbreaker",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#0A0F25",
|
||||||
|
"italic": false,
|
||||||
|
"bold": true,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 63,
|
||||||
|
"labelHeight": 58,
|
||||||
|
"labelPosition": "INSIDE_MIDDLE_CENTER",
|
||||||
|
"zIndex": 0,
|
||||||
|
"level": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bob",
|
||||||
|
"type": "person",
|
||||||
|
"pos": {
|
||||||
|
"x": 480,
|
||||||
|
"y": 162
|
||||||
|
},
|
||||||
|
"width": 132,
|
||||||
|
"height": 126,
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 0,
|
||||||
|
"strokeWidth": 5,
|
||||||
|
"borderRadius": 0,
|
||||||
|
"fill": "#E3E9FD",
|
||||||
|
"stroke": "#0D32B2",
|
||||||
|
"shadow": false,
|
||||||
|
"3d": false,
|
||||||
|
"multiple": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"link": "",
|
||||||
|
"icon": null,
|
||||||
|
"iconPosition": "",
|
||||||
|
"fields": null,
|
||||||
|
"methods": null,
|
||||||
|
"columns": null,
|
||||||
|
"label": "Bob",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#0A0F25",
|
||||||
|
"italic": false,
|
||||||
|
"bold": true,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 32,
|
||||||
|
"labelHeight": 26,
|
||||||
|
"labelPosition": "INSIDE_MIDDLE_CENTER",
|
||||||
|
"zIndex": 0,
|
||||||
|
"level": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "db",
|
||||||
|
"type": "cylinder",
|
||||||
|
"pos": {
|
||||||
|
"x": 929,
|
||||||
|
"y": 162
|
||||||
|
},
|
||||||
|
"width": 124,
|
||||||
|
"height": 126,
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 0,
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"borderRadius": 0,
|
||||||
|
"fill": "#EDF0FD",
|
||||||
|
"stroke": "#0D32B2",
|
||||||
|
"shadow": false,
|
||||||
|
"3d": false,
|
||||||
|
"multiple": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"link": "",
|
||||||
|
"icon": null,
|
||||||
|
"iconPosition": "",
|
||||||
|
"fields": null,
|
||||||
|
"methods": null,
|
||||||
|
"columns": null,
|
||||||
|
"label": "db",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#0A0F25",
|
||||||
|
"italic": false,
|
||||||
|
"bold": true,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 24,
|
||||||
|
"labelHeight": 26,
|
||||||
|
"labelPosition": "INSIDE_MIDDLE_CENTER",
|
||||||
|
"zIndex": 0,
|
||||||
|
"level": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "queue",
|
||||||
|
"type": "queue",
|
||||||
|
"pos": {
|
||||||
|
"x": 1370,
|
||||||
|
"y": 162
|
||||||
|
},
|
||||||
|
"width": 149,
|
||||||
|
"height": 126,
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 0,
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"borderRadius": 0,
|
||||||
|
"fill": "#F0F3F9",
|
||||||
|
"stroke": "#0D32B2",
|
||||||
|
"shadow": false,
|
||||||
|
"3d": false,
|
||||||
|
"multiple": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"link": "",
|
||||||
|
"icon": null,
|
||||||
|
"iconPosition": "",
|
||||||
|
"fields": null,
|
||||||
|
"methods": null,
|
||||||
|
"columns": null,
|
||||||
|
"label": "queue",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#0A0F25",
|
||||||
|
"italic": false,
|
||||||
|
"bold": true,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 49,
|
||||||
|
"labelHeight": 26,
|
||||||
|
"labelPosition": "INSIDE_MIDDLE_CENTER",
|
||||||
|
"zIndex": 0,
|
||||||
|
"level": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "service",
|
||||||
|
"type": "",
|
||||||
|
"pos": {
|
||||||
|
"x": 1836,
|
||||||
|
"y": 50
|
||||||
|
},
|
||||||
|
"width": 202,
|
||||||
|
"height": 238,
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 0,
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"borderRadius": 0,
|
||||||
|
"fill": "#F7F8FE",
|
||||||
|
"stroke": "#0D32B2",
|
||||||
|
"shadow": false,
|
||||||
|
"3d": false,
|
||||||
|
"multiple": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"link": "",
|
||||||
|
"icon": null,
|
||||||
|
"iconPosition": "",
|
||||||
|
"fields": null,
|
||||||
|
"methods": null,
|
||||||
|
"columns": null,
|
||||||
|
"label": "an\nodd\nservice\nwith\na\nname\nin\nmultiple lines",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#0A0F25",
|
||||||
|
"italic": false,
|
||||||
|
"bold": true,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 102,
|
||||||
|
"labelHeight": 138,
|
||||||
|
"labelPosition": "INSIDE_MIDDLE_CENTER",
|
||||||
|
"zIndex": 0,
|
||||||
|
"level": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": [
|
||||||
|
{
|
||||||
|
"id": "(alice -> bob)[0]",
|
||||||
|
"src": "alice",
|
||||||
|
"srcArrow": "none",
|
||||||
|
"srcLabel": "",
|
||||||
|
"dst": "bob",
|
||||||
|
"dstArrow": "triangle",
|
||||||
|
"dstLabel": "",
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 0,
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"stroke": "#0D32B2",
|
||||||
|
"label": "Authentication Request",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#676C7E",
|
||||||
|
"italic": true,
|
||||||
|
"bold": false,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 155,
|
||||||
|
"labelHeight": 21,
|
||||||
|
"labelPosition": "OUTSIDE_TOP_CENTER",
|
||||||
|
"labelPercentage": 0,
|
||||||
|
"route": [
|
||||||
|
{
|
||||||
|
"x": 86.5,
|
||||||
|
"y": 438
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 541,
|
||||||
|
"y": 438
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"animated": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"icon": null,
|
||||||
|
"zIndex": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "(bob -> service)[0]",
|
||||||
|
"src": "bob",
|
||||||
|
"srcArrow": "none",
|
||||||
|
"srcLabel": "",
|
||||||
|
"dst": "service",
|
||||||
|
"dstArrow": "triangle",
|
||||||
|
"dstLabel": "",
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 0,
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"stroke": "#0D32B2",
|
||||||
|
"label": "make request for something that is quite far away and requires a really long label to take all the space between the objects",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#676C7E",
|
||||||
|
"italic": true,
|
||||||
|
"bold": false,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 801,
|
||||||
|
"labelHeight": 21,
|
||||||
|
"labelPosition": "OUTSIDE_TOP_CENTER",
|
||||||
|
"labelPercentage": 0,
|
||||||
|
"route": [
|
||||||
|
{
|
||||||
|
"x": 551,
|
||||||
|
"y": 588
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1932,
|
||||||
|
"y": 588
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"animated": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"icon": null,
|
||||||
|
"zIndex": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "(service -> db)[0]",
|
||||||
|
"src": "service",
|
||||||
|
"srcArrow": "none",
|
||||||
|
"srcLabel": "",
|
||||||
|
"dst": "db",
|
||||||
|
"dstArrow": "triangle",
|
||||||
|
"dstLabel": "",
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 0,
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"stroke": "#0D32B2",
|
||||||
|
"label": "validate credentials",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#676C7E",
|
||||||
|
"italic": true,
|
||||||
|
"bold": false,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 131,
|
||||||
|
"labelHeight": 21,
|
||||||
|
"labelPosition": "OUTSIDE_BOTTOM_CENTER",
|
||||||
|
"labelPercentage": 0,
|
||||||
|
"route": [
|
||||||
|
{
|
||||||
|
"x": 1932,
|
||||||
|
"y": 738
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 996,
|
||||||
|
"y": 738
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"animated": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"icon": null,
|
||||||
|
"zIndex": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "(db -> service)[0]",
|
||||||
|
"src": "db",
|
||||||
|
"srcArrow": "none",
|
||||||
|
"srcLabel": "",
|
||||||
|
"dst": "service",
|
||||||
|
"dstArrow": "triangle",
|
||||||
|
"dstLabel": "",
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 4,
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"stroke": "#0D32B2",
|
||||||
|
"label": "",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#676C7E",
|
||||||
|
"italic": true,
|
||||||
|
"bold": false,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 0,
|
||||||
|
"labelHeight": 0,
|
||||||
|
"labelPosition": "",
|
||||||
|
"labelPercentage": 0,
|
||||||
|
"route": [
|
||||||
|
{
|
||||||
|
"x": 996,
|
||||||
|
"y": 888
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1932,
|
||||||
|
"y": 888
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"animated": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"icon": null,
|
||||||
|
"zIndex": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "(service -> bob)[0]",
|
||||||
|
"src": "service",
|
||||||
|
"srcArrow": "none",
|
||||||
|
"srcLabel": "",
|
||||||
|
"dst": "bob",
|
||||||
|
"dstArrow": "triangle",
|
||||||
|
"dstLabel": "",
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 4,
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"stroke": "#0D32B2",
|
||||||
|
"label": "",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#676C7E",
|
||||||
|
"italic": true,
|
||||||
|
"bold": false,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 0,
|
||||||
|
"labelHeight": 0,
|
||||||
|
"labelPosition": "",
|
||||||
|
"labelPercentage": 0,
|
||||||
|
"route": [
|
||||||
|
{
|
||||||
|
"x": 1932,
|
||||||
|
"y": 1038
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 551,
|
||||||
|
"y": 1038
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"animated": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"icon": null,
|
||||||
|
"zIndex": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "(bob -> alice)[0]",
|
||||||
|
"src": "bob",
|
||||||
|
"srcArrow": "none",
|
||||||
|
"srcLabel": "",
|
||||||
|
"dst": "alice",
|
||||||
|
"dstArrow": "triangle",
|
||||||
|
"dstLabel": "",
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 0,
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"stroke": "#0D32B2",
|
||||||
|
"label": "Authentication Response",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#676C7E",
|
||||||
|
"italic": true,
|
||||||
|
"bold": false,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 164,
|
||||||
|
"labelHeight": 21,
|
||||||
|
"labelPosition": "OUTSIDE_BOTTOM_CENTER",
|
||||||
|
"labelPercentage": 0,
|
||||||
|
"route": [
|
||||||
|
{
|
||||||
|
"x": 541,
|
||||||
|
"y": 1188
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 86.5,
|
||||||
|
"y": 1188
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"animated": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"icon": null,
|
||||||
|
"zIndex": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "(alice -> bob)[1]",
|
||||||
|
"src": "alice",
|
||||||
|
"srcArrow": "none",
|
||||||
|
"srcLabel": "",
|
||||||
|
"dst": "bob",
|
||||||
|
"dstArrow": "triangle",
|
||||||
|
"dstLabel": "",
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 0,
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"stroke": "#0D32B2",
|
||||||
|
"label": "Another authentication Request",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#676C7E",
|
||||||
|
"italic": true,
|
||||||
|
"bold": false,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 210,
|
||||||
|
"labelHeight": 21,
|
||||||
|
"labelPosition": "OUTSIDE_TOP_CENTER",
|
||||||
|
"labelPercentage": 0,
|
||||||
|
"route": [
|
||||||
|
{
|
||||||
|
"x": 86.5,
|
||||||
|
"y": 1338
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 541,
|
||||||
|
"y": 1338
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"animated": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"icon": null,
|
||||||
|
"zIndex": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "(bob -> queue)[0]",
|
||||||
|
"src": "bob",
|
||||||
|
"srcArrow": "none",
|
||||||
|
"srcLabel": "",
|
||||||
|
"dst": "queue",
|
||||||
|
"dstArrow": "triangle",
|
||||||
|
"dstLabel": "",
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 0,
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"stroke": "#0D32B2",
|
||||||
|
"label": "do it later",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#676C7E",
|
||||||
|
"italic": true,
|
||||||
|
"bold": false,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 65,
|
||||||
|
"labelHeight": 21,
|
||||||
|
"labelPosition": "OUTSIDE_TOP_CENTER",
|
||||||
|
"labelPercentage": 0,
|
||||||
|
"route": [
|
||||||
|
{
|
||||||
|
"x": 551,
|
||||||
|
"y": 1488
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1439.5,
|
||||||
|
"y": 1488
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"animated": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"icon": null,
|
||||||
|
"zIndex": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "(queue -> bob)[0]",
|
||||||
|
"src": "queue",
|
||||||
|
"srcArrow": "none",
|
||||||
|
"srcLabel": "",
|
||||||
|
"dst": "bob",
|
||||||
|
"dstArrow": "triangle",
|
||||||
|
"dstLabel": "",
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 3,
|
||||||
|
"strokeWidth": 5,
|
||||||
|
"stroke": "green",
|
||||||
|
"label": "stored",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#676C7E",
|
||||||
|
"italic": true,
|
||||||
|
"bold": false,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 44,
|
||||||
|
"labelHeight": 21,
|
||||||
|
"labelPosition": "OUTSIDE_BOTTOM_CENTER",
|
||||||
|
"labelPercentage": 0,
|
||||||
|
"route": [
|
||||||
|
{
|
||||||
|
"x": 1439.5,
|
||||||
|
"y": 1638
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 551,
|
||||||
|
"y": 1638
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"animated": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"icon": null,
|
||||||
|
"zIndex": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "(bob -> alice)[1]",
|
||||||
|
"src": "bob",
|
||||||
|
"srcArrow": "none",
|
||||||
|
"srcLabel": "",
|
||||||
|
"dst": "alice",
|
||||||
|
"dstArrow": "triangle",
|
||||||
|
"dstLabel": "",
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 0,
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"stroke": "#0D32B2",
|
||||||
|
"label": "Another authentication Response",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#676C7E",
|
||||||
|
"italic": true,
|
||||||
|
"bold": false,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 219,
|
||||||
|
"labelHeight": 21,
|
||||||
|
"labelPosition": "OUTSIDE_BOTTOM_CENTER",
|
||||||
|
"labelPercentage": 0,
|
||||||
|
"route": [
|
||||||
|
{
|
||||||
|
"x": 541,
|
||||||
|
"y": 1788
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 86.5,
|
||||||
|
"y": 1788
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"animated": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"icon": null,
|
||||||
|
"zIndex": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "(alice -- )[0]",
|
||||||
|
"src": "alice",
|
||||||
|
"srcArrow": "none",
|
||||||
|
"srcLabel": "",
|
||||||
|
"dst": "alice-lifeline-end-3851299086",
|
||||||
|
"dstArrow": "none",
|
||||||
|
"dstLabel": "",
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 10,
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"stroke": "red",
|
||||||
|
"label": "",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#676C7E",
|
||||||
|
"italic": true,
|
||||||
|
"bold": false,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 0,
|
||||||
|
"labelHeight": 0,
|
||||||
|
"labelPosition": "",
|
||||||
|
"labelPercentage": 0,
|
||||||
|
"route": [
|
||||||
|
{
|
||||||
|
"x": 81.5,
|
||||||
|
"y": 288
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 81.5,
|
||||||
|
"y": 1938
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"animated": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"icon": null,
|
||||||
|
"zIndex": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "(bob -- )[0]",
|
||||||
|
"src": "bob",
|
||||||
|
"srcArrow": "none",
|
||||||
|
"srcLabel": "",
|
||||||
|
"dst": "bob-lifeline-end-3036726343",
|
||||||
|
"dstArrow": "none",
|
||||||
|
"dstLabel": "",
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 10,
|
||||||
|
"strokeWidth": 5,
|
||||||
|
"stroke": "#0D32B2",
|
||||||
|
"label": "",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#676C7E",
|
||||||
|
"italic": true,
|
||||||
|
"bold": false,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 0,
|
||||||
|
"labelHeight": 0,
|
||||||
|
"labelPosition": "",
|
||||||
|
"labelPercentage": 0,
|
||||||
|
"route": [
|
||||||
|
{
|
||||||
|
"x": 546,
|
||||||
|
"y": 288
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 546,
|
||||||
|
"y": 1938
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"animated": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"icon": null,
|
||||||
|
"zIndex": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "(db -- )[0]",
|
||||||
|
"src": "db",
|
||||||
|
"srcArrow": "none",
|
||||||
|
"srcLabel": "",
|
||||||
|
"dst": "db-lifeline-end-2675250554",
|
||||||
|
"dstArrow": "none",
|
||||||
|
"dstLabel": "",
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 10,
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"stroke": "#0D32B2",
|
||||||
|
"label": "",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#676C7E",
|
||||||
|
"italic": true,
|
||||||
|
"bold": false,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 0,
|
||||||
|
"labelHeight": 0,
|
||||||
|
"labelPosition": "",
|
||||||
|
"labelPercentage": 0,
|
||||||
|
"route": [
|
||||||
|
{
|
||||||
|
"x": 991,
|
||||||
|
"y": 288
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 991,
|
||||||
|
"y": 1938
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"animated": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"icon": null,
|
||||||
|
"zIndex": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "(queue -- )[0]",
|
||||||
|
"src": "queue",
|
||||||
|
"srcArrow": "none",
|
||||||
|
"srcLabel": "",
|
||||||
|
"dst": "queue-lifeline-end-1097346683",
|
||||||
|
"dstArrow": "none",
|
||||||
|
"dstLabel": "",
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 10,
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"stroke": "#0D32B2",
|
||||||
|
"label": "",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#676C7E",
|
||||||
|
"italic": true,
|
||||||
|
"bold": false,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 0,
|
||||||
|
"labelHeight": 0,
|
||||||
|
"labelPosition": "",
|
||||||
|
"labelPercentage": 0,
|
||||||
|
"route": [
|
||||||
|
{
|
||||||
|
"x": 1444.5,
|
||||||
|
"y": 288
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1444.5,
|
||||||
|
"y": 1938
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"animated": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"icon": null,
|
||||||
|
"zIndex": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "(service -- )[0]",
|
||||||
|
"src": "service",
|
||||||
|
"srcArrow": "none",
|
||||||
|
"srcLabel": "",
|
||||||
|
"dst": "service-lifeline-end-22863415",
|
||||||
|
"dstArrow": "none",
|
||||||
|
"dstLabel": "",
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 10,
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"stroke": "#0D32B2",
|
||||||
|
"label": "",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#676C7E",
|
||||||
|
"italic": true,
|
||||||
|
"bold": false,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 0,
|
||||||
|
"labelHeight": 0,
|
||||||
|
"labelPosition": "",
|
||||||
|
"labelPercentage": 0,
|
||||||
|
"route": [
|
||||||
|
{
|
||||||
|
"x": 1937,
|
||||||
|
"y": 288
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1937,
|
||||||
|
"y": 1938
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"animated": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"icon": null,
|
||||||
|
"zIndex": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
31
e2etests/testdata/stable/sequence_diagram_simple/dagre/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 474 KiB |
787
e2etests/testdata/stable/sequence_diagram_simple/elk/board.exp.json
generated
vendored
Normal file
|
|
@ -0,0 +1,787 @@
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"shapes": [
|
||||||
|
{
|
||||||
|
"id": "alice",
|
||||||
|
"type": "person",
|
||||||
|
"pos": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 130
|
||||||
|
},
|
||||||
|
"width": 163,
|
||||||
|
"height": 158,
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 0,
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"borderRadius": 0,
|
||||||
|
"fill": "#E3E9FD",
|
||||||
|
"stroke": "red",
|
||||||
|
"shadow": false,
|
||||||
|
"3d": false,
|
||||||
|
"multiple": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"link": "",
|
||||||
|
"icon": null,
|
||||||
|
"iconPosition": "",
|
||||||
|
"fields": null,
|
||||||
|
"methods": null,
|
||||||
|
"columns": null,
|
||||||
|
"label": "Alice\nline\nbreaker",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#0A0F25",
|
||||||
|
"italic": false,
|
||||||
|
"bold": true,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 63,
|
||||||
|
"labelHeight": 58,
|
||||||
|
"labelPosition": "INSIDE_MIDDLE_CENTER",
|
||||||
|
"zIndex": 0,
|
||||||
|
"level": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bob",
|
||||||
|
"type": "person",
|
||||||
|
"pos": {
|
||||||
|
"x": 480,
|
||||||
|
"y": 162
|
||||||
|
},
|
||||||
|
"width": 132,
|
||||||
|
"height": 126,
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 0,
|
||||||
|
"strokeWidth": 5,
|
||||||
|
"borderRadius": 0,
|
||||||
|
"fill": "#E3E9FD",
|
||||||
|
"stroke": "#0D32B2",
|
||||||
|
"shadow": false,
|
||||||
|
"3d": false,
|
||||||
|
"multiple": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"link": "",
|
||||||
|
"icon": null,
|
||||||
|
"iconPosition": "",
|
||||||
|
"fields": null,
|
||||||
|
"methods": null,
|
||||||
|
"columns": null,
|
||||||
|
"label": "Bob",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#0A0F25",
|
||||||
|
"italic": false,
|
||||||
|
"bold": true,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 32,
|
||||||
|
"labelHeight": 26,
|
||||||
|
"labelPosition": "INSIDE_MIDDLE_CENTER",
|
||||||
|
"zIndex": 0,
|
||||||
|
"level": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "db",
|
||||||
|
"type": "cylinder",
|
||||||
|
"pos": {
|
||||||
|
"x": 929,
|
||||||
|
"y": 162
|
||||||
|
},
|
||||||
|
"width": 124,
|
||||||
|
"height": 126,
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 0,
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"borderRadius": 0,
|
||||||
|
"fill": "#EDF0FD",
|
||||||
|
"stroke": "#0D32B2",
|
||||||
|
"shadow": false,
|
||||||
|
"3d": false,
|
||||||
|
"multiple": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"link": "",
|
||||||
|
"icon": null,
|
||||||
|
"iconPosition": "",
|
||||||
|
"fields": null,
|
||||||
|
"methods": null,
|
||||||
|
"columns": null,
|
||||||
|
"label": "db",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#0A0F25",
|
||||||
|
"italic": false,
|
||||||
|
"bold": true,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 24,
|
||||||
|
"labelHeight": 26,
|
||||||
|
"labelPosition": "INSIDE_MIDDLE_CENTER",
|
||||||
|
"zIndex": 0,
|
||||||
|
"level": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "queue",
|
||||||
|
"type": "queue",
|
||||||
|
"pos": {
|
||||||
|
"x": 1370,
|
||||||
|
"y": 162
|
||||||
|
},
|
||||||
|
"width": 149,
|
||||||
|
"height": 126,
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 0,
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"borderRadius": 0,
|
||||||
|
"fill": "#F0F3F9",
|
||||||
|
"stroke": "#0D32B2",
|
||||||
|
"shadow": false,
|
||||||
|
"3d": false,
|
||||||
|
"multiple": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"link": "",
|
||||||
|
"icon": null,
|
||||||
|
"iconPosition": "",
|
||||||
|
"fields": null,
|
||||||
|
"methods": null,
|
||||||
|
"columns": null,
|
||||||
|
"label": "queue",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#0A0F25",
|
||||||
|
"italic": false,
|
||||||
|
"bold": true,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 49,
|
||||||
|
"labelHeight": 26,
|
||||||
|
"labelPosition": "INSIDE_MIDDLE_CENTER",
|
||||||
|
"zIndex": 0,
|
||||||
|
"level": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "service",
|
||||||
|
"type": "",
|
||||||
|
"pos": {
|
||||||
|
"x": 1836,
|
||||||
|
"y": 50
|
||||||
|
},
|
||||||
|
"width": 202,
|
||||||
|
"height": 238,
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 0,
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"borderRadius": 0,
|
||||||
|
"fill": "#F7F8FE",
|
||||||
|
"stroke": "#0D32B2",
|
||||||
|
"shadow": false,
|
||||||
|
"3d": false,
|
||||||
|
"multiple": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"link": "",
|
||||||
|
"icon": null,
|
||||||
|
"iconPosition": "",
|
||||||
|
"fields": null,
|
||||||
|
"methods": null,
|
||||||
|
"columns": null,
|
||||||
|
"label": "an\nodd\nservice\nwith\na\nname\nin\nmultiple lines",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#0A0F25",
|
||||||
|
"italic": false,
|
||||||
|
"bold": true,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 102,
|
||||||
|
"labelHeight": 138,
|
||||||
|
"labelPosition": "INSIDE_MIDDLE_CENTER",
|
||||||
|
"zIndex": 0,
|
||||||
|
"level": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": [
|
||||||
|
{
|
||||||
|
"id": "(alice -> bob)[0]",
|
||||||
|
"src": "alice",
|
||||||
|
"srcArrow": "none",
|
||||||
|
"srcLabel": "",
|
||||||
|
"dst": "bob",
|
||||||
|
"dstArrow": "triangle",
|
||||||
|
"dstLabel": "",
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 0,
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"stroke": "#0D32B2",
|
||||||
|
"label": "Authentication Request",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#676C7E",
|
||||||
|
"italic": true,
|
||||||
|
"bold": false,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 155,
|
||||||
|
"labelHeight": 21,
|
||||||
|
"labelPosition": "OUTSIDE_TOP_CENTER",
|
||||||
|
"labelPercentage": 0,
|
||||||
|
"route": [
|
||||||
|
{
|
||||||
|
"x": 86.5,
|
||||||
|
"y": 438
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 541,
|
||||||
|
"y": 438
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"animated": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"icon": null,
|
||||||
|
"zIndex": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "(bob -> service)[0]",
|
||||||
|
"src": "bob",
|
||||||
|
"srcArrow": "none",
|
||||||
|
"srcLabel": "",
|
||||||
|
"dst": "service",
|
||||||
|
"dstArrow": "triangle",
|
||||||
|
"dstLabel": "",
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 0,
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"stroke": "#0D32B2",
|
||||||
|
"label": "make request for something that is quite far away and requires a really long label to take all the space between the objects",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#676C7E",
|
||||||
|
"italic": true,
|
||||||
|
"bold": false,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 801,
|
||||||
|
"labelHeight": 21,
|
||||||
|
"labelPosition": "OUTSIDE_TOP_CENTER",
|
||||||
|
"labelPercentage": 0,
|
||||||
|
"route": [
|
||||||
|
{
|
||||||
|
"x": 551,
|
||||||
|
"y": 588
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1932,
|
||||||
|
"y": 588
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"animated": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"icon": null,
|
||||||
|
"zIndex": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "(service -> db)[0]",
|
||||||
|
"src": "service",
|
||||||
|
"srcArrow": "none",
|
||||||
|
"srcLabel": "",
|
||||||
|
"dst": "db",
|
||||||
|
"dstArrow": "triangle",
|
||||||
|
"dstLabel": "",
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 0,
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"stroke": "#0D32B2",
|
||||||
|
"label": "validate credentials",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#676C7E",
|
||||||
|
"italic": true,
|
||||||
|
"bold": false,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 131,
|
||||||
|
"labelHeight": 21,
|
||||||
|
"labelPosition": "OUTSIDE_BOTTOM_CENTER",
|
||||||
|
"labelPercentage": 0,
|
||||||
|
"route": [
|
||||||
|
{
|
||||||
|
"x": 1932,
|
||||||
|
"y": 738
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 996,
|
||||||
|
"y": 738
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"animated": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"icon": null,
|
||||||
|
"zIndex": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "(db -> service)[0]",
|
||||||
|
"src": "db",
|
||||||
|
"srcArrow": "none",
|
||||||
|
"srcLabel": "",
|
||||||
|
"dst": "service",
|
||||||
|
"dstArrow": "triangle",
|
||||||
|
"dstLabel": "",
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 4,
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"stroke": "#0D32B2",
|
||||||
|
"label": "",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#676C7E",
|
||||||
|
"italic": true,
|
||||||
|
"bold": false,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 0,
|
||||||
|
"labelHeight": 0,
|
||||||
|
"labelPosition": "",
|
||||||
|
"labelPercentage": 0,
|
||||||
|
"route": [
|
||||||
|
{
|
||||||
|
"x": 996,
|
||||||
|
"y": 888
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1932,
|
||||||
|
"y": 888
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"animated": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"icon": null,
|
||||||
|
"zIndex": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "(service -> bob)[0]",
|
||||||
|
"src": "service",
|
||||||
|
"srcArrow": "none",
|
||||||
|
"srcLabel": "",
|
||||||
|
"dst": "bob",
|
||||||
|
"dstArrow": "triangle",
|
||||||
|
"dstLabel": "",
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 4,
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"stroke": "#0D32B2",
|
||||||
|
"label": "",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#676C7E",
|
||||||
|
"italic": true,
|
||||||
|
"bold": false,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 0,
|
||||||
|
"labelHeight": 0,
|
||||||
|
"labelPosition": "",
|
||||||
|
"labelPercentage": 0,
|
||||||
|
"route": [
|
||||||
|
{
|
||||||
|
"x": 1932,
|
||||||
|
"y": 1038
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 551,
|
||||||
|
"y": 1038
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"animated": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"icon": null,
|
||||||
|
"zIndex": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "(bob -> alice)[0]",
|
||||||
|
"src": "bob",
|
||||||
|
"srcArrow": "none",
|
||||||
|
"srcLabel": "",
|
||||||
|
"dst": "alice",
|
||||||
|
"dstArrow": "triangle",
|
||||||
|
"dstLabel": "",
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 0,
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"stroke": "#0D32B2",
|
||||||
|
"label": "Authentication Response",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#676C7E",
|
||||||
|
"italic": true,
|
||||||
|
"bold": false,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 164,
|
||||||
|
"labelHeight": 21,
|
||||||
|
"labelPosition": "OUTSIDE_BOTTOM_CENTER",
|
||||||
|
"labelPercentage": 0,
|
||||||
|
"route": [
|
||||||
|
{
|
||||||
|
"x": 541,
|
||||||
|
"y": 1188
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 86.5,
|
||||||
|
"y": 1188
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"animated": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"icon": null,
|
||||||
|
"zIndex": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "(alice -> bob)[1]",
|
||||||
|
"src": "alice",
|
||||||
|
"srcArrow": "none",
|
||||||
|
"srcLabel": "",
|
||||||
|
"dst": "bob",
|
||||||
|
"dstArrow": "triangle",
|
||||||
|
"dstLabel": "",
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 0,
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"stroke": "#0D32B2",
|
||||||
|
"label": "Another authentication Request",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#676C7E",
|
||||||
|
"italic": true,
|
||||||
|
"bold": false,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 210,
|
||||||
|
"labelHeight": 21,
|
||||||
|
"labelPosition": "OUTSIDE_TOP_CENTER",
|
||||||
|
"labelPercentage": 0,
|
||||||
|
"route": [
|
||||||
|
{
|
||||||
|
"x": 86.5,
|
||||||
|
"y": 1338
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 541,
|
||||||
|
"y": 1338
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"animated": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"icon": null,
|
||||||
|
"zIndex": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "(bob -> queue)[0]",
|
||||||
|
"src": "bob",
|
||||||
|
"srcArrow": "none",
|
||||||
|
"srcLabel": "",
|
||||||
|
"dst": "queue",
|
||||||
|
"dstArrow": "triangle",
|
||||||
|
"dstLabel": "",
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 0,
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"stroke": "#0D32B2",
|
||||||
|
"label": "do it later",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#676C7E",
|
||||||
|
"italic": true,
|
||||||
|
"bold": false,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 65,
|
||||||
|
"labelHeight": 21,
|
||||||
|
"labelPosition": "OUTSIDE_TOP_CENTER",
|
||||||
|
"labelPercentage": 0,
|
||||||
|
"route": [
|
||||||
|
{
|
||||||
|
"x": 551,
|
||||||
|
"y": 1488
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1439.5,
|
||||||
|
"y": 1488
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"animated": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"icon": null,
|
||||||
|
"zIndex": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "(queue -> bob)[0]",
|
||||||
|
"src": "queue",
|
||||||
|
"srcArrow": "none",
|
||||||
|
"srcLabel": "",
|
||||||
|
"dst": "bob",
|
||||||
|
"dstArrow": "triangle",
|
||||||
|
"dstLabel": "",
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 3,
|
||||||
|
"strokeWidth": 5,
|
||||||
|
"stroke": "green",
|
||||||
|
"label": "stored",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#676C7E",
|
||||||
|
"italic": true,
|
||||||
|
"bold": false,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 44,
|
||||||
|
"labelHeight": 21,
|
||||||
|
"labelPosition": "OUTSIDE_BOTTOM_CENTER",
|
||||||
|
"labelPercentage": 0,
|
||||||
|
"route": [
|
||||||
|
{
|
||||||
|
"x": 1439.5,
|
||||||
|
"y": 1638
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 551,
|
||||||
|
"y": 1638
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"animated": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"icon": null,
|
||||||
|
"zIndex": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "(bob -> alice)[1]",
|
||||||
|
"src": "bob",
|
||||||
|
"srcArrow": "none",
|
||||||
|
"srcLabel": "",
|
||||||
|
"dst": "alice",
|
||||||
|
"dstArrow": "triangle",
|
||||||
|
"dstLabel": "",
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 0,
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"stroke": "#0D32B2",
|
||||||
|
"label": "Another authentication Response",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#676C7E",
|
||||||
|
"italic": true,
|
||||||
|
"bold": false,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 219,
|
||||||
|
"labelHeight": 21,
|
||||||
|
"labelPosition": "OUTSIDE_BOTTOM_CENTER",
|
||||||
|
"labelPercentage": 0,
|
||||||
|
"route": [
|
||||||
|
{
|
||||||
|
"x": 541,
|
||||||
|
"y": 1788
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 86.5,
|
||||||
|
"y": 1788
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"animated": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"icon": null,
|
||||||
|
"zIndex": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "(alice -- )[0]",
|
||||||
|
"src": "alice",
|
||||||
|
"srcArrow": "none",
|
||||||
|
"srcLabel": "",
|
||||||
|
"dst": "alice-lifeline-end-3851299086",
|
||||||
|
"dstArrow": "none",
|
||||||
|
"dstLabel": "",
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 10,
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"stroke": "red",
|
||||||
|
"label": "",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#676C7E",
|
||||||
|
"italic": true,
|
||||||
|
"bold": false,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 0,
|
||||||
|
"labelHeight": 0,
|
||||||
|
"labelPosition": "",
|
||||||
|
"labelPercentage": 0,
|
||||||
|
"route": [
|
||||||
|
{
|
||||||
|
"x": 81.5,
|
||||||
|
"y": 288
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 81.5,
|
||||||
|
"y": 1938
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"animated": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"icon": null,
|
||||||
|
"zIndex": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "(bob -- )[0]",
|
||||||
|
"src": "bob",
|
||||||
|
"srcArrow": "none",
|
||||||
|
"srcLabel": "",
|
||||||
|
"dst": "bob-lifeline-end-3036726343",
|
||||||
|
"dstArrow": "none",
|
||||||
|
"dstLabel": "",
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 10,
|
||||||
|
"strokeWidth": 5,
|
||||||
|
"stroke": "#0D32B2",
|
||||||
|
"label": "",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#676C7E",
|
||||||
|
"italic": true,
|
||||||
|
"bold": false,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 0,
|
||||||
|
"labelHeight": 0,
|
||||||
|
"labelPosition": "",
|
||||||
|
"labelPercentage": 0,
|
||||||
|
"route": [
|
||||||
|
{
|
||||||
|
"x": 546,
|
||||||
|
"y": 288
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 546,
|
||||||
|
"y": 1938
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"animated": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"icon": null,
|
||||||
|
"zIndex": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "(db -- )[0]",
|
||||||
|
"src": "db",
|
||||||
|
"srcArrow": "none",
|
||||||
|
"srcLabel": "",
|
||||||
|
"dst": "db-lifeline-end-2675250554",
|
||||||
|
"dstArrow": "none",
|
||||||
|
"dstLabel": "",
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 10,
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"stroke": "#0D32B2",
|
||||||
|
"label": "",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#676C7E",
|
||||||
|
"italic": true,
|
||||||
|
"bold": false,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 0,
|
||||||
|
"labelHeight": 0,
|
||||||
|
"labelPosition": "",
|
||||||
|
"labelPercentage": 0,
|
||||||
|
"route": [
|
||||||
|
{
|
||||||
|
"x": 991,
|
||||||
|
"y": 288
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 991,
|
||||||
|
"y": 1938
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"animated": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"icon": null,
|
||||||
|
"zIndex": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "(queue -- )[0]",
|
||||||
|
"src": "queue",
|
||||||
|
"srcArrow": "none",
|
||||||
|
"srcLabel": "",
|
||||||
|
"dst": "queue-lifeline-end-1097346683",
|
||||||
|
"dstArrow": "none",
|
||||||
|
"dstLabel": "",
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 10,
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"stroke": "#0D32B2",
|
||||||
|
"label": "",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#676C7E",
|
||||||
|
"italic": true,
|
||||||
|
"bold": false,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 0,
|
||||||
|
"labelHeight": 0,
|
||||||
|
"labelPosition": "",
|
||||||
|
"labelPercentage": 0,
|
||||||
|
"route": [
|
||||||
|
{
|
||||||
|
"x": 1444.5,
|
||||||
|
"y": 288
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1444.5,
|
||||||
|
"y": 1938
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"animated": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"icon": null,
|
||||||
|
"zIndex": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "(service -- )[0]",
|
||||||
|
"src": "service",
|
||||||
|
"srcArrow": "none",
|
||||||
|
"srcLabel": "",
|
||||||
|
"dst": "service-lifeline-end-22863415",
|
||||||
|
"dstArrow": "none",
|
||||||
|
"dstLabel": "",
|
||||||
|
"opacity": 1,
|
||||||
|
"strokeDash": 10,
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"stroke": "#0D32B2",
|
||||||
|
"label": "",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontFamily": "DEFAULT",
|
||||||
|
"language": "",
|
||||||
|
"color": "#676C7E",
|
||||||
|
"italic": true,
|
||||||
|
"bold": false,
|
||||||
|
"underline": false,
|
||||||
|
"labelWidth": 0,
|
||||||
|
"labelHeight": 0,
|
||||||
|
"labelPosition": "",
|
||||||
|
"labelPercentage": 0,
|
||||||
|
"route": [
|
||||||
|
{
|
||||||
|
"x": 1937,
|
||||||
|
"y": 288
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1937,
|
||||||
|
"y": 1938
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"animated": false,
|
||||||
|
"tooltip": "",
|
||||||
|
"icon": null,
|
||||||
|
"zIndex": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
31
e2etests/testdata/stable/sequence_diagram_simple/elk/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 474 KiB |
1400
e2etests/testdata/stable/sequence_diagram_span/dagre/board.exp.json
generated
vendored
Normal file
31
e2etests/testdata/stable/sequence_diagram_span/dagre/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 476 KiB |
1400
e2etests/testdata/stable/sequence_diagram_span/elk/board.exp.json
generated
vendored
Normal file
31
e2etests/testdata/stable/sequence_diagram_span/elk/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 476 KiB |
4792
e2etests/testdata/stable/sequence_diagrams/dagre/board.exp.json
generated
vendored
Normal file
31
e2etests/testdata/stable/sequence_diagrams/dagre/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 498 KiB |
4711
e2etests/testdata/stable/sequence_diagrams/elk/board.exp.json
generated
vendored
Normal file
31
e2etests/testdata/stable/sequence_diagrams/elk/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 498 KiB |
6
main.go
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -17,7 +16,6 @@ import (
|
||||||
|
|
||||||
"oss.terrastruct.com/util-go/xmain"
|
"oss.terrastruct.com/util-go/xmain"
|
||||||
|
|
||||||
"oss.terrastruct.com/d2/d2layouts/d2sequence"
|
|
||||||
"oss.terrastruct.com/d2/d2lib"
|
"oss.terrastruct.com/d2/d2lib"
|
||||||
"oss.terrastruct.com/d2/d2plugin"
|
"oss.terrastruct.com/d2/d2plugin"
|
||||||
"oss.terrastruct.com/d2/d2renderers/d2svg"
|
"oss.terrastruct.com/d2/d2renderers/d2svg"
|
||||||
|
|
@ -202,10 +200,6 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, theme
|
||||||
}
|
}
|
||||||
|
|
||||||
layout := plugin.Layout
|
layout := plugin.Layout
|
||||||
// TODO: remove, this is just a feature flag to test sequence diagrams as we work on them
|
|
||||||
if os.Getenv("D2_SEQUENCE") == "1" {
|
|
||||||
layout = d2sequence.Layout
|
|
||||||
}
|
|
||||||
diagram, _, err := d2lib.Compile(ctx, string(input), &d2lib.CompileOptions{
|
diagram, _, err := d2lib.Compile(ctx, string(input), &d2lib.CompileOptions{
|
||||||
Layout: layout,
|
Layout: layout,
|
||||||
Ruler: ruler,
|
Ruler: ruler,
|
||||||
|
|
|
||||||