1 package d2sequence
2
3 import (
4 "errors"
5 "fmt"
6 "math"
7 "sort"
8 "strconv"
9 "strings"
10
11 "oss.terrastruct.com/util-go/go2"
12
13 "oss.terrastruct.com/d2/d2graph"
14 "oss.terrastruct.com/d2/d2target"
15 "oss.terrastruct.com/d2/lib/geo"
16 "oss.terrastruct.com/d2/lib/label"
17 "oss.terrastruct.com/d2/lib/shape"
18 )
19
20 type sequenceDiagram struct {
21 root *d2graph.Object
22 messages []*d2graph.Edge
23 lifelines []*d2graph.Edge
24 actors []*d2graph.Object
25 groups []*d2graph.Object
26 spans []*d2graph.Object
27 notes []*d2graph.Object
28
29
30
31 objectRank map[*d2graph.Object]int
32
33
34 firstMessage map[*d2graph.Object]*d2graph.Edge
35 lastMessage map[*d2graph.Object]*d2graph.Edge
36
37
38
39 actorXStep []float64
40
41 yStep float64
42 maxActorHeight float64
43
44 verticalIndices map[string]int
45 }
46
47 func getObjEarliestLineNum(o *d2graph.Object) int {
48 min := int(math.MaxInt32)
49 for _, ref := range o.References {
50 if ref.MapKey == nil {
51 continue
52 }
53 min = go2.IntMin(min, ref.MapKey.Range.Start.Line)
54 }
55 return min
56 }
57
58 func getEdgeEarliestLineNum(e *d2graph.Edge) int {
59 min := int(math.MaxInt32)
60 for _, ref := range e.References {
61 if ref.MapKey == nil {
62 continue
63 }
64 min = go2.IntMin(min, ref.MapKey.Range.Start.Line)
65 }
66 return min
67 }
68
69 func newSequenceDiagram(objects []*d2graph.Object, messages []*d2graph.Edge) (*sequenceDiagram, error) {
70 var actors []*d2graph.Object
71 var groups []*d2graph.Object
72
73 for _, obj := range objects {
74 if obj.IsSequenceDiagramGroup() {
75 queue := []*d2graph.Object{obj}
76
77 for len(queue) > 0 {
78 curr := queue[0]
79 curr.LabelPosition = go2.Pointer(label.InsideTopLeft.String())
80 groups = append(groups, curr)
81 queue = queue[1:]
82 queue = append(queue, curr.ChildrenArray...)
83 }
84 } else {
85 actors = append(actors, obj)
86 }
87 }
88
89 if len(actors) == 0 {
90 return nil, errors.New("no actors declared in sequence diagram")
91 }
92
93 sd := &sequenceDiagram{
94 messages: messages,
95 actors: actors,
96 groups: groups,
97 spans: nil,
98 notes: nil,
99 lifelines: nil,
100 objectRank: make(map[*d2graph.Object]int),
101 firstMessage: make(map[*d2graph.Object]*d2graph.Edge),
102 lastMessage: make(map[*d2graph.Object]*d2graph.Edge),
103 actorXStep: make([]float64, len(actors)-1),
104 yStep: MIN_MESSAGE_DISTANCE,
105 maxActorHeight: 0.,
106 verticalIndices: make(map[string]int),
107 }
108
109 for rank, actor := range actors {
110 sd.root = actor.Parent
111 sd.objectRank[actor] = rank
112
113 if actor.Width < MIN_ACTOR_WIDTH {
114 dslShape := strings.ToLower(actor.Shape.Value)
115 switch dslShape {
116 case d2target.ShapePerson, d2target.ShapeOval, d2target.ShapeSquare, d2target.ShapeCircle:
117
118 actor.Height *= MIN_ACTOR_WIDTH / actor.Width
119 }
120 actor.Width = MIN_ACTOR_WIDTH
121 }
122 sd.maxActorHeight = math.Max(sd.maxActorHeight, actor.Height)
123
124 queue := make([]*d2graph.Object, len(actor.ChildrenArray))
125 copy(queue, actor.ChildrenArray)
126 maxNoteWidth := 0.
127 for len(queue) > 0 {
128 child := queue[0]
129 queue = queue[1:]
130
131
132
133 if child.IsSequenceDiagramNote() {
134 sd.verticalIndices[child.AbsID()] = getObjEarliestLineNum(child)
135 child.Shape = d2graph.Scalar{Value: shape.PAGE_TYPE}
136 sd.notes = append(sd.notes, child)
137 sd.objectRank[child] = rank
138 child.LabelPosition = go2.Pointer(label.InsideMiddleCenter.String())
139 maxNoteWidth = math.Max(maxNoteWidth, child.Width)
140 } else {
141
142
143 child.Label = d2graph.Scalar{Value: ""}
144 child.Shape = d2graph.Scalar{Value: shape.SQUARE_TYPE}
145 sd.spans = append(sd.spans, child)
146 sd.objectRank[child] = rank
147 }
148
149 queue = append(queue, child.ChildrenArray...)
150 }
151
152 if rank != len(actors)-1 {
153 actorHW := actor.Width / 2.
154 nextActorHW := actors[rank+1].Width / 2.
155 sd.actorXStep[rank] = math.Max(actorHW+nextActorHW+HORIZONTAL_PAD, MIN_ACTOR_DISTANCE)
156 sd.actorXStep[rank] = math.Max(maxNoteWidth/2.+HORIZONTAL_PAD, sd.actorXStep[rank])
157 if rank > 0 {
158 sd.actorXStep[rank-1] = math.Max(maxNoteWidth/2.+HORIZONTAL_PAD, sd.actorXStep[rank-1])
159 }
160 }
161 }
162
163 for _, message := range sd.messages {
164 sd.verticalIndices[message.AbsID()] = getEdgeEarliestLineNum(message)
165
166 sd.yStep = math.Max(sd.yStep, float64(message.LabelDimensions.Height))
167
168
169
170 rankDiff := math.Abs(float64(sd.objectRank[message.Src]) - float64(sd.objectRank[message.Dst]))
171 if rankDiff != 0 {
172
173 distributedLabelWidth := float64(message.LabelDimensions.Width) / rankDiff
174 for rank := go2.IntMin(sd.objectRank[message.Src], sd.objectRank[message.Dst]); rank <= go2.IntMax(sd.objectRank[message.Src], sd.objectRank[message.Dst])-1; rank++ {
175 sd.actorXStep[rank] = math.Max(sd.actorXStep[rank], distributedLabelWidth+HORIZONTAL_PAD)
176 }
177 }
178 sd.lastMessage[message.Src] = message
179 if _, exists := sd.firstMessage[message.Src]; !exists {
180 sd.firstMessage[message.Src] = message
181 }
182 sd.lastMessage[message.Dst] = message
183 if _, exists := sd.firstMessage[message.Dst]; !exists {
184 sd.firstMessage[message.Dst] = message
185 }
186 }
187
188 sd.yStep += VERTICAL_PAD
189 sd.maxActorHeight += VERTICAL_PAD
190 if sd.root.HasLabel() {
191 sd.maxActorHeight += float64(sd.root.LabelDimensions.Height)
192 }
193
194 return sd, nil
195 }
196
197 func (sd *sequenceDiagram) layout() error {
198 sd.placeActors()
199 sd.placeNotes()
200 if err := sd.routeMessages(); err != nil {
201 return err
202 }
203 sd.placeSpans()
204 sd.adjustRouteEndpoints()
205 sd.placeGroups()
206 sd.addLifelineEdges()
207 return nil
208 }
209
210 func (sd *sequenceDiagram) placeGroups() {
211 sort.SliceStable(sd.groups, func(i, j int) bool {
212 return sd.groups[i].Level() > sd.groups[j].Level()
213 })
214 for _, group := range sd.groups {
215 group.ZIndex = GROUP_Z_INDEX
216 sd.placeGroup(group)
217 }
218 for _, group := range sd.groups {
219 sd.adjustGroupLabel(group)
220 }
221 }
222
223 func (sd *sequenceDiagram) placeGroup(group *d2graph.Object) {
224 minX := math.Inf(1)
225 minY := math.Inf(1)
226 maxX := math.Inf(-1)
227 maxY := math.Inf(-1)
228
229 for _, m := range sd.messages {
230 if m.ContainedBy(group) {
231 for _, p := range m.Route {
232 minX = math.Min(minX, p.X-HORIZONTAL_PAD)
233 minY = math.Min(minY, p.Y-MIN_MESSAGE_DISTANCE/2.)
234 maxX = math.Max(maxX, p.X+HORIZONTAL_PAD)
235 maxY = math.Max(maxY, p.Y+MIN_MESSAGE_DISTANCE/2.)
236 }
237 }
238 }
239
240 for _, n := range sd.notes {
241 inGroup := false
242 for _, ref := range n.References {
243 curr := ref.ScopeObj
244 for curr != nil {
245 if curr == group {
246 inGroup = true
247 break
248 }
249 curr = curr.Parent
250 }
251 if inGroup {
252 break
253 }
254 }
255 if inGroup {
256 minX = math.Min(minX, n.TopLeft.X-HORIZONTAL_PAD)
257 minY = math.Min(minY, n.TopLeft.Y-MIN_MESSAGE_DISTANCE/2.)
258 maxX = math.Max(maxX, n.TopLeft.X+n.Width+HORIZONTAL_PAD)
259 maxY = math.Max(maxY, n.TopLeft.Y+n.Height+MIN_MESSAGE_DISTANCE/2.)
260 }
261 }
262
263 for _, ch := range group.ChildrenArray {
264 for _, g := range sd.groups {
265 if ch == g {
266 minX = math.Min(minX, ch.TopLeft.X-GROUP_CONTAINER_PADDING)
267 minY = math.Min(minY, ch.TopLeft.Y-GROUP_CONTAINER_PADDING)
268 maxX = math.Max(maxX, ch.TopLeft.X+ch.Width+GROUP_CONTAINER_PADDING)
269 maxY = math.Max(maxY, ch.TopLeft.Y+ch.Height+GROUP_CONTAINER_PADDING)
270 break
271 }
272 }
273 }
274
275 group.Box = geo.NewBox(
276 geo.NewPoint(
277 minX,
278 minY,
279 ),
280 maxX-minX,
281 maxY-minY,
282 )
283 }
284
285 func (sd *sequenceDiagram) adjustGroupLabel(group *d2graph.Object) {
286 if !group.HasLabel() {
287 return
288 }
289
290 heightAdd := (group.LabelDimensions.Height + EDGE_GROUP_LABEL_PADDING) - GROUP_CONTAINER_PADDING
291 if heightAdd < 0 {
292 return
293 }
294
295 group.Height += float64(heightAdd)
296
297
298 for _, g := range sd.groups {
299 if g.TopLeft.Y < group.TopLeft.Y && g.TopLeft.Y+g.Height > group.TopLeft.Y {
300 g.Height += float64(heightAdd)
301 }
302 }
303 for _, s := range sd.spans {
304 if s.TopLeft.Y < group.TopLeft.Y && s.TopLeft.Y+s.Height > group.TopLeft.Y {
305 s.Height += float64(heightAdd)
306 }
307 }
308
309
310 for _, m := range sd.messages {
311 if go2.Min(m.Route[0].Y, m.Route[len(m.Route)-1].Y) > group.TopLeft.Y {
312 for _, p := range m.Route {
313 p.Y += float64(heightAdd)
314 }
315 }
316 }
317 for _, s := range sd.spans {
318 if s.TopLeft.Y > group.TopLeft.Y {
319 s.TopLeft.Y += float64(heightAdd)
320 }
321 }
322 for _, g := range sd.groups {
323 if g.TopLeft.Y > group.TopLeft.Y {
324 g.TopLeft.Y += float64(heightAdd)
325 }
326 }
327 for _, n := range sd.notes {
328 if n.TopLeft.Y > group.TopLeft.Y {
329 n.TopLeft.Y += float64(heightAdd)
330 }
331 }
332
333 }
334
335
336 func (sd *sequenceDiagram) placeActors() {
337 centerX := sd.actors[0].Width / 2.
338 for rank, actor := range sd.actors {
339 var yOffset float64
340 if actor.HasOutsideBottomLabel() {
341 actor.LabelPosition = go2.Pointer(label.OutsideBottomCenter.String())
342 yOffset = sd.maxActorHeight - actor.Height
343 if actor.HasLabel() {
344 yOffset -= float64(actor.LabelDimensions.Height)
345 }
346 } else {
347 actor.LabelPosition = go2.Pointer(label.InsideMiddleCenter.String())
348 yOffset = sd.maxActorHeight - actor.Height
349 }
350 halfWidth := actor.Width / 2.
351 actor.TopLeft = geo.NewPoint(math.Round(centerX-halfWidth), yOffset)
352 if rank != len(sd.actors)-1 {
353 centerX += sd.actorXStep[rank]
354 }
355 }
356 }
357
358
359
360
361
362
363
364
365
366 func (sd *sequenceDiagram) addLifelineEdges() {
367 endY := 0.
368 if len(sd.messages) > 0 {
369 lastRoute := sd.messages[len(sd.messages)-1].Route
370 for _, p := range lastRoute {
371 endY = math.Max(endY, p.Y)
372 }
373 }
374 for _, note := range sd.notes {
375 endY = math.Max(endY, note.TopLeft.Y+note.Height)
376 }
377 for _, actor := range sd.actors {
378 endY = math.Max(endY, actor.TopLeft.Y+actor.Height)
379 }
380 endY += sd.yStep
381
382 for _, actor := range sd.actors {
383 actorBottom := actor.Center()
384 actorBottom.Y = actor.TopLeft.Y + actor.Height
385 if *actor.LabelPosition == label.OutsideBottomCenter.String() && actor.HasLabel() {
386 actorBottom.Y += float64(actor.LabelDimensions.Height) + LIFELINE_LABEL_PAD
387 }
388 actorLifelineEnd := actor.Center()
389 actorLifelineEnd.Y = endY
390 style := d2graph.Style{
391 StrokeDash: &d2graph.Scalar{Value: fmt.Sprintf("%d", LIFELINE_STROKE_DASH)},
392 StrokeWidth: &d2graph.Scalar{Value: fmt.Sprintf("%d", LIFELINE_STROKE_WIDTH)},
393 }
394 if actor.Style.StrokeDash != nil {
395 style.StrokeDash = &d2graph.Scalar{Value: actor.Style.StrokeDash.Value}
396 }
397 if actor.Style.Stroke != nil {
398 style.Stroke = &d2graph.Scalar{Value: actor.Style.Stroke.Value}
399 }
400
401 sd.lifelines = append(sd.lifelines, &d2graph.Edge{
402 Attributes: d2graph.Attributes{Style: style},
403 Src: actor,
404 SrcArrow: false,
405 Dst: &d2graph.Object{
406 ID: actor.ID + fmt.Sprintf("-lifeline-end-%d", go2.StringToIntHash(actor.ID+"-lifeline-end")),
407 },
408 DstArrow: false,
409 Route: []*geo.Point{actorBottom, actorLifelineEnd},
410 ZIndex: LIFELINE_Z_INDEX,
411 })
412 }
413 }
414
415 func IsLifelineEnd(obj *d2graph.Object) bool {
416
417 if obj.Graph != nil || obj.Parent != nil || obj.Box != nil {
418 return false
419 }
420 if !strings.Contains(obj.ID, "-lifeline-end-") {
421 return false
422 }
423 parts := strings.Split(obj.ID, "-lifeline-end-")
424 if len(parts) > 1 {
425 hash := parts[len(parts)-1]
426 actorID := strings.Join(parts[:len(parts)-1], "-lifeline-end-")
427 if strconv.Itoa(go2.StringToIntHash(actorID+"-lifeline-end")) == hash {
428 return true
429 }
430 }
431 return false
432 }
433
434 func (sd *sequenceDiagram) placeNotes() {
435 rankToX := make(map[int]float64)
436 for _, actor := range sd.actors {
437 rankToX[sd.objectRank[actor]] = actor.Center().X
438 }
439
440 for _, note := range sd.notes {
441 verticalIndex := sd.verticalIndices[note.AbsID()]
442 y := sd.maxActorHeight + sd.yStep
443
444 for _, msg := range sd.messages {
445 if sd.verticalIndices[msg.AbsID()] < verticalIndex {
446 y += sd.yStep
447 }
448 }
449 for _, otherNote := range sd.notes {
450 if sd.verticalIndices[otherNote.AbsID()] < verticalIndex {
451 y += otherNote.Height + sd.yStep
452 }
453 }
454
455 x := rankToX[sd.objectRank[note]] - (note.Width / 2.)
456 note.Box.TopLeft = geo.NewPoint(x, y)
457 note.ZIndex = NOTE_Z_INDEX
458 }
459 }
460
461
462
463
464
465
466
467
468
469
470
471
472
473 func (sd *sequenceDiagram) placeSpans() {
474
475 rankToX := make(map[int]float64)
476 for _, actor := range sd.actors {
477 rankToX[sd.objectRank[actor]] = actor.Center().X
478 }
479
480
481
482
483
484
485 spanFromMostNested := make([]*d2graph.Object, len(sd.spans))
486 copy(spanFromMostNested, sd.spans)
487 sort.SliceStable(spanFromMostNested, func(i, j int) bool {
488 return spanFromMostNested[i].Level() > spanFromMostNested[j].Level()
489 })
490 for _, span := range spanFromMostNested {
491
492 minChildY := math.Inf(1)
493 maxChildY := math.Inf(-1)
494 for _, child := range span.ChildrenArray {
495 minChildY = math.Min(minChildY, child.TopLeft.Y)
496 maxChildY = math.Max(maxChildY, child.TopLeft.Y+child.Height)
497 }
498
499
500 minMessageY := math.Inf(1)
501 if firstMessage, exists := sd.firstMessage[span]; exists {
502 if firstMessage.Src == firstMessage.Dst || span == firstMessage.Src {
503 minMessageY = firstMessage.Route[0].Y
504 } else {
505 minMessageY = firstMessage.Route[len(firstMessage.Route)-1].Y
506 }
507 }
508 maxMessageY := math.Inf(-1)
509 if lastMessage, exists := sd.lastMessage[span]; exists {
510 if lastMessage.Src == lastMessage.Dst || span == lastMessage.Dst {
511 maxMessageY = lastMessage.Route[len(lastMessage.Route)-1].Y
512 } else {
513 maxMessageY = lastMessage.Route[0].Y
514 }
515 }
516
517
518 minY := math.Min(minMessageY, minChildY)
519 if minY == minChildY || minY == minMessageY {
520 minY -= SPAN_MESSAGE_PAD
521 }
522 maxY := math.Max(maxMessageY, maxChildY)
523 if maxY == maxChildY || maxY == maxMessageY {
524 maxY += SPAN_MESSAGE_PAD
525 }
526
527 height := math.Max(maxY-minY, MIN_SPAN_HEIGHT)
528
529 width := SPAN_BASE_WIDTH + (float64(span.Level()-sd.root.Level()-2) * SPAN_DEPTH_GROWTH_FACTOR)
530 x := rankToX[sd.objectRank[span]] - (width / 2.)
531 span.Box = geo.NewBox(geo.NewPoint(x, minY), width, height)
532 span.ZIndex = SPAN_Z_INDEX
533 }
534 }
535
536
537
538 func (sd *sequenceDiagram) routeMessages() error {
539 var prevIsLoop bool
540 var prevGroup *d2graph.Object
541 messageOffset := sd.maxActorHeight + sd.yStep
542 for _, message := range sd.messages {
543 message.ZIndex = MESSAGE_Z_INDEX
544 noteOffset := 0.
545 for _, note := range sd.notes {
546 if sd.verticalIndices[note.AbsID()] < sd.verticalIndices[message.AbsID()] {
547 noteOffset += note.Height + sd.yStep
548 }
549 }
550
551
552 group := message.GetGroup()
553 if prevIsLoop && prevGroup != group {
554 messageOffset += MIN_MESSAGE_DISTANCE
555 }
556 prevGroup = group
557
558 startY := messageOffset + noteOffset
559
560 var startX, endX float64
561 if startCenter := getCenter(message.Src); startCenter != nil {
562 startX = startCenter.X
563 } else {
564 return fmt.Errorf("could not find center of %s. Is it declared as an actor?", message.Src.ID)
565 }
566 if endCenter := getCenter(message.Dst); endCenter != nil {
567 endX = endCenter.X
568 } else {
569 return fmt.Errorf("could not find center of %s. Is it declared as an actor?", message.Dst.ID)
570 }
571 isToDescendant := strings.HasPrefix(message.Dst.AbsID(), message.Src.AbsID()+".")
572 isFromDescendant := strings.HasPrefix(message.Src.AbsID(), message.Dst.AbsID()+".")
573 isSelfMessage := message.Src == message.Dst
574
575 currSrc := message.Src
576 for !currSrc.Parent.IsSequenceDiagram() {
577 currSrc = currSrc.Parent
578 }
579 currDst := message.Dst
580 for !currDst.Parent.IsSequenceDiagram() {
581 currDst = currDst.Parent
582 }
583 isToSibling := currSrc == currDst
584
585 if isSelfMessage || isToDescendant || isFromDescendant || isToSibling {
586 midX := startX + SELF_MESSAGE_HORIZONTAL_TRAVEL
587 endY := startY + MIN_MESSAGE_DISTANCE*1.5
588 message.Route = []*geo.Point{
589 geo.NewPoint(startX, startY),
590 geo.NewPoint(midX, startY),
591 geo.NewPoint(midX, endY),
592 geo.NewPoint(endX, endY),
593 }
594 prevIsLoop = true
595 } else {
596 message.Route = []*geo.Point{
597 geo.NewPoint(startX, startY),
598 geo.NewPoint(endX, startY),
599 }
600 prevIsLoop = false
601 }
602 messageOffset += sd.yStep
603
604 if message.Label.Value != "" {
605 message.LabelPosition = go2.Pointer(label.InsideMiddleCenter.String())
606 }
607 }
608 return nil
609 }
610
611 func getCenter(obj *d2graph.Object) *geo.Point {
612 if obj == nil {
613 return nil
614 } else if obj.Box != nil && obj.Box.TopLeft != nil {
615 return obj.Center()
616 }
617 return getCenter(obj.Parent)
618 }
619
620
621
622
623 func (sd *sequenceDiagram) adjustRouteEndpoints() {
624 for _, message := range sd.messages {
625 route := message.Route
626 if !sd.isActor(message.Src) {
627 if sd.objectRank[message.Src] <= sd.objectRank[message.Dst] {
628 route[0].X += message.Src.Width / 2.
629 } else {
630 route[0].X -= message.Src.Width / 2.
631 }
632 }
633 if !sd.isActor(message.Dst) {
634 if sd.objectRank[message.Src] < sd.objectRank[message.Dst] {
635 route[len(route)-1].X -= message.Dst.Width / 2.
636 } else {
637 route[len(route)-1].X += message.Dst.Width / 2.
638 }
639 }
640 }
641 }
642
643 func (sd *sequenceDiagram) isActor(obj *d2graph.Object) bool {
644 return obj.Parent == sd.root
645 }
646
647 func (sd *sequenceDiagram) getWidth() float64 {
648
649 lastActor := sd.actors[len(sd.actors)-1]
650 return lastActor.TopLeft.X + lastActor.Width
651 }
652
653 func (sd *sequenceDiagram) getHeight() float64 {
654 return sd.lifelines[0].Route[1].Y
655 }
656
657 func (sd *sequenceDiagram) shift(tl *geo.Point) {
658 allObjects := append([]*d2graph.Object{}, sd.actors...)
659 allObjects = append(allObjects, sd.spans...)
660 allObjects = append(allObjects, sd.groups...)
661 allObjects = append(allObjects, sd.notes...)
662 for _, obj := range allObjects {
663 obj.TopLeft.X += tl.X
664 obj.TopLeft.Y += tl.Y
665 }
666
667 allEdges := append([]*d2graph.Edge{}, sd.messages...)
668 allEdges = append(allEdges, sd.lifelines...)
669 for _, edge := range allEdges {
670 for _, p := range edge.Route {
671 p.X += tl.X
672 p.Y += tl.Y
673 }
674 }
675 }
676
View as plain text