1 package d2sketch
2
3 import (
4 "bytes"
5 "encoding/json"
6 "fmt"
7 "regexp"
8 "strings"
9
10 _ "embed"
11
12 "github.com/dop251/goja"
13
14 "oss.terrastruct.com/d2/d2target"
15 "oss.terrastruct.com/d2/d2themes"
16 "oss.terrastruct.com/d2/lib/color"
17 "oss.terrastruct.com/d2/lib/geo"
18 "oss.terrastruct.com/d2/lib/label"
19 "oss.terrastruct.com/d2/lib/svg"
20 "oss.terrastruct.com/util-go/go2"
21 )
22
23
24 var roughJS string
25
26
27 var setupJS string
28
29
30 var streaks string
31
32 type Runner goja.Runtime
33
34 var baseRoughProps = `fillWeight: 2.0,
35 hachureGap: 16,
36 fillStyle: "solid",
37 bowing: 2,
38 seed: 1,`
39
40 var floatRE = regexp.MustCompile(`(\d+)\.(\d+)`)
41
42 const (
43 BG_COLOR = color.N7
44 FG_COLOR = color.N1
45 )
46
47 func (r *Runner) run(js string) (goja.Value, error) {
48 vm := (*goja.Runtime)(r)
49 return vm.RunString(js)
50 }
51
52 func InitSketchVM() (*Runner, error) {
53 vm := goja.New()
54 if _, err := vm.RunString(roughJS); err != nil {
55 return nil, err
56 }
57 if _, err := vm.RunString(setupJS); err != nil {
58 return nil, err
59 }
60 r := Runner(*vm)
61 return &r, nil
62 }
63
64
65
66
67 func DefineFillPatterns(buf *bytes.Buffer) {
68 source := buf.String()
69 fmt.Fprint(buf, "<defs>")
70
71 defineFillPattern(buf, source, "bright", "rgba(0, 0, 0, 0.1)")
72 defineFillPattern(buf, source, "normal", "rgba(0, 0, 0, 0.16)")
73 defineFillPattern(buf, source, "dark", "rgba(0, 0, 0, 0.32)")
74 defineFillPattern(buf, source, "darker", "rgba(255, 255, 255, 0.24)")
75
76 fmt.Fprint(buf, "</defs>")
77 }
78
79 func defineFillPattern(buf *bytes.Buffer, source string, luminanceCategory, fill string) {
80 trigger := fmt.Sprintf(`url(#streaks-%s)`, luminanceCategory)
81 if strings.Contains(source, trigger) {
82 fmt.Fprintf(buf, streaks, luminanceCategory, fill)
83 }
84 }
85
86 func Rect(r *Runner, shape d2target.Shape) (string, error) {
87 js := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, {
88 fill: "#000",
89 stroke: "#000",
90 strokeWidth: %d,
91 %s
92 });`, shape.Width, shape.Height, shape.StrokeWidth, baseRoughProps)
93 paths, err := computeRoughPathData(r, js)
94 if err != nil {
95 return "", err
96 }
97 output := ""
98 pathEl := d2themes.NewThemableElement("path")
99 pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
100 pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
101 pathEl.FillPattern = shape.FillPattern
102 pathEl.ClassName = "shape"
103 pathEl.Style = shape.CSSStyle()
104 for _, p := range paths {
105 pathEl.D = p
106 output += pathEl.Render()
107 }
108
109 sketchOEl := d2themes.NewThemableElement("rect")
110 sketchOEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
111 sketchOEl.Width = float64(shape.Width)
112 sketchOEl.Height = float64(shape.Height)
113 renderedSO, err := d2themes.NewThemableSketchOverlay(sketchOEl, pathEl.Fill).Render()
114 if err != nil {
115 return "", err
116 }
117 output += renderedSO
118
119 return output, nil
120 }
121
122 func DoubleRect(r *Runner, shape d2target.Shape) (string, error) {
123 jsBigRect := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, {
124 fill: "#000",
125 stroke: "#000",
126 strokeWidth: %d,
127 %s
128 });`, shape.Width, shape.Height, shape.StrokeWidth, baseRoughProps)
129 pathsBigRect, err := computeRoughPathData(r, jsBigRect)
130 if err != nil {
131 return "", err
132 }
133 jsSmallRect := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, {
134 fill: "#000",
135 stroke: "#000",
136 strokeWidth: %d,
137 %s
138 });`, shape.Width-d2target.INNER_BORDER_OFFSET*2, shape.Height-d2target.INNER_BORDER_OFFSET*2, shape.StrokeWidth, baseRoughProps)
139 pathsSmallRect, err := computeRoughPathData(r, jsSmallRect)
140 if err != nil {
141 return "", err
142 }
143
144 output := ""
145
146 pathEl := d2themes.NewThemableElement("path")
147 pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
148 pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
149 pathEl.FillPattern = shape.FillPattern
150 pathEl.ClassName = "shape"
151 pathEl.Style = shape.CSSStyle()
152 for _, p := range pathsBigRect {
153 pathEl.D = p
154 output += pathEl.Render()
155 }
156
157 pathEl = d2themes.NewThemableElement("path")
158 pathEl.SetTranslate(float64(shape.Pos.X+d2target.INNER_BORDER_OFFSET), float64(shape.Pos.Y+d2target.INNER_BORDER_OFFSET))
159 pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
160
161 pathEl.Fill = "transparent"
162 pathEl.ClassName = "shape"
163 pathEl.Style = shape.CSSStyle()
164 for _, p := range pathsSmallRect {
165 pathEl.D = p
166 output += pathEl.Render()
167 }
168
169 sketchOEl := d2themes.NewThemableElement("rect")
170 sketchOEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
171 sketchOEl.Width = float64(shape.Width)
172 sketchOEl.Height = float64(shape.Height)
173 renderedSO, err := d2themes.NewThemableSketchOverlay(sketchOEl, shape.Fill).Render()
174 if err != nil {
175 return "", err
176 }
177 output += renderedSO
178
179 return output, nil
180 }
181
182 func Oval(r *Runner, shape d2target.Shape) (string, error) {
183 js := fmt.Sprintf(`node = rc.ellipse(%d, %d, %d, %d, {
184 fill: "#000",
185 stroke: "#000",
186 strokeWidth: %d,
187 %s
188 });`, shape.Width/2, shape.Height/2, shape.Width, shape.Height, shape.StrokeWidth, baseRoughProps)
189 paths, err := computeRoughPathData(r, js)
190 if err != nil {
191 return "", err
192 }
193 output := ""
194 pathEl := d2themes.NewThemableElement("path")
195 pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
196 pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
197 pathEl.FillPattern = shape.FillPattern
198 pathEl.ClassName = "shape"
199 pathEl.Style = shape.CSSStyle()
200 for _, p := range paths {
201 pathEl.D = p
202 output += pathEl.Render()
203 }
204
205 soElement := d2themes.NewThemableElement("ellipse")
206 soElement.SetTranslate(float64(shape.Pos.X+shape.Width/2), float64(shape.Pos.Y+shape.Height/2))
207 soElement.Rx = float64(shape.Width / 2)
208 soElement.Ry = float64(shape.Height / 2)
209 renderedSO, err := d2themes.NewThemableSketchOverlay(
210 soElement,
211 pathEl.Fill,
212 ).Render()
213 if err != nil {
214 return "", err
215 }
216 output += renderedSO
217
218 return output, nil
219 }
220
221 func DoubleOval(r *Runner, shape d2target.Shape) (string, error) {
222 jsBigCircle := fmt.Sprintf(`node = rc.ellipse(%d, %d, %d, %d, {
223 fill: "#000",
224 stroke: "#000",
225 strokeWidth: %d,
226 %s
227 });`, shape.Width/2, shape.Height/2, shape.Width, shape.Height, shape.StrokeWidth, baseRoughProps)
228 jsSmallCircle := fmt.Sprintf(`node = rc.ellipse(%d, %d, %d, %d, {
229 fill: "#000",
230 stroke: "#000",
231 strokeWidth: %d,
232 %s
233 });`, shape.Width/2, shape.Height/2, shape.Width-d2target.INNER_BORDER_OFFSET*2, shape.Height-d2target.INNER_BORDER_OFFSET*2, shape.StrokeWidth, baseRoughProps)
234 pathsBigCircle, err := computeRoughPathData(r, jsBigCircle)
235 if err != nil {
236 return "", err
237 }
238 pathsSmallCircle, err := computeRoughPathData(r, jsSmallCircle)
239 if err != nil {
240 return "", err
241 }
242
243 output := ""
244
245 pathEl := d2themes.NewThemableElement("path")
246 pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
247 pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
248 pathEl.FillPattern = shape.FillPattern
249 pathEl.ClassName = "shape"
250 pathEl.Style = shape.CSSStyle()
251 for _, p := range pathsBigCircle {
252 pathEl.D = p
253 output += pathEl.Render()
254 }
255
256 pathEl = d2themes.NewThemableElement("path")
257 pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
258 pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
259
260 pathEl.Fill = "transparent"
261 pathEl.ClassName = "shape"
262 pathEl.Style = shape.CSSStyle()
263 for _, p := range pathsSmallCircle {
264 pathEl.D = p
265 output += pathEl.Render()
266 }
267 soElement := d2themes.NewThemableElement("ellipse")
268 soElement.SetTranslate(float64(shape.Pos.X+shape.Width/2), float64(shape.Pos.Y+shape.Height/2))
269 soElement.Rx = float64(shape.Width / 2)
270 soElement.Ry = float64(shape.Height / 2)
271 renderedSO, err := d2themes.NewThemableSketchOverlay(
272 soElement,
273 shape.Fill,
274 ).Render()
275 if err != nil {
276 return "", err
277 }
278 output += renderedSO
279
280 return output, nil
281 }
282
283
284 func Paths(r *Runner, shape d2target.Shape, paths []string) (string, error) {
285 output := ""
286 for _, path := range paths {
287 js := fmt.Sprintf(`node = rc.path("%s", {
288 fill: "#000",
289 stroke: "#000",
290 strokeWidth: %d,
291 %s
292 });`, path, shape.StrokeWidth, baseRoughProps)
293 sketchPaths, err := computeRoughPathData(r, js)
294 if err != nil {
295 return "", err
296 }
297 pathEl := d2themes.NewThemableElement("path")
298 pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
299 pathEl.FillPattern = shape.FillPattern
300 pathEl.ClassName = "shape"
301 pathEl.Style = shape.CSSStyle()
302 for _, p := range sketchPaths {
303 pathEl.D = p
304 output += pathEl.Render()
305 }
306
307 soElement := d2themes.NewThemableElement("path")
308 for _, p := range sketchPaths {
309 soElement.D = p
310 renderedSO, err := d2themes.NewThemableSketchOverlay(
311 soElement,
312 pathEl.Fill,
313 ).Render()
314 if err != nil {
315 return "", err
316 }
317 output += renderedSO
318 }
319 }
320 return output, nil
321 }
322
323 func Connection(r *Runner, connection d2target.Connection, path, attrs string) (string, error) {
324 roughness := 0.5
325 js := fmt.Sprintf(`node = rc.path("%s", {roughness: %f, seed: 1});`, path, roughness)
326 paths, err := computeRoughPathData(r, js)
327 if err != nil {
328 return "", err
329 }
330 output := ""
331 animatedClass := ""
332 if connection.Animated {
333 animatedClass = " animated-connection"
334 }
335
336 pathEl := d2themes.NewThemableElement("path")
337 pathEl.Fill = color.None
338 pathEl.Stroke = connection.Stroke
339 pathEl.ClassName = fmt.Sprintf("connection%s", animatedClass)
340 pathEl.Style = connection.CSSStyle()
341 pathEl.Attributes = attrs
342 for _, p := range paths {
343 pathEl.D = p
344 output += pathEl.Render()
345 }
346 return output, nil
347 }
348
349
350 func Table(r *Runner, shape d2target.Shape) (string, error) {
351 output := ""
352 js := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, {
353 fill: "#000",
354 stroke: "#000",
355 strokeWidth: %d,
356 %s
357 });`, shape.Width, shape.Height, shape.StrokeWidth, baseRoughProps)
358 paths, err := computeRoughPathData(r, js)
359 if err != nil {
360 return "", err
361 }
362 pathEl := d2themes.NewThemableElement("path")
363 pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
364 pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
365 pathEl.FillPattern = shape.FillPattern
366 pathEl.ClassName = "shape"
367 pathEl.Style = shape.CSSStyle()
368 for _, p := range paths {
369 pathEl.D = p
370 output += pathEl.Render()
371 }
372
373 box := geo.NewBox(
374 geo.NewPoint(float64(shape.Pos.X), float64(shape.Pos.Y)),
375 float64(shape.Width),
376 float64(shape.Height),
377 )
378 rowHeight := box.Height / float64(1+len(shape.SQLTable.Columns))
379 headerBox := geo.NewBox(box.TopLeft, box.Width, rowHeight)
380
381 js = fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %f, {
382 fill: "#000",
383 %s
384 });`, shape.Width, rowHeight, baseRoughProps)
385 paths, err = computeRoughPathData(r, js)
386 if err != nil {
387 return "", err
388 }
389 pathEl = d2themes.NewThemableElement("path")
390 pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
391 pathEl.Fill = shape.Fill
392 pathEl.FillPattern = shape.FillPattern
393 pathEl.ClassName = "class_header"
394 for _, p := range paths {
395 pathEl.D = p
396 output += pathEl.Render()
397 }
398
399 if shape.Label != "" {
400 tl := label.InsideMiddleLeft.GetPointOnBox(
401 headerBox,
402 20,
403 float64(shape.LabelWidth),
404 float64(shape.LabelHeight),
405 )
406
407 textEl := d2themes.NewThemableElement("text")
408 textEl.X = tl.X
409 textEl.Y = tl.Y + float64(shape.LabelHeight)*3/4
410 textEl.Fill = shape.GetFontColor()
411 textEl.ClassName = "text"
412 textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx",
413 "start", 4+shape.FontSize,
414 )
415 textEl.Content = svg.EscapeText(shape.Label)
416 output += textEl.Render()
417 }
418
419 var longestNameWidth int
420 for _, f := range shape.Columns {
421 longestNameWidth = go2.Max(longestNameWidth, f.Name.LabelWidth)
422 }
423
424 rowBox := geo.NewBox(box.TopLeft.Copy(), box.Width, rowHeight)
425 rowBox.TopLeft.Y += headerBox.Height
426 for _, f := range shape.Columns {
427 nameTL := label.InsideMiddleLeft.GetPointOnBox(
428 rowBox,
429 d2target.NamePadding,
430 rowBox.Width,
431 float64(shape.FontSize),
432 )
433 constraintTR := label.InsideMiddleRight.GetPointOnBox(
434 rowBox,
435 d2target.TypePadding,
436 0,
437 float64(shape.FontSize),
438 )
439
440 textEl := d2themes.NewThemableElement("text")
441 textEl.X = nameTL.X
442 textEl.Y = nameTL.Y + float64(shape.FontSize)*3/4
443 textEl.Fill = shape.PrimaryAccentColor
444 textEl.ClassName = "text"
445 textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx", "start", float64(shape.FontSize))
446 textEl.Content = svg.EscapeText(f.Name.Label)
447 output += textEl.Render()
448
449 textEl.X = nameTL.X + float64(longestNameWidth) + 2*d2target.NamePadding
450 textEl.Fill = shape.NeutralAccentColor
451 textEl.Content = svg.EscapeText(f.Type.Label)
452 output += textEl.Render()
453
454 textEl.X = constraintTR.X
455 textEl.Y = constraintTR.Y + float64(shape.FontSize)*3/4
456 textEl.Fill = shape.SecondaryAccentColor
457 textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx;letter-spacing:2px", "end", float64(shape.FontSize))
458 textEl.Content = f.ConstraintAbbr()
459 output += textEl.Render()
460
461 rowBox.TopLeft.Y += rowHeight
462
463 js = fmt.Sprintf(`node = rc.line(%f, %f, %f, %f, {
464 %s
465 });`, rowBox.TopLeft.X, rowBox.TopLeft.Y, rowBox.TopLeft.X+rowBox.Width, rowBox.TopLeft.Y, baseRoughProps)
466 paths, err = computeRoughPathData(r, js)
467 if err != nil {
468 return "", err
469 }
470 pathEl := d2themes.NewThemableElement("path")
471 pathEl.Fill = shape.Fill
472 pathEl.FillPattern = shape.FillPattern
473 for _, p := range paths {
474 pathEl.D = p
475 output += pathEl.Render()
476 }
477 }
478
479 sketchOEl := d2themes.NewThemableElement("rect")
480 sketchOEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
481 sketchOEl.Width = float64(shape.Width)
482 sketchOEl.Height = float64(shape.Height)
483 renderedSO, err := d2themes.NewThemableSketchOverlay(sketchOEl, pathEl.Fill).Render()
484 if err != nil {
485 return "", err
486 }
487 output += renderedSO
488
489 return output, nil
490 }
491
492 func Class(r *Runner, shape d2target.Shape) (string, error) {
493 output := ""
494 js := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, {
495 fill: "#000",
496 stroke: "#000",
497 strokeWidth: %d,
498 %s
499 });`, shape.Width, shape.Height, shape.StrokeWidth, baseRoughProps)
500 paths, err := computeRoughPathData(r, js)
501 if err != nil {
502 return "", err
503 }
504 pathEl := d2themes.NewThemableElement("path")
505 pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
506 pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
507 pathEl.FillPattern = shape.FillPattern
508 pathEl.ClassName = "shape"
509 pathEl.Style = shape.CSSStyle()
510 for _, p := range paths {
511 pathEl.D = p
512 output += pathEl.Render()
513 }
514
515 box := geo.NewBox(
516 geo.NewPoint(float64(shape.Pos.X), float64(shape.Pos.Y)),
517 float64(shape.Width),
518 float64(shape.Height),
519 )
520
521 rowHeight := box.Height / float64(2+len(shape.Class.Fields)+len(shape.Class.Methods))
522 headerBox := geo.NewBox(box.TopLeft, box.Width, 2*rowHeight)
523
524 js = fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %f, {
525 fill: "#000",
526 %s
527 });`, shape.Width, headerBox.Height, baseRoughProps)
528 paths, err = computeRoughPathData(r, js)
529 if err != nil {
530 return "", err
531 }
532 pathEl = d2themes.NewThemableElement("path")
533 pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
534 pathEl.Fill = shape.Fill
535 pathEl.FillPattern = shape.FillPattern
536 pathEl.ClassName = "class_header"
537 for _, p := range paths {
538 pathEl.D = p
539 output += pathEl.Render()
540 }
541
542 sketchOEl := d2themes.NewThemableElement("rect")
543 sketchOEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
544 sketchOEl.Width = float64(shape.Width)
545 sketchOEl.Height = headerBox.Height
546 renderedSO, err := d2themes.NewThemableSketchOverlay(sketchOEl, pathEl.Fill).Render()
547 if err != nil {
548 return "", err
549 }
550 output += renderedSO
551
552 if shape.Label != "" {
553 tl := label.InsideMiddleCenter.GetPointOnBox(
554 headerBox,
555 0,
556 float64(shape.LabelWidth),
557 float64(shape.LabelHeight),
558 )
559
560 textEl := d2themes.NewThemableElement("text")
561 textEl.X = tl.X + float64(shape.LabelWidth)/2
562 textEl.Y = tl.Y + float64(shape.LabelHeight)*3/4
563 textEl.Fill = shape.GetFontColor()
564 textEl.ClassName = "text-mono"
565 textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx",
566 "middle",
567 4+shape.FontSize,
568 )
569 textEl.Content = svg.EscapeText(shape.Label)
570 output += textEl.Render()
571 }
572
573 rowBox := geo.NewBox(box.TopLeft.Copy(), box.Width, rowHeight)
574 rowBox.TopLeft.Y += headerBox.Height
575 for _, f := range shape.Fields {
576 output += classRow(shape, rowBox, f.VisibilityToken(), f.Name, f.Type, float64(shape.FontSize))
577 rowBox.TopLeft.Y += rowHeight
578 }
579
580 js = fmt.Sprintf(`node = rc.line(%f, %f, %f, %f, {
581 %s
582 });`, rowBox.TopLeft.X, rowBox.TopLeft.Y, rowBox.TopLeft.X+rowBox.Width, rowBox.TopLeft.Y, baseRoughProps)
583 paths, err = computeRoughPathData(r, js)
584 if err != nil {
585 return "", err
586 }
587 pathEl = d2themes.NewThemableElement("path")
588 pathEl.Fill = shape.Fill
589 pathEl.FillPattern = shape.FillPattern
590 pathEl.ClassName = "class_header"
591 for _, p := range paths {
592 pathEl.D = p
593 output += pathEl.Render()
594 }
595
596 for _, m := range shape.Methods {
597 output += classRow(shape, rowBox, m.VisibilityToken(), m.Name, m.Return, float64(shape.FontSize))
598 rowBox.TopLeft.Y += rowHeight
599 }
600
601 return output, nil
602 }
603
604 func classRow(shape d2target.Shape, box *geo.Box, prefix, nameText, typeText string, fontSize float64) string {
605 output := ""
606 prefixTL := label.InsideMiddleLeft.GetPointOnBox(
607 box,
608 d2target.PrefixPadding,
609 box.Width,
610 fontSize,
611 )
612 typeTR := label.InsideMiddleRight.GetPointOnBox(
613 box,
614 d2target.TypePadding,
615 0,
616 fontSize,
617 )
618
619 textEl := d2themes.NewThemableElement("text")
620 textEl.X = prefixTL.X
621 textEl.Y = prefixTL.Y + fontSize*3/4
622 textEl.Fill = shape.PrimaryAccentColor
623 textEl.ClassName = "text-mono"
624 textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx", "start", fontSize)
625 textEl.Content = prefix
626 output += textEl.Render()
627
628 textEl.X = prefixTL.X + d2target.PrefixWidth
629 textEl.Fill = shape.Fill
630 textEl.Content = svg.EscapeText(nameText)
631 output += textEl.Render()
632
633 textEl.X = typeTR.X
634 textEl.Y = typeTR.Y + fontSize*3/4
635 textEl.Fill = shape.SecondaryAccentColor
636 textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx", "end", fontSize)
637 textEl.Content = svg.EscapeText(typeText)
638 output += textEl.Render()
639
640 return output
641 }
642
643 func computeRoughPathData(r *Runner, js string) ([]string, error) {
644 if _, err := r.run(js); err != nil {
645 return nil, err
646 }
647 roughPaths, err := extractRoughPaths(r)
648 if err != nil {
649 return nil, err
650 }
651 return extractPathData(roughPaths)
652 }
653
654 func computeRoughPaths(r *Runner, js string) ([]roughPath, error) {
655 if _, err := r.run(js); err != nil {
656 return nil, err
657 }
658 return extractRoughPaths(r)
659 }
660
661 type attrs struct {
662 D string `json:"d"`
663 }
664
665 type style struct {
666 Stroke string `json:"stroke,omitempty"`
667 StrokeWidth string `json:"strokeWidth,omitempty"`
668 Fill string `json:"fill,omitempty"`
669 }
670
671 type roughPath struct {
672 Attrs attrs `json:"attrs"`
673 Style style `json:"style"`
674 }
675
676 func (rp roughPath) StyleCSS() string {
677 style := ""
678 if rp.Style.StrokeWidth != "" {
679 style += fmt.Sprintf("stroke-width:%s;", rp.Style.StrokeWidth)
680 }
681 return style
682 }
683
684 func extractRoughPaths(r *Runner) ([]roughPath, error) {
685 val, err := r.run("JSON.stringify(node.children, null, ' ')")
686 if err != nil {
687 return nil, err
688 }
689
690 var roughPaths []roughPath
691 err = json.Unmarshal([]byte(val.String()), &roughPaths)
692 if err != nil {
693 return nil, err
694 }
695
696
697 for i := range roughPaths {
698
699 roughPaths[i].Attrs.D = floatRE.ReplaceAllStringFunc(roughPaths[i].Attrs.D, func(floatStr string) string {
700 i := strings.Index(floatStr, ".")
701 decimalLen := len(floatStr) - i - 1
702 end := i + go2.Min(decimalLen, 6)
703 return floatStr[:end+1]
704 })
705 }
706
707 return roughPaths, nil
708 }
709
710 func extractPathData(roughPaths []roughPath) ([]string, error) {
711 var paths []string
712 for _, rp := range roughPaths {
713 paths = append(paths, rp.Attrs.D)
714 }
715 return paths, nil
716 }
717
718 func ArrowheadJS(r *Runner, arrowhead d2target.Arrowhead, stroke string, strokeWidth int) (arrowJS, extraJS string) {
719
720 switch arrowhead {
721 case d2target.ArrowArrowhead:
722 arrowJS = fmt.Sprintf(
723 `node = rc.linearPath(%s, { strokeWidth: %d, stroke: "%s", seed: 3 })`,
724 `[[-10, -4], [0, 0], [-10, 4]]`,
725 strokeWidth,
726 stroke,
727 )
728 case d2target.TriangleArrowhead:
729 arrowJS = fmt.Sprintf(
730 `node = rc.polygon(%s, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "solid", seed: 2 })`,
731 `[[-10, -4], [0, 0], [-10, 4]]`,
732 strokeWidth,
733 stroke,
734 stroke,
735 )
736 case d2target.UnfilledTriangleArrowhead:
737 arrowJS = fmt.Sprintf(
738 `node = rc.polygon(%s, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "solid", seed: 2 })`,
739 `[[-10, -4], [0, 0], [-10, 4]]`,
740 strokeWidth,
741 stroke,
742 BG_COLOR,
743 )
744 case d2target.DiamondArrowhead:
745 arrowJS = fmt.Sprintf(
746 `node = rc.polygon(%s, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "solid", seed: 1 })`,
747 `[[-20, 0], [-10, 5], [0, 0], [-10, -5], [-20, 0]]`,
748 strokeWidth,
749 stroke,
750 BG_COLOR,
751 )
752 case d2target.FilledDiamondArrowhead:
753 arrowJS = fmt.Sprintf(
754 `node = rc.polygon(%s, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "zigzag", fillWeight: 4, seed: 1 })`,
755 `[[-20, 0], [-10, 5], [0, 0], [-10, -5], [-20, 0]]`,
756 strokeWidth,
757 stroke,
758 stroke,
759 )
760 case d2target.CfManyRequired:
761 arrowJS = fmt.Sprintf(
762
763 `node = rc.path(%s, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "solid", fillWeight: 4, seed: 2 })`,
764 `"M-15,-10 -15,10 M0,10 -15,0 M0,-10 -15,0"`,
765 strokeWidth,
766 stroke,
767 stroke,
768 )
769 case d2target.CfMany:
770 arrowJS = fmt.Sprintf(
771 `node = rc.path(%s, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "solid", fillWeight: 4, seed: 8 })`,
772 `"M0,10 -15,0 M0,-10 -15,0"`,
773 strokeWidth,
774 stroke,
775 stroke,
776 )
777 extraJS = fmt.Sprintf(
778 `node = rc.circle(-20, 0, 8, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "solid", fillWeight: 1, seed: 4 })`,
779 strokeWidth,
780 stroke,
781 BG_COLOR,
782 )
783 case d2target.CfOneRequired:
784 arrowJS = fmt.Sprintf(
785 `node = rc.path(%s, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "solid", fillWeight: 4, seed: 2 })`,
786 `"M-15,-10 -15,10 M-10,-10 -10,10"`,
787 strokeWidth,
788 stroke,
789 stroke,
790 )
791 case d2target.CfOne:
792 arrowJS = fmt.Sprintf(
793 `node = rc.path(%s, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "solid", fillWeight: 4, seed: 3 })`,
794 `"M-10,-10 -10,10"`,
795 strokeWidth,
796 stroke,
797 stroke,
798 )
799 extraJS = fmt.Sprintf(
800 `node = rc.circle(-20, 0, 8, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "solid", fillWeight: 1, seed: 5 })`,
801 strokeWidth,
802 stroke,
803 BG_COLOR,
804 )
805 }
806 return
807 }
808
809 func Arrowheads(r *Runner, connection d2target.Connection, srcAdj, dstAdj *geo.Point) (string, error) {
810 arrowPaths := []string{}
811
812 if connection.SrcArrow != d2target.NoArrowhead {
813 arrowJS, extraJS := ArrowheadJS(r, connection.SrcArrow, connection.Stroke, connection.StrokeWidth)
814 if arrowJS == "" {
815 return "", nil
816 }
817
818 startingSegment := geo.NewSegment(connection.Route[0], connection.Route[1])
819 startingVector := startingSegment.ToVector().Reverse()
820 angle := startingVector.Degrees()
821
822 transform := fmt.Sprintf(`transform="translate(%f %f) rotate(%v)"`,
823 startingSegment.Start.X+srcAdj.X, startingSegment.Start.Y+srcAdj.Y, angle,
824 )
825
826 roughPaths, err := computeRoughPaths(r, arrowJS)
827 if err != nil {
828 return "", err
829 }
830 if extraJS != "" {
831 extraPaths, err := computeRoughPaths(r, extraJS)
832 if err != nil {
833 return "", err
834 }
835 roughPaths = append(roughPaths, extraPaths...)
836 }
837
838 pathEl := d2themes.NewThemableElement("path")
839 pathEl.ClassName = "connection"
840 pathEl.Attributes = transform
841 for _, rp := range roughPaths {
842 pathEl.D = rp.Attrs.D
843 pathEl.Fill = rp.Style.Fill
844 pathEl.Stroke = rp.Style.Stroke
845 pathEl.Style = rp.StyleCSS()
846 arrowPaths = append(arrowPaths, pathEl.Render())
847 }
848 }
849
850 if connection.DstArrow != d2target.NoArrowhead {
851 arrowJS, extraJS := ArrowheadJS(r, connection.DstArrow, connection.Stroke, connection.StrokeWidth)
852 if arrowJS == "" {
853 return "", nil
854 }
855
856 length := len(connection.Route)
857 endingSegment := geo.NewSegment(connection.Route[length-2], connection.Route[length-1])
858 endingVector := endingSegment.ToVector()
859 angle := endingVector.Degrees()
860
861 transform := fmt.Sprintf(`transform="translate(%f %f) rotate(%v)"`,
862 endingSegment.End.X+dstAdj.X, endingSegment.End.Y+dstAdj.Y, angle,
863 )
864
865 roughPaths, err := computeRoughPaths(r, arrowJS)
866 if err != nil {
867 return "", err
868 }
869 if extraJS != "" {
870 extraPaths, err := computeRoughPaths(r, extraJS)
871 if err != nil {
872 return "", err
873 }
874 roughPaths = append(roughPaths, extraPaths...)
875 }
876
877 pathEl := d2themes.NewThemableElement("path")
878 pathEl.ClassName = "connection"
879 pathEl.Attributes = transform
880 for _, rp := range roughPaths {
881 pathEl.D = rp.Attrs.D
882 pathEl.Fill = rp.Style.Fill
883 pathEl.Stroke = rp.Style.Stroke
884 pathEl.Style = rp.StyleCSS()
885 arrowPaths = append(arrowPaths, pathEl.Render())
886 }
887 }
888
889 return strings.Join(arrowPaths, " "), nil
890 }
891
View as plain text