...

Source file src/oss.terrastruct.com/d2/d2layouts/d2sequence/sequence_diagram.go

Documentation: oss.terrastruct.com/d2/d2layouts/d2sequence

     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  	// can be either actors or spans
    30  	// rank: left to right position of actors/spans (spans have the same rank as their parents)
    31  	objectRank map[*d2graph.Object]int
    32  
    33  	// keep track of the first and last message of a given actor/span
    34  	firstMessage map[*d2graph.Object]*d2graph.Edge
    35  	lastMessage  map[*d2graph.Object]*d2graph.Edge
    36  
    37  	// the distance from actor[i] center to actor[i+1] center
    38  	// every neighbor actors need different distances depending on the message labels between them
    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  			// Groups may have more nested groups
    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  				// scale shape up to min width uniformly
   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  			// spans are children of actors that have edges
   132  			// edge groups are children of actors with no edges and children edges
   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  				// spans have no labels
   142  				// TODO why not? Spans should be able to
   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  		// TODO this should not be global yStep, only affect the neighbors
   166  		sd.yStep = math.Max(sd.yStep, float64(message.LabelDimensions.Height))
   167  
   168  		// ensures that long labels, spanning over multiple actors, don't make for large gaps between actors
   169  		// by distributing the label length across the actors rank difference
   170  		rankDiff := math.Abs(float64(sd.objectRank[message.Src]) - float64(sd.objectRank[message.Dst]))
   171  		if rankDiff != 0 {
   172  			// rankDiff = 0 for self edges
   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  	// Groups should horizontally encompass all notes of the actor
   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  	// Extend stuff within this group
   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  	// Move stuff down
   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  // placeActors places actors bottom aligned, side by side with centers spaced by sd.actorXStep
   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  // addLifelineEdges adds a new edge for each actor in the graph that represents the its lifeline
   359  // . ┌──────────────┐
   360  // . │     actor    │
   361  // . └──────┬───────┘
   362  // .        │
   363  // .        │ lifeline
   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  	// lifeline ends only have ID and no graph parent or box set
   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  // placeSpans places spans over the object lifeline
   462  // . ┌──────────┐
   463  // . │  actor   │
   464  // . └────┬─────┘
   465  // .    ┌─┴──┐
   466  // .    │    │
   467  // .    |span|
   468  // .    │    │
   469  // .    └─┬──┘
   470  // .      │
   471  // .   lifeline
   472  // .      │
   473  func (sd *sequenceDiagram) placeSpans() {
   474  	// quickly find the span center X
   475  	rankToX := make(map[int]float64)
   476  	for _, actor := range sd.actors {
   477  		rankToX[sd.objectRank[actor]] = actor.Center().X
   478  	}
   479  
   480  	// places spans from most to least nested
   481  	// the order is important because the only way a child span exists is if there's a message to it
   482  	// however, the parent span might not have a message to it and then its position is based on the child position
   483  	// 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
   484  	// and not on its own message
   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  		// finds the position based on children
   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  		// finds the position if there are messages to this span
   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  		// if it is the same as the child top left, add some padding
   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  		// -1 because the actors count as 1 level
   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  // routeMessages routes horizontal edges (messages) from Src to Dst lifeline (actor/span center)
   537  // in another step, routes are adjusted to spans borders when necessary
   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  		// we need extra space if the previous message is a loop in a different group
   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  // adjustRouteEndpoints adjust the first and last points of message routes when they are spans
   621  // routeMessages() will route to the actor lifelife as a reference point and this function
   622  // adjust to span width when necessary
   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  	// the layout is always placed starting at 0, so the width is just the last actor
   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