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
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
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
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
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
472
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
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
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
521
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
669 index := 0
670 if isDst {
671 index = len(connection.Route) - 2
672 }
673 start, end := connection.Route[index], connection.Route[index+1]
674
675 normalX, normalY := geo.GetUnitNormalVector(end.X, end.Y, start.X, start.Y)
676
677
678
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
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
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
741 LineArrowhead Arrowhead = "line"
742
743
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
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
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
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