...

Source file src/oss.terrastruct.com/d2/lib/svg/path.go

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

     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  // TODO probably use math.Big
    22  func chopPrecision(f float64) float64 {
    23  	// bring down to float32 precision before rounding for consistency across architectures
    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  	// as the stroke width gets thicker, the dash gap gets smaller
   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