...

Source file src/oss.terrastruct.com/d2/lib/label/label.go

Documentation: oss.terrastruct.com/d2/lib/label

     1  package label
     2  
     3  import (
     4  	"math"
     5  
     6  	"oss.terrastruct.com/d2/lib/geo"
     7  )
     8  
     9  // These are % locations where labels will be placed along the connection
    10  const LEFT_LABEL_POSITION = 1.0 / 4.0
    11  const CENTER_LABEL_POSITION = 2.0 / 4.0
    12  const RIGHT_LABEL_POSITION = 3.0 / 4.0
    13  
    14  // This is the space between a node border and its outside label
    15  const PADDING = 5
    16  
    17  type Position int8
    18  
    19  const (
    20  	Unset Position = iota
    21  
    22  	OutsideTopLeft
    23  	OutsideTopCenter
    24  	OutsideTopRight
    25  
    26  	OutsideLeftTop
    27  	OutsideLeftMiddle
    28  	OutsideLeftBottom
    29  
    30  	OutsideRightTop
    31  	OutsideRightMiddle
    32  	OutsideRightBottom
    33  
    34  	OutsideBottomLeft
    35  	OutsideBottomCenter
    36  	OutsideBottomRight
    37  
    38  	InsideTopLeft
    39  	InsideTopCenter
    40  	InsideTopRight
    41  
    42  	InsideMiddleLeft
    43  	InsideMiddleCenter
    44  	InsideMiddleRight
    45  
    46  	InsideBottomLeft
    47  	InsideBottomCenter
    48  	InsideBottomRight
    49  
    50  	UnlockedTop
    51  	UnlockedMiddle
    52  	UnlockedBottom
    53  )
    54  
    55  func FromString(s string) Position {
    56  	switch s {
    57  	case "OUTSIDE_TOP_LEFT":
    58  		return OutsideTopLeft
    59  	case "OUTSIDE_TOP_CENTER":
    60  		return OutsideTopCenter
    61  	case "OUTSIDE_TOP_RIGHT":
    62  		return OutsideTopRight
    63  
    64  	case "OUTSIDE_LEFT_TOP":
    65  		return OutsideLeftTop
    66  	case "OUTSIDE_LEFT_MIDDLE":
    67  		return OutsideLeftMiddle
    68  	case "OUTSIDE_LEFT_BOTTOM":
    69  		return OutsideLeftBottom
    70  
    71  	case "OUTSIDE_RIGHT_TOP":
    72  		return OutsideRightTop
    73  	case "OUTSIDE_RIGHT_MIDDLE":
    74  		return OutsideRightMiddle
    75  	case "OUTSIDE_RIGHT_BOTTOM":
    76  		return OutsideRightBottom
    77  
    78  	case "OUTSIDE_BOTTOM_LEFT":
    79  		return OutsideBottomLeft
    80  	case "OUTSIDE_BOTTOM_CENTER":
    81  		return OutsideBottomCenter
    82  	case "OUTSIDE_BOTTOM_RIGHT":
    83  		return OutsideBottomRight
    84  
    85  	case "INSIDE_TOP_LEFT":
    86  		return InsideTopLeft
    87  	case "INSIDE_TOP_CENTER":
    88  		return InsideTopCenter
    89  	case "INSIDE_TOP_RIGHT":
    90  		return InsideTopRight
    91  
    92  	case "INSIDE_MIDDLE_LEFT":
    93  		return InsideMiddleLeft
    94  	case "INSIDE_MIDDLE_CENTER":
    95  		return InsideMiddleCenter
    96  	case "INSIDE_MIDDLE_RIGHT":
    97  		return InsideMiddleRight
    98  
    99  	case "INSIDE_BOTTOM_LEFT":
   100  		return InsideBottomLeft
   101  	case "INSIDE_BOTTOM_CENTER":
   102  		return InsideBottomCenter
   103  	case "INSIDE_BOTTOM_RIGHT":
   104  		return InsideBottomRight
   105  
   106  	case "UNLOCKED_TOP":
   107  		return UnlockedTop
   108  	case "UNLOCKED_MIDDLE":
   109  		return UnlockedMiddle
   110  	case "UNLOCKED_BOTTOM":
   111  		return UnlockedBottom
   112  	default:
   113  		return Unset
   114  	}
   115  }
   116  
   117  func (position Position) String() string {
   118  	switch position {
   119  	case OutsideTopLeft:
   120  		return "OUTSIDE_TOP_LEFT"
   121  	case OutsideTopCenter:
   122  		return "OUTSIDE_TOP_CENTER"
   123  	case OutsideTopRight:
   124  		return "OUTSIDE_TOP_RIGHT"
   125  
   126  	case OutsideLeftTop:
   127  		return "OUTSIDE_LEFT_TOP"
   128  	case OutsideLeftMiddle:
   129  		return "OUTSIDE_LEFT_MIDDLE"
   130  	case OutsideLeftBottom:
   131  		return "OUTSIDE_LEFT_BOTTOM"
   132  
   133  	case OutsideRightTop:
   134  		return "OUTSIDE_RIGHT_TOP"
   135  	case OutsideRightMiddle:
   136  		return "OUTSIDE_RIGHT_MIDDLE"
   137  	case OutsideRightBottom:
   138  		return "OUTSIDE_RIGHT_BOTTOM"
   139  
   140  	case OutsideBottomLeft:
   141  		return "OUTSIDE_BOTTOM_LEFT"
   142  	case OutsideBottomCenter:
   143  		return "OUTSIDE_BOTTOM_CENTER"
   144  	case OutsideBottomRight:
   145  		return "OUTSIDE_BOTTOM_RIGHT"
   146  
   147  	case InsideTopLeft:
   148  		return "INSIDE_TOP_LEFT"
   149  	case InsideTopCenter:
   150  		return "INSIDE_TOP_CENTER"
   151  	case InsideTopRight:
   152  		return "INSIDE_TOP_RIGHT"
   153  
   154  	case InsideMiddleLeft:
   155  		return "INSIDE_MIDDLE_LEFT"
   156  	case InsideMiddleCenter:
   157  		return "INSIDE_MIDDLE_CENTER"
   158  	case InsideMiddleRight:
   159  		return "INSIDE_MIDDLE_RIGHT"
   160  
   161  	case InsideBottomLeft:
   162  		return "INSIDE_BOTTOM_LEFT"
   163  	case InsideBottomCenter:
   164  		return "INSIDE_BOTTOM_CENTER"
   165  	case InsideBottomRight:
   166  		return "INSIDE_BOTTOM_RIGHT"
   167  
   168  	case UnlockedTop:
   169  		return "UNLOCKED_TOP"
   170  	case UnlockedMiddle:
   171  		return "UNLOCKED_MIDDLE"
   172  	case UnlockedBottom:
   173  		return "UNLOCKED_BOTTOM"
   174  
   175  	default:
   176  		return ""
   177  	}
   178  }
   179  
   180  func (position Position) IsShapePosition() bool {
   181  	switch position {
   182  	case OutsideTopLeft, OutsideTopCenter, OutsideTopRight,
   183  		OutsideBottomLeft, OutsideBottomCenter, OutsideBottomRight,
   184  		OutsideLeftTop, OutsideLeftMiddle, OutsideLeftBottom,
   185  		OutsideRightTop, OutsideRightMiddle, OutsideRightBottom,
   186  
   187  		InsideTopLeft, InsideTopCenter, InsideTopRight,
   188  		InsideMiddleLeft, InsideMiddleCenter, InsideMiddleRight,
   189  		InsideBottomLeft, InsideBottomCenter, InsideBottomRight:
   190  		return true
   191  	default:
   192  		return false
   193  	}
   194  }
   195  
   196  func (position Position) IsEdgePosition() bool {
   197  	switch position {
   198  	case OutsideTopLeft, OutsideTopCenter, OutsideTopRight,
   199  		InsideMiddleLeft, InsideMiddleCenter, InsideMiddleRight,
   200  		OutsideBottomLeft, OutsideBottomCenter, OutsideBottomRight,
   201  		UnlockedTop, UnlockedMiddle, UnlockedBottom:
   202  		return true
   203  	default:
   204  		return false
   205  	}
   206  }
   207  
   208  func (position Position) IsOutside() bool {
   209  	switch position {
   210  	case OutsideTopLeft, OutsideTopCenter, OutsideTopRight,
   211  		OutsideBottomLeft, OutsideBottomCenter, OutsideBottomRight,
   212  		OutsideLeftTop, OutsideLeftMiddle, OutsideLeftBottom,
   213  		OutsideRightTop, OutsideRightMiddle, OutsideRightBottom:
   214  		return true
   215  	default:
   216  		return false
   217  	}
   218  }
   219  
   220  func (position Position) IsUnlocked() bool {
   221  	switch position {
   222  	case UnlockedTop, UnlockedMiddle, UnlockedBottom:
   223  		return true
   224  	default:
   225  		return false
   226  	}
   227  }
   228  
   229  func (position Position) IsOnEdge() bool {
   230  	switch position {
   231  	case InsideMiddleLeft, InsideMiddleCenter, InsideMiddleRight, UnlockedMiddle:
   232  		return true
   233  	default:
   234  		return false
   235  	}
   236  }
   237  
   238  func (position Position) Mirrored() Position {
   239  	switch position {
   240  	case OutsideTopLeft:
   241  		return OutsideBottomRight
   242  	case OutsideTopCenter:
   243  		return OutsideBottomCenter
   244  	case OutsideTopRight:
   245  		return OutsideBottomLeft
   246  
   247  	case OutsideLeftTop:
   248  		return OutsideRightBottom
   249  	case OutsideLeftMiddle:
   250  		return OutsideRightMiddle
   251  	case OutsideLeftBottom:
   252  		return OutsideRightTop
   253  
   254  	case OutsideRightTop:
   255  		return OutsideLeftBottom
   256  	case OutsideRightMiddle:
   257  		return OutsideLeftMiddle
   258  	case OutsideRightBottom:
   259  		return OutsideLeftTop
   260  
   261  	case OutsideBottomLeft:
   262  		return OutsideTopRight
   263  	case OutsideBottomCenter:
   264  		return OutsideTopCenter
   265  	case OutsideBottomRight:
   266  		return OutsideTopLeft
   267  
   268  	case InsideTopLeft:
   269  		return InsideBottomRight
   270  	case InsideTopCenter:
   271  		return InsideBottomCenter
   272  	case InsideTopRight:
   273  		return InsideBottomLeft
   274  
   275  	case InsideMiddleLeft:
   276  		return InsideMiddleRight
   277  	case InsideMiddleCenter:
   278  		return InsideMiddleCenter
   279  	case InsideMiddleRight:
   280  		return InsideMiddleLeft
   281  
   282  	case InsideBottomLeft:
   283  		return InsideTopRight
   284  	case InsideBottomCenter:
   285  		return InsideTopCenter
   286  	case InsideBottomRight:
   287  		return InsideTopLeft
   288  
   289  	case UnlockedTop:
   290  		return UnlockedBottom
   291  	case UnlockedBottom:
   292  		return UnlockedTop
   293  	case UnlockedMiddle:
   294  		return UnlockedMiddle
   295  
   296  	default:
   297  		return Unset
   298  	}
   299  }
   300  
   301  func (labelPosition Position) GetPointOnBox(box *geo.Box, padding, width, height float64) *geo.Point {
   302  	p := box.TopLeft.Copy()
   303  	boxCenter := box.Center()
   304  
   305  	switch labelPosition {
   306  	case OutsideTopLeft:
   307  		p.X -= padding
   308  		p.Y -= padding + height
   309  	case OutsideTopCenter:
   310  		p.X = boxCenter.X - width/2
   311  		p.Y -= padding + height
   312  	case OutsideTopRight:
   313  		p.X += box.Width - width - padding
   314  		p.Y -= padding + height
   315  
   316  	case OutsideLeftTop:
   317  		p.X -= padding + width
   318  		p.Y += padding
   319  	case OutsideLeftMiddle:
   320  		p.X -= padding + width
   321  		p.Y = boxCenter.Y - height/2
   322  	case OutsideLeftBottom:
   323  		p.X -= padding + width
   324  		p.Y += box.Height - height - padding
   325  
   326  	case OutsideRightTop:
   327  		p.X += box.Width + padding
   328  		p.Y += padding
   329  	case OutsideRightMiddle:
   330  		p.X += box.Width + padding
   331  		p.Y = boxCenter.Y - height/2
   332  	case OutsideRightBottom:
   333  		p.X += box.Width + padding
   334  		p.Y += box.Height - height - padding
   335  
   336  	case OutsideBottomLeft:
   337  		p.X += padding
   338  		p.Y += box.Height + padding
   339  	case OutsideBottomCenter:
   340  		p.X = boxCenter.X - width/2
   341  		p.Y += box.Height + padding
   342  	case OutsideBottomRight:
   343  		p.X += box.Width - width - padding
   344  		p.Y += box.Height + padding
   345  
   346  	case InsideTopLeft:
   347  		p.X += padding
   348  		p.Y += padding
   349  	case InsideTopCenter:
   350  		p.X = boxCenter.X - width/2
   351  		p.Y += padding
   352  	case InsideTopRight:
   353  		p.X += box.Width - width - padding
   354  		p.Y += padding
   355  
   356  	case InsideMiddleLeft:
   357  		p.X += padding
   358  		p.Y = boxCenter.Y - height/2
   359  	case InsideMiddleCenter:
   360  		p.X = boxCenter.X - width/2
   361  		p.Y = boxCenter.Y - height/2
   362  	case InsideMiddleRight:
   363  		p.X += box.Width - width - padding
   364  		p.Y = boxCenter.Y - height/2
   365  
   366  	case InsideBottomLeft:
   367  		p.X += padding
   368  		p.Y += box.Height - height - padding
   369  	case InsideBottomCenter:
   370  		p.X = boxCenter.X - width/2
   371  		p.Y += box.Height - height - padding
   372  	case InsideBottomRight:
   373  		p.X += box.Width - width - padding
   374  		p.Y += box.Height - height - padding
   375  	}
   376  
   377  	return p
   378  }
   379  
   380  // return the top left point of a width x height label at the given label position on the route
   381  // also return the index of the route segment that point is on
   382  func (labelPosition Position) GetPointOnRoute(route geo.Route, strokeWidth, labelPercentage, width, height float64) (point *geo.Point, index int) {
   383  	totalLength := route.Length()
   384  	leftPosition := LEFT_LABEL_POSITION * totalLength
   385  	centerPosition := CENTER_LABEL_POSITION * totalLength
   386  	rightPosition := RIGHT_LABEL_POSITION * totalLength
   387  	unlockedPosition := labelPercentage * totalLength
   388  
   389  	// outside labels have to be offset in the direction of the edge's normal Vector
   390  	// Note: we flip the normal for Top labels but keep it as is for Bottom labels since positive Y is below in SVG
   391  	getOffsetLabelPosition := func(basePoint, normStart, normEnd *geo.Point, flip bool) *geo.Point {
   392  		// get the normal as a unit Vector so we can multiply to project in its direction
   393  		normalX, normalY := geo.GetUnitNormalVector(
   394  			normStart.X,
   395  			normStart.Y,
   396  			normEnd.X,
   397  			normEnd.Y,
   398  		)
   399  		if flip {
   400  			normalX *= -1
   401  			normalY *= -1
   402  		}
   403  
   404  		// Horizontal Edge with Outside Label          |      Vertical Edge with Outside Label
   405  		//  ┌────────────────────┐    ┬                |       ┌─┬─┐
   406  		//  │                    │    │                |       │ │ │    ┌───────────┬───────────┐
   407  		//  │                    │    │                |       │ e │    │           │           │
   408  		//  ├────label─center────┤  ┬ ┼label height    |       │ d │    │         label         │
   409  		//  │                    │  │ │                |       │ g │    │         center        │
   410  		//  │                    │  │ │                |       │ e │    │           │           │
   411  		//  └────────────────────┘  │ ┴ ┬              |       │ │ │    └───────────┴───────────┘
   412  		//                          │   │              |       └─┴─┘   offset
   413  		//                    offset│   │label padding |         ├──────────────────┤
   414  		//                          │   │              |
   415  		// ┌──────────────────────┐ │ ┬ ┴              |                ├───────────┼───────────┤
   416  		// │                      │ │ │                |           ├────┤      label width
   417  		// ├─────edge─center──────┤ ┴ ┼stroke width    |        label padding
   418  		// │                      │   │                |       ├─┼─┤
   419  		// └──────────────────────┘   ┴                |    stroke width
   420  		//
   421  		// TODO: get actual edge stroke width on edge
   422  		offsetX := strokeWidth/2 + float64(PADDING) + width/2
   423  		offsetY := strokeWidth/2 + float64(PADDING) + height/2
   424  
   425  		return geo.NewPoint(basePoint.X+normalX*offsetX, basePoint.Y+normalY*offsetY)
   426  	}
   427  
   428  	var labelCenter *geo.Point
   429  	switch labelPosition {
   430  	case InsideMiddleLeft:
   431  		labelCenter, index = route.GetPointAtDistance(leftPosition)
   432  	case InsideMiddleCenter:
   433  		labelCenter, index = route.GetPointAtDistance(centerPosition)
   434  	case InsideMiddleRight:
   435  		labelCenter, index = route.GetPointAtDistance(rightPosition)
   436  
   437  	case OutsideTopLeft:
   438  		basePoint, index := route.GetPointAtDistance(leftPosition)
   439  		labelCenter = getOffsetLabelPosition(basePoint, route[index], route[index+1], true)
   440  	case OutsideTopCenter:
   441  		basePoint, index := route.GetPointAtDistance(centerPosition)
   442  		labelCenter = getOffsetLabelPosition(basePoint, route[index], route[index+1], true)
   443  	case OutsideTopRight:
   444  		basePoint, index := route.GetPointAtDistance(rightPosition)
   445  		labelCenter = getOffsetLabelPosition(basePoint, route[index], route[index+1], true)
   446  
   447  	case OutsideBottomLeft:
   448  		basePoint, index := route.GetPointAtDistance(leftPosition)
   449  		labelCenter = getOffsetLabelPosition(basePoint, route[index], route[index+1], false)
   450  	case OutsideBottomCenter:
   451  		basePoint, index := route.GetPointAtDistance(centerPosition)
   452  		labelCenter = getOffsetLabelPosition(basePoint, route[index], route[index+1], false)
   453  	case OutsideBottomRight:
   454  		basePoint, index := route.GetPointAtDistance(rightPosition)
   455  		labelCenter = getOffsetLabelPosition(basePoint, route[index], route[index+1], false)
   456  
   457  	case UnlockedTop:
   458  		basePoint, index := route.GetPointAtDistance(unlockedPosition)
   459  		labelCenter = getOffsetLabelPosition(basePoint, route[index], route[index+1], true)
   460  	case UnlockedMiddle:
   461  		labelCenter, index = route.GetPointAtDistance(unlockedPosition)
   462  	case UnlockedBottom:
   463  		basePoint, index := route.GetPointAtDistance(unlockedPosition)
   464  		labelCenter = getOffsetLabelPosition(basePoint, route[index], route[index+1], false)
   465  	default:
   466  		return nil, -1
   467  	}
   468  	// convert from center to top left
   469  	labelCenter.X = chopPrecision(labelCenter.X - width/2)
   470  	labelCenter.Y = chopPrecision(labelCenter.Y - height/2)
   471  	return labelCenter, index
   472  }
   473  
   474  // TODO probably use math.Big
   475  func chopPrecision(f float64) float64 {
   476  	// bring down to float32 precision before rounding for consistency across architectures
   477  	return math.Round(float64(float32(f*10000)) / 10000)
   478  }
   479  

View as plain text