d2/d2renderers/d2sketch/sketch.go

842 lines
33 KiB
Go
Raw Normal View History

2022-12-21 07:43:45 +00:00
package d2sketch
import (
"encoding/json"
"fmt"
2023-01-13 04:25:02 +00:00
"regexp"
2022-12-22 19:06:57 +00:00
"strings"
2022-12-21 07:43:45 +00:00
_ "embed"
"github.com/dop251/goja"
"oss.terrastruct.com/d2/d2target"
2023-01-09 18:16:28 +00:00
"oss.terrastruct.com/d2/lib/color"
2022-12-22 19:06:57 +00:00
"oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/label"
2022-12-21 07:43:45 +00:00
"oss.terrastruct.com/d2/lib/svg"
2023-01-30 11:06:54 +00:00
svgstyle "oss.terrastruct.com/d2/lib/svg/style"
2022-12-22 19:06:57 +00:00
"oss.terrastruct.com/util-go/go2"
2022-12-21 07:43:45 +00:00
)
//go:embed rough.js
var roughJS string
//go:embed setup.js
var setupJS string
type Runner goja.Runtime
var baseRoughProps = `fillWeight: 2.0,
hachureGap: 16,
fillStyle: "solid",
bowing: 2,
seed: 1,`
2023-01-13 04:25:02 +00:00
var floatRE = regexp.MustCompile(`(\d+)\.(\d+)`)
2022-12-21 07:43:45 +00:00
func (r *Runner) run(js string) (goja.Value, error) {
vm := (*goja.Runtime)(r)
return vm.RunString(js)
}
func InitSketchVM() (*Runner, error) {
vm := goja.New()
if _, err := vm.RunString(roughJS); err != nil {
return nil, err
}
if _, err := vm.RunString(setupJS); err != nil {
return nil, err
}
r := Runner(*vm)
return &r, nil
}
// DefineFillPatterns adds reusable patterns that are overlayed on shapes with
2022-12-21 07:43:45 +00:00
// fill. This gives it a subtle streaky effect that subtly looks hand-drawn but
// not distractingly so.
func DefineFillPatterns() string {
out := "<defs>"
out += defineFillPattern("bright", "rgba(0, 0, 0, 0.1)")
out += defineFillPattern("normal", "rgba(0, 0, 0, 0.16)")
out += defineFillPattern("dark", "rgba(0, 0, 0, 0.32)")
out += defineFillPattern("darker", "rgba(255, 255, 255, 0.24)")
out += "</defs>"
return out
}
func defineFillPattern(luminanceCategory, fill string) string {
return fmt.Sprintf(`<pattern id="streaks-%s" x="0" y="0" width="100" height="100" patternUnits="userSpaceOnUse">
<path fill="%s" fill-rule="evenodd" clip-rule="evenodd" d="M58.1193 0H58.1703L55.4939 2.67644L58.1193 0ZM45.7725 0H45.811L41.2851 4.61498L42.7191 3.29325L37.0824 8.92997L35.0554 10.9569L32.0719 13.9404L29.6229 16.5017L27.1738 19.0631L25.8089 20.2034L23.2195 22.6244L18.181 27.6068L23.8178 21.97L27.0615 18.9508L33.8666 11.9773L33.1562 12.5194L37.0262 8.87383L40.784 5.11602L38.0299 7.64561L45.7725 0ZM23.1079 0H23.108L21.5814 1.66688L20.3126 2.79534L23.1079 0ZM7.53869 0H7.54254L7.50005 0.035944L7.53869 0ZM2.49995 0H2.52362L0.900245 1.59971L2.49995 0ZM0 3.64398V3.60744L0.278386 3.36559L0 3.64398ZM0 18.6564V18.5398L0.67985 17.8416L3.4459 15.0755L1.15701 17.1333L2.78713 15.6022L6.01437 12.507L8.5168 9.87253L5.15803 13.2313L11.0357 7.25453L10.4926 7.89678L13.6868 4.7686L8.54982 9.90555L7.05177 11.5687L4.68087 13.9396L0.729379 17.8911L3.01827 15.8333L0 18.6564ZM0 69.2431V69.178L1.64651 67.4763L1.46347 67.7796L5.84063 63.4025L4.42167 64.9016L0 69.4007V69.3408L0.247596 68.9955L0 69.2431ZM2.51594 100H2.49238L5.19989 97.2925L7.70071 95.0162L12.8713 89.6772L12.3094 90.0707L15.288 87.3167L18.1542 84.4504L16.0269 86.3532L22.8752 79.6172L18.5364 84.0683L19.6435 83.0734L15.3441 87.3728L13.798 88.9189L11.5224 91.1945L9.66768 93.1615L7.81297 95.1285L6.74529 95.9716L4.75024 97.7983L2.51594 100ZM7.54255 100H7.5387L9.81396 97.884L8.46606 99.2189L7.54255 100ZM45.8189 100H45.7807L46.9912 98.8047L45.8189 100ZM58.1784 100H58.1272L62.2952 95.7511L66.1408 91.9055L63.0037 94.8115L65.2507 92.6635L69.7117 88.3346L73.2165 84.6977L68.5469 89.3673L76.7379 81.0773L75.9634 81.9509L80.3913 77.5889L73.2496 84.7307L71.1346 87.0107L67.8384 90.3069L62.3447 95.8006L65.4818 92.8947L61.2625 96.9159L58.1784 100ZM75.4277 100H75.229L82.1834 92.9039L81.3403 93.5787L86.0063 89.1371L90.5601 84.5833L87.2464 87.6725L98.0937 76.9375L91.1673 83.9761L92.8932 82.3625L86.0625 89.1933L83.6062 91.6496L79.9907 95.265L77.011 98.357L75.4277 100ZM100 18.5398V18.6563L99.9556 18.6979L95.8065 22.847L100 18.5398ZM100 3.60743V3.64398L99.6791 3.9649L99.2094 4.29428L100 3.60743ZM75.4201 0L74.0312 1.4412L72.401 2.84687L69.281 5.79854L63.1812 11.8422L70.0119 5.01151L73.919 1.32893L75.2214 0H75.4201ZM100 69.1858V69.2509L98.059 71.1919L100 69.1858ZM100 69.3486V69.4085L99.8414 69.5698L100 69.3486ZM41.9398 28.8254L53.6223 16.993L52.5215 18.2437L54.7428 16.0575L54.6875 16.0759L54.8008 16.0004L58.842 12.0231L54.9925 15.8726L55.1085 15.7953L54.898 16.0058L54.84 16.0251L48.6523 22.2128L45.6419 25.473L40.9389 30.1759L33.1007 38.0142L37.5866 33.878L31.558 39.6068L23.3278 47.837L33.0257 37.9393L38.5125 32.4525L34.0266 36.5887L37.2369 33.5283L43.6074 27.3576L48.6023 22.1628L41.9398 28.8254ZM41.0977 17.0531L39.718 18.2925L40.312 17.8388L41.0977 17.0531ZM36.875 20.3106L48.1601 7.88137L42.3438 13.7478L36.875 20.3106ZM35.7125 25.8109L34.3328 27.0503L34.9268 26.5966L35.7125 25.8109ZM17.7022 39.7534L19.0819 38.514L18.8092 38.7867L36.7575 21.8045L23.1569 35.3051L13.5771 43.7372L18.1448 39.4154L17.7022 39.7534ZM3.48102 28.9281L1.53562 30.8735L1.22228 31.0465L0.0765686 32.3326L1.60579 30.9437L2.57849 29.971L3.48102 28.9281ZM0.953463 26.2027L19.5702 7.58594L9.31575 18.6078L0.953463 26.2027ZM23.7175 12.11L17.9339 18.0875L21.4622 14.5592L20.8074 15.4725L28.1915 7.95918L30.4791 5.54232L23.4224 12.599L23.7175 12.11ZM43.4641 43.1538L40.7872 46.1552L42.4907 44.4517L42.3285 45.0465L45.8166 41.3421L46.8441 40.0983L43.4371 43.5053L43.4641 43.1538ZM1.32715 48.3271L8.0918 41.5625L4.3657 45.5674L1.32715 48.3271ZM11.1479 31.2556L11.5689 30.975L11.3584 31.1855L11.1479 31.2556ZM11.9898 27.4667L12.2003 27.2562L11.7793 27.5369L11.9898 27.4667ZM11.3585 34.5531L11.148 34.7636L10.9375 34.8338L11.3585 34.5531ZM72.929 28.5457L82.2965 19.0792L81.4043 20.0705L86.4597 15.0811L78.2983 23.2425L75.8697 25.8362L72.1029 29.603L65.8249 35.881L69.3934 32.5437L64.5858 37.1531L57.994 43.745L65.7754 35.8314L70.17 31.4369L66.6015 34.7742L69.1623 32.3125L74.2507 27.3562L78.2653 23.2095L72.929 28.5457ZM82.6674 1.83549L84.3245 0.31872L83.3724 1.27088L82.6674 1.83549ZM64.5872 16.1312L62.9301 17.648L63.6351 17.0834L64.5872 16.1312ZM70.868 9.85044L80.0
</pattern>`, luminanceCategory, fill)
2022-12-21 07:43:45 +00:00
}
func Rect(r *Runner, shape d2target.Shape) (string, error) {
js := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, {
2023-01-09 18:16:28 +00:00
fill: "#000",
stroke: "#000",
2022-12-21 07:43:45 +00:00
strokeWidth: %d,
%s
2023-01-09 18:16:28 +00:00
});`, shape.Width, shape.Height, shape.StrokeWidth, baseRoughProps)
2023-01-13 02:27:53 +00:00
paths, err := computeRoughPathData(r, js)
2022-12-21 07:43:45 +00:00
if err != nil {
return "", err
}
output := ""
2023-01-30 11:06:54 +00:00
pathEl := svgstyle.NewThemableElement("path")
2023-01-09 18:16:28 +00:00
pathEl.Transform = fmt.Sprintf("translate(%d %d)", shape.Pos.X, shape.Pos.Y)
2023-01-30 11:06:54 +00:00
pathEl.Fill, pathEl.Stroke = svgstyle.ShapeTheme(shape)
2023-01-09 18:16:28 +00:00
pathEl.Class = "shape"
2023-01-15 20:36:43 +00:00
pathEl.Style = shape.CSSStyle()
2022-12-21 07:43:45 +00:00
for _, p := range paths {
2023-01-09 18:16:28 +00:00
pathEl.D = p
output += pathEl.Render()
2022-12-21 07:43:45 +00:00
}
2023-01-30 11:06:54 +00:00
sketchOEl := svgstyle.NewThemableElement("rect")
sketchOEl.Transform = fmt.Sprintf("translate(%d %d)", shape.Pos.X, shape.Pos.Y)
sketchOEl.Width = float64(shape.Width)
sketchOEl.Height = float64(shape.Height)
2023-01-30 11:06:54 +00:00
renderedSO, err := svgstyle.NewThemableSketchOverlay(sketchOEl, pathEl.Fill).Render()
if err != nil {
return "", err
}
output += renderedSO
2022-12-21 07:43:45 +00:00
return output, nil
}
2022-12-31 07:57:22 +00:00
func DoubleRect(r *Runner, shape d2target.Shape) (string, error) {
jsBigRect := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, {
2023-01-27 21:30:44 +00:00
fill: "#000",
stroke: "#000",
2022-12-31 07:57:22 +00:00
strokeWidth: %d,
%s
2023-01-27 21:30:44 +00:00
});`, shape.Width, shape.Height, shape.StrokeWidth, baseRoughProps)
2023-01-24 09:29:38 +00:00
pathsBigRect, err := computeRoughPathData(r, jsBigRect)
2022-12-31 07:57:22 +00:00
if err != nil {
return "", err
}
jsSmallRect := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, {
2023-01-27 21:30:44 +00:00
fill: "#000",
stroke: "#000",
2022-12-31 07:57:22 +00:00
strokeWidth: %d,
%s
2023-01-27 21:30:44 +00:00
});`, shape.Width-d2target.INNER_BORDER_OFFSET*2, shape.Height-d2target.INNER_BORDER_OFFSET*2, shape.StrokeWidth, baseRoughProps)
2023-01-24 09:29:38 +00:00
pathsSmallRect, err := computeRoughPathData(r, jsSmallRect)
2022-12-31 07:57:22 +00:00
if err != nil {
return "", err
}
2023-01-27 21:30:44 +00:00
2022-12-31 07:57:22 +00:00
output := ""
2023-01-27 21:30:44 +00:00
2023-01-30 11:06:54 +00:00
pathEl := svgstyle.NewThemableElement("path")
2023-01-27 21:30:44 +00:00
pathEl.Transform = fmt.Sprintf("translate(%d %d)", shape.Pos.X, shape.Pos.Y)
2023-01-30 11:06:54 +00:00
pathEl.Fill, pathEl.Stroke = svgstyle.ShapeTheme(shape)
2023-01-27 21:30:44 +00:00
pathEl.Class = "shape"
pathEl.Style = shape.CSSStyle()
2022-12-31 07:57:22 +00:00
for _, p := range pathsBigRect {
2023-01-27 21:30:44 +00:00
pathEl.D = p
output += pathEl.Render()
2022-12-31 07:57:22 +00:00
}
2023-01-27 21:30:44 +00:00
2023-01-30 11:06:54 +00:00
pathEl = svgstyle.NewThemableElement("path")
2023-01-27 21:30:44 +00:00
pathEl.Transform = fmt.Sprintf("translate(%d %d)", shape.Pos.X+d2target.INNER_BORDER_OFFSET, shape.Pos.Y+d2target.INNER_BORDER_OFFSET)
2023-01-30 11:06:54 +00:00
pathEl.Fill, pathEl.Stroke = svgstyle.ShapeTheme(shape)
2023-01-27 21:30:44 +00:00
pathEl.Class = "shape"
pathEl.Style = shape.CSSStyle()
2022-12-31 07:57:22 +00:00
for _, p := range pathsSmallRect {
2023-01-27 21:30:44 +00:00
pathEl.D = p
output += pathEl.Render()
2022-12-31 07:57:22 +00:00
}
2023-01-27 21:30:44 +00:00
2022-12-31 07:57:22 +00:00
output += fmt.Sprintf(
`<rect class="sketch-overlay" transform="translate(%d %d)" width="%d" height="%d" />`,
shape.Pos.X, shape.Pos.Y, shape.Width, shape.Height,
)
return output, nil
}
2022-12-21 07:43:45 +00:00
func Oval(r *Runner, shape d2target.Shape) (string, error) {
js := fmt.Sprintf(`node = rc.ellipse(%d, %d, %d, %d, {
2023-01-09 18:16:28 +00:00
fill: "#000",
stroke: "#000",
2022-12-21 07:43:45 +00:00
strokeWidth: %d,
%s
2023-01-09 18:16:28 +00:00
});`, shape.Width/2, shape.Height/2, shape.Width, shape.Height, shape.StrokeWidth, baseRoughProps)
2023-01-13 02:27:53 +00:00
paths, err := computeRoughPathData(r, js)
2022-12-21 07:43:45 +00:00
if err != nil {
return "", err
}
output := ""
2023-01-30 11:06:54 +00:00
pathEl := svgstyle.NewThemableElement("path")
2023-01-09 18:16:28 +00:00
pathEl.Transform = fmt.Sprintf("translate(%d %d)", shape.Pos.X, shape.Pos.Y)
2023-01-30 11:06:54 +00:00
pathEl.Fill, pathEl.Stroke = svgstyle.ShapeTheme(shape)
2023-01-09 18:16:28 +00:00
pathEl.Class = "shape"
2023-01-15 20:36:43 +00:00
pathEl.Style = shape.CSSStyle()
2022-12-21 07:43:45 +00:00
for _, p := range paths {
2023-01-09 18:16:28 +00:00
pathEl.D = p
output += pathEl.Render()
2022-12-21 07:43:45 +00:00
}
2023-01-30 11:06:54 +00:00
soElement := svgstyle.NewThemableElement("ellipse")
soElement.Transform = fmt.Sprintf("translate(%d %d)", shape.Pos.X+shape.Width/2, shape.Pos.Y+shape.Height/2)
soElement.Rx = float64(shape.Width / 2)
soElement.Ry = float64(shape.Height / 2)
2023-01-30 11:06:54 +00:00
renderedSO, err := svgstyle.NewThemableSketchOverlay(
soElement,
pathEl.Fill,
).Render()
if err != nil {
return "", err
}
output += renderedSO
2022-12-21 07:43:45 +00:00
return output, nil
}
2022-12-30 09:30:29 +00:00
func DoubleOval(r *Runner, shape d2target.Shape) (string, error) {
2022-12-31 06:17:34 +00:00
jsBigCircle := fmt.Sprintf(`node = rc.ellipse(%d, %d, %d, %d, {
2023-01-27 21:30:44 +00:00
fill: "#000",
stroke: "#000",
2022-12-30 09:30:29 +00:00
strokeWidth: %d,
%s
2023-01-27 21:30:44 +00:00
});`, shape.Width/2, shape.Height/2, shape.Width, shape.Height, shape.StrokeWidth, baseRoughProps)
2022-12-31 06:17:34 +00:00
jsSmallCircle := fmt.Sprintf(`node = rc.ellipse(%d, %d, %d, %d, {
2023-01-27 21:30:44 +00:00
fill: "#000",
stroke: "#000",
2022-12-30 09:30:29 +00:00
strokeWidth: %d,
%s
2023-01-27 21:30:44 +00:00
});`, shape.Width/2, shape.Height/2, shape.Width-d2target.INNER_BORDER_OFFSET*2, shape.Height-d2target.INNER_BORDER_OFFSET*2, shape.StrokeWidth, baseRoughProps)
2023-01-24 09:29:38 +00:00
pathsBigCircle, err := computeRoughPathData(r, jsBigCircle)
2022-12-30 09:30:29 +00:00
if err != nil {
return "", err
}
2023-01-24 09:29:38 +00:00
pathsSmallCircle, err := computeRoughPathData(r, jsSmallCircle)
2022-12-30 09:30:29 +00:00
if err != nil {
return "", err
}
2023-01-27 21:30:44 +00:00
2022-12-30 09:30:29 +00:00
output := ""
2023-01-27 21:30:44 +00:00
2023-01-30 11:06:54 +00:00
pathEl := svgstyle.NewThemableElement("path")
2023-01-27 21:30:44 +00:00
pathEl.Transform = fmt.Sprintf("translate(%d %d)", shape.Pos.X, shape.Pos.Y)
2023-01-30 11:06:54 +00:00
pathEl.Fill, pathEl.Stroke = svgstyle.ShapeTheme(shape)
2023-01-27 21:30:44 +00:00
pathEl.Class = "shape"
pathEl.Style = shape.CSSStyle()
2022-12-31 07:57:22 +00:00
for _, p := range pathsBigCircle {
2023-01-27 21:30:44 +00:00
pathEl.D = p
output += pathEl.Render()
2022-12-30 09:30:29 +00:00
}
2023-01-27 21:30:44 +00:00
2023-01-30 11:06:54 +00:00
pathEl = svgstyle.NewThemableElement("path")
2023-01-27 21:30:44 +00:00
pathEl.Transform = fmt.Sprintf("translate(%d %d)", shape.Pos.X, shape.Pos.Y)
2023-01-30 11:06:54 +00:00
pathEl.Fill, pathEl.Stroke = svgstyle.ShapeTheme(shape)
2023-01-27 21:30:44 +00:00
pathEl.Class = "shape"
pathEl.Style = shape.CSSStyle()
2022-12-31 07:57:22 +00:00
for _, p := range pathsSmallCircle {
2023-01-27 21:30:44 +00:00
pathEl.D = p
output += pathEl.Render()
2022-12-30 09:30:29 +00:00
}
2023-01-27 21:30:44 +00:00
2022-12-30 09:30:29 +00:00
output += fmt.Sprintf(
`<ellipse class="sketch-overlay" transform="translate(%d %d)" rx="%d" ry="%d" />`,
shape.Pos.X+shape.Width/2, shape.Pos.Y+shape.Height/2, shape.Width/2, shape.Height/2,
)
return output, nil
}
2022-12-21 07:43:45 +00:00
// TODO need to personalize this per shape like we do in Terrastruct app
func Paths(r *Runner, shape d2target.Shape, paths []string) (string, error) {
output := ""
for _, path := range paths {
js := fmt.Sprintf(`node = rc.path("%s", {
2023-01-09 18:16:28 +00:00
fill: "#000",
stroke: "#000",
2022-12-21 07:43:45 +00:00
strokeWidth: %d,
%s
2023-01-09 18:16:28 +00:00
});`, path, shape.StrokeWidth, baseRoughProps)
2023-01-13 02:27:53 +00:00
sketchPaths, err := computeRoughPathData(r, js)
2022-12-21 07:43:45 +00:00
if err != nil {
return "", err
}
2023-01-30 11:06:54 +00:00
pathEl := svgstyle.NewThemableElement("path")
pathEl.Fill, pathEl.Stroke = svgstyle.ShapeTheme(shape)
2023-01-09 18:16:28 +00:00
pathEl.Class = "shape"
2023-01-15 20:36:43 +00:00
pathEl.Style = shape.CSSStyle()
2022-12-21 07:43:45 +00:00
for _, p := range sketchPaths {
2023-01-09 18:16:28 +00:00
pathEl.D = p
output += pathEl.Render()
2022-12-21 07:43:45 +00:00
}
2023-01-30 11:06:54 +00:00
soElement := svgstyle.NewThemableElement("path")
2022-12-21 07:43:45 +00:00
for _, p := range sketchPaths {
soElement.D = p
2023-01-30 11:06:54 +00:00
renderedSO, err := svgstyle.NewThemableSketchOverlay(
soElement,
pathEl.Fill,
).Render()
if err != nil {
return "", err
}
output += renderedSO
2022-12-21 07:43:45 +00:00
}
}
return output, nil
}
func Connection(r *Runner, connection d2target.Connection, path, attrs string) (string, error) {
roughness := 1.0
js := fmt.Sprintf(`node = rc.path("%s", {roughness: %f, seed: 1});`, path, roughness)
2023-01-13 02:27:53 +00:00
paths, err := computeRoughPathData(r, js)
2022-12-21 07:43:45 +00:00
if err != nil {
return "", err
}
output := ""
2023-01-16 11:15:59 +00:00
animatedClass := ""
if connection.Animated {
animatedClass = " animated-connection"
}
2023-01-30 11:06:54 +00:00
pathEl := svgstyle.NewThemableElement("path")
2023-01-09 18:16:28 +00:00
pathEl.Fill = color.None
2023-01-30 11:06:54 +00:00
pathEl.Stroke = svgstyle.ConnectionTheme(connection)
2023-01-16 11:15:59 +00:00
pathEl.Class = fmt.Sprintf("connection%s", animatedClass)
2023-01-15 20:36:43 +00:00
pathEl.Style = connection.CSSStyle()
2023-01-09 18:16:28 +00:00
pathEl.Attributes = attrs
2022-12-21 07:43:45 +00:00
for _, p := range paths {
2023-01-09 18:16:28 +00:00
pathEl.D = p
output += pathEl.Render()
2022-12-21 07:43:45 +00:00
}
return output, nil
}
2022-12-22 19:06:57 +00:00
// TODO cleanup
func Table(r *Runner, shape d2target.Shape) (string, error) {
output := ""
js := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, {
2023-01-09 18:16:28 +00:00
fill: "#000",
stroke: "#000",
2022-12-22 19:06:57 +00:00
strokeWidth: %d,
%s
2023-01-09 18:16:28 +00:00
});`, shape.Width, shape.Height, shape.StrokeWidth, baseRoughProps)
2023-01-13 02:27:53 +00:00
paths, err := computeRoughPathData(r, js)
2022-12-22 19:06:57 +00:00
if err != nil {
return "", err
}
2023-01-30 11:06:54 +00:00
pathEl := svgstyle.NewThemableElement("path")
2023-01-09 18:16:28 +00:00
pathEl.Transform = fmt.Sprintf("translate(%d %d)", shape.Pos.X, shape.Pos.Y)
2023-01-30 11:06:54 +00:00
pathEl.Fill, pathEl.Stroke = svgstyle.ShapeTheme(shape)
2023-01-09 18:16:28 +00:00
pathEl.Class = "shape"
2023-01-15 20:36:43 +00:00
pathEl.Style = shape.CSSStyle()
2022-12-22 19:06:57 +00:00
for _, p := range paths {
2023-01-09 18:16:28 +00:00
pathEl.D = p
output += pathEl.Render()
2022-12-22 19:06:57 +00:00
}
box := geo.NewBox(
geo.NewPoint(float64(shape.Pos.X), float64(shape.Pos.Y)),
float64(shape.Width),
float64(shape.Height),
)
rowHeight := box.Height / float64(1+len(shape.SQLTable.Columns))
headerBox := geo.NewBox(box.TopLeft, box.Width, rowHeight)
js = fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %f, {
2023-01-09 18:16:28 +00:00
fill: "#000",
2022-12-22 19:06:57 +00:00
%s
2023-01-09 18:16:28 +00:00
});`, shape.Width, rowHeight, baseRoughProps)
2023-01-13 02:27:53 +00:00
paths, err = computeRoughPathData(r, js)
2022-12-22 19:06:57 +00:00
if err != nil {
return "", err
}
2023-01-30 11:06:54 +00:00
pathEl = svgstyle.NewThemableElement("path")
2023-01-09 18:16:28 +00:00
pathEl.Transform = fmt.Sprintf("translate(%d %d)", shape.Pos.X, shape.Pos.Y)
pathEl.Fill = shape.Fill
pathEl.Class = "class_header"
2022-12-22 19:06:57 +00:00
for _, p := range paths {
2023-01-09 18:16:28 +00:00
pathEl.D = p
output += pathEl.Render()
2022-12-22 19:06:57 +00:00
}
if shape.Label != "" {
tl := label.InsideMiddleLeft.GetPointOnBox(
headerBox,
20,
float64(shape.LabelWidth),
float64(shape.LabelHeight),
)
2023-01-30 11:06:54 +00:00
textEl := svgstyle.NewThemableElement("text")
2023-01-09 18:16:28 +00:00
textEl.X = tl.X
textEl.Y = tl.Y + float64(shape.LabelHeight)*3/4
textEl.Fill = shape.Stroke
textEl.Class = "text"
textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx",
"start", 4+shape.FontSize,
2022-12-22 19:06:57 +00:00
)
2023-01-09 18:16:28 +00:00
textEl.Content = svg.EscapeText(shape.Label)
output += textEl.Render()
2022-12-22 19:06:57 +00:00
}
var longestNameWidth int
for _, f := range shape.Columns {
longestNameWidth = go2.Max(longestNameWidth, f.Name.LabelWidth)
}
rowBox := geo.NewBox(box.TopLeft.Copy(), box.Width, rowHeight)
rowBox.TopLeft.Y += headerBox.Height
for _, f := range shape.Columns {
nameTL := label.InsideMiddleLeft.GetPointOnBox(
rowBox,
d2target.NamePadding,
rowBox.Width,
float64(shape.FontSize),
)
constraintTR := label.InsideMiddleRight.GetPointOnBox(
rowBox,
d2target.TypePadding,
0,
float64(shape.FontSize),
)
2023-01-30 11:06:54 +00:00
textEl := svgstyle.NewThemableElement("text")
2023-01-09 18:16:28 +00:00
textEl.X = nameTL.X
textEl.Y = nameTL.Y + float64(shape.FontSize)*3/4
textEl.Fill = shape.PrimaryAccentColor
textEl.Class = "text"
textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx", "start", float64(shape.FontSize))
textEl.Content = svg.EscapeText(f.Name.Label)
output += textEl.Render()
textEl.X = nameTL.X + float64(longestNameWidth) + 2*d2target.NamePadding
textEl.Fill = shape.NeutralAccentColor
textEl.Content = svg.EscapeText(f.Type.Label)
output += textEl.Render()
textEl.X = constraintTR.X
textEl.Y = constraintTR.Y + float64(shape.FontSize)*3/4
textEl.Fill = shape.SecondaryAccentColor
textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx;letter-spacing:2px", "end", float64(shape.FontSize))
textEl.Content = f.ConstraintAbbr()
output += textEl.Render()
2022-12-22 19:06:57 +00:00
rowBox.TopLeft.Y += rowHeight
js = fmt.Sprintf(`node = rc.line(%f, %f, %f, %f, {
%s
});`, rowBox.TopLeft.X, rowBox.TopLeft.Y, rowBox.TopLeft.X+rowBox.Width, rowBox.TopLeft.Y, baseRoughProps)
2023-01-13 02:27:53 +00:00
paths, err = computeRoughPathData(r, js)
2022-12-22 19:06:57 +00:00
if err != nil {
return "", err
}
2023-01-30 11:06:54 +00:00
pathEl := svgstyle.NewThemableElement("path")
2023-01-09 18:16:28 +00:00
pathEl.Fill = shape.Fill
2022-12-22 19:06:57 +00:00
for _, p := range paths {
2023-01-09 18:16:28 +00:00
pathEl.D = p
output += pathEl.Render()
2022-12-22 19:06:57 +00:00
}
}
2023-01-30 11:06:54 +00:00
sketchOEl := svgstyle.NewThemableElement("rect")
sketchOEl.Transform = fmt.Sprintf("translate(%d %d)", shape.Pos.X, shape.Pos.Y)
sketchOEl.Width = float64(shape.Width)
sketchOEl.Height = float64(shape.Height)
2023-01-30 11:06:54 +00:00
renderedSO, err := svgstyle.NewThemableSketchOverlay(sketchOEl, pathEl.Fill).Render()
if err != nil {
return "", err
}
output += renderedSO
2022-12-22 19:06:57 +00:00
return output, nil
}
2022-12-22 19:32:41 +00:00
func Class(r *Runner, shape d2target.Shape) (string, error) {
output := ""
js := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, {
2023-01-09 18:16:28 +00:00
fill: "#000",
stroke: "#000",
2022-12-22 19:32:41 +00:00
strokeWidth: %d,
%s
2023-01-09 18:16:28 +00:00
});`, shape.Width, shape.Height, shape.StrokeWidth, baseRoughProps)
2023-01-13 02:27:53 +00:00
paths, err := computeRoughPathData(r, js)
2022-12-22 19:32:41 +00:00
if err != nil {
return "", err
}
2023-01-30 11:06:54 +00:00
pathEl := svgstyle.NewThemableElement("path")
2023-01-09 18:16:28 +00:00
pathEl.Transform = fmt.Sprintf("translate(%d %d)", shape.Pos.X, shape.Pos.Y)
2023-01-30 11:06:54 +00:00
pathEl.Fill, pathEl.Stroke = svgstyle.ShapeTheme(shape)
2023-01-09 18:16:28 +00:00
pathEl.Class = "shape"
2023-01-15 20:36:43 +00:00
pathEl.Style = shape.CSSStyle()
2022-12-22 19:32:41 +00:00
for _, p := range paths {
2023-01-09 18:16:28 +00:00
pathEl.D = p
output += pathEl.Render()
2022-12-22 19:32:41 +00:00
}
box := geo.NewBox(
geo.NewPoint(float64(shape.Pos.X), float64(shape.Pos.Y)),
float64(shape.Width),
float64(shape.Height),
)
rowHeight := box.Height / float64(2+len(shape.Class.Fields)+len(shape.Class.Methods))
headerBox := geo.NewBox(box.TopLeft, box.Width, 2*rowHeight)
js = fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %f, {
2023-01-09 18:16:28 +00:00
fill: "#000",
2022-12-22 19:32:41 +00:00
%s
2023-01-09 18:16:28 +00:00
});`, shape.Width, headerBox.Height, baseRoughProps)
2023-01-13 02:27:53 +00:00
paths, err = computeRoughPathData(r, js)
2022-12-22 19:32:41 +00:00
if err != nil {
return "", err
}
2023-01-30 11:06:54 +00:00
pathEl = svgstyle.NewThemableElement("path")
2023-01-09 18:16:28 +00:00
pathEl.Transform = fmt.Sprintf("translate(%d %d)", shape.Pos.X, shape.Pos.Y)
pathEl.Fill = shape.Fill
pathEl.Class = "class_header"
2022-12-22 19:32:41 +00:00
for _, p := range paths {
2023-01-09 18:16:28 +00:00
pathEl.D = p
output += pathEl.Render()
2022-12-22 19:32:41 +00:00
}
2023-01-30 11:06:54 +00:00
sketchOEl := svgstyle.NewThemableElement("rect")
sketchOEl.Transform = fmt.Sprintf("translate(%d %d)", shape.Pos.X, shape.Pos.Y)
sketchOEl.Width = float64(shape.Width)
sketchOEl.Height = headerBox.Height
2023-01-30 11:06:54 +00:00
renderedSO, err := svgstyle.NewThemableSketchOverlay(sketchOEl, pathEl.Fill).Render()
if err != nil {
return "", err
}
output += renderedSO
2022-12-22 19:32:41 +00:00
if shape.Label != "" {
2022-12-25 00:10:58 +00:00
tl := label.InsideMiddleCenter.GetPointOnBox(
2022-12-22 19:32:41 +00:00
headerBox,
0,
float64(shape.LabelWidth),
float64(shape.LabelHeight),
)
2023-01-30 11:06:54 +00:00
textEl := svgstyle.NewThemableElement("text")
2023-01-09 18:16:28 +00:00
textEl.X = tl.X + float64(shape.LabelWidth)/2
textEl.Y = tl.Y + float64(shape.LabelHeight)*3/4
textEl.Fill = shape.Stroke
textEl.Class = "text-mono"
textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx",
"middle",
4+shape.FontSize,
2022-12-22 19:32:41 +00:00
)
2023-01-09 18:16:28 +00:00
textEl.Content = svg.EscapeText(shape.Label)
output += textEl.Render()
2022-12-22 19:32:41 +00:00
}
rowBox := geo.NewBox(box.TopLeft.Copy(), box.Width, rowHeight)
rowBox.TopLeft.Y += headerBox.Height
for _, f := range shape.Fields {
2022-12-24 23:56:22 +00:00
output += classRow(shape, rowBox, f.VisibilityToken(), f.Name, f.Type, float64(shape.FontSize))
2022-12-22 19:32:41 +00:00
rowBox.TopLeft.Y += rowHeight
}
js = fmt.Sprintf(`node = rc.line(%f, %f, %f, %f, {
%s
});`, rowBox.TopLeft.X, rowBox.TopLeft.Y, rowBox.TopLeft.X+rowBox.Width, rowBox.TopLeft.Y, baseRoughProps)
2023-01-13 02:27:53 +00:00
paths, err = computeRoughPathData(r, js)
2022-12-22 19:32:41 +00:00
if err != nil {
return "", err
}
2023-01-30 11:06:54 +00:00
pathEl = svgstyle.NewThemableElement("path")
2023-01-09 18:16:28 +00:00
pathEl.Fill = shape.Fill
pathEl.Class = "class_header"
2022-12-22 19:32:41 +00:00
for _, p := range paths {
2023-01-09 18:16:28 +00:00
pathEl.D = p
output += pathEl.Render()
2022-12-22 19:32:41 +00:00
}
for _, m := range shape.Methods {
2022-12-24 23:56:22 +00:00
output += classRow(shape, rowBox, m.VisibilityToken(), m.Name, m.Return, float64(shape.FontSize))
2022-12-22 19:32:41 +00:00
rowBox.TopLeft.Y += rowHeight
}
return output, nil
}
2022-12-24 23:56:22 +00:00
func classRow(shape d2target.Shape, box *geo.Box, prefix, nameText, typeText string, fontSize float64) string {
2022-12-22 19:32:41 +00:00
output := ""
prefixTL := label.InsideMiddleLeft.GetPointOnBox(
box,
d2target.PrefixPadding,
box.Width,
fontSize,
)
typeTR := label.InsideMiddleRight.GetPointOnBox(
box,
d2target.TypePadding,
0,
fontSize,
)
2023-01-30 11:06:54 +00:00
textEl := svgstyle.NewThemableElement("text")
2023-01-09 18:16:28 +00:00
textEl.X = prefixTL.X
textEl.Y = prefixTL.Y + fontSize*3/4
textEl.Fill = shape.PrimaryAccentColor
textEl.Class = "text-mono"
textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx", "start", fontSize)
textEl.Content = prefix
output += textEl.Render()
textEl.X = prefixTL.X + d2target.PrefixWidth
textEl.Fill = shape.Fill
textEl.Content = svg.EscapeText(nameText)
output += textEl.Render()
textEl.X = typeTR.X
textEl.Y = typeTR.Y + fontSize*3/4
textEl.Fill = shape.SecondaryAccentColor
textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx", "end", fontSize)
textEl.Content = svg.EscapeText(typeText)
output += textEl.Render()
2022-12-22 19:32:41 +00:00
return output
}
2023-01-13 02:27:53 +00:00
func computeRoughPathData(r *Runner, js string) ([]string, error) {
2022-12-22 19:06:57 +00:00
if _, err := r.run(js); err != nil {
return nil, err
}
2023-01-13 02:27:53 +00:00
roughPaths, err := extractRoughPaths(r)
if err != nil {
return nil, err
}
return extractPathData(roughPaths)
}
func computeRoughPaths(r *Runner, js string) ([]roughPath, error) {
2022-12-22 19:06:57 +00:00
if _, err := r.run(js); err != nil {
return nil, err
}
2023-01-13 02:27:53 +00:00
return extractRoughPaths(r)
2022-12-22 19:06:57 +00:00
}
2022-12-21 07:43:45 +00:00
type attrs struct {
D string `json:"d"`
}
2023-01-13 02:27:53 +00:00
type style struct {
Stroke string `json:"stroke,omitempty"`
StrokeWidth string `json:"strokeWidth,omitempty"`
Fill string `json:"fill,omitempty"`
}
type roughPath struct {
2022-12-21 07:43:45 +00:00
Attrs attrs `json:"attrs"`
2023-01-13 02:27:53 +00:00
Style style `json:"style"`
2022-12-21 07:43:45 +00:00
}
2023-01-13 02:27:53 +00:00
func (rp roughPath) StyleCSS() string {
style := ""
if rp.Style.StrokeWidth != "" {
style += fmt.Sprintf("stroke-width:%s;", rp.Style.StrokeWidth)
}
return style
2022-12-21 07:43:45 +00:00
}
2023-01-13 02:27:53 +00:00
func extractRoughPaths(r *Runner) ([]roughPath, error) {
val, err := r.run("JSON.stringify(node.children, null, ' ')")
2022-12-21 07:43:45 +00:00
if err != nil {
return nil, err
}
2023-01-13 02:27:53 +00:00
var roughPaths []roughPath
err = json.Unmarshal([]byte(val.String()), &roughPaths)
2022-12-21 07:43:45 +00:00
if err != nil {
return nil, err
}
2023-01-13 04:25:02 +00:00
// we want to have a fixed precision to the decimals in the path data
for i := range roughPaths {
// truncate all floats in path to only use up to 6 decimal places
roughPaths[i].Attrs.D = floatRE.ReplaceAllStringFunc(roughPaths[i].Attrs.D, func(floatStr string) string {
i := strings.Index(floatStr, ".")
decimalLen := len(floatStr) - i - 1
end := i + go2.Min(decimalLen, 6)
return floatStr[:end+1]
})
2022-12-21 07:43:45 +00:00
}
2023-01-13 02:27:53 +00:00
return roughPaths, nil
}
func extractPathData(roughPaths []roughPath) ([]string, error) {
2022-12-21 07:43:45 +00:00
var paths []string
2023-01-13 02:27:53 +00:00
for _, rp := range roughPaths {
paths = append(paths, rp.Attrs.D)
2022-12-21 07:43:45 +00:00
}
return paths, nil
}
2023-01-13 02:27:53 +00:00
func ArrowheadJS(r *Runner, bgColor string, arrowhead d2target.Arrowhead, stroke string, strokeWidth int) (arrowJS, extraJS string) {
2023-01-13 02:27:53 +00:00
// Note: selected each seed that looks the good for consistent renders
switch arrowhead {
case d2target.ArrowArrowhead:
arrowJS = fmt.Sprintf(
`node = rc.linearPath(%s, { strokeWidth: %d, stroke: "%s", seed: 3 })`,
`[[-10, -4], [0, 0], [-10, 4]]`,
strokeWidth,
stroke,
)
case d2target.TriangleArrowhead:
arrowJS = fmt.Sprintf(
`node = rc.polygon(%s, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "solid", seed: 2 })`,
`[[-10, -4], [0, 0], [-10, 4]]`,
strokeWidth,
stroke,
stroke,
)
case d2target.DiamondArrowhead:
arrowJS = fmt.Sprintf(
`node = rc.polygon(%s, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "solid", seed: 1 })`,
2023-01-13 02:27:53 +00:00
`[[-20, 0], [-10, 5], [0, 0], [-10, -5], [-20, 0]]`,
strokeWidth,
stroke,
bgColor,
2023-01-13 02:27:53 +00:00
)
case d2target.FilledDiamondArrowhead:
arrowJS = fmt.Sprintf(
`node = rc.polygon(%s, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "zigzag", fillWeight: 4, seed: 1 })`,
`[[-20, 0], [-10, 5], [0, 0], [-10, -5], [-20, 0]]`,
strokeWidth,
stroke,
stroke,
)
case d2target.CfManyRequired:
arrowJS = fmt.Sprintf(
// TODO why does fillStyle: "zigzag" error with path
`node = rc.path(%s, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "solid", fillWeight: 4, seed: 2 })`,
`"M-15,-10 -15,10 M0,10 -15,0 M0,-10 -15,0"`,
2023-01-13 02:27:53 +00:00
strokeWidth,
stroke,
stroke,
)
case d2target.CfMany:
arrowJS = fmt.Sprintf(
`node = rc.path(%s, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "solid", fillWeight: 4, seed: 8 })`,
`"M0,10 -15,0 M0,-10 -15,0"`,
2023-01-13 02:27:53 +00:00
strokeWidth,
stroke,
stroke,
)
extraJS = fmt.Sprintf(
`node = rc.circle(-20, 0, 8, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "solid", fillWeight: 1, seed: 4 })`,
2023-01-13 02:27:53 +00:00
strokeWidth,
stroke,
bgColor,
2023-01-13 02:27:53 +00:00
)
case d2target.CfOneRequired:
arrowJS = fmt.Sprintf(
2023-01-14 03:20:05 +00:00
`node = rc.path(%s, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "solid", fillWeight: 4, seed: 2 })`,
`"M-15,-10 -15,10 M-10,-10 -10,10"`,
2023-01-13 02:27:53 +00:00
strokeWidth,
stroke,
stroke,
)
case d2target.CfOne:
arrowJS = fmt.Sprintf(
2023-01-14 02:29:11 +00:00
`node = rc.path(%s, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "solid", fillWeight: 4, seed: 3 })`,
`"M-10,-10 -10,10"`,
2023-01-13 02:27:53 +00:00
strokeWidth,
stroke,
stroke,
)
extraJS = fmt.Sprintf(
`node = rc.circle(-20, 0, 8, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "solid", fillWeight: 1, seed: 5 })`,
2023-01-13 02:27:53 +00:00
strokeWidth,
stroke,
bgColor,
2023-01-13 02:27:53 +00:00
)
}
return
}
func Arrowheads(r *Runner, bgColor string, connection d2target.Connection, srcAdj, dstAdj *geo.Point) (string, error) {
2023-01-13 02:27:53 +00:00
arrowPaths := []string{}
if connection.SrcArrow != d2target.NoArrowhead {
arrowJS, extraJS := ArrowheadJS(r, bgColor, connection.SrcArrow, connection.Stroke, connection.StrokeWidth)
2023-01-13 02:27:53 +00:00
if arrowJS == "" {
return "", nil
}
startingSegment := geo.NewSegment(connection.Route[0], connection.Route[1])
startingVector := startingSegment.ToVector().Reverse()
angle := startingVector.Degrees()
transform := fmt.Sprintf(`transform="translate(%f %f) rotate(%v)"`,
startingSegment.Start.X+srcAdj.X, startingSegment.Start.Y+srcAdj.Y, angle,
2023-01-13 02:27:53 +00:00
)
roughPaths, err := computeRoughPaths(r, arrowJS)
if err != nil {
return "", err
}
if extraJS != "" {
extraPaths, err := computeRoughPaths(r, extraJS)
if err != nil {
return "", err
}
roughPaths = append(roughPaths, extraPaths...)
}
2023-01-30 11:06:54 +00:00
pathEl := svgstyle.NewThemableElement("path")
2023-01-16 11:15:59 +00:00
pathEl.Class = "connection"
pathEl.Attributes = transform
2023-01-13 02:27:53 +00:00
for _, rp := range roughPaths {
2023-01-16 11:15:59 +00:00
pathEl.D = rp.Attrs.D
pathEl.Fill = rp.Style.Fill
pathEl.Stroke = rp.Style.Stroke
pathEl.Style = rp.StyleCSS()
arrowPaths = append(arrowPaths, pathEl.Render())
2023-01-13 02:27:53 +00:00
}
}
if connection.DstArrow != d2target.NoArrowhead {
arrowJS, extraJS := ArrowheadJS(r, bgColor, connection.DstArrow, connection.Stroke, connection.StrokeWidth)
2023-01-13 02:27:53 +00:00
if arrowJS == "" {
return "", nil
}
length := len(connection.Route)
endingSegment := geo.NewSegment(connection.Route[length-2], connection.Route[length-1])
endingVector := endingSegment.ToVector()
angle := endingVector.Degrees()
transform := fmt.Sprintf(`transform="translate(%f %f) rotate(%v)"`,
endingSegment.End.X+dstAdj.X, endingSegment.End.Y+dstAdj.Y, angle,
2023-01-13 02:27:53 +00:00
)
roughPaths, err := computeRoughPaths(r, arrowJS)
if err != nil {
return "", err
}
if extraJS != "" {
extraPaths, err := computeRoughPaths(r, extraJS)
if err != nil {
return "", err
}
roughPaths = append(roughPaths, extraPaths...)
}
2023-01-30 11:06:54 +00:00
pathEl := svgstyle.NewThemableElement("path")
2023-01-16 11:15:59 +00:00
pathEl.Class = "connection"
pathEl.Attributes = transform
2023-01-13 02:27:53 +00:00
for _, rp := range roughPaths {
2023-01-16 11:15:59 +00:00
pathEl.D = rp.Attrs.D
pathEl.Fill = rp.Style.Fill
pathEl.Stroke = rp.Style.Stroke
pathEl.Style = rp.StyleCSS()
arrowPaths = append(arrowPaths, pathEl.Render())
2023-01-13 02:27:53 +00:00
}
}
return strings.Join(arrowPaths, " "), nil
}