...

Source file src/oss.terrastruct.com/d2/d2target/d2target.go

Documentation: oss.terrastruct.com/d2/d2target

     1  package d2target
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"hash/fnv"
     7  	"math"
     8  	"net/url"
     9  	"strings"
    10  
    11  	"oss.terrastruct.com/util-go/go2"
    12  
    13  	"oss.terrastruct.com/d2/d2renderers/d2fonts"
    14  	"oss.terrastruct.com/d2/lib/color"
    15  	"oss.terrastruct.com/d2/lib/geo"
    16  	"oss.terrastruct.com/d2/lib/label"
    17  	"oss.terrastruct.com/d2/lib/shape"
    18  	"oss.terrastruct.com/d2/lib/svg"
    19  )
    20  
    21  const (
    22  	DEFAULT_ICON_SIZE = 32
    23  	MAX_ICON_SIZE     = 64
    24  
    25  	SHADOW_SIZE_X    = 3
    26  	SHADOW_SIZE_Y    = 5
    27  	THREE_DEE_OFFSET = 15
    28  	MULTIPLE_OFFSET  = 10
    29  
    30  	INNER_BORDER_OFFSET = 5
    31  
    32  	BG_COLOR = color.N7
    33  	FG_COLOR = color.N1
    34  
    35  	MIN_ARROWHEAD_STROKE_WIDTH = 2
    36  	ARROWHEAD_PADDING          = 2.
    37  )
    38  
    39  var BorderOffset = geo.NewVector(5, 5)
    40  
    41  type Config struct {
    42  	Sketch             *bool           `json:"sketch"`
    43  	ThemeID            *int64          `json:"themeID"`
    44  	DarkThemeID        *int64          `json:"darkThemeID"`
    45  	Pad                *int64          `json:"pad"`
    46  	Center             *bool           `json:"center"`
    47  	LayoutEngine       *string         `json:"layoutEngine"`
    48  	ThemeOverrides     *ThemeOverrides `json:"themeOverrides,omitempty"`
    49  	DarkThemeOverrides *ThemeOverrides `json:"darkThemeOverrides,omitempty"`
    50  }
    51  
    52  type ThemeOverrides struct {
    53  	N1  *string `json:"n1"`
    54  	N2  *string `json:"n2"`
    55  	N3  *string `json:"n3"`
    56  	N4  *string `json:"n4"`
    57  	N5  *string `json:"n5"`
    58  	N6  *string `json:"n6"`
    59  	N7  *string `json:"n7"`
    60  	B1  *string `json:"b1"`
    61  	B2  *string `json:"b2"`
    62  	B3  *string `json:"b3"`
    63  	B4  *string `json:"b4"`
    64  	B5  *string `json:"b5"`
    65  	B6  *string `json:"b6"`
    66  	AA2 *string `json:"aa2"`
    67  	AA4 *string `json:"aa4"`
    68  	AA5 *string `json:"aa5"`
    69  	AB4 *string `json:"ab4"`
    70  	AB5 *string `json:"ab5"`
    71  }
    72  
    73  type Diagram struct {
    74  	Name   string  `json:"name"`
    75  	Config *Config `json:"config,omitempty"`
    76  	// See docs on the same field in d2graph to understand what it means.
    77  	IsFolderOnly bool                `json:"isFolderOnly"`
    78  	Description  string              `json:"description,omitempty"`
    79  	FontFamily   *d2fonts.FontFamily `json:"fontFamily,omitempty"`
    80  
    81  	Shapes      []Shape      `json:"shapes"`
    82  	Connections []Connection `json:"connections"`
    83  
    84  	Root Shape `json:"root"`
    85  	// Maybe Icon can be used as a watermark in the root shape
    86  
    87  	Layers    []*Diagram `json:"layers,omitempty"`
    88  	Scenarios []*Diagram `json:"scenarios,omitempty"`
    89  	Steps     []*Diagram `json:"steps,omitempty"`
    90  }
    91  
    92  func (d *Diagram) GetBoard(boardPath []string) *Diagram {
    93  	if len(boardPath) == 0 {
    94  		return d
    95  	}
    96  
    97  	head := boardPath[0]
    98  
    99  	if len(boardPath) == 1 && d.Name == head {
   100  		return d
   101  	}
   102  
   103  	switch head {
   104  	case "layers":
   105  		if len(boardPath) < 2 {
   106  			return nil
   107  		}
   108  		for _, b := range d.Layers {
   109  			if b.Name == boardPath[1] {
   110  				return b.GetBoard(boardPath[2:])
   111  			}
   112  		}
   113  	case "scenarios":
   114  		if len(boardPath) < 2 {
   115  			return nil
   116  		}
   117  		for _, b := range d.Scenarios {
   118  			if b.Name == boardPath[1] {
   119  				return b.GetBoard(boardPath[2:])
   120  			}
   121  		}
   122  	case "steps":
   123  		if len(boardPath) < 2 {
   124  			return nil
   125  		}
   126  		for _, b := range d.Steps {
   127  			if b.Name == boardPath[1] {
   128  				return b.GetBoard(boardPath[2:])
   129  			}
   130  		}
   131  	}
   132  
   133  	for _, b := range d.Layers {
   134  		if b.Name == head {
   135  			return b.GetBoard(boardPath[1:])
   136  		}
   137  	}
   138  	for _, b := range d.Scenarios {
   139  		if b.Name == head {
   140  			return b.GetBoard(boardPath[1:])
   141  		}
   142  	}
   143  	for _, b := range d.Steps {
   144  		if b.Name == head {
   145  			return b.GetBoard(boardPath[1:])
   146  		}
   147  	}
   148  	return nil
   149  }
   150  
   151  func (diagram Diagram) Bytes() ([]byte, error) {
   152  	b1, err := json.Marshal(diagram.Shapes)
   153  	if err != nil {
   154  		return nil, err
   155  	}
   156  	b2, err := json.Marshal(diagram.Connections)
   157  	if err != nil {
   158  		return nil, err
   159  	}
   160  	b3, err := json.Marshal(diagram.Root)
   161  	if err != nil {
   162  		return nil, err
   163  	}
   164  	base := append(append(b1, b2...), b3...)
   165  
   166  	if diagram.Config != nil {
   167  		b, err := json.Marshal(diagram.Config)
   168  		if err != nil {
   169  			return nil, err
   170  		}
   171  		base = append(base, b...)
   172  	}
   173  
   174  	for _, d := range diagram.Layers {
   175  		slices, err := d.Bytes()
   176  		if err != nil {
   177  			return nil, err
   178  		}
   179  		base = append(base, slices...)
   180  	}
   181  	for _, d := range diagram.Scenarios {
   182  		slices, err := d.Bytes()
   183  		if err != nil {
   184  			return nil, err
   185  		}
   186  		base = append(base, slices...)
   187  	}
   188  	for _, d := range diagram.Steps {
   189  		slices, err := d.Bytes()
   190  		if err != nil {
   191  			return nil, err
   192  		}
   193  		base = append(base, slices...)
   194  	}
   195  
   196  	return base, nil
   197  }
   198  
   199  func (diagram Diagram) HasShape(condition func(Shape) bool) bool {
   200  	for _, d := range diagram.Layers {
   201  		if d.HasShape(condition) {
   202  			return true
   203  		}
   204  	}
   205  	for _, d := range diagram.Scenarios {
   206  		if d.HasShape(condition) {
   207  			return true
   208  		}
   209  	}
   210  	for _, d := range diagram.Steps {
   211  		if d.HasShape(condition) {
   212  			return true
   213  		}
   214  	}
   215  	for _, s := range diagram.Shapes {
   216  		if condition(s) {
   217  			return true
   218  		}
   219  	}
   220  	return false
   221  }
   222  
   223  func (diagram Diagram) HashID() (string, error) {
   224  	bytes, err := diagram.Bytes()
   225  	if err != nil {
   226  		return "", err
   227  	}
   228  	h := fnv.New32a()
   229  	h.Write(bytes)
   230  	// CSS names can't start with numbers, so prepend a little something
   231  	return fmt.Sprintf("d2-%d", h.Sum32()), nil
   232  }
   233  
   234  func (diagram Diagram) NestedBoundingBox() (topLeft, bottomRight Point) {
   235  	tl, br := diagram.BoundingBox()
   236  	for _, d := range diagram.Layers {
   237  		tl2, br2 := d.NestedBoundingBox()
   238  		tl.X = go2.Min(tl.X, tl2.X)
   239  		tl.Y = go2.Min(tl.Y, tl2.Y)
   240  		br.X = go2.Max(br.X, br2.X)
   241  		br.Y = go2.Max(br.Y, br2.Y)
   242  	}
   243  	for _, d := range diagram.Scenarios {
   244  		tl2, br2 := d.NestedBoundingBox()
   245  		tl.X = go2.Min(tl.X, tl2.X)
   246  		tl.Y = go2.Min(tl.Y, tl2.Y)
   247  		br.X = go2.Max(br.X, br2.X)
   248  		br.Y = go2.Max(br.Y, br2.Y)
   249  	}
   250  	for _, d := range diagram.Steps {
   251  		tl2, br2 := d.NestedBoundingBox()
   252  		tl.X = go2.Min(tl.X, tl2.X)
   253  		tl.Y = go2.Min(tl.Y, tl2.Y)
   254  		br.X = go2.Max(br.X, br2.X)
   255  		br.Y = go2.Max(br.Y, br2.Y)
   256  	}
   257  	return tl, br
   258  }
   259  
   260  func (diagram Diagram) BoundingBox() (topLeft, bottomRight Point) {
   261  	if len(diagram.Shapes) == 0 {
   262  		return Point{0, 0}, Point{0, 0}
   263  	}
   264  	x1 := int(math.MaxInt32)
   265  	y1 := int(math.MaxInt32)
   266  	x2 := int(math.MinInt32)
   267  	y2 := int(math.MinInt32)
   268  
   269  	for _, targetShape := range diagram.Shapes {
   270  		x1 = go2.Min(x1, targetShape.Pos.X-int(math.Ceil(float64(targetShape.StrokeWidth)/2.)))
   271  		y1 = go2.Min(y1, targetShape.Pos.Y-int(math.Ceil(float64(targetShape.StrokeWidth)/2.)))
   272  		x2 = go2.Max(x2, targetShape.Pos.X+targetShape.Width+int(math.Ceil(float64(targetShape.StrokeWidth)/2.)))
   273  		y2 = go2.Max(y2, targetShape.Pos.Y+targetShape.Height+int(math.Ceil(float64(targetShape.StrokeWidth)/2.)))
   274  
   275  		if targetShape.Tooltip != "" || targetShape.Link != "" {
   276  			// 16 is the icon radius
   277  			y1 = go2.Min(y1, targetShape.Pos.Y-targetShape.StrokeWidth-16)
   278  			x2 = go2.Max(x2, targetShape.Pos.X+targetShape.StrokeWidth+targetShape.Width+16)
   279  		}
   280  		if targetShape.Shadow {
   281  			y2 = go2.Max(y2, targetShape.Pos.Y+targetShape.Height+int(math.Ceil(float64(targetShape.StrokeWidth)/2.))+SHADOW_SIZE_Y)
   282  			x2 = go2.Max(x2, targetShape.Pos.X+targetShape.Width+int(math.Ceil(float64(targetShape.StrokeWidth)/2.))+SHADOW_SIZE_X)
   283  		}
   284  
   285  		if targetShape.ThreeDee {
   286  			offsetY := THREE_DEE_OFFSET
   287  			if targetShape.Type == ShapeHexagon {
   288  				offsetY /= 2
   289  			}
   290  			y1 = go2.Min(y1, targetShape.Pos.Y-offsetY-targetShape.StrokeWidth)
   291  			x2 = go2.Max(x2, targetShape.Pos.X+THREE_DEE_OFFSET+targetShape.Width+targetShape.StrokeWidth)
   292  		}
   293  		if targetShape.Multiple {
   294  			y1 = go2.Min(y1, targetShape.Pos.Y-MULTIPLE_OFFSET-targetShape.StrokeWidth)
   295  			x2 = go2.Max(x2, targetShape.Pos.X+MULTIPLE_OFFSET+targetShape.Width+targetShape.StrokeWidth)
   296  		}
   297  
   298  		if targetShape.Icon != nil && label.FromString(targetShape.IconPosition).IsOutside() {
   299  			contentBox := geo.NewBox(geo.NewPoint(0, 0), float64(targetShape.Width), float64(targetShape.Height))
   300  			s := shape.NewShape(targetShape.Type, contentBox)
   301  			size := GetIconSize(s.GetInnerBox(), targetShape.IconPosition)
   302  
   303  			if strings.HasPrefix(targetShape.IconPosition, "OUTSIDE_TOP") {
   304  				y1 = go2.Min(y1, targetShape.Pos.Y-label.PADDING-size)
   305  			} else if strings.HasPrefix(targetShape.IconPosition, "OUTSIDE_BOTTOM") {
   306  				y2 = go2.Max(y2, targetShape.Pos.Y+targetShape.Height+label.PADDING+size)
   307  			} else if strings.HasPrefix(targetShape.IconPosition, "OUTSIDE_LEFT") {
   308  				x1 = go2.Min(x1, targetShape.Pos.X-label.PADDING-size)
   309  			} else if strings.HasPrefix(targetShape.IconPosition, "OUTSIDE_RIGHT") {
   310  				x2 = go2.Max(x2, targetShape.Pos.X+targetShape.Width+label.PADDING+size)
   311  			}
   312  		}
   313  
   314  		if targetShape.Label != "" {
   315  			labelPosition := label.FromString(targetShape.LabelPosition)
   316  			if !labelPosition.IsOutside() {
   317  				continue
   318  			}
   319  
   320  			shapeType := DSL_SHAPE_TO_SHAPE_TYPE[targetShape.Type]
   321  			s := shape.NewShape(shapeType,
   322  				geo.NewBox(
   323  					geo.NewPoint(float64(targetShape.Pos.X), float64(targetShape.Pos.Y)),
   324  					float64(targetShape.Width),
   325  					float64(targetShape.Height),
   326  				),
   327  			)
   328  
   329  			labelTL := labelPosition.GetPointOnBox(s.GetBox(), label.PADDING, float64(targetShape.LabelWidth), float64(targetShape.LabelHeight))
   330  			x1 = go2.Min(x1, int(labelTL.X))
   331  			y1 = go2.Min(y1, int(labelTL.Y))
   332  			x2 = go2.Max(x2, int(labelTL.X)+targetShape.LabelWidth)
   333  			y2 = go2.Max(y2, int(labelTL.Y)+targetShape.LabelHeight)
   334  		}
   335  	}
   336  
   337  	for _, connection := range diagram.Connections {
   338  		for _, point := range connection.Route {
   339  			x1 = go2.Min(x1, int(math.Floor(point.X))-int(math.Ceil(float64(connection.StrokeWidth)/2.)))
   340  			y1 = go2.Min(y1, int(math.Floor(point.Y))-int(math.Ceil(float64(connection.StrokeWidth)/2.)))
   341  			x2 = go2.Max(x2, int(math.Ceil(point.X))+int(math.Ceil(float64(connection.StrokeWidth)/2.)))
   342  			y2 = go2.Max(y2, int(math.Ceil(point.Y))+int(math.Ceil(float64(connection.StrokeWidth)/2.)))
   343  		}
   344  
   345  		if connection.Label != "" {
   346  			labelTL := connection.GetLabelTopLeft()
   347  			x1 = go2.Min(x1, int(labelTL.X))
   348  			y1 = go2.Min(y1, int(labelTL.Y))
   349  			x2 = go2.Max(x2, int(labelTL.X)+connection.LabelWidth)
   350  			y2 = go2.Max(y2, int(labelTL.Y)+connection.LabelHeight)
   351  		}
   352  		if connection.SrcLabel != nil && connection.SrcLabel.Label != "" {
   353  			labelTL := connection.GetArrowheadLabelPosition(false)
   354  			x1 = go2.Min(x1, int(labelTL.X))
   355  			y1 = go2.Min(y1, int(labelTL.Y))
   356  			x2 = go2.Max(x2, int(labelTL.X)+connection.SrcLabel.LabelWidth)
   357  			y2 = go2.Max(y2, int(labelTL.Y)+connection.SrcLabel.LabelHeight)
   358  		}
   359  		if connection.DstLabel != nil && connection.DstLabel.Label != "" {
   360  			labelTL := connection.GetArrowheadLabelPosition(true)
   361  			x1 = go2.Min(x1, int(labelTL.X))
   362  			y1 = go2.Min(y1, int(labelTL.Y))
   363  			x2 = go2.Max(x2, int(labelTL.X)+connection.DstLabel.LabelWidth)
   364  			y2 = go2.Max(y2, int(labelTL.Y)+connection.DstLabel.LabelHeight)
   365  		}
   366  	}
   367  
   368  	return Point{x1, y1}, Point{x2, y2}
   369  }
   370  
   371  func (diagram Diagram) GetNestedCorpus() string {
   372  	corpus := diagram.GetCorpus()
   373  	for _, d := range diagram.Layers {
   374  		corpus += d.GetNestedCorpus()
   375  	}
   376  	for _, d := range diagram.Scenarios {
   377  		corpus += d.GetNestedCorpus()
   378  	}
   379  	for _, d := range diagram.Steps {
   380  		corpus += d.GetNestedCorpus()
   381  	}
   382  
   383  	return corpus
   384  }
   385  
   386  func (diagram Diagram) GetCorpus() string {
   387  	var corpus string
   388  	appendixCount := 0
   389  	for _, s := range diagram.Shapes {
   390  		corpus += s.Label
   391  		if s.Tooltip != "" {
   392  			corpus += s.Tooltip
   393  			appendixCount++
   394  			corpus += fmt.Sprint(appendixCount)
   395  		}
   396  		if s.Link != "" {
   397  			corpus += s.Link
   398  			appendixCount++
   399  			corpus += fmt.Sprint(appendixCount)
   400  		}
   401  		corpus += s.PrettyLink
   402  		if s.Type == ShapeClass {
   403  			for _, cf := range s.Fields {
   404  				corpus += cf.Text(0).Text + cf.VisibilityToken()
   405  			}
   406  			for _, cm := range s.Methods {
   407  				corpus += cm.Text(0).Text + cm.VisibilityToken()
   408  			}
   409  		}
   410  		if s.Type == ShapeSQLTable {
   411  			for _, c := range s.Columns {
   412  				for _, t := range c.Texts(0) {
   413  					corpus += t.Text
   414  				}
   415  				corpus += c.ConstraintAbbr()
   416  			}
   417  		}
   418  	}
   419  	for _, c := range diagram.Connections {
   420  		corpus += c.Label
   421  		if c.SrcLabel != nil {
   422  			corpus += c.SrcLabel.Label
   423  		}
   424  		if c.DstLabel != nil {
   425  			corpus += c.DstLabel.Label
   426  		}
   427  	}
   428  
   429  	return corpus
   430  }
   431  
   432  func NewDiagram() *Diagram {
   433  	return &Diagram{
   434  		Root: Shape{
   435  			Fill: BG_COLOR,
   436  		},
   437  	}
   438  }
   439  
   440  type Shape struct {
   441  	ID   string `json:"id"`
   442  	Type string `json:"type"`
   443  
   444  	Classes []string `json:"classes,omitempty"`
   445  
   446  	Pos    Point `json:"pos"`
   447  	Width  int   `json:"width"`
   448  	Height int   `json:"height"`
   449  
   450  	Opacity     float64 `json:"opacity"`
   451  	StrokeDash  float64 `json:"strokeDash"`
   452  	StrokeWidth int     `json:"strokeWidth"`
   453  
   454  	BorderRadius int `json:"borderRadius"`
   455  
   456  	Fill        string `json:"fill"`
   457  	FillPattern string `json:"fillPattern,omitempty"`
   458  	Stroke      string `json:"stroke"`
   459  
   460  	Shadow       bool `json:"shadow"`
   461  	ThreeDee     bool `json:"3d"`
   462  	Multiple     bool `json:"multiple"`
   463  	DoubleBorder bool `json:"double-border"`
   464  
   465  	Tooltip      string   `json:"tooltip"`
   466  	Link         string   `json:"link"`
   467  	PrettyLink   string   `json:"prettyLink,omitempty"`
   468  	Icon         *url.URL `json:"icon"`
   469  	IconPosition string   `json:"iconPosition"`
   470  
   471  	// Whether the shape should allow shapes behind it to bleed through
   472  	// Currently just used for sequence diagram groups
   473  	Blend bool `json:"blend"`
   474  
   475  	Class
   476  	SQLTable
   477  
   478  	ContentAspectRatio *float64 `json:"contentAspectRatio,omitempty"`
   479  
   480  	Text
   481  
   482  	LabelPosition string `json:"labelPosition,omitempty"`
   483  
   484  	ZIndex int `json:"zIndex"`
   485  	Level  int `json:"level"`
   486  
   487  	// These are used for special shapes, sql_table and class
   488  	PrimaryAccentColor   string `json:"primaryAccentColor,omitempty"`
   489  	SecondaryAccentColor string `json:"secondaryAccentColor,omitempty"`
   490  	NeutralAccentColor   string `json:"neutralAccentColor,omitempty"`
   491  }
   492  
   493  func (s Shape) GetFontColor() string {
   494  	if s.Type == ShapeClass || s.Type == ShapeSQLTable {
   495  		if !color.IsThemeColor(s.Color) {
   496  			return s.Color
   497  		}
   498  		return s.Stroke
   499  	}
   500  	if s.Color != color.Empty {
   501  		return s.Color
   502  	}
   503  	return color.N1
   504  }
   505  
   506  // TODO remove this function, just set fields on themeable
   507  func (s Shape) CSSStyle() string {
   508  	out := ""
   509  
   510  	out += fmt.Sprintf(`stroke-width:%d;`, s.StrokeWidth)
   511  	if s.StrokeDash != 0 {
   512  		dashSize, gapSize := svg.GetStrokeDashAttributes(float64(s.StrokeWidth), s.StrokeDash)
   513  		out += fmt.Sprintf(`stroke-dasharray:%f,%f;`, dashSize, gapSize)
   514  	}
   515  
   516  	return out
   517  }
   518  
   519  func (s *Shape) SetType(t string) {
   520  	// Some types are synonyms of other types, but with hinting for autolayout
   521  	// They should only have one representation in the final export
   522  	if strings.EqualFold(t, ShapeCircle) {
   523  		t = ShapeOval
   524  	} else if strings.EqualFold(t, ShapeSquare) {
   525  		t = ShapeRectangle
   526  	}
   527  	s.Type = strings.ToLower(t)
   528  }
   529  
   530  func (s Shape) GetZIndex() int {
   531  	return s.ZIndex
   532  }
   533  
   534  func (s Shape) GetID() string {
   535  	return s.ID
   536  }
   537  
   538  type Text struct {
   539  	Label      string `json:"label"`
   540  	FontSize   int    `json:"fontSize"`
   541  	FontFamily string `json:"fontFamily"`
   542  	Language   string `json:"language"`
   543  	Color      string `json:"color"`
   544  
   545  	Italic    bool `json:"italic"`
   546  	Bold      bool `json:"bold"`
   547  	Underline bool `json:"underline"`
   548  
   549  	LabelWidth  int    `json:"labelWidth"`
   550  	LabelHeight int    `json:"labelHeight"`
   551  	LabelFill   string `json:"labelFill,omitempty"`
   552  }
   553  
   554  func BaseShape() *Shape {
   555  	return &Shape{
   556  		Opacity:     1,
   557  		StrokeDash:  0,
   558  		StrokeWidth: 2,
   559  		Text: Text{
   560  			Bold:       true,
   561  			FontFamily: "DEFAULT",
   562  		},
   563  	}
   564  }
   565  
   566  type Connection struct {
   567  	ID string `json:"id"`
   568  
   569  	Classes []string `json:"classes,omitempty"`
   570  
   571  	Src      string    `json:"src"`
   572  	SrcArrow Arrowhead `json:"srcArrow"`
   573  	SrcLabel *Text     `json:"srcLabel,omitempty"`
   574  
   575  	Dst      string    `json:"dst"`
   576  	DstArrow Arrowhead `json:"dstArrow"`
   577  	DstLabel *Text     `json:"dstLabel,omitempty"`
   578  
   579  	Opacity      float64 `json:"opacity"`
   580  	StrokeDash   float64 `json:"strokeDash"`
   581  	StrokeWidth  int     `json:"strokeWidth"`
   582  	Stroke       string  `json:"stroke"`
   583  	Fill         string  `json:"fill,omitempty"`
   584  	BorderRadius float64 `json:"borderRadius,omitempty"`
   585  
   586  	Text
   587  	LabelPosition   string  `json:"labelPosition"`
   588  	LabelPercentage float64 `json:"labelPercentage"`
   589  
   590  	Route   []*geo.Point `json:"route"`
   591  	IsCurve bool         `json:"isCurve,omitempty"`
   592  
   593  	Animated bool     `json:"animated"`
   594  	Tooltip  string   `json:"tooltip"`
   595  	Icon     *url.URL `json:"icon"`
   596  
   597  	ZIndex int `json:"zIndex"`
   598  }
   599  
   600  func BaseConnection() *Connection {
   601  	return &Connection{
   602  		SrcArrow:     NoArrowhead,
   603  		DstArrow:     NoArrowhead,
   604  		Route:        make([]*geo.Point, 0),
   605  		Opacity:      1,
   606  		StrokeDash:   0,
   607  		StrokeWidth:  2,
   608  		BorderRadius: 10,
   609  		Text: Text{
   610  			Italic:     true,
   611  			FontFamily: "DEFAULT",
   612  		},
   613  	}
   614  }
   615  
   616  func (c Connection) GetFontColor() string {
   617  	if c.Color != color.Empty {
   618  		return c.Color
   619  	}
   620  	return color.N1
   621  }
   622  
   623  func (c Connection) CSSStyle() string {
   624  	out := ""
   625  
   626  	out += fmt.Sprintf(`stroke-width:%d;`, c.StrokeWidth)
   627  	strokeDash := c.StrokeDash
   628  	if strokeDash == 0 && c.Animated {
   629  		strokeDash = 5
   630  	}
   631  	if strokeDash != 0 {
   632  		dashSize, gapSize := svg.GetStrokeDashAttributes(float64(c.StrokeWidth), strokeDash)
   633  		out += fmt.Sprintf(`stroke-dasharray:%f,%f;`, dashSize, gapSize)
   634  
   635  		if c.Animated {
   636  			dashOffset := -10
   637  			if c.SrcArrow != NoArrowhead && c.DstArrow == NoArrowhead {
   638  				dashOffset = 10
   639  			}
   640  			out += fmt.Sprintf(`stroke-dashoffset:%f;`, float64(dashOffset)*(dashSize+gapSize))
   641  			out += fmt.Sprintf(`animation: dashdraw %fs linear infinite;`, gapSize*0.5)
   642  		}
   643  	}
   644  	return out
   645  }
   646  
   647  func (c *Connection) GetLabelTopLeft() *geo.Point {
   648  	point, _ := label.FromString(c.LabelPosition).GetPointOnRoute(
   649  		c.Route,
   650  		float64(c.StrokeWidth),
   651  		c.LabelPercentage,
   652  		float64(c.LabelWidth),
   653  		float64(c.LabelHeight),
   654  	)
   655  	return point
   656  }
   657  
   658  func (connection *Connection) GetArrowheadLabelPosition(isDst bool) *geo.Point {
   659  	var width, height float64
   660  	if isDst {
   661  		width = float64(connection.DstLabel.LabelWidth)
   662  		height = float64(connection.DstLabel.LabelHeight)
   663  	} else {
   664  		width = float64(connection.SrcLabel.LabelWidth)
   665  		height = float64(connection.SrcLabel.LabelHeight)
   666  	}
   667  
   668  	// get the start/end points of edge segment with arrowhead
   669  	index := 0
   670  	if isDst {
   671  		index = len(connection.Route) - 2
   672  	}
   673  	start, end := connection.Route[index], connection.Route[index+1]
   674  	// Note: end to start to get normal towards unlocked top position
   675  	normalX, normalY := geo.GetUnitNormalVector(end.X, end.Y, start.X, start.Y)
   676  
   677  	// determine how much to move the label back from the very end of the edge
   678  	// e.g. if normal points up {x: 0, y:1}, shift width/2 + padding to fit
   679  	shift := math.Abs(normalX)*(height/2.+label.PADDING) +
   680  		math.Abs(normalY)*(width/2.+label.PADDING)
   681  
   682  	length := geo.Route(connection.Route).Length()
   683  	var position float64
   684  	if isDst {
   685  		position = 1.
   686  		if length > 0 {
   687  			position -= shift / length
   688  		}
   689  	} else {
   690  		position = 0.
   691  		if length > 0 {
   692  			position = shift / length
   693  		}
   694  	}
   695  
   696  	strokeWidth := float64(connection.StrokeWidth)
   697  
   698  	labelTL, _ := label.UnlockedTop.GetPointOnRoute(connection.Route, strokeWidth, position, width, height)
   699  
   700  	var arrowSize float64
   701  	if isDst && connection.DstArrow != NoArrowhead {
   702  		// Note: these dimensions are for rendering arrowheads on their side so we want the height
   703  		_, arrowSize = connection.DstArrow.Dimensions(strokeWidth)
   704  	} else if connection.SrcArrow != NoArrowhead {
   705  		_, arrowSize = connection.SrcArrow.Dimensions(strokeWidth)
   706  	}
   707  
   708  	if arrowSize > 0 {
   709  		// labelTL already accounts for strokeWidth and padding, we only want to shift further if the arrow is larger than this
   710  		offset := (arrowSize/2 + ARROWHEAD_PADDING) - strokeWidth/2 - label.PADDING
   711  		if offset > 0 {
   712  			labelTL.X += normalX * offset
   713  			labelTL.Y += normalY * offset
   714  		}
   715  	}
   716  
   717  	return labelTL
   718  }
   719  
   720  func (c Connection) GetZIndex() int {
   721  	return c.ZIndex
   722  }
   723  
   724  func (c Connection) GetID() string {
   725  	return c.ID
   726  }
   727  
   728  type Arrowhead string
   729  
   730  const (
   731  	NoArrowhead               Arrowhead = "none"
   732  	ArrowArrowhead            Arrowhead = "arrow"
   733  	UnfilledTriangleArrowhead Arrowhead = "unfilled-triangle"
   734  	TriangleArrowhead         Arrowhead = "triangle"
   735  	DiamondArrowhead          Arrowhead = "diamond"
   736  	FilledDiamondArrowhead    Arrowhead = "filled-diamond"
   737  	CircleArrowhead           Arrowhead = "circle"
   738  	FilledCircleArrowhead     Arrowhead = "filled-circle"
   739  
   740  	// For fat arrows
   741  	LineArrowhead Arrowhead = "line"
   742  
   743  	// Crows feet notation
   744  	CfOne          Arrowhead = "cf-one"
   745  	CfMany         Arrowhead = "cf-many"
   746  	CfOneRequired  Arrowhead = "cf-one-required"
   747  	CfManyRequired Arrowhead = "cf-many-required"
   748  
   749  	DefaultArrowhead Arrowhead = TriangleArrowhead
   750  )
   751  
   752  // valid values for arrowhead.shape
   753  var Arrowheads = map[string]struct{}{
   754  	string(NoArrowhead):       {},
   755  	string(ArrowArrowhead):    {},
   756  	string(TriangleArrowhead): {},
   757  	string(DiamondArrowhead):  {},
   758  	string(CircleArrowhead):   {},
   759  	string(CfOne):             {},
   760  	string(CfMany):            {},
   761  	string(CfOneRequired):     {},
   762  	string(CfManyRequired):    {},
   763  }
   764  
   765  func ToArrowhead(arrowheadType string, filled *bool) Arrowhead {
   766  	switch arrowheadType {
   767  	case string(DiamondArrowhead):
   768  		if filled != nil && *filled {
   769  			return FilledDiamondArrowhead
   770  		}
   771  		return DiamondArrowhead
   772  	case string(CircleArrowhead):
   773  		if filled != nil && *filled {
   774  			return FilledCircleArrowhead
   775  		}
   776  		return CircleArrowhead
   777  	case string(NoArrowhead):
   778  		return NoArrowhead
   779  	case string(ArrowArrowhead):
   780  		return ArrowArrowhead
   781  	case string(TriangleArrowhead):
   782  		if filled != nil && !(*filled) {
   783  			return UnfilledTriangleArrowhead
   784  		}
   785  		return TriangleArrowhead
   786  	case string(CfOne):
   787  		return CfOne
   788  	case string(CfMany):
   789  		return CfMany
   790  	case string(CfOneRequired):
   791  		return CfOneRequired
   792  	case string(CfManyRequired):
   793  		return CfManyRequired
   794  	default:
   795  		if DefaultArrowhead == TriangleArrowhead &&
   796  			filled != nil && !(*filled) {
   797  			return UnfilledTriangleArrowhead
   798  		}
   799  		return DefaultArrowhead
   800  	}
   801  }
   802  
   803  func (arrowhead Arrowhead) Dimensions(strokeWidth float64) (width, height float64) {
   804  	var baseWidth, baseHeight float64
   805  	var widthMultiplier, heightMultiplier float64
   806  	switch arrowhead {
   807  	case ArrowArrowhead:
   808  		baseWidth = 4
   809  		baseHeight = 4
   810  		widthMultiplier = 4
   811  		heightMultiplier = 4
   812  	case TriangleArrowhead:
   813  		baseWidth = 4
   814  		baseHeight = 4
   815  		widthMultiplier = 3
   816  		heightMultiplier = 4
   817  	case UnfilledTriangleArrowhead:
   818  		baseWidth = 7
   819  		baseHeight = 7
   820  		widthMultiplier = 3
   821  		heightMultiplier = 4
   822  	case LineArrowhead:
   823  		widthMultiplier = 5
   824  		heightMultiplier = 8
   825  	case FilledDiamondArrowhead:
   826  		baseWidth = 11
   827  		baseHeight = 7
   828  		widthMultiplier = 5.5
   829  		heightMultiplier = 3.5
   830  	case DiamondArrowhead:
   831  		baseWidth = 11
   832  		baseHeight = 9
   833  		widthMultiplier = 5.5
   834  		heightMultiplier = 4.5
   835  	case FilledCircleArrowhead, CircleArrowhead:
   836  		baseWidth = 8
   837  		baseHeight = 8
   838  		widthMultiplier = 5
   839  		heightMultiplier = 5
   840  	case CfOne, CfMany, CfOneRequired, CfManyRequired:
   841  		baseWidth = 9
   842  		baseHeight = 9
   843  		widthMultiplier = 4.5
   844  		heightMultiplier = 4.5
   845  	}
   846  
   847  	clippedStrokeWidth := go2.Max(MIN_ARROWHEAD_STROKE_WIDTH, strokeWidth)
   848  	return baseWidth + clippedStrokeWidth*widthMultiplier, baseHeight + clippedStrokeWidth*heightMultiplier
   849  }
   850  
   851  type Point struct {
   852  	X int `json:"x"`
   853  	Y int `json:"y"`
   854  }
   855  
   856  func NewPoint(x, y int) Point {
   857  	return Point{X: x, Y: y}
   858  }
   859  
   860  const (
   861  	ShapeRectangle       = "rectangle"
   862  	ShapeSquare          = "square"
   863  	ShapePage            = "page"
   864  	ShapeParallelogram   = "parallelogram"
   865  	ShapeDocument        = "document"
   866  	ShapeCylinder        = "cylinder"
   867  	ShapeQueue           = "queue"
   868  	ShapePackage         = "package"
   869  	ShapeStep            = "step"
   870  	ShapeCallout         = "callout"
   871  	ShapeStoredData      = "stored_data"
   872  	ShapePerson          = "person"
   873  	ShapeDiamond         = "diamond"
   874  	ShapeOval            = "oval"
   875  	ShapeCircle          = "circle"
   876  	ShapeHexagon         = "hexagon"
   877  	ShapeCloud           = "cloud"
   878  	ShapeText            = "text"
   879  	ShapeCode            = "code"
   880  	ShapeClass           = "class"
   881  	ShapeSQLTable        = "sql_table"
   882  	ShapeImage           = "image"
   883  	ShapeSequenceDiagram = "sequence_diagram"
   884  )
   885  
   886  var Shapes = []string{
   887  	ShapeRectangle,
   888  	ShapeSquare,
   889  	ShapePage,
   890  	ShapeParallelogram,
   891  	ShapeDocument,
   892  	ShapeCylinder,
   893  	ShapeQueue,
   894  	ShapePackage,
   895  	ShapeStep,
   896  	ShapeCallout,
   897  	ShapeStoredData,
   898  	ShapePerson,
   899  	ShapeDiamond,
   900  	ShapeOval,
   901  	ShapeCircle,
   902  	ShapeHexagon,
   903  	ShapeCloud,
   904  	ShapeText,
   905  	ShapeCode,
   906  	ShapeClass,
   907  	ShapeSQLTable,
   908  	ShapeImage,
   909  	ShapeSequenceDiagram,
   910  }
   911  
   912  func IsShape(s string) bool {
   913  	if s == "" {
   914  		// Default shape is rectangle.
   915  		return true
   916  	}
   917  	for _, s2 := range Shapes {
   918  		if strings.EqualFold(s, s2) {
   919  			return true
   920  		}
   921  	}
   922  	return false
   923  }
   924  
   925  type MText struct {
   926  	Text     string `json:"text"`
   927  	FontSize int    `json:"fontSize"`
   928  	IsBold   bool   `json:"isBold"`
   929  	IsItalic bool   `json:"isItalic"`
   930  	Language string `json:"language"`
   931  	Shape    string `json:"shape"`
   932  
   933  	Dimensions TextDimensions `json:"dimensions,omitempty"`
   934  }
   935  
   936  type TextDimensions struct {
   937  	Width  int `json:"width"`
   938  	Height int `json:"height"`
   939  }
   940  
   941  func NewTextDimensions(w, h int) *TextDimensions {
   942  	return &TextDimensions{Width: w, Height: h}
   943  }
   944  
   945  func (text MText) GetColor(isItalic bool) string {
   946  	if isItalic {
   947  		return color.N2
   948  	}
   949  	return color.N1
   950  }
   951  
   952  var DSL_SHAPE_TO_SHAPE_TYPE = map[string]string{
   953  	"":                   shape.SQUARE_TYPE,
   954  	ShapeRectangle:       shape.SQUARE_TYPE,
   955  	ShapeSquare:          shape.REAL_SQUARE_TYPE,
   956  	ShapePage:            shape.PAGE_TYPE,
   957  	ShapeParallelogram:   shape.PARALLELOGRAM_TYPE,
   958  	ShapeDocument:        shape.DOCUMENT_TYPE,
   959  	ShapeCylinder:        shape.CYLINDER_TYPE,
   960  	ShapeQueue:           shape.QUEUE_TYPE,
   961  	ShapePackage:         shape.PACKAGE_TYPE,
   962  	ShapeStep:            shape.STEP_TYPE,
   963  	ShapeCallout:         shape.CALLOUT_TYPE,
   964  	ShapeStoredData:      shape.STORED_DATA_TYPE,
   965  	ShapePerson:          shape.PERSON_TYPE,
   966  	ShapeDiamond:         shape.DIAMOND_TYPE,
   967  	ShapeOval:            shape.OVAL_TYPE,
   968  	ShapeCircle:          shape.CIRCLE_TYPE,
   969  	ShapeHexagon:         shape.HEXAGON_TYPE,
   970  	ShapeCloud:           shape.CLOUD_TYPE,
   971  	ShapeText:            shape.TEXT_TYPE,
   972  	ShapeCode:            shape.CODE_TYPE,
   973  	ShapeClass:           shape.CLASS_TYPE,
   974  	ShapeSQLTable:        shape.TABLE_TYPE,
   975  	ShapeImage:           shape.IMAGE_TYPE,
   976  	ShapeSequenceDiagram: shape.SQUARE_TYPE,
   977  }
   978  
   979  var SHAPE_TYPE_TO_DSL_SHAPE map[string]string
   980  
   981  func init() {
   982  	SHAPE_TYPE_TO_DSL_SHAPE = make(map[string]string, len(DSL_SHAPE_TO_SHAPE_TYPE))
   983  	for k, v := range DSL_SHAPE_TO_SHAPE_TYPE {
   984  		SHAPE_TYPE_TO_DSL_SHAPE[v] = k
   985  	}
   986  	// SQUARE_TYPE is defined twice in the map, make sure it doesn't get set to the empty string one
   987  	SHAPE_TYPE_TO_DSL_SHAPE[shape.SQUARE_TYPE] = ShapeRectangle
   988  }
   989  
   990  func GetIconSize(box *geo.Box, position string) int {
   991  	iconPosition := label.FromString(position)
   992  
   993  	minDimension := int(math.Min(box.Width, box.Height))
   994  	halfMinDimension := int(math.Ceil(0.5 * float64(minDimension)))
   995  
   996  	var size int
   997  
   998  	if iconPosition == label.InsideMiddleCenter {
   999  		size = halfMinDimension
  1000  	} else {
  1001  		size = go2.Min(
  1002  			minDimension,
  1003  			go2.Max(DEFAULT_ICON_SIZE, halfMinDimension),
  1004  		)
  1005  	}
  1006  
  1007  	size = go2.Min(size, MAX_ICON_SIZE)
  1008  
  1009  	if !iconPosition.IsOutside() {
  1010  		size = go2.Min(size,
  1011  			go2.Min(
  1012  				go2.Max(int(box.Width)-2*label.PADDING, 0),
  1013  				go2.Max(int(box.Height)-2*label.PADDING, 0),
  1014  			),
  1015  		)
  1016  	}
  1017  
  1018  	return size
  1019  }
  1020  

View as plain text