1 package svg
2
3 import (
4 "fmt"
5 "math"
6 "strings"
7
8 "oss.terrastruct.com/d2/lib/geo"
9 )
10
11 type SvgPathContext struct {
12 Path []geo.Intersectable
13 Commands []string
14 Start *geo.Point
15 Current *geo.Point
16 TopLeft *geo.Point
17 ScaleX float64
18 ScaleY float64
19 }
20
21
22 func chopPrecision(f float64) float64 {
23
24 return math.Round(float64(float32(f*10000)) / 10000)
25 }
26
27 func NewSVGPathContext(tl *geo.Point, sx, sy float64) *SvgPathContext {
28 return &SvgPathContext{TopLeft: tl.Copy(), ScaleX: sx, ScaleY: sy}
29 }
30
31 func (c *SvgPathContext) Relative(base *geo.Point, dx, dy float64) *geo.Point {
32 return geo.NewPoint(chopPrecision(base.X+c.ScaleX*dx), chopPrecision(base.Y+c.ScaleY*dy))
33 }
34 func (c *SvgPathContext) Absolute(x, y float64) *geo.Point {
35 return c.Relative(c.TopLeft, x, y)
36 }
37
38 func (c *SvgPathContext) StartAt(p *geo.Point) {
39 c.Start = p
40 c.Commands = append(c.Commands, fmt.Sprintf("M %v %v", p.X, p.Y))
41 c.Current = p.Copy()
42 }
43
44 func (c *SvgPathContext) Z() {
45 c.Path = append(c.Path, &geo.Segment{Start: c.Current.Copy(), End: c.Start.Copy()})
46 c.Commands = append(c.Commands, "Z")
47 c.Current = c.Start.Copy()
48 }
49
50 func (c *SvgPathContext) L(isLowerCase bool, x, y float64) {
51 var endPoint *geo.Point
52 if isLowerCase {
53 endPoint = c.Relative(c.Current, x, y)
54 } else {
55 endPoint = c.Absolute(x, y)
56 }
57 c.Path = append(c.Path, &geo.Segment{Start: c.Current.Copy(), End: endPoint})
58 c.Commands = append(c.Commands, fmt.Sprintf("L %v %v", endPoint.X, endPoint.Y))
59 c.Current = endPoint.Copy()
60 }
61
62 func (c *SvgPathContext) C(isLowerCase bool, x1, y1, x2, y2, x3, y3 float64) {
63 p := func(x, y float64) *geo.Point {
64 if isLowerCase {
65 return c.Relative(c.Current, x, y)
66 }
67 return c.Absolute(x, y)
68 }
69 points := []*geo.Point{c.Current.Copy(), p(x1, y1), p(x2, y2), p(x3, y3)}
70 c.Path = append(c.Path, geo.NewBezierCurve(points))
71 c.Commands = append(c.Commands, fmt.Sprintf(
72 "C %v %v %v %v %v %v",
73 points[1].X, points[1].Y,
74 points[2].X, points[2].Y,
75 points[3].X, points[3].Y,
76 ))
77 c.Current = points[3].Copy()
78 }
79
80 func (c *SvgPathContext) H(isLowerCase bool, x float64) {
81 var endPoint *geo.Point
82 if isLowerCase {
83 endPoint = c.Relative(c.Current, x, 0)
84 } else {
85 endPoint = c.Absolute(x, 0)
86 endPoint.Y = c.Current.Y
87 }
88 c.Path = append(c.Path, &geo.Segment{Start: c.Current.Copy(), End: endPoint.Copy()})
89 c.Commands = append(c.Commands, fmt.Sprintf("H %v", endPoint.X))
90 c.Current = endPoint.Copy()
91 }
92
93 func (c *SvgPathContext) V(isLowerCase bool, y float64) {
94 var endPoint *geo.Point
95 if isLowerCase {
96 endPoint = c.Relative(c.Current, 0, y)
97 } else {
98 endPoint = c.Absolute(0, y)
99 endPoint.X = c.Current.X
100 }
101 c.Path = append(c.Path, &geo.Segment{Start: c.Current.Copy(), End: endPoint})
102 c.Commands = append(c.Commands, fmt.Sprintf("V %v", endPoint.Y))
103 c.Current = endPoint.Copy()
104 }
105
106 func (c *SvgPathContext) PathData() string {
107 return strings.Join(c.Commands, " ")
108 }
109
110 func GetStrokeDashAttributes(strokeWidth, dashGapSize float64) (float64, float64) {
111
112 scale := math.Log10(-0.6*strokeWidth+10.6)*0.5 + 0.5
113 scaledDashSize := strokeWidth * dashGapSize
114 scaledGapSize := scale * scaledDashSize
115 return scaledDashSize, scaledGapSize
116 }
117
View as plain text